* Add modservice screen and profile-header-card * Drop the guidelines for now * Remove ununsed constants * Add label & label group descriptions * Not found state * Reorg, add icon * Subheader * Header * Complete header * Clean up * Add all groups * Fix scroll view * Dialogs side quest * Remove log * Add (WIP) debug mod page * Dialog solution * Add note * Clean up and reorganize localized moderation strings * Memoize * Add example * Add first ReportDialog screen * Report dialog step 2 * Submit * Integrate updates * Move moderation screen * Migrate buttons * Migrate everything * Rough sketch * Fix types * Update atoms values * Abstract ModerationServiceCard * Hook up data to settings page * Handle subscription * Rough enablement * Rough enablement * Some validation, fixes * More work on the mod debug screen * Hook up data * Update invalidation * Hook up data to ReportDialog * Fix native error * Refactor/rewrite the entire moderation-application system * Fix toggles * Add copyright and other option to report * Handle reports on profile vs content * Little cleanup * Get post hiding back in gear * Better loading flow on Mod screen * Clean up Mod screen * Clean up ProfileMod screen * Handle muting correctly * Update enablement on ProfileMod screen * Improve Moderation screen and dialog * Styling, handle disabled labelers * Rework list of labels on own content * Use moderateNotification() * ReportDialog updates * Fix button overflow * Simplify the ProfileModerationService ui * Mod screen design * Move moderation card from the profile header to a tab * Small tweaks to the moderation screen * Enable toggle on mod page * Add notifs to debugmod and dont filter notifs from followed users * Add moderator-service profile view * Wire up more of the modservice data to profiles * A bunch of speculative non-working UI * Cleanup: delete old code * Update ModerationDetailsDialog * Update ReportDialog * Update LabelsOnMe dialog * Handle ReportDialog load better * Rename LabelsOnMeDialog, fix close * Experiment to put labeling under a tab of a normal profile * Moderator variation of profile * Remove dead code and start moving toward latest modsdk * Remove a bunch of now-dead label strings * Update ModDebug to be a bit more intuitive and support custom labels * Minor ui tweaks * Improve consistency of display name blurring * Fix profile-card warning rendering * More debugmod UI tuning * Update to use new labeler semantics * Delete some dead code and do some refactoring * Update profile to pull from labeler definition * Implement new label config controls (wip) * Tweak ui * Implement preference controls on labelers * Rework label pref ui * Get moderation screen working * Add asyncstorage query persistence * Implement label handling * Small cleanup * Implement Likes dialog * Fix: remove text outside of text element * Cleanup * Fix likes dialog on mobile * Implement the label appeal flow * Get report flow working again with temporarily fixed report options * Update onboarding * Enforce limit of ten labeler subscriptions * Fix type errors * Fix lint errors * Improve types of RQ * Some work on Likes dialog, needs discussion * Bit of ReportDialog cleanup * Replace non-single-path SVG * Update nudity descriptions * Update to use new sdk updates * Add adult-content-enabled behavior to label config * Use the default setting of custom labels * Handle global moderation label prefs with the global settings * Fix missing postAuthor * Fix empty moderation page * Add mutewords control back to Mod screen * Tweak adult setting styles * Remove deprecated global labels * Handle underage users on mod screen * Adjust font sizes * Swap in RichText * Like button improvements * Tweaks to Labeler profile * Design tweaks for mod pref dialog * Add tertiary button color * Switch moderation UIs to tertiary color * Update mutewords and hiddenposts to use the new sdk * Add test-environment mod authority * Switch 'gore' to 'graphic-media' * Move nudity out of the adult content control * Remove focus styles from buttons - let the browser behavior handle it * Fixes to the adult content age-gating in moderaiton * Ditch tertiary button color, lighten secondary button * Fix some colors * Remove focused overrides from toggles * Liked by screen * Rework the moderationlabelpref * Fix optimistic like * Cleanup * Change how onboarding handles adult content enabled/disabled * Add special handling of the mod authorities * Tweaks * Update the default labeler avatar to a shield * Add route to go server * Avoid dups due to bad config * Fix attrs * Fix: dont try to detect link/label mismatches on post meta * Correctly show the label behavior when adult content is disabled * Readd the local hiddenPosts handling * WIP * Fix bad merge * Conten hider design tweaks * Fix text string breakage * Adjust source text in ContentHider * Fix link bug * Design tweaks to ContentHider and ModDetailsDialog * Adjust spacing of inform badges * Adjust spacing of embeds in posts * Style tweaks to post/profile alerts * Labels on me and dialog * Remove bad focus styles from post dropdown * Better spacing solution * Tune moderation UIs * Moderation UI tweaks for mobile * Move labelers query on Mod screen * Update to use new SDK appLabelers semantics * Implement report submission * Replace the report modal entirely with the report dialog * Add @ to mod details dialog handle * Bump SDK package * Remove silly type * Add to AWS build CI * Fix ToggleButton overflow * Clean up ModServiceCard, rename to LabelingServiceCard * Hackfix to translate gore labels to graphic-media * Tune content hider sizing on web desktop * Handle self labels * Fix spacing below text-only posts * Fix: send appeals to the right labeler * Give mod page links interactive states * Fix references * Remove focus handling * Remove remnant * Remove the like count from the subscribed labeler listing * Bump @atproto/api@0.11.1 * Remove extra @ * Fix: persist labels to local storage to reduce coverage gaps * update dipendencies * revert dipendencies * Add some explainers on how blocking affects labelers * Tweak copy * Fix underline color in header * Fix profile menu * Handle card overflow * Remove metrics from header * Mute 'account' not 'user' * Show metrics if self * Show the labels tab on logged out view * Fix bad merge * Use purple theming on labelers * Tighten space on LabelerCard * Set staleTime to 6hrs for labeler details * Memoize the memoizers * Drop staleTime to 60s * Move label defs into a context to reduce recomputes * Submit view tweaks * Move labeler fetch below auth * Mitigation: hardcode the bluesky moderation labeler name * Bump sdk * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Hailey's fix for incorrect profile tabs Co-authored-by: Hailey <me@haileyok.com> * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey <me@haileyok.com> * Fix post tabs * Integrate feedback pt 1 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 2 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 3 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 4 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 5 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 6 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 7 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 8 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Format * Integrate new bday modal * Use public agent for getServices * Update casing --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
298 lines
8.9 KiB
TypeScript
298 lines
8.9 KiB
TypeScript
import {useCallback, useReducer} from 'react'
|
|
import {
|
|
ComAtprotoServerDescribeServer,
|
|
ComAtprotoServerCreateAccount,
|
|
} from '@atproto/api'
|
|
import {I18nContext, useLingui} from '@lingui/react'
|
|
import {msg} from '@lingui/macro'
|
|
import * as EmailValidator from 'email-validator'
|
|
import {getAge} from 'lib/strings/time'
|
|
import {logger} from '#/logger'
|
|
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
|
|
import {cleanError} from '#/lib/strings/errors'
|
|
import {useOnboardingDispatch} from '#/state/shell/onboarding'
|
|
import {useSessionApi} from '#/state/session'
|
|
import {DEFAULT_SERVICE, IS_TEST_USER} from '#/lib/constants'
|
|
import {
|
|
DEFAULT_PROD_FEEDS,
|
|
usePreferencesSetBirthDateMutation,
|
|
useSetSaveFeedsMutation,
|
|
} from 'state/queries/preferences'
|
|
|
|
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
|
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
|
|
|
export type CreateAccountAction =
|
|
| {type: 'set-step'; value: number}
|
|
| {type: 'set-error'; value: string | undefined}
|
|
| {type: 'set-processing'; value: boolean}
|
|
| {type: 'set-service-url'; value: string}
|
|
| {type: 'set-service-description'; value: ServiceDescription | undefined}
|
|
| {type: 'set-user-domain'; value: string}
|
|
| {type: 'set-invite-code'; value: string}
|
|
| {type: 'set-email'; value: string}
|
|
| {type: 'set-password'; value: string}
|
|
| {type: 'set-handle'; value: string}
|
|
| {type: 'set-birth-date'; value: Date}
|
|
| {type: 'next'}
|
|
| {type: 'back'}
|
|
|
|
export interface CreateAccountState {
|
|
// state
|
|
step: number
|
|
error: string | undefined
|
|
isProcessing: boolean
|
|
serviceUrl: string
|
|
serviceDescription: ServiceDescription | undefined
|
|
userDomain: string
|
|
inviteCode: string
|
|
email: string
|
|
password: string
|
|
handle: string
|
|
birthDate: Date
|
|
|
|
// computed
|
|
canBack: boolean
|
|
canNext: boolean
|
|
isInviteCodeRequired: boolean
|
|
isCaptchaRequired: boolean
|
|
}
|
|
|
|
export type CreateAccountDispatch = (action: CreateAccountAction) => void
|
|
|
|
export function useCreateAccount() {
|
|
const {_} = useLingui()
|
|
|
|
return useReducer(createReducer({_}), {
|
|
step: 1,
|
|
error: undefined,
|
|
isProcessing: false,
|
|
serviceUrl: DEFAULT_SERVICE,
|
|
serviceDescription: undefined,
|
|
userDomain: '',
|
|
inviteCode: '',
|
|
email: '',
|
|
password: '',
|
|
handle: '',
|
|
birthDate: DEFAULT_DATE,
|
|
|
|
canBack: false,
|
|
canNext: false,
|
|
isInviteCodeRequired: false,
|
|
isCaptchaRequired: false,
|
|
})
|
|
}
|
|
|
|
export function useSubmitCreateAccount(
|
|
uiState: CreateAccountState,
|
|
uiDispatch: CreateAccountDispatch,
|
|
) {
|
|
const {_} = useLingui()
|
|
const {createAccount} = useSessionApi()
|
|
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
|
|
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
|
const onboardingDispatch = useOnboardingDispatch()
|
|
|
|
return useCallback(
|
|
async (verificationCode?: string) => {
|
|
if (!uiState.email) {
|
|
uiDispatch({type: 'set-step', value: 1})
|
|
console.log('no email?')
|
|
return uiDispatch({
|
|
type: 'set-error',
|
|
value: _(msg`Please enter your email.`),
|
|
})
|
|
}
|
|
if (!EmailValidator.validate(uiState.email)) {
|
|
uiDispatch({type: 'set-step', value: 1})
|
|
return uiDispatch({
|
|
type: 'set-error',
|
|
value: _(msg`Your email appears to be invalid.`),
|
|
})
|
|
}
|
|
if (!uiState.password) {
|
|
uiDispatch({type: 'set-step', value: 1})
|
|
return uiDispatch({
|
|
type: 'set-error',
|
|
value: _(msg`Please choose your password.`),
|
|
})
|
|
}
|
|
if (!uiState.handle) {
|
|
uiDispatch({type: 'set-step', value: 2})
|
|
return uiDispatch({
|
|
type: 'set-error',
|
|
value: _(msg`Please choose your handle.`),
|
|
})
|
|
}
|
|
if (uiState.isCaptchaRequired && !verificationCode) {
|
|
uiDispatch({type: 'set-step', value: 3})
|
|
return uiDispatch({
|
|
type: 'set-error',
|
|
value: _(msg`Please complete the verification captcha.`),
|
|
})
|
|
}
|
|
uiDispatch({type: 'set-error', value: ''})
|
|
uiDispatch({type: 'set-processing', value: true})
|
|
|
|
try {
|
|
onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
|
|
await createAccount({
|
|
service: uiState.serviceUrl,
|
|
email: uiState.email,
|
|
handle: createFullHandle(uiState.handle, uiState.userDomain),
|
|
password: uiState.password,
|
|
inviteCode: uiState.inviteCode.trim(),
|
|
verificationCode: uiState.isCaptchaRequired
|
|
? verificationCode
|
|
: undefined,
|
|
})
|
|
setBirthDate({birthDate: uiState.birthDate})
|
|
if (!IS_TEST_USER(uiState.handle)) {
|
|
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
|
}
|
|
} catch (e: any) {
|
|
onboardingDispatch({type: 'skip'}) // undo starting the onboard
|
|
let errMsg = e.toString()
|
|
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
|
errMsg = _(
|
|
msg`Invite code not accepted. Check that you input it correctly and try again.`,
|
|
)
|
|
uiDispatch({type: 'set-step', value: 1})
|
|
}
|
|
|
|
if ([400, 429].includes(e.status)) {
|
|
logger.warn('Failed to create account', {message: e})
|
|
} else {
|
|
logger.error(`Failed to create account (${e.status} status)`, {
|
|
message: e,
|
|
})
|
|
}
|
|
|
|
const error = cleanError(errMsg)
|
|
const isHandleError = error.toLowerCase().includes('handle')
|
|
|
|
uiDispatch({type: 'set-processing', value: false})
|
|
uiDispatch({type: 'set-error', value: cleanError(errMsg)})
|
|
uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1})
|
|
}
|
|
},
|
|
[
|
|
uiState.email,
|
|
uiState.password,
|
|
uiState.handle,
|
|
uiState.isCaptchaRequired,
|
|
uiState.serviceUrl,
|
|
uiState.userDomain,
|
|
uiState.inviteCode,
|
|
uiState.birthDate,
|
|
uiDispatch,
|
|
_,
|
|
onboardingDispatch,
|
|
createAccount,
|
|
setBirthDate,
|
|
setSavedFeeds,
|
|
],
|
|
)
|
|
}
|
|
|
|
export function is13(state: CreateAccountState) {
|
|
return getAge(state.birthDate) >= 13
|
|
}
|
|
|
|
export function is18(state: CreateAccountState) {
|
|
return getAge(state.birthDate) >= 18
|
|
}
|
|
|
|
function createReducer({_}: {_: I18nContext['_']}) {
|
|
return function reducer(
|
|
state: CreateAccountState,
|
|
action: CreateAccountAction,
|
|
): CreateAccountState {
|
|
switch (action.type) {
|
|
case 'set-step': {
|
|
return compute({...state, step: action.value})
|
|
}
|
|
case 'set-error': {
|
|
return compute({...state, error: action.value})
|
|
}
|
|
case 'set-processing': {
|
|
return compute({...state, isProcessing: action.value})
|
|
}
|
|
case 'set-service-url': {
|
|
return compute({
|
|
...state,
|
|
serviceUrl: action.value,
|
|
serviceDescription:
|
|
state.serviceUrl !== action.value
|
|
? undefined
|
|
: state.serviceDescription,
|
|
})
|
|
}
|
|
case 'set-service-description': {
|
|
return compute({
|
|
...state,
|
|
serviceDescription: action.value,
|
|
userDomain: action.value?.availableUserDomains[0] || '',
|
|
})
|
|
}
|
|
case 'set-user-domain': {
|
|
return compute({...state, userDomain: action.value})
|
|
}
|
|
case 'set-invite-code': {
|
|
return compute({...state, inviteCode: action.value})
|
|
}
|
|
case 'set-email': {
|
|
return compute({...state, email: action.value})
|
|
}
|
|
case 'set-password': {
|
|
return compute({...state, password: action.value})
|
|
}
|
|
case 'set-handle': {
|
|
return compute({...state, handle: action.value})
|
|
}
|
|
case 'set-birth-date': {
|
|
return compute({...state, birthDate: action.value})
|
|
}
|
|
case 'next': {
|
|
if (state.step === 1) {
|
|
if (!is13(state)) {
|
|
return compute({
|
|
...state,
|
|
error: _(
|
|
msg`Unfortunately, you do not meet the requirements to create an account.`,
|
|
),
|
|
})
|
|
}
|
|
}
|
|
return compute({...state, error: '', step: state.step + 1})
|
|
}
|
|
case 'back': {
|
|
return compute({...state, error: '', step: state.step - 1})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function compute(state: CreateAccountState): CreateAccountState {
|
|
let canNext = true
|
|
if (state.step === 1) {
|
|
canNext =
|
|
!!state.serviceDescription &&
|
|
(!state.isInviteCodeRequired || !!state.inviteCode) &&
|
|
!!state.email &&
|
|
!!state.password
|
|
} else if (state.step === 2) {
|
|
canNext =
|
|
!!state.handle && validateHandle(state.handle, state.userDomain).overall
|
|
} else if (state.step === 3) {
|
|
// Step 3 will automatically redirect as soon as the captcha completes
|
|
canNext = false
|
|
}
|
|
return {
|
|
...state,
|
|
canBack: state.step > 1,
|
|
canNext,
|
|
isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired,
|
|
isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired,
|
|
}
|
|
}
|