From 094fe2fa892b24f86d189294e7d3a8e1ae46a306 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Thu, 16 Mar 2023 02:25:48 +0800 Subject: [PATCH 01/16] Convert constants to typescript --- constants/{articleFilters.js => articleFilters.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename constants/{articleFilters.js => articleFilters.ts} (100%) diff --git a/constants/articleFilters.js b/constants/articleFilters.ts similarity index 100% rename from constants/articleFilters.js rename to constants/articleFilters.ts From df9425a17152424167d51565ef6aafd8da3817b3 Mon Sep 17 00:00:00 2001 From: Johnson Liang Date: Thu, 16 Mar 2023 13:36:21 +0800 Subject: [PATCH 02/16] fix(tsconfig): add paths in tsconfig to match babel-plugin-module-resolve's --- tsconfig.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 35d51eac..345e3b6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,14 @@ { "compilerOptions": { "target": "es5", + "baseUrl": ".", + "paths": { + "components/*": ["./components/*"], + "constants/*": ["./constants/*"], + "pages/*": ["./pages/*"], + "lib/*": ["./lib/*"], + "typegen/*": ["./typegen/*"], + }, "lib": [ "dom", "dom.iterable", From d55daaf0cc865cdb7b61d366dbed619502ae3bc5 Mon Sep 17 00:00:00 2001 From: Johnson Liang Date: Thu, 16 Mar 2023 14:08:25 +0800 Subject: [PATCH 03/16] refactor(ListPageControls) move BaseFilter and BaseFilterOption to Typescript --- .../{BaseFilter.js => BaseFilter.tsx} | 18 ++++++++++-- ...seFilterOption.js => BaseFilterOption.tsx} | 28 +++++++++++-------- lib/{theme.js => theme.tsx} | 24 ++++++++++++++-- 3 files changed, 54 insertions(+), 16 deletions(-) rename components/ListPageControls/{BaseFilter.js => BaseFilter.tsx} (92%) rename components/ListPageControls/{BaseFilterOption.js => BaseFilterOption.tsx} (71%) rename lib/{theme.js => theme.tsx} (81%) 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..88f881c2 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,6 +88,18 @@ const useStyles = makeStyles(theme => ({ * @param {Array} props.options * @param {(selected: string[]) => void} props.onChange */ + +type Props = { + title: string; + expandable?: boolean; + placeholder?: string; + selected: ReadonlyArray; + options: ReadonlyArray< + React.ComponentPropsWithoutRef + >; + onChange: (selected: string[]) => void; +}; + function BaseFilter({ title, onChange = () => null, @@ -93,13 +107,13 @@ function BaseFilter({ 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 71% rename from components/ListPageControls/BaseFilterOption.js rename to components/ListPageControls/BaseFilterOption.tsx index cebff7a6..094cd6a8 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: string; + disabled?: boolean; + selected?: boolean; + onClick?: (value: string) => void; +}; + +type StyleProps = Pick; + 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({ chip, selected, label, value, disabled, - onClick = () => {}, -}) { + onClick = () => undefined, +}: Props) { const classes = useStyles({ chip, selected }); const handleClick = () => { onClick(value); 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 ( From 5adf7c0ca9e3ce35ad7795ad81ccca13fc1a60d0 Mon Sep 17 00:00:00 2001 From: Johnson Liang Date: Thu, 16 Mar 2023 14:12:52 +0800 Subject: [PATCH 04/16] refactor(ListPageControls): move ArticleStatusFilter to Typescript --- .../{ArticleStatusFilter.js => ArticleStatusFilter.tsx} | 6 ++---- components/ListPageControls/{index.js => index.ts} | 5 ++++- pages/articles.js | 3 ++- pages/replies.js | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) rename components/ListPageControls/{ArticleStatusFilter.js => ArticleStatusFilter.tsx} (89%) rename components/ListPageControls/{index.js => index.ts} (79%) diff --git a/components/ListPageControls/ArticleStatusFilter.js b/components/ListPageControls/ArticleStatusFilter.tsx similarity index 89% rename from components/ListPageControls/ArticleStatusFilter.js rename to components/ListPageControls/ArticleStatusFilter.tsx index e90148a8..2059ab43 100644 --- a/components/ListPageControls/ArticleStatusFilter.js +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -25,7 +25,7 @@ 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) { +export function getValues(query) { return query[PARAM_NAME] ? query[PARAM_NAME].split(',') : []; } @@ -58,6 +58,4 @@ function ArticleStatusFilter() { ); } -const MemoizedArticleStatusFilter = memo(ArticleStatusFilter); -MemoizedArticleStatusFilter.getValues = getValues; -export default MemoizedArticleStatusFilter; +export default memo(ArticleStatusFilter); diff --git a/components/ListPageControls/index.js b/components/ListPageControls/index.ts similarity index 79% rename from components/ListPageControls/index.js rename to components/ListPageControls/index.ts index e311e7a5..24053683 100644 --- a/components/ListPageControls/index.js +++ b/components/ListPageControls/index.ts @@ -1,7 +1,9 @@ import Tools from './Tools'; import Filters from './Filters'; import BaseFilter from './BaseFilter'; -import ArticleStatusFilter from './ArticleStatusFilter'; +import ArticleStatusFilter, { + getValues as getArticleStatusFilterValues, +} from './ArticleStatusFilter'; import CategoryFilter from './CategoryFilter'; import ArticleTypeFilter from './ArticleTypeFilter'; import ReplyTypeFilter from './ReplyTypeFilter'; @@ -14,6 +16,7 @@ export { Filters, BaseFilter, ArticleStatusFilter, + getArticleStatusFilterValues, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, diff --git a/pages/articles.js b/pages/articles.js index e4c04fc4..e55ff4ac 100644 --- a/pages/articles.js +++ b/pages/articles.js @@ -15,6 +15,7 @@ import { Tools, Filters, ArticleStatusFilter, + getArticleStatusFilterValues, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -68,7 +69,7 @@ function urlQuery2Filter({ userId, ...query } = {}) { const selectedCategoryIds = CategoryFilter.getValues(query); if (selectedCategoryIds.length) filterObj.categoryIds = selectedCategoryIds; - const selectedFilters = ArticleStatusFilter.getValues(query); + const selectedFilters = getArticleStatusFilterValues(query); selectedFilters.forEach(filter => { switch (filter) { case FILTERS.REPLIED_BY_ME: diff --git a/pages/replies.js b/pages/replies.js index 4a1a2b17..a8ebc618 100644 --- a/pages/replies.js +++ b/pages/replies.js @@ -21,6 +21,7 @@ import { Tools, Filters, ArticleStatusFilter, + getArticleStatusFilterValues, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -92,7 +93,7 @@ function urlQuery2Filter({ userId, ...query } = {}) { const selectedCategoryIds = CategoryFilter.getValues(query); if (selectedCategoryIds.length) filterObj.categoryIds = selectedCategoryIds; - const selectedFilters = ArticleStatusFilter.getValues(query); + const selectedFilters = getArticleStatusFilterValues(query); selectedFilters.forEach(filter => { switch (filter) { case FILTERS.REPLIED_BY_ME: From 6ddf57a342ccb73578e79b2d872b42bcbb1a4f54 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Fri, 17 Mar 2023 03:49:18 +0800 Subject: [PATCH 05/16] refactor(tsconfig): remove paths config; baseUrl can already do the trick --- tsconfig.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 345e3b6f..68e7ac64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,6 @@ "compilerOptions": { "target": "es5", "baseUrl": ".", - "paths": { - "components/*": ["./components/*"], - "constants/*": ["./constants/*"], - "pages/*": ["./pages/*"], - "lib/*": ["./lib/*"], - "typegen/*": ["./typegen/*"], - }, "lib": [ "dom", "dom.iterable", From 5591a831d7e1afd08f58b6d5a228e5a4f41774d1 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Fri, 17 Mar 2023 04:17:57 +0800 Subject: [PATCH 06/16] feat(ArticleStatusFilter): new filter ASKED_ONCE and NO_REPLY --- .../ListPageControls/ArticleStatusFilter.tsx | 68 +++++++++++++++---- components/ListPageControls/BaseFilter.tsx | 10 +-- .../ListPageControls/BaseFilterOption.tsx | 10 +-- constants/articleFilters.ts | 4 +- pages/articles.js | 6 ++ pages/replies.js | 13 +++- 6 files changed, 84 insertions(+), 27 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 2059ab43..b92ac44a 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -1,6 +1,8 @@ import { memo } from 'react'; import { useRouter } from 'next/router'; import { t } from 'ttag'; +import { ParsedUrlQuery } from 'querystring'; + import BaseFilter from './BaseFilter'; import useCurrentUser from 'lib/useCurrentUser'; import { goToUrlQueryAndResetPagination } from 'lib/listPage'; @@ -13,46 +15,82 @@ import * as FILTERS from 'constants/articleFilters'; 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_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.REPLIED_BY_ME, label: t`Replied by me` }, ]; const LOGIN_ONLY_OPTIONS = [FILTERS.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], +]; + /** * @param {object} query - query from router * @returns {Arary} list of selected filter values; see constants/articleFilters for all possible values */ -export function getValues(query) { - return query[PARAM_NAME] ? query[PARAM_NAME].split(',') : []; +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) + : []; } -function ArticleStatusFilter() { +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 - const options = user - ? OPTIONS - : OPTIONS.map(option => ({ - ...option, - disabled: LOGIN_ONLY_OPTIONS.includes(option.value), - })); + let options = OPTIONS.filter(f => filterMap[f.value] !== false); + + if (user) { + options = options.map(option => ({ + ...option, + disabled: LOGIN_ONLY_OPTIONS.includes(option.value), + })); + } return ( + onChange={newValues => { + 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 + ); + } + } + }); + goToUrlQueryAndResetPagination({ ...query, - [PARAM_NAME]: values.join(','), - }) - } + [PARAM_NAME]: newValues.join(','), + }); + }} data-ga="Filter(filter)" /> ); diff --git a/components/ListPageControls/BaseFilter.tsx b/components/ListPageControls/BaseFilter.tsx index 88f881c2..19e22e31 100644 --- a/components/ListPageControls/BaseFilter.tsx +++ b/components/ListPageControls/BaseFilter.tsx @@ -89,25 +89,25 @@ const useStyles = makeStyles(theme => ({ * @param {(selected: string[]) => void} props.onChange */ -type Props = { +type Props = { title: string; expandable?: boolean; placeholder?: string; - selected: ReadonlyArray; + selected: ReadonlyArray; options: ReadonlyArray< React.ComponentPropsWithoutRef >; - onChange: (selected: string[]) => void; + onChange: (selected: V[]) => void; }; -function BaseFilter({ +function BaseFilter({ title, onChange = () => null, placeholder, expandable, selected = [], options = [], -}: Props) { +}: Props) { const classes = useStyles(); const [expandEl, setExpandEl] = useState(null); diff --git a/components/ListPageControls/BaseFilterOption.tsx b/components/ListPageControls/BaseFilterOption.tsx index 094cd6a8..5556a2ed 100644 --- a/components/ListPageControls/BaseFilterOption.tsx +++ b/components/ListPageControls/BaseFilterOption.tsx @@ -1,17 +1,17 @@ import { makeStyles } from '@material-ui/core/styles'; import Chip from '@material-ui/core/Chip'; -type Props = { +type Props = { /** toggle chip-like border on display */ chip?: boolean; label: string; - value: string; + value: V; disabled?: boolean; selected?: boolean; onClick?: (value: string) => void; }; -type StyleProps = Pick; +type StyleProps = Pick, 'selected' | 'chip'>; const useStyles = makeStyles(theme => ({ root: { @@ -38,14 +38,14 @@ const useStyles = makeStyles(theme => ({ }, })); -function BaseFilterOption({ +function BaseFilterOption({ chip, selected, label, value, disabled, onClick = () => undefined, -}: Props) { +}: Props) { const classes = useStyles({ chip, selected }); const handleClick = () => { onClick(value); diff --git a/constants/articleFilters.ts b/constants/articleFilters.ts index 6f4c04bf..a49c886f 100644 --- a/constants/articleFilters.ts +++ b/constants/articleFilters.ts @@ -1,4 +1,6 @@ export const REPLIED_BY_ME = 'REPLIED_BY_ME'; -export const NO_USEFUL_REPLY_YET = 'NO_USEFUL_REPLY_YET'; +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'; diff --git a/pages/articles.js b/pages/articles.js index e55ff4ac..be53d839 100644 --- a/pages/articles.js +++ b/pages/articles.js @@ -82,9 +82,15 @@ function urlQuery2Filter({ userId, ...query } = {}) { case FILTERS.NO_USEFUL_REPLY_YET: filterObj.hasArticleReplyWithMorePositiveFeedback = false; 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; diff --git a/pages/replies.js b/pages/replies.js index a8ebc618..729001c3 100644 --- a/pages/replies.js +++ b/pages/replies.js @@ -80,6 +80,11 @@ 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 @@ -106,9 +111,15 @@ function urlQuery2Filter({ userId, ...query } = {}) { case FILTERS.NO_USEFUL_REPLY_YET: filterObj.hasArticleReplyWithMorePositiveFeedback = false; 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; @@ -237,7 +248,7 @@ function ReplyListPage() { - + From 94e55c7aee09adc0545e50009ac75e055d297692 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Fri, 17 Mar 2023 04:22:24 +0800 Subject: [PATCH 07/16] chore(i18n): Translate new options --- i18n/zh_TW.po | 196 +++++++++++++++++++++++++++----------------------- 1 file changed, 108 insertions(+), 88 deletions(-) diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po index 406b4d43..d9f62a50 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:173 +#: pages/replies.js:155 msgid "Most recently asked" msgstr "最近被查詢" #: components/ProfilePage/RepliedArticleTab.js:33 -#: pages/articles.js:168 -#: pages/replies.js:142 +#: pages/articles.js:175 +#: pages/replies.js:154 msgid "Most recently replied" msgstr "最近被回應" #: components/ProfilePage/RepliedArticleTab.js:37 -#: pages/articles.js:167 -#: pages/replies.js:144 +#: pages/articles.js:174 +#: pages/replies.js:156 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 "已經送出回應。" @@ -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:287 msgid "Bust Hoaxes" msgstr "查核闢謠" @@ -331,24 +333,24 @@ msgstr "查核闢謠" msgid "Messages" msgstr "可疑訊息" -#: components/ListPageControls/ArticleStatusFilter.js:16 +#: components/ListPageControls/ArticleStatusFilter.tsx:23 #: pages/search.js:316 msgid "Replied by me" msgstr "我查核過" -#: components/ListPageControls/ArticleStatusFilter.js:17 +#: components/ListPageControls/ArticleStatusFilter.tsx:22 msgid "No useful reply yet" msgstr "還未有有效查核" -#: components/ListPageControls/ArticleStatusFilter.js:18 +#: components/ListPageControls/ArticleStatusFilter.tsx:19 msgid "Asked many times" msgstr "熱門回報" -#: components/ListPageControls/ArticleStatusFilter.js:19 +#: components/ListPageControls/ArticleStatusFilter.tsx:21 msgid "Replied many times" -msgstr "熱門討論" +msgstr "多份查核" -#: components/ListPageControls/ArticleStatusFilter.js:47 +#: components/ListPageControls/ArticleStatusFilter.tsx:70 #: components/ListPageControls/Filters.js:70 #: pages/search.js:314 msgid "Filter" @@ -371,8 +373,8 @@ msgstr "查看更多" msgid "Hoax for you" msgstr "等你來答" -#: pages/replies.js:226 -#: pages/replies.js:229 +#: pages/replies.js:238 +#: pages/replies.js:241 msgid "Latest replies" msgstr "最新查核" @@ -438,8 +440,8 @@ msgstr "在 可疑訊息" msgid "in Replies" msgstr "在 最新查核" -#: pages/articles.js:155 -#: pages/articles.js:157 +#: pages/articles.js:162 +#: pages/articles.js:164 msgid "Dubious Messages" msgstr "可疑訊息" @@ -454,9 +456,9 @@ msgstr "您認為沒有幫助的理由是什麼呢?" #: components/LandingPage/SectionArticles.js:153 #: components/ProfilePage/CommentTab.js:151 #: components/ProfilePage/RepliedArticleTab.js:251 -#: pages/articles.js:181 +#: pages/articles.js:188 #: pages/hoax-for-you.js:133 -#: pages/replies.js:246 +#: pages/replies.js:258 #: pages/search.js:181 #: pages/search.js:258 msgid "Loading..." @@ -492,17 +494,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 +593,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 +714,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:268 #, javascript-format msgid "${ article.replyRequestCount } occurrence" msgid_plural "${ article.replyRequestCount } occurrences" @@ -725,25 +727,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 +769,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 +785,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 +874,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 +1046,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 +1369,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 +2310,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:275 msgid "First reported ${ timeAgo }" msgstr "首次回報於 ${ timeAgo }" @@ -2343,7 +2342,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 +2395,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 +2413,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 +2426,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 +2479,27 @@ 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:18 +msgid "Asked only once" +msgstr "僅一人回報" + +#: components/ListPageControls/ArticleStatusFilter.tsx:20 +msgid "Zero replies" +msgstr "無人查核" + #: pages/index.js:29 msgctxt "site title" msgid "Cofacts" From a1347198109325f64dc99bdc11481f8d54fd2709 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Fri, 17 Mar 2023 04:30:59 +0800 Subject: [PATCH 08/16] fix(dockerignore): Include tsconfig in docker --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 70197281..65b1caa8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,4 @@ !public/* !pages/* !typegen/* +!tsconfig.json From 10d8463ce46ca85b42e7fe6406b1bed3e7088166 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 02:44:00 +0800 Subject: [PATCH 09/16] refactor(ListPageControls): move ArticleStatusFilter object logic to within component --- .../ListPageControls/ArticleStatusFilter.tsx | 40 +++++++++++++++++- components/ListPageControls/index.ts | 4 +- pages/articles.js | 34 +-------------- pages/replies.js | 41 ++++--------------- 4 files changed, 50 insertions(+), 69 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index b92ac44a..d4bbe1e2 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -3,6 +3,7 @@ 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'; @@ -37,7 +38,7 @@ const MUTUALLY_EXCLUSIVE_FILTERS: ReadonlyArray< * @param {object} query - query from router * @returns {Arary} list of selected filter values; see constants/articleFilters for all possible values */ -export function getValues(query: ParsedUrlQuery): Array { +function getValues(query: ParsedUrlQuery): Array { return query[PARAM_NAME] ? query[PARAM_NAME].toString() .split(',') @@ -45,6 +46,43 @@ export function getValues(query: ParsedUrlQuery): Array { : []; } +export function getFilter( + query: ParsedUrlQuery, + userId?: string +): ListArticleFilter { + const filterObj: ListArticleFilter = {}; + + for (const filter of getValues(query)) { + 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_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: + } + } + + return filterObj; +} + type Props = { /** setting FILTERS.XXX false means that XXX option should be hidden in ArticleStatusFilter. */ filterMap?: Partial>; diff --git a/components/ListPageControls/index.ts b/components/ListPageControls/index.ts index 24053683..2dcd0b64 100644 --- a/components/ListPageControls/index.ts +++ b/components/ListPageControls/index.ts @@ -2,7 +2,7 @@ import Tools from './Tools'; import Filters from './Filters'; import BaseFilter from './BaseFilter'; import ArticleStatusFilter, { - getValues as getArticleStatusFilterValues, + getFilter as getArticleStatusFilter, } from './ArticleStatusFilter'; import CategoryFilter from './CategoryFilter'; import ArticleTypeFilter from './ArticleTypeFilter'; @@ -16,7 +16,7 @@ export { Filters, BaseFilter, ArticleStatusFilter, - getArticleStatusFilterValues, + getArticleStatusFilter, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, diff --git a/pages/articles.js b/pages/articles.js index be53d839..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,7 +14,7 @@ import { Tools, Filters, ArticleStatusFilter, - getArticleStatusFilterValues, + getArticleStatusFilter, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -64,40 +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 = getArticleStatusFilterValues(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_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 [start, end] = TimeRange.getValues(query); if (start) { diff --git a/pages/replies.js b/pages/replies.js index 729001c3..10f05dc2 100644 --- a/pages/replies.js +++ b/pages/replies.js @@ -21,7 +21,7 @@ import { Tools, Filters, ArticleStatusFilter, - getArticleStatusFilterValues, + getArticleStatusFilter, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, @@ -90,43 +90,16 @@ const ARTICLE_STATUS_FILTER_MAP = { * @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 = getArticleStatusFilterValues(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_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 [start, end] = TimeRange.getValues(query); if (start) { From 66d7bb736df60bf7d942fbfc127734adfb239415 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 03:11:39 +0800 Subject: [PATCH 10/16] feat(search): let article search have same filters as article page --- .../ListPageControls/ArticleStatusFilter.tsx | 13 +++-- components/ListPageControls/index.ts | 2 + pages/search.js | 54 +++++++++---------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index d4bbe1e2..5b09ce4e 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -35,10 +35,10 @@ const MUTUALLY_EXCLUSIVE_FILTERS: ReadonlyArray< ]; /** - * @param {object} query - query from router - * @returns {Arary} list of selected filter values; see constants/articleFilters for all possible values + * @param query - query from router + * @returns list of selected filter values; see constants/articleFilters for all possible values */ -function getValues(query: ParsedUrlQuery): Array { +export function getValues(query: ParsedUrlQuery): Array { return query[PARAM_NAME] ? query[PARAM_NAME].toString() .split(',') @@ -46,6 +46,11 @@ function getValues(query: ParsedUrlQuery): Array { : []; } +/** + * @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 @@ -96,7 +101,7 @@ function ArticleStatusFilter({ filterMap = {} }: Props) { // Disable login-only options when not logged in let options = OPTIONS.filter(f => filterMap[f.value] !== false); - if (user) { + if (!user) { options = options.map(option => ({ ...option, disabled: LOGIN_ONLY_OPTIONS.includes(option.value), diff --git a/components/ListPageControls/index.ts b/components/ListPageControls/index.ts index 2dcd0b64..8d1a8a52 100644 --- a/components/ListPageControls/index.ts +++ b/components/ListPageControls/index.ts @@ -3,6 +3,7 @@ import Filters from './Filters'; import BaseFilter from './BaseFilter'; import ArticleStatusFilter, { getFilter as getArticleStatusFilter, + getValues as getArticleStatusFilterValues, } from './ArticleStatusFilter'; import CategoryFilter from './CategoryFilter'; import ArticleTypeFilter from './ArticleTypeFilter'; @@ -17,6 +18,7 @@ export { BaseFilter, ArticleStatusFilter, getArticleStatusFilter, + getArticleStatusFilterValues, CategoryFilter, ArticleTypeFilter, ReplyTypeFilter, diff --git a/pages/search.js b/pages/search.js index 51b4e309..c12d71a6 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,15 @@ 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, +}; + function SearchPage() { const { query } = useRouter(); const user = useCurrentUser(); - const [selfOnly, setSelfOnly] = useState(false); return ( @@ -310,26 +313,21 @@ function SearchPage() { - setSelfOnly(values.includes('self'))} + {query.type === 'messages' && } - {query.type === 'messages' && ( - + )} {query.type === 'replies' && ( - + )} From d1531d57469fd26c76876f6f2c59b5406cd6d9ec Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 03:21:27 +0800 Subject: [PATCH 11/16] feat(ArticleStatusFilter): add "has useful replies" filter --- .../ListPageControls/ArticleStatusFilter.tsx | 5 ++ constants/articleFilters.ts | 1 + i18n/zh_TW.po | 58 ++++++++++--------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 5b09ce4e..1c9e2c86 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -21,6 +21,7 @@ const OPTIONS = [ { 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` }, ]; @@ -32,6 +33,7 @@ const MUTUALLY_EXCLUSIVE_FILTERS: 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], ]; /** @@ -69,6 +71,9 @@ export function getFilter( 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; diff --git a/constants/articleFilters.ts b/constants/articleFilters.ts index a49c886f..8a0d6948 100644 --- a/constants/articleFilters.ts +++ b/constants/articleFilters.ts @@ -4,3 +4,4 @@ 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 d9f62a50..83aee0b7 100644 --- a/i18n/zh_TW.po +++ b/i18n/zh_TW.po @@ -52,20 +52,20 @@ msgid "Sort by" msgstr "排序方式" #: components/ProfilePage/RepliedArticleTab.js:36 -#: pages/articles.js:173 -#: pages/replies.js:155 +#: pages/articles.js:143 +#: pages/replies.js:128 msgid "Most recently asked" msgstr "最近被查詢" #: components/ProfilePage/RepliedArticleTab.js:33 -#: pages/articles.js:175 -#: pages/replies.js:154 +#: pages/articles.js:145 +#: pages/replies.js:127 msgid "Most recently replied" msgstr "最近被回應" #: components/ProfilePage/RepliedArticleTab.js:37 -#: pages/articles.js:174 -#: pages/replies.js:156 +#: pages/articles.js:144 +#: pages/replies.js:129 msgid "Most asked" msgstr "最多人詢問" @@ -150,7 +150,7 @@ msgstr "已經送出回應。" msgid "Please login first." msgstr "請先登入。" -#: pages/search.js:302 +#: pages/search.js:305 msgid "Search" msgstr "搜尋" @@ -323,7 +323,7 @@ msgstr "含有正確訊息" msgid "contains misinformation" msgstr "含有錯誤訊息" -#: pages/replies.js:287 +#: pages/replies.js:260 msgid "Bust Hoaxes" msgstr "查核闢謠" @@ -333,26 +333,24 @@ msgstr "查核闢謠" msgid "Messages" msgstr "可疑訊息" -#: components/ListPageControls/ArticleStatusFilter.tsx:23 -#: pages/search.js:316 +#: components/ListPageControls/ArticleStatusFilter.tsx:25 msgid "Replied by me" msgstr "我查核過" -#: components/ListPageControls/ArticleStatusFilter.tsx:22 +#: components/ListPageControls/ArticleStatusFilter.tsx:23 msgid "No useful reply yet" -msgstr "還未有有效查核" +msgstr "尚無有效查核" -#: components/ListPageControls/ArticleStatusFilter.tsx:19 +#: components/ListPageControls/ArticleStatusFilter.tsx:20 msgid "Asked many times" msgstr "熱門回報" -#: components/ListPageControls/ArticleStatusFilter.tsx:21 +#: components/ListPageControls/ArticleStatusFilter.tsx:22 msgid "Replied many times" msgstr "多份查核" -#: components/ListPageControls/ArticleStatusFilter.tsx:70 +#: components/ListPageControls/ArticleStatusFilter.tsx:118 #: components/ListPageControls/Filters.js:70 -#: pages/search.js:314 msgid "Filter" msgstr "篩選" @@ -373,8 +371,8 @@ msgstr "查看更多" msgid "Hoax for you" msgstr "等你來答" -#: pages/replies.js:238 -#: pages/replies.js:241 +#: pages/replies.js:211 +#: pages/replies.js:214 msgid "Latest replies" msgstr "最新查核" @@ -440,8 +438,8 @@ msgstr "在 可疑訊息" msgid "in Replies" msgstr "在 最新查核" -#: pages/articles.js:162 -#: pages/articles.js:164 +#: pages/articles.js:132 +#: pages/articles.js:134 msgid "Dubious Messages" msgstr "可疑訊息" @@ -456,11 +454,11 @@ msgstr "您認為沒有幫助的理由是什麼呢?" #: components/LandingPage/SectionArticles.js:153 #: components/ProfilePage/CommentTab.js:151 #: components/ProfilePage/RepliedArticleTab.js:251 -#: pages/articles.js:188 +#: pages/articles.js:158 #: pages/hoax-for-you.js:133 -#: pages/replies.js:258 -#: pages/search.js:181 -#: pages/search.js:258 +#: pages/replies.js:231 +#: pages/search.js:177 +#: pages/search.js:256 msgid "Loading..." msgstr "載入中⋯" @@ -714,7 +712,7 @@ msgstr "${ totalVisits } 次瀏覽" #: components/ListPageDisplays/ReplySearchItem.js:103 #: components/ProfilePage/CommentTab.js:169 #: components/ProfilePage/RepliedArticleTab.js:269 -#: pages/replies.js:268 +#: pages/replies.js:241 #, javascript-format msgid "${ article.replyRequestCount } occurrence" msgid_plural "${ article.replyRequestCount } occurrences" @@ -2311,7 +2309,7 @@ msgstr "目前已經存有4萬5千筆以上查證過的資訊,透過投入資 #: components/ProfilePage/CommentTab.js:178 #: components/ProfilePage/RepliedArticleTab.js:278 #: pages/article/[id].js:385 -#: pages/replies.js:275 +#: pages/replies.js:248 msgid "First reported ${ timeAgo }" msgstr "首次回報於 ${ timeAgo }" @@ -2492,14 +2490,18 @@ msgstr "" msgid "Log in to view content" msgstr "" -#: components/ListPageControls/ArticleStatusFilter.tsx:18 +#: components/ListPageControls/ArticleStatusFilter.tsx:19 msgid "Asked only once" msgstr "僅一人回報" -#: components/ListPageControls/ArticleStatusFilter.tsx:20 +#: components/ListPageControls/ArticleStatusFilter.tsx:21 msgid "Zero replies" msgstr "無人查核" +#: components/ListPageControls/ArticleStatusFilter.tsx:24 +msgid "Has useful replies" +msgstr "查核有效" + #: pages/index.js:29 msgctxt "site title" msgid "Cofacts" From c34eee40061010a6ceb22b6e8d085be9fbd1d6b2 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 04:30:54 +0800 Subject: [PATCH 12/16] feat(ArticleStatusFilter): add "not replied by me" filter --- .../ListPageControls/ArticleStatusFilter.tsx | 16 ++++++++++++++-- constants/articleFilters.ts | 1 + i18n/zh_TW.po | 8 ++++++-- pages/search.js | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 1c9e2c86..2fb3234f 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -23,9 +23,10 @@ const OPTIONS = [ { 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` }, ]; -const LOGIN_ONLY_OPTIONS = [FILTERS.REPLIED_BY_ME]; +const LOGIN_ONLY_OPTIONS = [FILTERS.REPLIED_BY_ME, FILTERS.NOT_REPLIED_BY_ME]; const MUTUALLY_EXCLUSIVE_FILTERS: ReadonlyArray< ReadonlyArray @@ -34,6 +35,7 @@ const MUTUALLY_EXCLUSIVE_FILTERS: ReadonlyArray< [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], ]; /** @@ -60,14 +62,21 @@ export function getFilter( const filterObj: ListArticleFilter = {}; for (const filter of getValues(query)) { + if (!userId && LOGIN_ONLY_OPTIONS.includes(filter)) break; + switch (filter) { case FILTERS.REPLIED_BY_ME: - if (!userId) break; 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; @@ -130,6 +139,9 @@ function ArticleStatusFilter({ filterMap = {} }: Props) { newValues = newValues.filter(v => mutuallyExclusiveFilters.includes(v) ? v === filter : true ); + + // Found the toggled filter, can skip the rest. + break; } } }); diff --git a/constants/articleFilters.ts b/constants/articleFilters.ts index 8a0d6948..539cde6a 100644 --- a/constants/articleFilters.ts +++ b/constants/articleFilters.ts @@ -1,4 +1,5 @@ export const REPLIED_BY_ME = 'REPLIED_BY_ME'; +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'; diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po index 83aee0b7..46cd1ee3 100644 --- a/i18n/zh_TW.po +++ b/i18n/zh_TW.po @@ -150,7 +150,7 @@ msgstr "已經送出回應。" msgid "Please login first." msgstr "請先登入。" -#: pages/search.js:305 +#: pages/search.js:306 msgid "Search" msgstr "搜尋" @@ -349,7 +349,7 @@ msgstr "熱門回報" msgid "Replied many times" msgstr "多份查核" -#: components/ListPageControls/ArticleStatusFilter.tsx:118 +#: components/ListPageControls/ArticleStatusFilter.tsx:127 #: components/ListPageControls/Filters.js:70 msgid "Filter" msgstr "篩選" @@ -2502,6 +2502,10 @@ msgstr "無人查核" 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/pages/search.js b/pages/search.js index c12d71a6..60d31622 100644 --- a/pages/search.js +++ b/pages/search.js @@ -293,6 +293,7 @@ function ReplySearchResult({ query, isLoggedIn }) { 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() { From 033e7a779adb78c8746501060d9be88366b6aedc Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 04:58:14 +0800 Subject: [PATCH 13/16] docs(ArticleStatusFilter): add comment to non-login logic --- components/ListPageControls/ArticleStatusFilter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 2fb3234f..11ad2851 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -62,6 +62,7 @@ export function getFilter( 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) { From fbee9698e6f5a387c057bead464147257d8c419a Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 04:58:35 +0800 Subject: [PATCH 14/16] refactor(ArticleStatusFilter): add exhausive check to ensure we are not missing any filter case --- components/ListPageControls/ArticleStatusFilter.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 11ad2851..201f3d8e 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -96,7 +96,10 @@ export function getFilter( case FILTERS.REPLIED_MANY_TIMES: filterObj.replyCount = { GTE: 3 }; break; - default: + default: { + const exhausiveCheck: never = filter; + return exhausiveCheck; + } } } From 8aa96528a5153075baf28da36c453395bdd18a25 Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 05:02:18 +0800 Subject: [PATCH 15/16] fix(ArticleStatusFilter): fix typo in exhaustive check --- components/ListPageControls/ArticleStatusFilter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index 201f3d8e..e1891514 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -97,8 +97,8 @@ export function getFilter( filterObj.replyCount = { GTE: 3 }; break; default: { - const exhausiveCheck: never = filter; - return exhausiveCheck; + const exhaustiveCheck: never = filter; + throw new Error(`Unhandled case: ${exhaustiveCheck}`); } } } From 60138cd5adfc108883083b87fbe9dbcba030431e Mon Sep 17 00:00:00 2001 From: MrOrz Date: Sat, 18 Mar 2023 05:05:13 +0800 Subject: [PATCH 16/16] docs(ArticleStatusFilter): describe use case of LOGIN_ONLY_OPTIONS --- components/ListPageControls/ArticleStatusFilter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ListPageControls/ArticleStatusFilter.tsx b/components/ListPageControls/ArticleStatusFilter.tsx index e1891514..86dd451f 100644 --- a/components/ListPageControls/ArticleStatusFilter.tsx +++ b/components/ListPageControls/ArticleStatusFilter.tsx @@ -26,6 +26,7 @@ const OPTIONS = [ { 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<