diff --git a/.husky/pre-commit b/.husky/pre-commit index cb9be4a68d..088f68408c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,4 @@ . "$(dirname "$0")/_/husky.sh" npm run format -- --list-different && \ -npm run lint +npm run lint \ No newline at end of file diff --git a/package.json b/package.json index 8c2efe1818..80bee3c9e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "4.10.0", + "version": "4.11.0", "description": "codebase of Matters' website", "sideEffects": false, "author": "Matters ", diff --git a/src/common/enums/errorCode.ts b/src/common/enums/errorCode.ts index b8ede49fcf..cbf2cf9ce8 100644 --- a/src/common/enums/errorCode.ts +++ b/src/common/enums/errorCode.ts @@ -62,6 +62,7 @@ export const ERROR_CODES = { // Verification Code CODE_INVALID: 'CODE_INVALID', + CODE_INACTIVE: 'CODE_INACTIVE', CODE_EXPIRED: 'CODE_EXPIRED', // GQL diff --git a/src/common/enums/payment.ts b/src/common/enums/payment.ts index 5bf34b4376..ee3fef1a40 100644 --- a/src/common/enums/payment.ts +++ b/src/common/enums/payment.ts @@ -32,6 +32,12 @@ export enum PAYMENT_CURRENCY { USDT = 'USDT', } +export enum QUOTE_CURRENCY { + TWD = 'TWD', + HKD = 'HKD', + USD = 'USD', +} + export enum CHAIN { POLYGON = 'Polygon', } diff --git a/src/common/enums/route.ts b/src/common/enums/route.ts index e76ac89482..faabbef613 100644 --- a/src/common/enums/route.ts +++ b/src/common/enums/route.ts @@ -83,8 +83,6 @@ export const ROUTES: { { key: 'FOLLOW', pathname: '/follow' }, { key: 'AUTHORS', pathname: '/authors' }, { key: 'SEARCH', pathname: '/search' }, - { key: 'SETTINGS', pathname: '/settings' }, - // experient page for recommendation engine testing { key: 'RECOMMENDATION', pathname: '/recommendation' }, diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index db21e9dcd3..ef6f91591d 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -60,8 +60,9 @@ export const TEXT = { circleEdited: '圍爐已更新', clear: '清空', close: '關閉', - CODE_EXPIRED: '驗證碼已過期', - CODE_INVALID: '驗證碼不正確', + CODE_EXPIRED: '驗證碼已過期,請重新發送', + CODE_INACTIVE: '驗證碼已失效,請使用最新驗證碼或重新發送', + CODE_INVALID: '驗證碼不正確,請檢查輸入內容或重新發送', collectedOnly: '只看衍生作品', COMMENT_NOT_FOUND: '評論不存在', comment: '評論', @@ -102,7 +103,7 @@ export const TEXT = { editUserProfile: '編輯資料', email: '電子信箱', emptySearchResults: '不好意思,什麼都沒搜到', - enterCustomAmount: '輸入自訂金額', + enterCustomAmount: '輸入金額', enterDisplayName: '請輸入姓名', enterEmail: '請輸入電子信箱', enterNewEmail: '請輸入新電子信箱', @@ -165,6 +166,7 @@ export const TEXT = { hintPaymentPointer: '錢包地址以“$”開頭', hintTerm: '我們的用戶協議和隱私政策發生了更改,請閱讀並同意後繼續使用。', hintUserName: '4-15 個字元,僅支持英文、數字或下劃線', + hintVerificationCode: '驗證碼有效期 20 分鐘', history: '足跡', hkd: '港幣', hottest: '熱門', @@ -450,8 +452,9 @@ export const TEXT = { circleEdited: '围炉已更新', clear: '清空', close: '关闭', - CODE_EXPIRED: '验证码已过期', - CODE_INVALID: '验证码不正确', + CODE_EXPIRED: '验证码已过期,请重新发送', + CODE_INACTIVE: '验证码已失效,请使用最新验证码或重新发送', + CODE_INVALID: '验证码不正确,请检查输入内容或重新发送', collectedOnly: '只看衍生作品', COMMENT_NOT_FOUND: '评论不存在', comment: '评论', @@ -492,7 +495,7 @@ export const TEXT = { editUserProfile: '编辑资料', email: '邮箱', emptySearchResults: '不好意思,什么都没搜到', - enterCustomAmount: '输入自订金额', + enterCustomAmount: '输入金额', enterDisplayName: '请输入姓名', enterEmail: '请输入邮箱', enterNewEmail: '请输入新邮箱', @@ -555,6 +558,7 @@ export const TEXT = { hintPaymentPointer: '钱包地址以“$”开头', hintTerm: '我们的用户协议和隐私政策发生了更改,请阅读并同意后继续使用。', hintUserName: '4-15 个字符,仅支持英文、数字或下划线', + hintVerificationCode: '验证码有效期 20 分钟', history: '足迹', hkd: '港币', hottest: '热门', @@ -840,8 +844,11 @@ export const TEXT = { circleEdited: 'Circle Edited', clear: 'Clear', close: 'Cancel', - CODE_EXPIRED: 'Verification code expired', - CODE_INVALID: 'Invalid verification code', + CODE_EXPIRED: 'Verification code expired, try resend', + CODE_INACTIVE: + 'Verification code is no longer valid. Please use the latest code we sent, or try resend', + CODE_INVALID: + 'Incorrect verification code. Please check your input, or try resend', collapseComment: 'Collapse response', collectedOnly: 'Articles Only', COMMENT_NOT_FOUND: 'Response not found', @@ -961,6 +968,7 @@ export const TEXT = { hintUserDescription: 'Recommended within 50 words and 200 words maximum.', hintUserName: 'Must be between 4-15 characters long. Only English alphabets, numbers and underline are accepted.', + hintVerificationCode: 'Code will expire after 20 minutes', history: 'History', hkd: 'HKD', hottest: 'Trending', diff --git a/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx b/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx index 567d7fd15a..55749fb765 100644 --- a/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx @@ -7,7 +7,6 @@ import { TextIcon, Translate, useMutation, - useRoute, } from '~/components' import updateTagArticlesCount from '~/components/GQL/updates/tagArticlesCount' @@ -44,16 +43,19 @@ const fragments = { `, } -const RemoveTagButton = ({ article }: { article: RemoveTagButtonArticle }) => { - const { getQuery } = useRoute() - const id = getQuery('tagId') - +const RemoveTagButton = ({ + article, + tagId, +}: { + article: RemoveTagButtonArticle + tagId: string +}) => { const [deleteArticlesTags] = useMutation( DELETE_ARTICLES_TAGS, { - variables: { id, articles: [article.id] }, + variables: { id: tagId, articles: [article.id] }, update: (cache) => { - updateTagArticlesCount({ cache, type: 'decrement', id }) + updateTagArticlesCount({ cache, type: 'decrement', id: tagId }) }, } ) diff --git a/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx b/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx index 4a60aded89..a1a04b519f 100644 --- a/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx @@ -1,13 +1,6 @@ import gql from 'graphql-tag' -import { - IconAdd24, - Menu, - TextIcon, - Translate, - useMutation, - useRoute, -} from '~/components' +import { IconAdd24, Menu, TextIcon, Translate, useMutation } from '~/components' import { ADD_TOAST } from '~/common/enums' @@ -46,11 +39,11 @@ const fragments = { const SetTagSelectedButton = ({ article, + tagId, }: { article: SetTagSelectedButtonArticle + tagId: string }) => { - const { getQuery } = useRoute() - const tagId = getQuery('tagId') const [update] = useMutation(SET_TAG_SELECTED, { variables: { id: tagId, articles: [article.id] }, }) diff --git a/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx b/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx index 0275070224..c1f0a39fde 100644 --- a/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx @@ -8,7 +8,6 @@ import { TextIcon, Translate, useMutation, - useRoute, } from '~/components' import { ADD_TOAST } from '~/common/enums' @@ -52,11 +51,11 @@ const fragments = { const SetTagUnselectedButton = ({ article, + tagId, }: { article: SetTagUnselectedButtonArticle + tagId: string }) => { - const { getQuery } = useRoute() - const tagId = getQuery('tagId') const [update] = useMutation(SET_TAG_UNSELECTED, { variables: { id: tagId, articles: [article.id] }, update: (cache) => { @@ -65,7 +64,11 @@ const SetTagUnselectedButton = ({ const { TAG_ARTICLES_PUBLIC: query, } = require('~/components/GQL/queries/tagArticles') - const variables = { id: tagId, selected: true } + const variables = { + id: tagId, + selected: true, + sortBy: 'byCreatedAtDesc', + } const data = cache.readQuery({ query, variables }) const node = _get(data, 'node', {}) as TagArticlesPublic_node_Tag if ( diff --git a/src/components/ArticleDigest/DropdownActions/index.tsx b/src/components/ArticleDigest/DropdownActions/index.tsx index a0ddc2197c..21bd19c44e 100644 --- a/src/components/ArticleDigest/DropdownActions/index.tsx +++ b/src/components/ArticleDigest/DropdownActions/index.tsx @@ -1,7 +1,5 @@ -import _find from 'lodash/find' import _isEmpty from 'lodash/isEmpty' import _pickBy from 'lodash/pickBy' -import _some from 'lodash/some' import { useContext } from 'react' import { @@ -16,7 +14,6 @@ import { Menu, ShareDialog, Translate, - useRoute, ViewerContext, } from '~/components' @@ -54,8 +51,12 @@ export interface DropdownActionsControls { // based on type inCard?: boolean inUserArticles?: boolean - inTagDetailLatest?: boolean - inTagDetailSelected?: boolean + + // tag + tagDetailId?: string + hasSetTagSelected?: boolean + hasSetTagUnselected?: boolean + hasRemoveTag?: boolean morePublicActions?: React.ReactNode } @@ -73,7 +74,7 @@ interface Controls { hasSticky: boolean hasArchive: boolean hasSetTagSelected: boolean - hasSetTagUnSelected: boolean + hasSetTagUnselected: boolean hasRemoveTag: boolean hasEdit: boolean } @@ -90,6 +91,7 @@ type BaseDropdownActionsProps = DropdownActionsProps & Controls & DialogProps const BaseDropdownActions = ({ article, + tagDetailId, icon, size, @@ -105,7 +107,7 @@ const BaseDropdownActions = ({ hasSticky, hasArchive, hasSetTagSelected, - hasSetTagUnSelected, + hasSetTagUnselected, hasRemoveTag, hasEdit, @@ -128,7 +130,7 @@ const BaseDropdownActions = ({ hasSticky || hasArchive || hasSetTagSelected || - hasSetTagUnSelected || + hasSetTagUnselected || hasRemoveTag const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( @@ -150,9 +152,15 @@ const BaseDropdownActions = ({ {hasSticky && } {hasArchive && } - {hasSetTagSelected && } - {hasSetTagUnSelected && } - {hasRemoveTag && } + {hasSetTagSelected && tagDetailId && ( + + )} + {hasSetTagUnselected && tagDetailId && ( + + )} + {hasRemoveTag && tagDetailId && ( + + )} {hasEdit && } ) @@ -195,29 +203,15 @@ const DropdownActions = (props: DropdownActionsProps) => { inCard, inUserArticles, - inTagDetailLatest, - inTagDetailSelected, + + hasSetTagSelected, + hasSetTagUnselected, + hasRemoveTag, } = props - const { getQuery } = useRoute() const viewer = useContext(ViewerContext) const isArticleAuthor = viewer.id === article.author.id const isActive = article.articleState === 'active' - const isInTagDetail = inTagDetailLatest || inTagDetailSelected - - // check permission if in tag detail - let canEditTag = false - if (isInTagDetail) { - const tagId = getQuery('tagId') - const tag = _find(article.tags || [], (item) => item.id === tagId) - const isEditor = _some( - tag?.editors || [], - (editor) => editor.id === viewer.id - ) - const isCreator = tag?.creator?.id === viewer.id - canEditTag = - isEditor || isCreator || viewer.info.email === 'hi@matters.news' - } const forbid = () => { window.dispatchEvent( @@ -247,9 +241,9 @@ const DropdownActions = (props: DropdownActionsProps) => { !viewer.isInactive ), hasArchive: isArticleAuthor && isActive && !viewer.isArchived, - hasSetTagSelected: !!(inTagDetailLatest && canEditTag), - hasSetTagUnSelected: !!(inTagDetailSelected && canEditTag), - hasRemoveTag: !!(isInTagDetail && canEditTag), + hasSetTagSelected: !!hasSetTagSelected, + hasSetTagUnselected: !!hasSetTagUnselected, + hasRemoveTag: !!hasRemoveTag, hasEdit: isActive && isArticleAuthor, } diff --git a/src/components/ArticleDigest/Feed/index.tsx b/src/components/ArticleDigest/Feed/index.tsx index 210fe8859e..4c6e2e9e1a 100644 --- a/src/components/ArticleDigest/Feed/index.tsx +++ b/src/components/ArticleDigest/Feed/index.tsx @@ -115,12 +115,15 @@ type MemoizedArticleDigestFeed = React.MemoExoticComponent< export const ArticleDigestFeed = React.memo( BaseArticleDigestFeed, - ({ article: prevArticle }, { article }) => { + ({ article: prevArticle, ...prevProps }, { article, ...props }) => { return ( prevArticle.subscribed === article.subscribed && prevArticle.articleState === article.articleState && prevArticle.sticky === article.sticky && - prevArticle.author.isFollowee === article.author.isFollowee + prevArticle.author.isFollowee === article.author.isFollowee && + prevProps.hasSetTagSelected === props.hasSetTagSelected && + prevProps.hasSetTagUnselected === props.hasSetTagUnselected && + prevProps.hasRemoveTag === props.hasRemoveTag ) } ) as MemoizedArticleDigestFeed diff --git a/src/components/Context/Viewer/index.tsx b/src/components/Context/Viewer/index.tsx index b1cf1a9d80..9abab33c20 100644 --- a/src/components/Context/Viewer/index.tsx +++ b/src/components/Context/Viewer/index.tsx @@ -69,6 +69,9 @@ const ViewerFragments = { articles(input: { first: 0 }) { totalCount } + settings { + currency + } } `, }, diff --git a/src/components/CurrencyFormatter/styles.css b/src/components/CurrencyFormatter/styles.css index beeab5457b..1a8bced246 100644 --- a/src/components/CurrencyFormatter/styles.css +++ b/src/components/CurrencyFormatter/styles.css @@ -10,6 +10,7 @@ } & .subCurrency { + padding-top: var(--spacing-x-tight); font-size: var(--font-size-xs); color: var(--color-grey); } diff --git a/src/components/Dialogs/TagAdoptionDialog/index.tsx b/src/components/Dialogs/TagAdoptionDialog/index.tsx index 1c6fa4b6b6..39c0cd6309 100644 --- a/src/components/Dialogs/TagAdoptionDialog/index.tsx +++ b/src/components/Dialogs/TagAdoptionDialog/index.tsx @@ -1,10 +1,4 @@ -import { - Dialog, - Translate, - useDialogSwitch, - useMutation, - // useRoute, -} from '~/components' +import { Dialog, Translate, useDialogSwitch, useMutation } from '~/components' import UPDATE_TAG_SETTING from '~/components/GQL/mutations/updateTagSetting' import { ADD_TOAST } from '~/common/enums' @@ -34,8 +28,6 @@ const textEn = const BaseDialog = ({ id, children }: Props) => { const { show, openDialog, closeDialog } = useDialogSwitch(true) - // const { getQuery } = useRoute() - // const id = getQuery('tagId') const [update, { loading }] = useMutation(UPDATE_TAG_SETTING) diff --git a/src/components/Dialogs/TagEditorDialog/index.tsx b/src/components/Dialogs/TagEditorDialog/index.tsx index 747445e374..4d09e973b0 100644 --- a/src/components/Dialogs/TagEditorDialog/index.tsx +++ b/src/components/Dialogs/TagEditorDialog/index.tsx @@ -49,9 +49,6 @@ const BaseDialog = ({ id, children }: Props) => { baseOpenDialog() } - // const { getQuery } = useRoute() - // const id = getQuery('tagId') - const isAdd = currStep === 'add' const isList = currStep === 'list' const isRemove = currStep === 'remove' diff --git a/src/components/Dialogs/TagLeaveDialog/index.tsx b/src/components/Dialogs/TagLeaveDialog/index.tsx index 3add76c805..e8e65a10cf 100644 --- a/src/components/Dialogs/TagLeaveDialog/index.tsx +++ b/src/components/Dialogs/TagLeaveDialog/index.tsx @@ -1,10 +1,4 @@ -import { - Dialog, - Translate, - useDialogSwitch, - useMutation, - // useRoute, -} from '~/components' +import { Dialog, Translate, useDialogSwitch, useMutation } from '~/components' import UPDATE_TAG_SETTING from '~/components/GQL/mutations/updateTagSetting' import { ADD_TOAST } from '~/common/enums' @@ -20,8 +14,6 @@ interface Props { const BaseDialog = ({ id, isOwner, children }: Props) => { const { show, openDialog, closeDialog } = useDialogSwitch(true) - // const { getQuery } = useRoute() - // const id = getQuery('tagId') const [update, { loading }] = useMutation(UPDATE_TAG_SETTING) diff --git a/src/components/Forms/ChangeEmailForm/Confirm.tsx b/src/components/Forms/ChangeEmailForm/Confirm.tsx index 6a3691abbe..5488136ae6 100644 --- a/src/components/Forms/ChangeEmailForm/Confirm.tsx +++ b/src/components/Forms/ChangeEmailForm/Confirm.tsx @@ -134,6 +134,7 @@ const Confirm: React.FC = ({ name="code" required placeholder={translate({ id: 'enterVerificationCode', lang })} + hint={translate({ id: 'hintVerificationCode', lang })} value={values.code} error={touched.code && errors.code} onBlur={handleBlur} diff --git a/src/components/Forms/ChangeEmailForm/Request.tsx b/src/components/Forms/ChangeEmailForm/Request.tsx index 5e4551d10a..4e9f6253c2 100644 --- a/src/components/Forms/ChangeEmailForm/Request.tsx +++ b/src/components/Forms/ChangeEmailForm/Request.tsx @@ -111,6 +111,7 @@ const Request: React.FC = ({ name="code" required placeholder={translate({ id: 'enterVerificationCode', lang })} + hint={translate({ id: 'hintVerificationCode', lang })} value={values.code} error={touched.code && errors.code} onBlur={handleBlur} diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx index ade159c4fe..8c89f5824c 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/LikeCoinChoice.tsx @@ -14,11 +14,14 @@ import { formatAmount } from '~/common/utils' import styles from './styles.css' +import { QuoteCurrency } from '@/__generated__/globalTypes' import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' type LikeCoinChoiceProps = { balance: number recipient: UserDonationRecipient + currency: QuoteCurrency + exchangeRate: number switchToSetAmount: () => void } @@ -36,6 +39,8 @@ const IconLikeDisabled = () => ( const LikeCoinChoice: React.FC = ({ balance, recipient, + currency, + exchangeRate, switchToSetAmount, }) => { const viewer = useContext(ViewerContext) @@ -100,7 +105,12 @@ const LikeCoinChoice: React.FC = ({ LikeCoin - + diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx index 89ccc4b515..dcadd001a6 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/USDTChoice.tsx @@ -18,12 +18,15 @@ import { formatAmount } from '~/common/utils' import styles from './styles.css' +import { QuoteCurrency } from '@/__generated__/globalTypes' import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' import { ArticleDetailPublic_article } from '~/views/ArticleDetail/__generated__/ArticleDetailPublic' interface FormProps { article: ArticleDetailPublic_article recipient: UserDonationRecipient + currency: QuoteCurrency + exchangeRate: number switchToSetAmount: () => void switchToWalletSelect: () => void } @@ -31,6 +34,8 @@ interface FormProps { const USDTChoice: React.FC = ({ article, recipient, + currency, + exchangeRate, switchToSetAmount, switchToWalletSelect, }) => { @@ -111,8 +116,10 @@ const USDTChoice: React.FC = ({ ) : ( )} diff --git a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx index 0457fbaa6f..e39be71906 100644 --- a/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx +++ b/src/components/Forms/PaymentForm/PayTo/CurrencyChoice/index.tsx @@ -1,5 +1,7 @@ import { useQuery } from '@apollo/react-hooks' -import _pickBy from 'lodash/pickBy' +import _find from 'lodash/find' +import _matchesProperty from 'lodash/matchesProperty' +import { useContext } from 'react' import { CurrencyFormatter, @@ -9,7 +11,9 @@ import { TextIcon, Translate, UserDigest, + ViewerContext, } from '~/components' +import EXCHANGE_RATES from '~/components/GQL/queries/exchangeRates' import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' import { PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' @@ -21,6 +25,7 @@ import Tips from './Tips' import USDTChoice from './USDTChoice' import { UserDonationRecipient } from '~/components/Dialogs/DonationDialog/__generated__/UserDonationRecipient' +import { ExchangeRates } from '~/components/GQL/queries/__generated__/ExchangeRates' import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' import { ArticleDetailPublic_article } from '~/views/ArticleDetail/__generated__/ArticleDetailPublic' @@ -37,11 +42,36 @@ const CurrencyChoice: React.FC = ({ switchToSetAmount, switchToWalletSelect, }) => { - // HKD balance + const viewer = useContext(ViewerContext) + const currency = viewer.settings.currency + + const { data: exchangeRateDate, loading: exchangeRateLoading } = + useQuery(EXCHANGE_RATES, { + variables: { + to: currency, + }, + }) + + // HKD、Like balance const { data, loading } = useQuery(WALLET_BALANCE, { fetchPolicy: 'network-only', }) + const exchangeRateUSDT = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.USDT) + ) + + const exchangeRateHKD = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.HKD) + ) + + const exchangeRateLIKE = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.LIKE) + ) + const balanceHKD = data?.viewer?.wallet.balance.HKD || 0 const balanceLike = data?.viewer?.liker.total || 0 @@ -72,6 +102,8 @@ const CurrencyChoice: React.FC = ({ switchToSetAmount(CURRENCY.USDT)} switchToWalletSelect={switchToWalletSelect} /> @@ -91,13 +123,20 @@ const CurrencyChoice: React.FC = ({ > - + {/* LikeCoin */} switchToSetAmount(CURRENCY.LIKE)} /> @@ -107,7 +146,7 @@ const CurrencyChoice: React.FC = ({ ) - if (loading) { + if (exchangeRateLoading || loading) { return } diff --git a/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx b/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx index 46c2527c86..754cac47eb 100644 --- a/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx +++ b/src/components/Forms/PaymentForm/PayTo/SetAmount/index.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/react-hooks' import { BigNumber } from 'ethers' import { useFormik } from 'formik' +import _get from 'lodash/get' import _pickBy from 'lodash/pickBy' import { useContext, useEffect, useRef, useState } from 'react' import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi' @@ -9,6 +10,7 @@ import { Dialog, Form, LanguageContext, + Spacer, Spinner, Translate, useAllowanceUSDT, @@ -18,6 +20,7 @@ import { ViewerContext, } from '~/components' import PAY_TO from '~/components/GQL/mutations/payTo' +import EXCHANGE_RATES from '~/components/GQL/queries/exchangeRates' import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' import updateDonation from '~/components/GQL/updates/donation' @@ -26,6 +29,7 @@ import { PAYMENT_MAXIMUM_PAYTO_AMOUNT, } from '~/common/enums' import { + formatAmount, numRound, validateCurrency, validateDonationAmount, @@ -42,6 +46,7 @@ import { PayTo as PayToMutate, PayTo_payTo_transaction as PayToTx, } from '~/components/GQL/mutations/__generated__/PayTo' +import { ExchangeRates } from '~/components/GQL/queries/__generated__/ExchangeRates' import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' import { ArticleDetailPublic_article } from '~/views/ArticleDetail/__generated__/ArticleDetailPublic' @@ -98,6 +103,7 @@ const SetAmount: React.FC = ({ // contexts const viewer = useContext(ViewerContext) + const quoteCurrency = viewer.settings.currency const { lang } = useContext(LanguageContext) const { address } = useAccount() const { chain } = useNetwork() @@ -117,6 +123,14 @@ const SetAmount: React.FC = ({ // states const [payTo] = useMutation(PAY_TO) + const { data: exchangeRateDate, loading: exchangeRateLoading } = + useQuery(EXCHANGE_RATES, { + variables: { + from: currency, + to: quoteCurrency, + }, + }) + // HKD balance const { data, loading, error } = useQuery(WALLET_BALANCE, { fetchPolicy: 'network-only', @@ -207,6 +221,37 @@ const SetAmount: React.FC = ({ const isBalanceInsufficient = balance < (values.customAmount || values.amount) + const ComposedAmountInputHint = () => { + const hkdHint = isHKD ? ( +
+ + +
+ ) : null + + const value = values.customAmount || values.amount + + if (value === 0) { + return hkdHint + } + + const rate = _get(exchangeRateDate, 'exchangeRates.0.rate', 0) + const convertedTotal = formatAmount(value * rate, 2) + + return ( +
+

+ ≈ {quoteCurrency} {convertedTotal} +

+ {hkdHint} +
+ ) + } + /** * useEffect Hooks */ @@ -304,20 +349,14 @@ const SetAmount: React.FC = ({ } }, error: errors.amount, - hint: isHKD ? ( - - ) : null, + hint: , ref: customInputRef, }} /> ) - if (loading) { + if (exchangeRateLoading || loading) { return } diff --git a/src/components/Forms/PaymentForm/ResetPassword/Request.tsx b/src/components/Forms/PaymentForm/ResetPassword/Request.tsx index 17692e3a2f..cdaf723d1c 100644 --- a/src/components/Forms/PaymentForm/ResetPassword/Request.tsx +++ b/src/components/Forms/PaymentForm/ResetPassword/Request.tsx @@ -114,6 +114,7 @@ const Request: React.FC = ({ name="code" required placeholder={translate({ id: 'enterVerificationCode', lang })} + hint={translate({ id: 'hintVerificationCode', lang })} value={values.code} error={touched.code && errors.code} onBlur={handleBlur} diff --git a/src/components/Forms/Verification/LinkSent.tsx b/src/components/Forms/Verification/LinkSent.tsx index 6d3c760b97..32bd795dc4 100644 --- a/src/components/Forms/Verification/LinkSent.tsx +++ b/src/components/Forms/Verification/LinkSent.tsx @@ -41,9 +41,9 @@ export const VerificationLinkSent = ({ />

diff --git a/src/components/Forms/WalletAuthForm/Connect.tsx b/src/components/Forms/WalletAuthForm/Connect.tsx index e3105647bf..75a2148c7c 100644 --- a/src/components/Forms/WalletAuthForm/Connect.tsx +++ b/src/components/Forms/WalletAuthForm/Connect.tsx @@ -347,6 +347,7 @@ const Connect: React.FC = ({ name="code" required placeholder={translate({ id: 'enterVerificationCode', lang })} + hint={translate({ id: 'hintVerificationCode', lang })} value={values.code} error={touched.code && errors.code} onBlur={handleBlur} diff --git a/src/components/GQL/queries/exchangeRates.ts b/src/components/GQL/queries/exchangeRates.ts new file mode 100644 index 0000000000..c3d8abdbc1 --- /dev/null +++ b/src/components/GQL/queries/exchangeRates.ts @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export default gql` + query ExchangeRates($from: TransactionCurrency, $to: QuoteCurrency) { + exchangeRates(input: { from: $from, to: $to }) { + from + to + rate + updatedAt + } + } +` diff --git a/src/components/Interaction/Card/index.tsx b/src/components/Interaction/Card/index.tsx index 5950f83bc7..73261f6585 100644 --- a/src/components/Interaction/Card/index.tsx +++ b/src/components/Interaction/Card/index.tsx @@ -92,10 +92,6 @@ export const Card: React.FC> = forwardRef( newTab: boolean event: React.MouseEvent | React.KeyboardEvent }) => { - if (window.getSelection()?.toString().length) { - return - } - const target = event.target as HTMLElement if (disabled) { diff --git a/src/components/Layout/NavBar/index.tsx b/src/components/Layout/NavBar/index.tsx index c6276400ea..1262797bac 100644 --- a/src/components/Layout/NavBar/index.tsx +++ b/src/components/Layout/NavBar/index.tsx @@ -79,7 +79,7 @@ const NavBar = () => { icon={} activeIcon={} active={isInSettings} - href={PATHS.SETTINGS} + href={PATHS.ME_SETTINGS} /> )} diff --git a/src/components/Layout/SideNav/index.tsx b/src/components/Layout/SideNav/index.tsx index 0ce5836574..5a3dff22a3 100644 --- a/src/components/Layout/SideNav/index.tsx +++ b/src/components/Layout/SideNav/index.tsx @@ -110,7 +110,7 @@ const SideNav = () => { activeIcon={} active={isInSettings} isMediumUp={isMediumUp} - href={PATHS.SETTINGS} + href={PATHS.ME_SETTINGS} /> )} diff --git a/src/pages/me/settings/index.tsx b/src/pages/me/settings/index.tsx index 4c6e3d7ac5..b3e0d9e168 100644 --- a/src/pages/me/settings/index.tsx +++ b/src/pages/me/settings/index.tsx @@ -1,11 +1,4 @@ import MeSettings from '~/views/Me/Settings/Settings' - -import { Protected } from '~/components' - -const ProtectedMeSettings = () => ( - - - -) +const ProtectedMeSettings = () => export default ProtectedMeSettings diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx deleted file mode 100644 index ca991702c5..0000000000 --- a/src/pages/settings.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import Settings from '~/views/Settings' - -export default Settings diff --git a/src/views/Settings/DisplayPreferences/SwitchLanguage/index.tsx b/src/views/Me/Settings/AnonymousSettings/DisplayPreferences/SwitchLanguage/index.tsx similarity index 100% rename from src/views/Settings/DisplayPreferences/SwitchLanguage/index.tsx rename to src/views/Me/Settings/AnonymousSettings/DisplayPreferences/SwitchLanguage/index.tsx diff --git a/src/views/Settings/DisplayPreferences/index.tsx b/src/views/Me/Settings/AnonymousSettings/DisplayPreferences/index.tsx similarity index 100% rename from src/views/Settings/DisplayPreferences/index.tsx rename to src/views/Me/Settings/AnonymousSettings/DisplayPreferences/index.tsx diff --git a/src/views/Settings/Enhance/index.tsx b/src/views/Me/Settings/AnonymousSettings/Enhance/index.tsx similarity index 100% rename from src/views/Settings/Enhance/index.tsx rename to src/views/Me/Settings/AnonymousSettings/Enhance/index.tsx diff --git a/src/views/Settings/Learn/index.tsx b/src/views/Me/Settings/AnonymousSettings/Learn/index.tsx similarity index 100% rename from src/views/Settings/Learn/index.tsx rename to src/views/Me/Settings/AnonymousSettings/Learn/index.tsx diff --git a/src/views/Settings/index.tsx b/src/views/Me/Settings/AnonymousSettings/index.tsx similarity index 100% rename from src/views/Settings/index.tsx rename to src/views/Me/Settings/AnonymousSettings/index.tsx diff --git a/src/views/Settings/styles.css b/src/views/Me/Settings/AnonymousSettings/styles.css similarity index 100% rename from src/views/Settings/styles.css rename to src/views/Me/Settings/AnonymousSettings/styles.css diff --git a/src/views/Me/Settings/Settings/UI/CurrencyConvertor/index.tsx b/src/views/Me/Settings/Settings/UI/CurrencyConvertor/index.tsx new file mode 100644 index 0000000000..1055407fef --- /dev/null +++ b/src/views/Me/Settings/Settings/UI/CurrencyConvertor/index.tsx @@ -0,0 +1,129 @@ +import gql from 'graphql-tag' +import { useContext } from 'react' + +import { + DropdownDialog, + Form, + Menu, + TextIcon, + Translate, + useMutation, + ViewerContext, +} from '~/components' + +import { ADD_TOAST } from '~/common/enums' + +import { QuoteCurrency } from '@/__generated__/globalTypes' +import { SetCurrency } from './__generated__/SetCurrency' + +const SET_CURRENCY = gql` + mutation SetCurrency($input: SetCurrencyInput!) { + setCurrency(input: $input) { + id + settings { + currency + } + } + } +` + +const CurrencyConvertor = () => { + const viewer = useContext(ViewerContext) + const currency = viewer.settings.currency + const [setCurrency] = useMutation(SET_CURRENCY) + + const updateCurrency = async (c: QuoteCurrency) => { + if (!viewer.isAuthed) { + return + } + + try { + await setCurrency({ + variables: { input: { currency: c } }, + optimisticResponse: { + setCurrency: { + id: viewer.id, + settings: { + currency, + __typename: 'UserSettings', + }, + __typename: 'User', + }, + }, + }) + } catch (e) { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: , + }, + }) + ) + } + } + + const isUSDActive = currency === QuoteCurrency.USD + const isHKDActive = currency === QuoteCurrency.HKD + const isTWDActive = currency === QuoteCurrency.TWD + + const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( + + updateCurrency(QuoteCurrency.USD)}> + + {QuoteCurrency.USD} + + + updateCurrency(QuoteCurrency.HKD)}> + + {QuoteCurrency.HKD} + + + updateCurrency(QuoteCurrency.TWD)}> + + {QuoteCurrency.TWD} + + + + ) + + const Title = () => { + return + } + + return ( + , + placement: 'bottom-end', + }} + dialog={{ + content: , + title: , + }} + > + {({ openDialog, ref }) => ( + <Form.List.Item + title={<Title />} + onClick={openDialog} + rightText={currency} + ref={ref} + /> + )} + </DropdownDialog> + ) +} + +export default CurrencyConvertor diff --git a/src/views/Me/Settings/Settings/UI/index.tsx b/src/views/Me/Settings/Settings/UI/index.tsx index 2ef4bf9bcc..12aaef0992 100644 --- a/src/views/Me/Settings/Settings/UI/index.tsx +++ b/src/views/Me/Settings/Settings/UI/index.tsx @@ -1,11 +1,24 @@ -import { Form, Translate } from '~/components' +import { Form, IconInfo16, TextIcon, Translate } from '~/components' +import CurrencyConvertor from './CurrencyConvertor' +import styles from './styles.css' import SwitchLanguage from './SwitchLanguage' const UISettings = () => { return ( <Form.List groupName={<Translate id="settingsUI" />}> <SwitchLanguage /> + <CurrencyConvertor /> + <section className="rate-hint"> + <TextIcon icon={<IconInfo16 size="xs" />} color="grey" size="xs"> + <Translate + zh_hans="加密货币汇率由 CoinGecko 提供" + zh_hant="加密貨幣匯率由 CoinGecko 提供" + en="Crypto rates provided by CoinGecko" + /> + </TextIcon> + </section> + <style jsx>{styles}</style> </Form.List> ) } diff --git a/src/views/Me/Settings/Settings/UI/styles.css b/src/views/Me/Settings/Settings/UI/styles.css new file mode 100644 index 0000000000..a11011e28c --- /dev/null +++ b/src/views/Me/Settings/Settings/UI/styles.css @@ -0,0 +1,5 @@ +.rate-hint { + @mixin flex-center-end; + + padding: var(--spacing-x-tight) var(--spacing-x-tight) 0; +} diff --git a/src/views/Me/Settings/Settings/index.tsx b/src/views/Me/Settings/Settings/index.tsx index 7e9d8b1e8f..b773e97831 100644 --- a/src/views/Me/Settings/Settings/index.tsx +++ b/src/views/Me/Settings/Settings/index.tsx @@ -1,10 +1,27 @@ -import { Head, Layout, PullToRefresh, Spacer } from '~/components' +import React, { useContext } from 'react' +import { + Head, + Layout, + PullToRefresh, + Spacer, + ViewerContext, +} from '~/components' + +// import { redirectToLogin } from '~/common/utils' + +import AnonymousSettings from '../AnonymousSettings' import AccountSettings from './Account' import UISettings from './UI' import WalletSettings from './Wallet' const Settings = () => { + const viewer = useContext(ViewerContext) + + if (viewer.privateFetched && !viewer.isAuthed) { + return <AnonymousSettings /> + } + return ( <Layout.Main bgColor="grey-lighter"> <Layout.Header diff --git a/src/views/Me/Wallet/Balance/FiatCurrency.tsx b/src/views/Me/Wallet/Balance/FiatCurrency.tsx index 7716478228..d075a4b0eb 100644 --- a/src/views/Me/Wallet/Balance/FiatCurrency.tsx +++ b/src/views/Me/Wallet/Balance/FiatCurrency.tsx @@ -16,10 +16,14 @@ import { analytics, formatAmount } from '~/common/utils' import styles from './styles.css' +import { QuoteCurrency } from '@/__generated__/globalTypes' + interface FiatCurrencyProps { balanceHKD: number canPayout: boolean hasStripeAccount: boolean + currency: QuoteCurrency + exchangeRate: number } interface ItemProps { @@ -85,6 +89,8 @@ export const FiatCurrencyBalance: React.FC<FiatCurrencyProps> = ({ balanceHKD, canPayout, hasStripeAccount, + currency, + exchangeRate, }) => { const Content = ({ isInDropdown, @@ -152,6 +158,8 @@ export const FiatCurrencyBalance: React.FC<FiatCurrencyProps> = ({ <CurrencyFormatter value={formatAmount(balanceHKD)} currency="HKD" + subCurrency={currency} + subValue={formatAmount(balanceHKD * exchangeRate, 2)} /> </TextIcon> <style jsx>{styles}</style> diff --git a/src/views/Me/Wallet/Balance/LikeCoin.tsx b/src/views/Me/Wallet/Balance/LikeCoin.tsx index 21e2ad7239..3fe5e24659 100644 --- a/src/views/Me/Wallet/Balance/LikeCoin.tsx +++ b/src/views/Me/Wallet/Balance/LikeCoin.tsx @@ -18,8 +18,14 @@ import { formatAmount } from '~/common/utils' import styles from './styles.css' +import { QuoteCurrency } from '@/__generated__/globalTypes' import { ViewerLikeBalance } from './__generated__/ViewerLikeBalance' +interface LikeCoinBalanceProps { + currency: QuoteCurrency + exchangeRate: number +} + const VIEWER_LIKE_BALANCE = gql` query ViewerLikeBalance { viewer { @@ -44,7 +50,10 @@ const Wrapper: React.FC = ({ children }) => ( </section> ) -export const LikeCoinBalance = () => { +export const LikeCoinBalance = ({ + currency, + exchangeRate, +}: LikeCoinBalanceProps) => { const viewer = useContext(ViewerContext) const likerId = viewer.liker.likerId @@ -94,7 +103,12 @@ export const LikeCoinBalance = () => { if (likerId) { return ( <Wrapper> - <CurrencyFormatter value={formatAmount(total, 0)} currency="LIKE" /> + <CurrencyFormatter + value={formatAmount(total, 0)} + currency="LIKE" + subValue={formatAmount(total * exchangeRate, 2)} + subCurrency={currency} + /> </Wrapper> ) } diff --git a/src/views/Me/Wallet/Balance/USDT.tsx b/src/views/Me/Wallet/Balance/USDT.tsx index ea97282dbd..69e0ad791c 100644 --- a/src/views/Me/Wallet/Balance/USDT.tsx +++ b/src/views/Me/Wallet/Balance/USDT.tsx @@ -15,7 +15,14 @@ import { formatAmount } from '~/common/utils' import styles from './styles.css' -export const USDTBalance = () => { +import { QuoteCurrency } from '@/__generated__/globalTypes' + +interface USDTBalanceProps { + currency: QuoteCurrency + exchangeRate: number +} + +export const USDTBalance = ({ currency, exchangeRate }: USDTBalanceProps) => { const viewer = useContext(ViewerContext) const address = viewer.info.ethAddress const { data: balanceUSDTData } = useBalanceUSDT({}) @@ -58,7 +65,12 @@ export const USDTBalance = () => { <Translate zh_hant="USDT" zh_hans="USDT" en="USDT" /> </TextIcon> - <CurrencyFormatter value={formatAmount(balanceUSDT)} currency="USDT" /> + <CurrencyFormatter + value={formatAmount(balanceUSDT)} + currency="USDT" + subCurrency={currency} + subValue={formatAmount(balanceUSDT * exchangeRate, 2)} + /> <style jsx>{styles}</style> </section> diff --git a/src/views/Me/Wallet/index.tsx b/src/views/Me/Wallet/index.tsx index 9efd597c54..14d7fe3703 100644 --- a/src/views/Me/Wallet/index.tsx +++ b/src/views/Me/Wallet/index.tsx @@ -1,4 +1,6 @@ import { useQuery } from '@apollo/react-hooks' +import _find from 'lodash/find' +import _matchesProperty from 'lodash/matchesProperty' import { useContext } from 'react' import { @@ -9,9 +11,13 @@ import { Spinner, ViewerContext, } from '~/components' +import EXCHANGE_RATES from '~/components/GQL/queries/exchangeRates' import WALLET_BALANCE from '~/components/GQL/queries/walletBalance' -import { PAYMENT_MINIMAL_PAYOUT_AMOUNT } from '~/common/enums' +import { + PAYMENT_CURRENCY as CURRENCY, + PAYMENT_MINIMAL_PAYOUT_AMOUNT, +} from '~/common/enums' import { FiatCurrencyBalance, LikeCoinBalance, USDTBalance } from './Balance' import PaymentPassword from './PaymentPassword' @@ -21,11 +27,36 @@ import TotalAssets from './TotalAssets' import ViewStripeAccount from './ViewStripeAccount' import ViewStripeCustomerPortal from './ViewStripeCustomerPortal' +import { ExchangeRates } from '~/components/GQL/queries/__generated__/ExchangeRates' import { WalletBalance } from '~/components/GQL/queries/__generated__/WalletBalance' const Wallet = () => { const viewer = useContext(ViewerContext) + const currency = viewer.settings.currency + + const { data: exchangeRateDate, loading: exchangeRateLoading } = + useQuery<ExchangeRates>(EXCHANGE_RATES, { + variables: { + to: currency, + }, + }) + + const exchangeRateUSDT = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.USDT) + ) + + const exchangeRateHKD = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.HKD) + ) + + const exchangeRateLIKE = _find( + exchangeRateDate?.exchangeRates, + _matchesProperty('from', CURRENCY.LIKE) + ) + const { data, loading, refetch } = useQuery<WalletBalance>(WALLET_BALANCE, { fetchPolicy: 'network-only', errorPolicy: 'none', @@ -36,7 +67,7 @@ const Wallet = () => { const hasStripeAccount = !!data?.viewer?.wallet.stripeAccount?.id const hasPaymentPassword = viewer.status?.hasPaymentPassword - if (loading) { + if (exchangeRateLoading || loading) { return ( <Layout.Main> <Layout.Header @@ -65,9 +96,17 @@ const Wallet = () => { balanceHKD={balanceHKD} canPayout={canPayout} hasStripeAccount={hasStripeAccount} + currency={currency} + exchangeRate={exchangeRateHKD?.rate || 0} + /> + <LikeCoinBalance + currency={currency} + exchangeRate={exchangeRateLIKE?.rate || 0} + /> + <USDTBalance + currency={currency} + exchangeRate={exchangeRateUSDT?.rate || 0} /> - <LikeCoinBalance /> - <USDTBalance /> </section> <Form.List> diff --git a/src/views/TagDetail/Articles/index.tsx b/src/views/TagDetail/Articles/index.tsx index 928c18f3d2..c45178efb0 100644 --- a/src/views/TagDetail/Articles/index.tsx +++ b/src/views/TagDetail/Articles/index.tsx @@ -1,4 +1,6 @@ import { NetworkStatus } from 'apollo-client' +import _find from 'lodash/find' +import _some from 'lodash/some' import React, { useContext, useEffect, useRef } from 'react' import { @@ -25,20 +27,20 @@ import { analytics, mergeConnections } from '~/common/utils' import RelatedTags from '../RelatedTags' import { TagArticlesPublic } from '~/components/GQL/queries/__generated__/TagArticlesPublic' +import { TagFragment } from '../__generated__/TagFragment' interface TagArticlesProps { - tagId: string + tag: TagFragment feedType: string } -const TagDetailArticles = ({ tagId, feedType }: TagArticlesProps) => { +const TagDetailArticles = ({ tag, feedType }: TagArticlesProps) => { const viewer = useContext(ViewerContext) const feed = useRef(feedType) const isLargeUp = useResponsive('lg-up') const isSelected = feedType === 'selected' const isHottest = feedType === 'hottest' - const isLatest = feedType === 'latest' /** * Data Fetching @@ -54,7 +56,7 @@ const TagDetailArticles = ({ tagId, feedType }: TagArticlesProps) => { client, } = usePublicQuery<TagArticlesPublic>(TAG_ARTICLES_PUBLIC, { variables: { - id: tagId, + id: tag.id, selected: feedType === 'selected', sortBy: feedType === 'hottest' ? 'byHottestDesc' : 'byCreatedAtDesc', }, @@ -145,13 +147,13 @@ const TagDetailArticles = ({ tagId, feedType }: TagArticlesProps) => { switch (event) { case 'add': const { data: addData } = await refetchPublic({ - variables: { id: tagId, first: count + differences }, + variables: { id: tag.id, first: count + differences }, }) loadPrivate(addData) break case 'delete': const { data: deleteData } = await refetchPublic({ - variables: { id: tagId, first: Math.max(count - 1, 0) }, + variables: { id: tag.id, first: Math.max(count - 1, 0) }, }) loadPrivate(deleteData) break @@ -176,6 +178,14 @@ const TagDetailArticles = ({ tagId, feedType }: TagArticlesProps) => { return <EmptyTagArticles /> } + const isEditor = _some( + tag?.editors || [], + (editor) => editor.id === viewer.id + ) + const isCreator = tag?.creator?.id === viewer.id + const canEditTag = + isEditor || isCreator || viewer.info.email === 'hi@matters.news' + return ( <InfiniteScroll hasNextPage={pageInfo.hasNextPage} loadMore={loadMore}> <List> @@ -200,19 +210,21 @@ const TagDetailArticles = ({ tagId, feedType }: TagArticlesProps) => { id: node.author.id, }) }} - inTagDetailSelected={isSelected} - inTagDetailLatest={isLatest} + tagDetailId={tag.id} + hasSetTagSelected={canEditTag && !isSelected} + hasSetTagUnselected={canEditTag && isSelected} + hasRemoveTag={canEditTag && !isHottest} /> </List.Item> {!isLargeUp && edges.length >= 4 && i === 3 && ( - <RelatedTags tagId={tagId} /> + <RelatedTags tagId={tag.id} /> )} </React.Fragment> ))} </List> - {!isLargeUp && edges.length < 4 && <RelatedTags tagId={tagId} />} + {!isLargeUp && edges.length < 4 && <RelatedTags tagId={tag.id} />} </InfiniteScroll> ) } diff --git a/src/views/TagDetail/index.tsx b/src/views/TagDetail/index.tsx index 066d48e997..6160027423 100644 --- a/src/views/TagDetail/index.tsx +++ b/src/views/TagDetail/index.tsx @@ -80,24 +80,31 @@ const TagDetail = ({ tag }: { tag: TagFragment }) => { setFeedType(newType) } + useEffect(() => { + setFeedType( + hasSelectedFeed && qsType === 'selected' + ? 'selected' + : qsType || 'hottest' + ) + }, [qsType]) + const isSelected = feedType === 'selected' const isHottest = feedType === 'hottest' const isLatest = feedType === 'latest' const isCreators = feedType === 'creators' useEffect(() => { - // if selected feed is empty, switch to latest feed + // if selected feed is empty, switch to hottest feed if (!hasSelectedFeed && isSelected) { - changeFeed('latest') + changeFeed('hottest') } // backward compatible with `/tags/:globalId:` const newPath = toPath({ page: 'tagDetail', tag, - feedType: isLatest ? '' : feedType, + feedType: isHottest ? '' : feedType, }) - if (newPath.href !== window.decodeURI(router.asPath)) { router.replace(newPath.href, undefined, { shallow: true }) } @@ -225,7 +232,7 @@ const TagDetail = ({ tag }: { tag: TagFragment }) => { </Tabs> {(isHottest || isLatest || isSelected) && ( - <TagDetailArticles tagId={tag.id} feedType={feedType} /> + <TagDetailArticles tag={tag} feedType={feedType} /> )} {isCreators && <DynamicCommunity id={tag.id} isOwner={isOwner} />}