diff --git a/lang/default.json b/lang/default.json index 59fc95394a..63708b08b1 100644 --- a/lang/default.json +++ b/lang/default.json @@ -40,6 +40,10 @@ "0Azlrb": { "defaultMessage": "Manage" }, + "0CyECR": { + "defaultMessage": "Matters ID has been set up. More account info can be found in Settings", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "0JLcHr": { "defaultMessage": "Caution: The following content may include age-restricted or explicit content, violence, gore, etc. Some may experience discomfort and psychological distress.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -59,6 +63,10 @@ "defaultMessage": "Failed", "description": "src/components/Transaction/State/index.tsx" }, + "1QrwIl": { + "defaultMessage": "Take a look", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "1Z1M77": { "defaultMessage": "No data yet", "description": "src/views/Me/History/index.tsx" @@ -71,6 +79,10 @@ "defaultMessage": "mentioned you in a comment at {commentArticle}", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "202PEj": { + "defaultMessage": "Confirm Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "20bImY": { "defaultMessage": "Discussion" }, @@ -397,6 +409,10 @@ "defaultMessage": "Collection is deleted", "description": "src/components/CollectionDigest/DropdownActions/DeleteCollection/Dialog.tsx" }, + "FxrSCh": { + "defaultMessage": "This ID cannot be modified. Are you sure you want to use {id} as your Matters ID?", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "GHxtae": { "defaultMessage": "Wallet", "description": "src/components/Forms/SelectAuthMethodForm/AuthTabs.tsx" @@ -459,6 +475,10 @@ "defaultMessage": "View", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" }, + "IPqNCS": { + "defaultMessage": "Confirm use", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "Ihwz5K": { "defaultMessage": "Unpinned from profile" }, @@ -540,6 +560,10 @@ "defaultMessage": "Wallet address will be part of your digital identity and shown in your profile page.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" }, + "LwFJTy": { + "defaultMessage": "Matters ID is your unique identifier, and cannot be modified once set.", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "MDNaxs": { "defaultMessage": "followers", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -987,10 +1011,18 @@ "kc79d3": { "defaultMessage": "Topics" }, + "kf5NAv": { + "defaultMessage": "English letters, numbers, and underscores", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "kkZioy": { "defaultMessage": "Remove", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "l0/EvT": { + "defaultMessage": "Last step: Set Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "lIir/P": { "defaultMessage": "I see" }, @@ -1223,6 +1255,10 @@ "wbcwKd": { "defaultMessage": "View All" }, + "x7O1/5": { + "defaultMessage": "This ID has been taken, please try another one", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "xMaFCO": { "defaultMessage": "Articles", "description": "src/views/Me/History/HistoryTabs.tsx" diff --git a/lang/en.json b/lang/en.json index 772018892f..df52d8fe0c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -41,6 +41,10 @@ "0Azlrb": { "defaultMessage": "Manage" }, + "0CyECR": { + "defaultMessage": "Matters ID has been set up. More account info can be found in Settings", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "0JLcHr": { "defaultMessage": "Caution: The following content may include age-restricted or explicit content, violence, gore, etc. Some may experience discomfort and psychological distress.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -60,6 +64,10 @@ "defaultMessage": "Failed", "description": "src/components/Transaction/State/index.tsx" }, + "1QrwIl": { + "defaultMessage": "Take a look", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "1Z1M77": { "defaultMessage": "No data yet", "description": "src/views/Me/History/index.tsx" @@ -72,6 +80,10 @@ "defaultMessage": "mentioned you in a comment at {commentArticle}", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "202PEj": { + "defaultMessage": "Confirm Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "20bImY": { "defaultMessage": "Discussion" }, @@ -400,6 +412,10 @@ "defaultMessage": "Collection is deleted", "description": "src/components/CollectionDigest/DropdownActions/DeleteCollection/Dialog.tsx" }, + "FxrSCh": { + "defaultMessage": "This ID cannot be modified. Are you sure you want to use {id} as your Matters ID?", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "GHxtae": { "defaultMessage": "Wallet", "description": "src/components/Forms/SelectAuthMethodForm/AuthTabs.tsx" @@ -462,6 +478,10 @@ "defaultMessage": "View", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" }, + "IPqNCS": { + "defaultMessage": "Confirm use", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "Ihwz5K": { "defaultMessage": "Unpinned from profile" }, @@ -543,6 +563,10 @@ "defaultMessage": "Wallet address will be part of your digital identity and shown in your profile page.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" }, + "LwFJTy": { + "defaultMessage": "Matters ID is your unique identifier, and cannot be modified once set.", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "MDNaxs": { "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx", "defaultMessage": "followers" @@ -993,10 +1017,18 @@ "kc79d3": { "defaultMessage": "Topics" }, + "kf5NAv": { + "defaultMessage": "English letters, numbers, and underscores", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "kkZioy": { "defaultMessage": "Remove", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "l0/EvT": { + "defaultMessage": "Last step: Set Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "lIir/P": { "description": "", "defaultMessage": "I see" @@ -1233,6 +1265,10 @@ "wbcwKd": { "defaultMessage": "View All" }, + "x7O1/5": { + "defaultMessage": "This ID has been taken, please try another one", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "xMaFCO": { "defaultMessage": "Articles", "description": "src/views/Me/History/HistoryTabs.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 5c2e580422..9e99c14979 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -40,6 +40,10 @@ "0Azlrb": { "defaultMessage": "管理" }, + "0CyECR": { + "defaultMessage": "Matters ID 已设置,更多帐号相关设置可前往设置页修改", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "0JLcHr": { "defaultMessage": "内容可能包含色情、暴力、血腥等限制级内容,部分用户可能不适合观看,或引起不适、心理负担,请谨慎判断是否阅读。", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -59,6 +63,10 @@ "defaultMessage": "失败", "description": "src/components/Transaction/State/index.tsx" }, + "1QrwIl": { + "defaultMessage": "去看看", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "1Z1M77": { "defaultMessage": "尚无阅读记录", "description": "src/views/Me/History/index.tsx" @@ -71,6 +79,10 @@ "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx", "defaultMessage": "在 {commentArticle} 评论提到了你" }, + "202PEj": { + "defaultMessage": "确认 Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "20bImY": { "defaultMessage": "众聊" }, @@ -398,6 +410,10 @@ "defaultMessage": "选集已刪除", "description": "src/components/CollectionDigest/DropdownActions/DeleteCollection/Dialog.tsx" }, + "FxrSCh": { + "defaultMessage": "ID 设置后无法修改,确认使用 {id} 作为 Matters ID 吗?", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "GHxtae": { "defaultMessage": "数字钱包", "description": "src/components/Forms/SelectAuthMethodForm/AuthTabs.tsx" @@ -460,6 +476,10 @@ "defaultMessage": "查看", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" }, + "IPqNCS": { + "defaultMessage": "确认使用", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "Ihwz5K": { "defaultMessage": "代表作已取消" }, @@ -541,6 +561,10 @@ "defaultMessage": "钱包地址将作为身份识别之一在个人页公开显示。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" }, + "LwFJTy": { + "defaultMessage": "Matters ID 为用户唯一标识,设置后无法修改。", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "MDNaxs": { "defaultMessage": "人", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -988,10 +1012,18 @@ "kc79d3": { "defaultMessage": "找你想看的" }, + "kf5NAv": { + "defaultMessage": "可使用英文、数字及下划线", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "kkZioy": { "defaultMessage": "确认移出", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "l0/EvT": { + "defaultMessage": "最后一步:设置 Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "lIir/P": { "defaultMessage": "我知道了" }, @@ -1224,6 +1256,10 @@ "wbcwKd": { "defaultMessage": "查看全部" }, + "x7O1/5": { + "defaultMessage": "ID 已被使用,请修改后再试", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "xMaFCO": { "defaultMessage": "阅读", "description": "src/views/Me/History/HistoryTabs.tsx" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index d5a7d0a74c..c0e3f3ebd0 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -40,6 +40,10 @@ "0Azlrb": { "defaultMessage": "管理" }, + "0CyECR": { + "defaultMessage": "Matters ID 已設置,更多帳號相關設置可前往設定頁修改", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "0JLcHr": { "defaultMessage": "內容可能包含色情、暴力、血腥等限制級內容,部分用戶可能不適合觀看,或引起不適、心理負擔,請謹慎判斷是否閱讀。", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -59,6 +63,10 @@ "defaultMessage": "失敗", "description": "src/components/Transaction/State/index.tsx" }, + "1QrwIl": { + "defaultMessage": "去看看", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "1Z1M77": { "defaultMessage": "尚無閱讀紀錄", "description": "src/views/Me/History/index.tsx" @@ -71,6 +79,10 @@ "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx", "defaultMessage": "在 {commentArticle} 評論提到了你" }, + "202PEj": { + "defaultMessage": "確認 Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "20bImY": { "defaultMessage": "眾聊" }, @@ -398,6 +410,10 @@ "defaultMessage": "選集已刪除", "description": "src/components/CollectionDigest/DropdownActions/DeleteCollection/Dialog.tsx" }, + "FxrSCh": { + "defaultMessage": "ID 設置後無法修改,確認使用 {id} 作為 Matters ID 嗎?", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "GHxtae": { "defaultMessage": "數字錢包", "description": "src/components/Forms/SelectAuthMethodForm/AuthTabs.tsx" @@ -460,6 +476,10 @@ "defaultMessage": "查看", "description": "src/components/Dialogs/CollectionSelectDialog/index.tsx" }, + "IPqNCS": { + "defaultMessage": "確認使用", + "description": "src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx" + }, "Ihwz5K": { "defaultMessage": "代表作已取消" }, @@ -541,6 +561,10 @@ "defaultMessage": "錢包地址將作為身份識別之一在個人頁公開顯示。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" }, + "LwFJTy": { + "defaultMessage": "Matters ID 為用戶唯一標識,設置後無法修改。", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "MDNaxs": { "defaultMessage": "人", "description": "src/views/Circle/Analytics/FollowerAnalytics/index.tsx" @@ -988,10 +1012,18 @@ "kc79d3": { "defaultMessage": "找你想看的" }, + "kf5NAv": { + "defaultMessage": "可使用英文、數字及下劃線", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "kkZioy": { "defaultMessage": "確認移出", "description": "src/components/Dialogs/RemoveArticleCollectionDialog/index.tsx" }, + "l0/EvT": { + "defaultMessage": "最後一步:设置 Matters ID", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "lIir/P": { "defaultMessage": "我知道了" }, @@ -1224,6 +1256,10 @@ "wbcwKd": { "defaultMessage": "查看全部" }, + "x7O1/5": { + "defaultMessage": "ID 已被使用,請修改後再試", + "description": "src/components/Dialogs/SetUserNameDialog/Content.tsx" + }, "xMaFCO": { "defaultMessage": "閱讀", "description": "src/views/Me/History/HistoryTabs.tsx" diff --git a/src/common/enums/events.ts b/src/common/enums/events.ts index ef73725c15..e5b2a08491 100644 --- a/src/common/enums/events.ts +++ b/src/common/enums/events.ts @@ -20,6 +20,7 @@ export const OPEN_UNIVERSAL_AUTH_DIALOG = 'openUniversalAuthDialog' export const CLOSE_ACTIVE_DIALOG = 'closeActiveDialog' export const OPEN_LIKE_COIN_DIALOG = 'openLikeCoinDialog' export const OPEN_SUBSCRIBE_CIRCLE_DIALOG = 'openSubscribeCircleDialog' +export const OPEN_SET_USER_NAME_DIALOG = 'openSetUserNameDialog' export enum UNIVERSAL_AUTH_SOURCE { enter = 'enter', diff --git a/src/common/enums/index.ts b/src/common/enums/index.ts index c37a349486..794e7cd00e 100644 --- a/src/common/enums/index.ts +++ b/src/common/enums/index.ts @@ -77,3 +77,5 @@ export const MAX_DESCRIPTION_LENGTH = 200 export const MIN_USER_DISPLAY_NAME_LENGTH = 2 export const MAX_USER_DISPLAY_NAME_LENGTH = 20 export const MAX_USER_DESCRIPTION_LENGTH = 140 +export const MIN_USER_NAME_LENGTH = 4 +export const MAX_USER_NAME_LENGTH = 15 diff --git a/src/common/utils/text/index.ts b/src/common/utils/text/index.ts index e6be55bddb..7b20240ddc 100644 --- a/src/common/utils/text/index.ts +++ b/src/common/utils/text/index.ts @@ -1,5 +1,6 @@ export * from './article' export * from './tag' +export * from './user' // for Twitter and others which do not support non-English in URL export const stripNonEnglishUrl = (url: string) => { diff --git a/src/common/utils/text/user.ts b/src/common/utils/text/user.ts new file mode 100644 index 0000000000..da2889e19e --- /dev/null +++ b/src/common/utils/text/user.ts @@ -0,0 +1,6 @@ +export const normalizeUserName = (userName: string) => { + return userName + .split('') + .filter((c) => /^[a-zA-Z0-9_]*$/.test(c)) + .join('') +} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 16fbef6632..976d2966f8 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -25,6 +25,7 @@ export interface DialogOverlayProps { onDismiss: () => void onRest?: () => void dismissOnClickOutside?: boolean + dismissOnHandle?: boolean } export type DialogProps = { @@ -50,6 +51,7 @@ const Container: React.FC< testId, onDismiss, dismissOnClickOutside = false, + dismissOnHandle = true, children, style, setDragGoal, @@ -109,6 +111,9 @@ const Container: React.FC< if (event.code.toLowerCase() !== KEYVALUE.escape) { return } + if (!dismissOnHandle) { + return + } closeTopDialog() }} > @@ -119,7 +124,7 @@ const Container: React.FC< {children} - + {dismissOnHandle && } ) diff --git a/src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx b/src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx new file mode 100644 index 0000000000..a61265987f --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/ConfirmStep.tsx @@ -0,0 +1,143 @@ +import _pickBy from 'lodash/pickBy' +import React from 'react' +import { FormattedMessage } from 'react-intl' + +import { PATHS } from '~/common/enums' +import { Dialog, toast, useMutation, useRoute } from '~/components' +import { ROOT_QUERY_PRIVATE } from '~/components/Root/gql' +import { SetUserNameMutation } from '~/gql/graphql' +import { USER_PROFILE_PUBLIC } from '~/views/User/UserProfile/gql' + +import { SET_USER_NAME } from './gql' + +interface Props { + userName: string + back: () => void + closeDialog: () => void +} + +const ConfirmStep: React.FC = ({ userName, back, closeDialog }) => { + const { router } = useRoute() + + const [update, { loading }] = useMutation( + SET_USER_NAME, + undefined, + { + showToast: true, + } + ) + + const confirmUse = async () => { + try { + await update({ + variables: { + userName, + }, + refetchQueries: [ + { + query: ROOT_QUERY_PRIVATE, + }, + { + query: USER_PROFILE_PUBLIC, + variables: { userName }, + }, + ], + }) + toast.success({ + duration: Infinity, + message: ( + + ), + actions: [ + { + content: ( + + ), + onClick: () => { + router.push(PATHS.ME_SETTINGS) + }, + }, + ], + }) + closeDialog() + } catch (error) { + console.error(error) + } + } + + return ( + <> + + } + /> + + +

+ {userName}, + }} + /> +

+
+ + + + } + loading={loading} + onClick={confirmUse} + /> + } + color="greyDarker" + onClick={back} + /> + + } + smUpBtns={ + <> + } + color="greyDarker" + onClick={back} + /> + + } + loading={loading} + onClick={confirmUse} + /> + + } + /> + + ) +} + +export default ConfirmStep diff --git a/src/components/Dialogs/SetUserNameDialog/Content.tsx b/src/components/Dialogs/SetUserNameDialog/Content.tsx new file mode 100644 index 0000000000..e605a4c3a1 --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/Content.tsx @@ -0,0 +1,114 @@ +import { useApolloClient } from '@apollo/react-hooks' +import _pickBy from 'lodash/pickBy' +import React, { useContext, useEffect, useState } from 'react' + +import { MAX_USER_NAME_LENGTH, MIN_USER_NAME_LENGTH } from '~/common/enums' +import { normalizeUserName } from '~/common/utils' +import { Spinner, ViewerContext } from '~/components' +import { SocialAccountType } from '~/gql/graphql' + +import ConfirmStep from './ConfirmStep' +import { QUERY_USER_NAME } from './gql' +import InputStep from './InputStep' + +interface FormProps { + closeDialog: () => void +} + +type Step = 'input' | 'confirm' + +const SetUserNameDialogContent: React.FC = ({ closeDialog }) => { + const viewer = useContext(ViewerContext) + const client = useApolloClient() + + const maxQueryCount = 10 + + const googleId = viewer.info.socialAccounts.find( + (s) => s.type === SocialAccountType.Google + )?.email + const facebookId = viewer.info.socialAccounts.find( + (s) => s.type === SocialAccountType.Facebook + )?.userName + const twitterId = viewer.info.socialAccounts.find( + (s) => s.type === SocialAccountType.Twitter + )?.userName + + const presetUserName = + viewer.info.email || googleId || facebookId || twitterId + const [loading, setLoading] = useState(presetUserName !== null) + + const [index, setIndex] = useState(1) + const normalizedUserName = + presetUserName && + normalizeUserName( + presetUserName.split('@')[0].slice(0, MAX_USER_NAME_LENGTH) + ) + let initUserName = normalizedUserName + if (initUserName && initUserName.length < MIN_USER_NAME_LENGTH) { + initUserName = initUserName + String(index).padStart(3, '0') + } + + const [userName, setUserName] = useState( + presetUserName === null ? '' : initUserName + ) + + const [step, setStep] = useState('input') + const isInput = step === 'input' + const isConfirm = step === 'confirm' + + useEffect(() => { + ;(async () => { + if (!presetUserName) { + return + } + const { data } = await client.query({ + query: QUERY_USER_NAME, + variables: { userName: initUserName }, + fetchPolicy: 'network-only', + }) + if (!!data.user) { + initUserName = + normalizedUserName.slice(0, MAX_USER_NAME_LENGTH - 3) + + String(index + 1).padStart(3, '0') + if (index < maxQueryCount) { + setIndex(index + 1) + } else { + setUserName('') + setLoading(false) + } + } else { + setUserName(initUserName) + setLoading(false) + } + })() + }, [index]) + + if (loading) { + return + } + + return ( + <> + {isInput && ( + { + setStep('confirm') + setUserName(userName) + }} + /> + )} + {isConfirm && ( + { + setStep('input') + }} + userName={userName} + closeDialog={closeDialog} + /> + )} + + ) +} + +export default SetUserNameDialogContent diff --git a/src/components/Dialogs/SetUserNameDialog/InputStep.tsx b/src/components/Dialogs/SetUserNameDialog/InputStep.tsx new file mode 100644 index 0000000000..38dda63fd1 --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/InputStep.tsx @@ -0,0 +1,183 @@ +import { useApolloClient } from '@apollo/react-hooks' +import { useFormik } from 'formik' +import _pickBy from 'lodash/pickBy' +import React, { useContext } from 'react' +import { FormattedMessage, useIntl } from 'react-intl' + +import { + KEYVALUE, + MAX_USER_NAME_LENGTH, + MIN_USER_NAME_LENGTH, +} from '~/common/enums' +import { normalizeUserName, validateUserName } from '~/common/utils' +import { Dialog, Form, LanguageContext, Spacer } from '~/components' + +import Field from '../../Form/Field' +import { QUERY_USER_NAME } from './gql' +import styles from './styles.module.css' + +interface Props { + userName: string + gotoConfirm: (userName: string) => void +} + +interface FormValues { + userName: string +} + +const InputStep: React.FC = ({ userName, gotoConfirm }) => { + const { lang } = useContext(LanguageContext) + + const client = useApolloClient() + + const maxUsername = MAX_USER_NAME_LENGTH + const formId = 'edit-user-name-input' + + const intl = useIntl() + const { + values, + errors, + handleBlur, + handleChange, + handleSubmit, + setFieldValue, + isSubmitting, + } = useFormik({ + initialValues: { + userName: userName, + }, + validateOnBlur: false, + validateOnChange: false, + validate: ({ userName }) => + _pickBy({ + userName: validateUserName(userName, lang), + }), + onSubmit: async ({ userName }, { setSubmitting, setFieldError }) => { + try { + const { data } = await client.query({ + query: QUERY_USER_NAME, + variables: { userName }, + fetchPolicy: 'network-only', + }) + setSubmitting(false) + + if (!!data.user) { + setFieldError( + 'userName', + intl.formatMessage({ + defaultMessage: 'This ID has been taken, please try another one', + description: + 'src/components/Dialogs/SetUserNameDialog/Content.tsx', + }) + ) + } else { + gotoConfirm(userName) + } + } catch (error) { + setSubmitting(false) + } + }, + }) + + const InnerForm = ( +
+ { + if (e.key.toLocaleLowerCase() === KEYVALUE.enter) { + e.stopPropagation() + } + }} + onPaste={(e) => { + e.preventDefault() + return false + }} + onKeyUp={() => { + const v = normalizeUserName(values.userName) + setFieldValue('userName', v.slice(0, maxUsername)) + }} + leftButton={@} + hasFooter={false} + /> + + {errors.userName && ( + + )} + + ) + + const SubmitButton = ( + } + loading={isSubmitting} + /> + ) + + return ( + <> + + } + /> + + +

+ +

+
+ + {InnerForm} + + + } + loading={isSubmitting} + /> + + } + smUpBtns={<>{SubmitButton}} + /> + + ) +} + +export default InputStep diff --git a/src/components/Dialogs/SetUserNameDialog/gql.ts b/src/components/Dialogs/SetUserNameDialog/gql.ts new file mode 100644 index 0000000000..585fbb0f11 --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/gql.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const QUERY_USER_NAME = gql` + query QueryUserName($userName: String!) { + user(input: { userName: $userName }) { + id + } + } +` + +export const SET_USER_NAME = gql` + mutation SetUserName($userName: String!) { + setUserName(input: { userName: $userName }) { + id + userName + } + } +` diff --git a/src/components/Dialogs/SetUserNameDialog/index.tsx b/src/components/Dialogs/SetUserNameDialog/index.tsx new file mode 100644 index 0000000000..ffda527c58 --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/index.tsx @@ -0,0 +1,42 @@ +import dynamic from 'next/dynamic' + +import { OPEN_SET_USER_NAME_DIALOG } from '~/common/enums' +import { + Dialog, + Spinner, + useDialogSwitch, + useEventListener, +} from '~/components' + +interface SetUserNameDialogProps { + children?: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} + +const DynamicContent = dynamic(() => import('./Content'), { loading: Spinner }) + +const BaseSetUserNameDialog = ({ children }: SetUserNameDialogProps) => { + const { show, openDialog, closeDialog } = useDialogSwitch(true) + + return ( + <> + {children && children({ openDialog })} + + + + + + ) +} + +export const SetUserNameDialog = (props: SetUserNameDialogProps) => { + const Children = ({ openDialog }: { openDialog: () => void }) => { + useEventListener(OPEN_SET_USER_NAME_DIALOG, openDialog) + return <>{props.children && props.children({ openDialog })} + } + + return ( + }> + {({ openDialog }) => } + + ) +} diff --git a/src/components/Dialogs/SetUserNameDialog/styles.module.css b/src/components/Dialogs/SetUserNameDialog/styles.module.css new file mode 100644 index 0000000000..c086ed79d0 --- /dev/null +++ b/src/components/Dialogs/SetUserNameDialog/styles.module.css @@ -0,0 +1,7 @@ +.atFlag { + display: inline-block; + padding-right: var(--spacing-xx-tight); + font-size: var(--font-size-md); + line-height: 1.375rem; + color: var(--color-grey-light); +} diff --git a/src/components/Dialogs/index.tsx b/src/components/Dialogs/index.tsx index d6600cdcfc..73067c168c 100644 --- a/src/components/Dialogs/index.tsx +++ b/src/components/Dialogs/index.tsx @@ -2,6 +2,7 @@ export * from './ENSDialog' export * from './LikeCoinDialog' export * from './RssFeedDialog' +export * from './SetUserNameDialog' // Article export * from './AppreciatorsDialog' diff --git a/src/components/Form/Field/Footer/index.tsx b/src/components/Form/Field/Footer/index.tsx index 2933e8aa8b..5c093bd00b 100644 --- a/src/components/Form/Field/Footer/index.tsx +++ b/src/components/Form/Field/Footer/index.tsx @@ -1,5 +1,7 @@ import classNames from 'classnames' +import { capitalizeFirstLetter } from '@/src/common/utils' + import styles from './styles.module.css' export interface FooterProps { @@ -8,7 +10,7 @@ export interface FooterProps { error?: string | React.ReactNode hintSize?: 'xs' | 'sm' hintAlign?: 'left' | 'right' | 'center' - hintSpace?: 'xTight' | 'baseLoose' + hintSpace?: 'xTight' | 'base' | 'baseLoose' } const Footer: React.FC = ({ @@ -21,7 +23,7 @@ const Footer: React.FC = ({ }) => { const footerClasses = classNames({ [styles.footer]: true, - [styles.spaceBaseLoose]: hintSpace === 'baseLoose', + [styles[`space${capitalizeFirstLetter(hintSpace)}`]]: true, }) const hintClasses = classNames({ diff --git a/src/components/Form/Field/Footer/styles.module.css b/src/components/Form/Field/Footer/styles.module.css index 0da70abf93..1a54f0c1e3 100644 --- a/src/components/Form/Field/Footer/styles.module.css +++ b/src/components/Form/Field/Footer/styles.module.css @@ -2,6 +2,14 @@ margin-top: var(--spacing-x-tight); } +.spaceXTight { + margin-top: var(--spacing-x-tight); +} + +.spaceBase { + margin-top: var(--spacing-base); +} + .spaceBaseLoose { margin-top: var(--spacing-base-loose); } diff --git a/src/components/Form/Input/index.tsx b/src/components/Form/Input/index.tsx index 50b3c7aea7..57fe84560b 100644 --- a/src/components/Form/Input/index.tsx +++ b/src/components/Form/Input/index.tsx @@ -24,6 +24,7 @@ type InputProps = { type: 'text' | 'password' | 'email' | 'number' name: string hasFooter?: boolean + leftButton?: React.ReactNode rightButton?: React.ReactNode } & Omit & React.DetailedHTMLProps< @@ -38,6 +39,7 @@ const Input = forwardRef( name, hasFooter = true, + leftButton, rightButton, label, @@ -62,7 +64,7 @@ const Input = forwardRef( const inputClasses = classNames({ [styles.input]: true, [styles.error]: error, - [styles.wrapper]: !!rightButton, + [styles.wrapper]: !!leftButton || !!rightButton, }) const input = ( @@ -77,7 +79,7 @@ const Input = forwardRef( autoCorrect="off" autoCapitalize="off" spellCheck="false" - className={!rightButton ? inputClasses : undefined} + className={!leftButton && !rightButton ? inputClasses : undefined} /> ) @@ -90,6 +92,15 @@ const Input = forwardRef( hasLabel={hasLabel} /> + {leftButton && ( + +
+ {leftButton} + {input} +
+
+ )} + {rightButton && (
@@ -98,7 +109,7 @@ const Input = forwardRef(
)} - {!rightButton && {input}} + {!leftButton && !rightButton && {input}} {hasFooter && ( { + const viewer = useContext(ViewerContext) + + useEffect(() => { + if (viewer.isAuthed && viewer.userName === null) { + window.dispatchEvent(new CustomEvent(OPEN_SET_USER_NAME_DIALOG)) + } + }, []) + return ( <> + ) } diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx index f3ca5c9d52..a38df0db4d 100644 --- a/src/components/Toast/index.tsx +++ b/src/components/Toast/index.tsx @@ -30,9 +30,13 @@ const ToastActions: React.FC = ({ }) => { return (
- {actions.map(({ content, ...props }, index) => ( + {actions.map(({ content, onClick, ...props }, index) => (