From 3054b53765a7e6f53bf527dc6d06661146736d01 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 23 Sep 2024 23:57:06 -0700 Subject: [PATCH 1/2] implemented app state api --- .../src/api/app_state/GetAppState.ts | 86 +++++++ .../session/SessionSubscriptionsWrapper.ts | 68 ++++-- src/website/packages/fakeyou.com/src/App.tsx | 213 +--------------- .../layout/AccountModal/useLogin.tsx | 63 +++-- .../layout/AccountModal/useSignup.tsx | 156 +++++++----- .../src/components/layout/TopNav/TopNav.tsx | 10 +- .../AppStateProvider/AppStateProvider.tsx | 229 ++++++++++++++++++ .../providers/AppStateProvider/index.tsx | 1 + .../CoreServicesProvider.tsx | 45 ++-- .../SessionProvider/SessionProvider.tsx | 38 ++- .../src/components/providers/index.tsx | 2 + .../src/hooks/useStatusPoll/useStatusPoll.tsx | 47 ++-- .../fakeyou.com/src/v2/view/nav/FooterNav.tsx | 46 +--- .../src/v2/view/pages/login/LoginPage.tsx | 9 +- .../PasswordResetVerificationPage.tsx | 9 +- .../src/v2/view/pages/signup/SignupPage.tsx | 9 +- 16 files changed, 589 insertions(+), 442 deletions(-) create mode 100644 src/website/packages/components/src/api/app_state/GetAppState.ts create mode 100644 src/website/packages/fakeyou.com/src/components/providers/AppStateProvider/AppStateProvider.tsx create mode 100644 src/website/packages/fakeyou.com/src/components/providers/AppStateProvider/index.tsx diff --git a/src/website/packages/components/src/api/app_state/GetAppState.ts b/src/website/packages/components/src/api/app_state/GetAppState.ts new file mode 100644 index 0000000000..3a0f62670a --- /dev/null +++ b/src/website/packages/components/src/api/app_state/GetAppState.ts @@ -0,0 +1,86 @@ +import MakeRequest from "../MakeRequest"; +import { UserDetailsLight } from "../_common/UserDetailsLight"; + +export interface UserLocale { + full_language_tags: [string, string]; + language_codes: [string, string]; +} + +export interface ServerAlert { + maybe_category: string; + maybe_message: string; +} + +export interface ActiveSubscription { + namespace: string; + product_slug: string; +} + +export interface PremiumStatus { + active_subscriptions: ActiveSubscription[]; + has_free_premium: boolean; + has_paid_premium: boolean; + has_premium: boolean; + maybe_loyalty_program: string | null; +} + +export type FeatureFlags = + | "explore_media" + | "studio" + | "upload_3d" + | "video_style_transfer"; + +export interface UserPermissions { + feature_flags: FeatureFlags[]; + is_moderator: boolean; + legacy_permission_flags: { + can_approve_w2l_templates: boolean; + can_ban_users: boolean; + can_delete_other_users_tts_models: boolean; + can_delete_other_users_tts_results: boolean; + can_delete_other_users_w2l_results: boolean; + can_delete_other_users_w2l_templates: boolean; + can_delete_own_account: boolean; + can_delete_own_tts_models: boolean; + can_delete_own_tts_results: boolean; + can_delete_own_w2l_results: boolean; + can_delete_own_w2l_templates: boolean; + can_delete_users: boolean; + can_edit_other_users_profiles: boolean; + can_edit_other_users_tts_models: boolean; + can_edit_other_users_w2l_templates: boolean; + can_upload_tts_models: boolean; + can_upload_w2l_templates: boolean; + can_use_tts: boolean; + can_use_w2l: boolean; + }; +} + +export interface GetAppStateRequest {} + +export interface GetAppStateResponse { + is_banned: boolean; + is_logged_in: boolean; + locale: UserLocale; + maybe_alert: ServerAlert | null; + maybe_premium: PremiumStatus | null; + maybe_user_info: UserDetailsLight | null; + permissions: UserPermissions; + refresh_interval_millis: number; + server_info: { + build_sha: string; + build_sha_short: string; + hostname: string; + }; + success: boolean; +} + +export const GetAppState = MakeRequest< + string, + GetAppStateRequest, + GetAppStateResponse, + {} +>({ + method: "GET", + routingFunction: () => `/v1/app_state`, +}); diff --git a/src/website/packages/components/src/session/SessionSubscriptionsWrapper.ts b/src/website/packages/components/src/session/SessionSubscriptionsWrapper.ts index a88ef53a9f..204702dc50 100644 --- a/src/website/packages/components/src/session/SessionSubscriptionsWrapper.ts +++ b/src/website/packages/components/src/session/SessionSubscriptionsWrapper.ts @@ -11,26 +11,35 @@ const FAKEYOU_NAMESPACE = "fakeyou"; export class SessionSubscriptionsWrapper { listActiveSubscriptionResponse?: ListActiveSubscriptionsSuccessResponse; - private constructor(listActiveSubscriptionsSuccessResponse?: ListActiveSubscriptionsSuccessResponse) { + private constructor( + listActiveSubscriptionsSuccessResponse?: ListActiveSubscriptionsSuccessResponse + ) { if (listActiveSubscriptionsSuccessResponse !== undefined) { - this.listActiveSubscriptionResponse = listActiveSubscriptionsSuccessResponse; + this.listActiveSubscriptionResponse = + listActiveSubscriptionsSuccessResponse; } } - public static async lookupActiveSubscriptions() : Promise { + public static async lookupActiveSubscriptions(): Promise { let response = await ListActiveSubscriptions(); if (ListActiveSubscriptionsIsSuccess(response)) { - return new SessionSubscriptionsWrapper(response); + return SessionSubscriptionsWrapper.wrapResponse(response); } else { return SessionSubscriptionsWrapper.emptySubscriptions(); } } - public static emptySubscriptions() : SessionSubscriptionsWrapper { + public static emptySubscriptions(): SessionSubscriptionsWrapper { return new SessionSubscriptionsWrapper(); } - public ttsMaximumLength() : number { + public static wrapResponse( + sessionStateResponse: ListActiveSubscriptionsSuccessResponse + ): SessionSubscriptionsWrapper { + return new SessionSubscriptionsWrapper(sessionStateResponse); + } + + public ttsMaximumLength(): number { if (this.hasActiveEliteSubscription()) { return 4096; } else if (this.hasActiveProSubscription()) { @@ -43,37 +52,50 @@ export class SessionSubscriptionsWrapper { return 1024; } - public hasFreeOrPaidPremiumFeatures() : boolean { + public hasFreeOrPaidPremiumFeatures(): boolean { return this.hasLoyaltyProgram() || this.hasPaidFeatures(); } - public hasLoyaltyProgram() : boolean { + public hasLoyaltyProgram(): boolean { return !!this.listActiveSubscriptionResponse?.maybe_loyalty_program; } - public hasPaidFeatures() : boolean { - const subs = this.listActiveSubscriptionResponse?.active_subscriptions || []; + public hasPaidFeatures(): boolean { + const subs = + this.listActiveSubscriptionResponse?.active_subscriptions || []; return subs.length > 0; } - public hasActivePlusSubscription() : boolean { - let maybePlan = FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() ? - this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_plus") : - this.findActiveSubscription(FAKEYOU_NAMESPACE, "development_fakeyou_plus"); + public hasActivePlusSubscription(): boolean { + let maybePlan = + FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() + ? this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_plus") + : this.findActiveSubscription( + FAKEYOU_NAMESPACE, + "development_fakeyou_plus" + ); return maybePlan !== undefined; } - public hasActiveProSubscription() : boolean { - let maybePlan = FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() ? - this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_pro") : - this.findActiveSubscription(FAKEYOU_NAMESPACE, "development_fakeyou_pro"); + public hasActiveProSubscription(): boolean { + let maybePlan = + FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() + ? this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_pro") + : this.findActiveSubscription( + FAKEYOU_NAMESPACE, + "development_fakeyou_pro" + ); return maybePlan !== undefined; } - public hasActiveEliteSubscription() : boolean { - let maybePlan = FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() ? - this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_elite") : - this.findActiveSubscription(FAKEYOU_NAMESPACE, "development_fakeyou_elite"); + public hasActiveEliteSubscription(): boolean { + let maybePlan = + FakeYouFrontendEnvironment.getInstance().useProductionStripePlans() + ? this.findActiveSubscription(FAKEYOU_NAMESPACE, "fakeyou_elite") + : this.findActiveSubscription( + FAKEYOU_NAMESPACE, + "development_fakeyou_elite" + ); return maybePlan !== undefined; } @@ -95,7 +117,7 @@ export class SessionSubscriptionsWrapper { const subs = this.listActiveSubscriptionResponse?.active_subscriptions || []; return subs.find( - (sub) => sub.namespace === namespace && sub.product_slug === product_slug + sub => sub.namespace === namespace && sub.product_slug === product_slug ); } } diff --git a/src/website/packages/fakeyou.com/src/App.tsx b/src/website/packages/fakeyou.com/src/App.tsx index 56ed8d4db9..8bdfc6096a 100644 --- a/src/website/packages/fakeyou.com/src/App.tsx +++ b/src/website/packages/fakeyou.com/src/App.tsx @@ -2,28 +2,13 @@ import "./AppNew.scss"; import "scss/custom-bootstrap.scss"; import React from "react"; -import { ApiConfig } from "@storyteller/components"; -import Cookies from "universal-cookie"; -import { - DetectLocale, - DetectLocaleIsOk, -} from "@storyteller/components/src/api/locale/DetectLocale"; -import { BrowserRouter, Route, Switch } from "react-router-dom"; +import { BrowserRouter } from "react-router-dom"; import PageContainer from "./v2/view/PageContainer"; -import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper"; -import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient"; -import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; import { TtsInferenceJob } from "@storyteller/components/src/jobs/TtsInferenceJobs"; import { W2lInferenceJob } from "@storyteller/components/src/jobs/W2lInferenceJobs"; import { FAKEYOU_MERGED_TRANSLATIONS } from "./_i18n/FakeYouTranslations"; import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import deepEqual from "deep-equal"; -import { - AvailableLanguageKey, - AVAILABLE_LANGUAGE_MAP, - ENGLISH_LANGUAGE, -} from "./_i18n/AvailableLanguageMap"; import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels"; import HttpBackend from "i18next-http-backend"; @@ -77,33 +62,7 @@ enum MigrationMode { interface Props {} interface State { - // Migration Mode - // migrationMode: MigrationMode; - - // Rollout of vocodes 2.0 - // enableAlpha: boolean; - - sessionFetched: boolean; - sessionWrapper: SessionWrapper; - sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; - - // Locale + show flash notice to Spanish speakers - //localeLanguageCodes: string[], - //localeFullLanguageTags: string[], - // isShowingLanguageNotice: boolean; - // displayLanguage: Language; - // primaryLanguageCode: string; - - // isShowingTwitchTtsNotice: boolean; - // isShowingPleaseFollowNotice: boolean; - - // An improved notice for "new" languages asking users to help. - // isShowingBootstrapLanguageNotice: boolean; - - // Current text entered textBuffer: string; - - // voiceConversionModels: VoiceConversionModelListItem[]; maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem; } @@ -113,11 +72,6 @@ class App extends React.Component { super(props); this.state = { - sessionFetched: false, - sessionWrapper: SessionWrapper.emptySession(), - sessionSubscriptionsWrapper: - SessionSubscriptionsWrapper.emptySubscriptions(), - textBuffer: "", maybeSelectedVoiceConversionModel: undefined, @@ -141,132 +95,6 @@ class App extends React.Component { require("./v2/view/_css/footer.scss"); } - async componentDidMount() { - await this.queryLanguage(); - await this.querySession(); - await this.querySessionSubscriptions(); - - setInterval(async () => { - // See warnings in the following methods when adding new methods - // that affect global "state" - await this.querySession(); - await this.querySessionSubscriptions(); - }, 60000); - // TODO: Use websockets, this is dumb - } - - querySession = async () => { - // WARNING: Making setState calls in this scope without checking existing - // state can cause the whole site to refresh/worsen UX. Double check if - // state needs to be set here, or if instead can be refreshed locally on - // the page where the new "state" needed - const sessionWrapper = await SessionWrapper.lookupSession(); - const username = sessionWrapper.getDisplayName(); - const cookies = new Cookies(); - - if (username !== undefined) { - // Track only logged-in users (for now) - PosthogClient.enablePosthog(); - PosthogClient.setUsername(username); - cookies.set("logged_in_username", username, { - path: "/", - expires: new Date(Date.now() + 3 * 86400000), - }); - window.dataLayer.push({ - user_id: username, - }); - } else { - cookies.remove("logged_in_username", { path: "/" }); - } - - if (!deepEqual(sessionWrapper, this.state.sessionWrapper)) { - this.setState({ - sessionWrapper: sessionWrapper, - }); - } - }; - - querySessionSubscriptions = async () => { - // WARNING: Making setState calls in this scope without checking existing - // state can cause the whole site to refresh/worsen UX. Double check if - // state needs to be set here, or if instead can be refreshed locally on - // the page where the new "state" needed - - if (this.state.sessionFetched === false) { - this.setState({ sessionFetched: true }); - } - - const cookies = new Cookies(); - - const sessionSubscriptionsWrapper = - await SessionSubscriptionsWrapper.lookupActiveSubscriptions(); - - const plan = sessionSubscriptionsWrapper.getActiveProductSlug(); - if (plan !== undefined) { - cookies.set("logged_in_user_plan", plan, { - path: "/", - expires: new Date(Date.now() + 3 * 86400000), - }); - } else { - cookies.remove("logged_in_user_plan", { path: "/" }); - } - - if ( - !deepEqual( - sessionSubscriptionsWrapper, - this.state.sessionSubscriptionsWrapper - ) - ) { - this.setState({ - sessionSubscriptionsWrapper: sessionSubscriptionsWrapper, - }); - } - }; - - queryLanguage = async () => { - let locale = await DetectLocale(); - if (DetectLocaleIsOk(locale)) { - // NB: We treat the language preference as being the order in the array. - // As of 2023-01-14, the backend does not handle quality values / q-weights, - // so these may be slightly wrong. An adjustment to the server will fix this. - - let preferredLanguage = ENGLISH_LANGUAGE; - - for (let languageCode of locale.language_codes) { - let maybeLanguage = - AVAILABLE_LANGUAGE_MAP[languageCode as AvailableLanguageKey]; - - if (maybeLanguage !== undefined) { - preferredLanguage = maybeLanguage; - break; - } - } - - i18n.changeLanguage(preferredLanguage.languageCode); - i18n2.changeLanguage(preferredLanguage.languageCode); - } - }; - - logoutSession = () => { - const api = new ApiConfig(); - const endpointUrl = api.logout(); - - fetch(endpointUrl, { - method: "POST", - credentials: "include", - headers: { - Accept: "application/json", - }, - }) - .then(_raw_response => { - this.querySession(); - this.querySessionSubscriptions(); - }) - .catch(e => { - /* Ignore. */ - }); - }; - setTextBuffer = (textBuffer: string) => { this.setState({ textBuffer: textBuffer }); }; @@ -280,36 +108,17 @@ class App extends React.Component {
- {/* This is the old vocodes1.0-compatible username and version switch - - */} -
- - - - - - - - + + +
diff --git a/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useLogin.tsx b/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useLogin.tsx index 51c03f319c..c80573f27b 100644 --- a/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useLogin.tsx +++ b/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useLogin.tsx @@ -1,41 +1,52 @@ -import { useState } from "react"; -import { useChanger, useSession } from "hooks"; +import { useContext, useState } from "react"; +import { useChanger } from "hooks"; import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes"; -import { CreateSession, CreateSessionResponse } from "@storyteller/components/src/api/account/CreateSession"; +import { + CreateSession, + CreateSessionResponse, +} from "@storyteller/components/src/api/account/CreateSession"; +import { AppStateContext } from "components/providers/AppStateProvider"; interface Props { - onSuccess?: (x?:any) => any, - status: FetchStatus, - statusSet: (x: FetchStatus) => void, + onSuccess?: (x?: any) => any; + status: FetchStatus; + statusSet: (x: FetchStatus) => void; } export default function useLogin({ onSuccess, status, statusSet }: Props) { - const [errorType,errorTypeSet] = useState(""); - const { querySession, querySubscriptions } = useSession(); + const [errorType, errorTypeSet] = useState(""); + const { queryAppState } = useContext(AppStateContext); const { allAreValid, setProps, state, update } = useChanger({ password: { value: "" }, - usernameOrEmail: { value: "" } + usernameOrEmail: { value: "" }, }); const login = () => { errorTypeSet(""); statusSet(FetchStatus.in_progress); // if (allAreValid()) { - CreateSession("",{ - username_or_email: state.usernameOrEmail.value, - password: state.password.value, - }).then((res: CreateSessionResponse) => { - if (!res.success && res.error_type) { - statusSet(FetchStatus.error); - errorTypeSet(res.error_type); - } else if (res.success) { - statusSet(FetchStatus.success); - querySession(); - querySubscriptions(); - if (onSuccess) onSuccess(res); - } - }); - }; + CreateSession("", { + username_or_email: state.usernameOrEmail.value, + password: state.password.value, + }).then((res: CreateSessionResponse) => { + if (!res.success && res.error_type) { + statusSet(FetchStatus.error); + errorTypeSet(res.error_type); + } else if (res.success) { + statusSet(FetchStatus.success); + queryAppState(); + if (onSuccess) onSuccess(res); + } + }); + }; - return { allAreValid, errorType, errorTypeSet, login, setProps, state, update }; -}; \ No newline at end of file + return { + allAreValid, + errorType, + errorTypeSet, + login, + setProps, + state, + update, + }; +} diff --git a/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useSignup.tsx b/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useSignup.tsx index 0b92cf57d8..5b78856636 100644 --- a/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useSignup.tsx +++ b/src/website/packages/fakeyou.com/src/components/layout/AccountModal/useSignup.tsx @@ -1,23 +1,27 @@ -import { useChanger, useSession } from "hooks"; +import { useContext } from "react"; +import { useChanger } from "hooks"; import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes"; -import { CreateAccount, CreateAccountResponse } from "@storyteller/components/src/api/account/CreateAccount"; +import { + CreateAccount, + CreateAccountResponse, +} from "@storyteller/components/src/api/account/CreateAccount"; import { InputValidation, ValidatorCallbacks } from "common/InputValidation"; - +import { AppStateContext } from "components/providers/AppStateProvider"; enum EmailReasons { EmailTaken = "emailTaken", Invalid = "invalid", TooShort = "tooShort", - BadInput = "badInput" + BadInput = "badInput", } enum PasswordReasons { - TooShort = "tooShort" + TooShort = "tooShort", } enum PasswordConfirmReasons { PasswordMismatch = "passwordMismatch", - TooShort = "tooShort" + TooShort = "tooShort", } enum UsernameReasons { @@ -26,14 +30,14 @@ enum UsernameReasons { IsReserved = "isReserved", IsTaken = "isTaken", TooLong = "tooLong", - TooShort = "tooShort" + TooShort = "tooShort", } const emailErrors = { emailTaken: "Email is taken", invalid: "Email is invalid", tooShort: "Email is too short", - badInput: "Bad email input, try another" + badInput: "Bad email input, try another", }; const passwordErrors = { @@ -42,7 +46,7 @@ const passwordErrors = { const passwordConfirmErrors = { passwordMismatch: "Passwords do not match", - tooShort: "Password is too short" + tooShort: "Password is too short", }; const usernameErrors = { @@ -51,42 +55,45 @@ const usernameErrors = { isReserved: "Username is reserved", isTaken: "Username is taken", tooLong: "Username is too long", - tooShort: "Username is too short" + tooShort: "Username is too short", }; const emailValidator = ({ inputValue }: ValidatorCallbacks) => { let reason = ""; let validation = InputValidation.Neutral; - - if (inputValue.length > 1) { - if (inputValue.length < 3) { - validation = InputValidation.Invalid; - reason = EmailReasons.TooShort; - } else if (!inputValue.includes("@")) { - validation = InputValidation.Invalid; - reason = EmailReasons.Invalid; - } else { - validation = InputValidation.Valid; - } + + if (inputValue.length > 1) { + if (inputValue.length < 3) { + validation = InputValidation.Invalid; + reason = EmailReasons.TooShort; + } else if (!inputValue.includes("@")) { + validation = InputValidation.Invalid; + reason = EmailReasons.Invalid; + } else { + validation = InputValidation.Valid; } + } return { value: inputValue, reason, validation }; }; -const passwordConfirmValidator = ({ inputValue, state }: ValidatorCallbacks) => { +const passwordConfirmValidator = ({ + inputValue, + state, +}: ValidatorCallbacks) => { const password = state.password; let validation = InputValidation.Neutral; let reason = ""; - if (inputValue.length > 1) { - if (inputValue !== password.value) { - validation = InputValidation.Invalid; - reason = PasswordConfirmReasons.PasswordMismatch; - } else { - validation = InputValidation.Valid; - reason = ""; - } + if (inputValue.length > 1) { + if (inputValue !== password.value) { + validation = InputValidation.Invalid; + reason = PasswordConfirmReasons.PasswordMismatch; + } else { + validation = InputValidation.Valid; + reason = ""; } + } return { value: inputValue, reason, validation }; }; @@ -108,14 +115,20 @@ const passwordValidator = ({ inputValue, state }: ValidatorCallbacks) => { if (inputValue !== passwordConfirm.value) { confirmValidation = InputValidation.Invalid; - confirmReason = PasswordConfirmReasons.PasswordMismatch + confirmReason = PasswordConfirmReasons.PasswordMismatch; } else { confirmValidation = InputValidation.Valid; confirmReason = ""; } } - const additional = { passwordConfirm: { ...passwordConfirm, reason: confirmReason, validation: confirmValidation } } + const additional = { + passwordConfirm: { + ...passwordConfirm, + reason: confirmReason, + validation: confirmValidation, + }, + }; return { additional, value: inputValue, reason, validation }; }; @@ -123,14 +136,14 @@ const passwordValidator = ({ inputValue, state }: ValidatorCallbacks) => { const usernameValidator = ({ inputValue }: ValidatorCallbacks) => { let reason = ""; let validation = InputValidation.Neutral; - + if (inputValue.length > 1) { if (inputValue.length < 3) { validation = InputValidation.Invalid; - reason = UsernameReasons.TooShort + reason = UsernameReasons.TooShort; } else if (inputValue.length > 15) { validation = InputValidation.Invalid; - reason = UsernameReasons.TooLong + reason = UsernameReasons.TooLong; } else { validation = InputValidation.Valid; } @@ -140,73 +153,98 @@ const usernameValidator = ({ inputValue }: ValidatorCallbacks) => { }; interface Props { - onSuccess?: (x?:any) => any, - status: FetchStatus, - statusSet: (x: FetchStatus) => void, + onSuccess?: (x?: any) => any; + status: FetchStatus; + statusSet: (x: FetchStatus) => void; } export default function useSignup({ onSuccess, status, statusSet }: Props) { - const { querySession } = useSession(); + const { queryAppState } = useContext(AppStateContext); const { allAreValid, setProps, state, update } = useChanger({ email: { errorText: emailErrors, validator: emailValidator, - value: "" + value: "", }, password: { errorText: passwordErrors, validator: passwordValidator, - value: "" + value: "", }, passwordConfirm: { errorText: passwordConfirmErrors, validator: passwordConfirmValidator, - value: "" + value: "", }, username: { errorText: usernameErrors, validator: usernameValidator, - value: "" + value: "", }, }); const signup = () => { statusSet(FetchStatus.in_progress); - CreateAccount("",{ + CreateAccount("", { username: state.username.value, email_address: state.email.value, password: state.password.value, password_confirmation: state.passwordConfirm.value, }).then((res: CreateAccountResponse) => { if (res && res.error_fields && res.error_type) { - statusSet(FetchStatus.error); + statusSet(FetchStatus.error); if (res.error_fields.email_address) { switch (res.error_type) { - case "BadInput": return update({ name: "email", reason: EmailReasons.BadInput, validation: InputValidation.Invalid }); - case "EmailTaken": return update({ name: "email", reason: EmailReasons.EmailTaken, validation: InputValidation.Invalid }); + case "BadInput": + return update({ + name: "email", + reason: EmailReasons.BadInput, + validation: InputValidation.Invalid, + }); + case "EmailTaken": + return update({ + name: "email", + reason: EmailReasons.EmailTaken, + validation: InputValidation.Invalid, + }); } return; } else if (res.error_fields.password) { switch (res.error_type) { - case "TooShort": update({ name: "password", reason: PasswordReasons.TooShort, validation: InputValidation.Invalid }); - } + case "TooShort": + update({ + name: "password", + reason: PasswordReasons.TooShort, + validation: InputValidation.Invalid, + }); + } } else if (res.error_fields?.username) { - let updateUsername = (reason: UsernameReasons) => update({ reason, name: "username", validation: InputValidation.Invalid }); + let updateUsername = (reason: UsernameReasons) => + update({ + reason, + name: "username", + validation: InputValidation.Invalid, + }); switch (res.error_fields.username) { - case "invalid username characters": return updateUsername(UsernameReasons.InvalidCharacters); - case "username is too long": return updateUsername(UsernameReasons.TooLong); - case "username is taken": return updateUsername(UsernameReasons.IsTaken); - case "username is reserved": return updateUsername(UsernameReasons.IsReserved); - case "username contains slurs": return updateUsername(UsernameReasons.ContainsSlurs); - } + case "invalid username characters": + return updateUsername(UsernameReasons.InvalidCharacters); + case "username is too long": + return updateUsername(UsernameReasons.TooLong); + case "username is taken": + return updateUsername(UsernameReasons.IsTaken); + case "username is reserved": + return updateUsername(UsernameReasons.IsReserved); + case "username contains slurs": + return updateUsername(UsernameReasons.ContainsSlurs); + } } } else if (res.success) { statusSet(FetchStatus.success); - querySession(); + queryAppState(); if (onSuccess) onSuccess(res); - } + } }); }; return { allAreValid, setProps, signup, state, update }; -}; \ No newline at end of file +} diff --git a/src/website/packages/fakeyou.com/src/components/layout/TopNav/TopNav.tsx b/src/website/packages/fakeyou.com/src/components/layout/TopNav/TopNav.tsx index 39d9316a4b..22dd32fea0 100644 --- a/src/website/packages/fakeyou.com/src/components/layout/TopNav/TopNav.tsx +++ b/src/website/packages/fakeyou.com/src/components/layout/TopNav/TopNav.tsx @@ -37,8 +37,7 @@ import { GetDiscordLink } from "@storyteller/components/src/env/GetDiscordLink"; import { WebUrl } from "common/WebUrl"; export default function TopNav() { - const { querySession, querySubscriptions, sessionWrapper, user } = - useSession(); + const { queryAppState, sessionWrapper, user } = useSession(); const domain = useDomainConfig(); let history = useHistory(); const [isMobileSearchBarVisible, setIsMobileSearchBarVisible] = @@ -141,8 +140,7 @@ export default function TopNav() { const logoutHandler = async () => { await Logout(); - querySession(); - querySubscriptions(); + queryAppState(); history.push("/"); }; @@ -152,8 +150,8 @@ export default function TopNav() { let displayName = user.display_name; let username = user.username; let emailHash = user.email_gravatar_hash; - let avatarIndex = user.core_info.default_avatar.image_index; - let backgroundColorIndex = user.core_info.default_avatar.color_index; + let avatarIndex = user.core_info?.default_avatar?.image_index || 0; + let backgroundColorIndex = user.core_info?.default_avatar?.color_index || 0; profileDropdown = ( void; +} + +export const emptyAppState: GetAppStateResponse = { + is_banned: false, + is_logged_in: false, + locale: { + full_language_tags: ["en-US", "en"], + language_codes: ["en", "en"], + }, + maybe_alert: null, + maybe_premium: null, + maybe_user_info: null, + permissions: { + feature_flags: [], + is_moderator: false, + legacy_permission_flags: { + can_approve_w2l_templates: false, + can_ban_users: false, + can_delete_other_users_tts_models: false, + can_delete_other_users_tts_results: false, + can_delete_other_users_w2l_results: false, + can_delete_other_users_w2l_templates: false, + can_delete_own_account: false, + can_delete_own_tts_models: false, + can_delete_own_tts_results: false, + can_delete_own_w2l_results: false, + can_delete_own_w2l_templates: false, + can_delete_users: false, + can_edit_other_users_profiles: false, + can_edit_other_users_tts_models: false, + can_edit_other_users_w2l_templates: false, + can_upload_tts_models: false, + can_upload_w2l_templates: false, + can_use_tts: false, + can_use_w2l: false, + }, + }, + server_info: { + build_sha: "", + build_sha_short: "", + hostname: "", + }, + success: false, + refresh_interval_millis: 60000, +}; + +const emptyState: CurrentAppState = { + appState: emptyAppState, + sessionWrapper: SessionWrapper.emptySession(), + sessionSubscriptions: SessionSubscriptionsWrapper.emptySubscriptions(), +}; + +export const AppStateContext = createContext({ + ...emptyState, + queryAppState: () => {}, +}); + +export default function AppStateProvider({ + children, + debug, +}: AppStatusProviderProps) { + const [stateAndWrappers, stateAndWrappersSet] = + useState(emptyState); + const [keepAlive, keepAliveSet] = useState(true); + + const onTick = () => { + if (debug) { + console.log("💾 App State Loop", stateAndWrappers); + } + GetAppState("", {}).then((res: GetAppStateResponse) => { + if (res.success) { + if (debug) { + console.log("App State fetch success ✅", res); + } + + // compare response and state without server info as server host can change + // that way we only write state when there are meaningful state updates + + const { server_info: unusedEventInfo, ...oldState } = + stateAndWrappers.appState; + const { server_info: unusedResInfo, ...updatedState } = res; + const isUpdated = !deepEqual(oldState, updatedState); + + if (isUpdated) { + const newSessionWrapper = SessionWrapper.wrapResponse({ + success: true, + logged_in: res.is_logged_in, + ...(res.is_logged_in && res.maybe_user_info && res.permissions + ? { + user: { + user_token: res.maybe_user_info.user_token, + username: res.maybe_user_info.username, + display_name: res.maybe_user_info.display_name, + email_gravatar_hash: res.maybe_user_info.gravatar_hash, + can_access_studio: + res.permissions.feature_flags.includes("studio"), + maybe_feature_flags: res.permissions.feature_flags, + ...res.permissions.legacy_permission_flags, + }, + } + : { user: null }), + }); + + const newSubsciptions = SessionSubscriptionsWrapper.wrapResponse({ + success: true, + active_subscriptions: res.maybe_premium + ? res.maybe_premium.active_subscriptions + : [], + ...(res.maybe_premium?.maybe_loyalty_program + ? { + maybe_loyalty_program: + res.maybe_premium.maybe_loyalty_program, + } + : {}), + }); + + const cookies = new Cookies(); + + stateAndWrappersSet({ + appState: res, + sessionWrapper: newSessionWrapper, + sessionSubscriptions: newSubsciptions, + }); + + keepAliveSet(false); + + if (res.maybe_user_info) { + if (debug) { + console.log("🌻 Logged in"); + } + PosthogClient.enablePosthog(); + PosthogClient.setUsername(res.maybe_user_info.username); + cookies.set("logged_in_username", res.maybe_user_info.username, { + path: "/", + expires: new Date(Date.now() + 3 * 86400000), + }); + // @ts-ignore + window.dataLayer.push({ + user_id: res.maybe_user_info.username, + }); + } else { + if (debug) { + console.log("🥀 Logged out", res); + } + cookies.remove("logged_in_username", { path: "/" }); + } + + // this is just a copy/paste of code from App.tsx. I'll probably rewrite it whem I understand it more -V + + let preferredLanguage = ENGLISH_LANGUAGE; + + for (let languageCode of res.locale.language_codes) { + let maybeLanguage = + AVAILABLE_LANGUAGE_MAP[languageCode as AvailableLanguageKey]; + + if (maybeLanguage !== undefined) { + preferredLanguage = maybeLanguage; + break; + } + } + + i18n2.changeLanguage(preferredLanguage.languageCode); + } + } + }); + }; + + const queryAppState = () => { + if (debug) { + console.log("❓ Manual App State query"); + } + onTick(); + }; + + useInterval({ + debug, + interval: stateAndWrappers.appState.refresh_interval_millis, + onTick, + locked: !keepAlive, + }); + + useEffect(() => { + if (!keepAlive) { + keepAliveSet(true); + } + }, [keepAlive]); + + return ( + + {children} + + ); +} diff --git a/src/website/packages/fakeyou.com/src/components/providers/AppStateProvider/index.tsx b/src/website/packages/fakeyou.com/src/components/providers/AppStateProvider/index.tsx new file mode 100644 index 0000000000..7fc1f00150 --- /dev/null +++ b/src/website/packages/fakeyou.com/src/components/providers/AppStateProvider/index.tsx @@ -0,0 +1 @@ +export { default, AppStateContext, emptyAppState } from "./AppStateProvider"; diff --git a/src/website/packages/fakeyou.com/src/components/providers/CoreServicesProvider/CoreServicesProvider.tsx b/src/website/packages/fakeyou.com/src/components/providers/CoreServicesProvider/CoreServicesProvider.tsx index 333ce5ed21..a325ae3016 100644 --- a/src/website/packages/fakeyou.com/src/components/providers/CoreServicesProvider/CoreServicesProvider.tsx +++ b/src/website/packages/fakeyou.com/src/components/providers/CoreServicesProvider/CoreServicesProvider.tsx @@ -1,5 +1,6 @@ import React from "react"; import { + AppStateProvider, InferenceJobsProvider, ModalProvider, NotificationProvider, @@ -10,37 +11,23 @@ import ServerStatusChecker from "./ServerStatusChecker"; interface Props { children?: any; - querySession: any; - querySubscriptions: any; - state: any; } -export default function CoreServicesProvider({ - children, - querySession, - querySubscriptions, - state, -}: Props) { - const sessionProps = { - querySession: querySession, - querySubscriptions, - sessionSubscriptions: state.sessionSubscriptionsWrapper, - sessionWrapper: state.sessionWrapper, - sessionFetched: state.sessionFetched, - }; - +export default function CoreServicesProvider({ children }: Props) { return ( - - - - - - - {children} - - - - - + + + + + + + + {children} + + + + + + ); } diff --git a/src/website/packages/fakeyou.com/src/components/providers/SessionProvider/SessionProvider.tsx b/src/website/packages/fakeyou.com/src/components/providers/SessionProvider/SessionProvider.tsx index 09cdc4a793..a9f71a7beb 100644 --- a/src/website/packages/fakeyou.com/src/components/providers/SessionProvider/SessionProvider.tsx +++ b/src/website/packages/fakeyou.com/src/components/providers/SessionProvider/SessionProvider.tsx @@ -1,4 +1,4 @@ -import React, { createContext } from "react"; +import React, { createContext, useContext } from "react"; import ModalLayer from "components/providers/ModalProvider/ModalLayer"; import { ModalConfig, useModalState } from "hooks"; import AccountModal from "components/layout/AccountModal"; @@ -7,6 +7,11 @@ import { StudioRolloutHostnameAllowed } from "@storyteller/components/src/utils/ import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper"; import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; import { StyleVideoNotAvailable } from "v2/view/_common/StyleVideoNotAvailable"; +import { GetAppStateResponse } from "@storyteller/components/src/api/app_state/GetAppState"; +import { + AppStateContext, + emptyAppState, +} from "components/providers/AppStateProvider"; export interface AccountModalMessages { loginMessage?: string; @@ -19,11 +24,11 @@ export interface AccountModalEvents { } export interface SessionUtilities { - querySession: () => void; - querySubscriptions: () => void; + queryAppState: () => void; } interface SessionContextType extends SessionUtilities { + appState: GetAppStateResponse; canAccessStudio: () => boolean; canEditTtsModel: (creatorUserToken: string) => boolean; canEditMediaFile: (creatorUserToken?: string) => boolean; @@ -46,11 +51,8 @@ interface SessionContextType extends SessionUtilities { userTokenMatch: (token: string) => boolean; } -interface SessionProviderProps extends SessionUtilities { +interface SessionProviderProps { children?: any; - sessionFetched: boolean; - sessionSubscriptions: SessionSubscriptionsWrapper; - sessionWrapper: SessionWrapper; } // Functions are initially No-ops/dummies so that they are never undefined and never have to be called conditionally. @@ -60,6 +62,7 @@ interface SessionProviderProps extends SessionUtilities { // const thingy = new SessionWrapper() export const SessionContext = createContext({ + appState: emptyAppState, canAccessStudio: () => false, canEditTtsModel: () => false, canEditMediaFile: () => false, @@ -74,19 +77,14 @@ export const SessionContext = createContext({ open: () => {}, }, userTokenMatch: () => false, - querySession: () => {}, - querySubscriptions: () => {}, + queryAppState: () => {}, sessionWrapper: SessionWrapper.emptySession(), }); -export default function SessionProvider({ - children, - querySession, - querySubscriptions, - sessionFetched, - sessionSubscriptions, - sessionWrapper, -}: SessionProviderProps) { +export default function SessionProvider({ children }: SessionProviderProps) { + const { appState, sessionSubscriptions, sessionWrapper, queryAppState } = + useContext(AppStateContext); + const sessionResponse = sessionWrapper?.sessionStateResponse || { logged_in: false, user: null, @@ -143,6 +141,7 @@ export default function SessionProvider({ ({}); - const [initialized,initializedSet] = useState(false); - const [downAlerted,downAlertedSet] = useState(false); - const [upAlerted,upAlertedSet] = useState(false); + const [downAlerted, downAlertedSet] = useState(false); + const [upAlerted, upAlertedSet] = useState(false); const notifications = useNotifications(); - const defaultInt = 60000; - const interval = Math.max(defaultInt, serverStatus.refresh_interval_millis || defaultInt); - const onTick = () => { - StatusAlertCheck("",{}) - .then((res: StatusAlertCheckResponse) => { - serverStatusSet(res); - }); - }; + const { + appState: { maybe_alert }, + } = useContext(AppStateContext); useEffect(() => { - if (!initialized) { - initializedSet(true); - onTick(); - } - if (serverStatus.maybe_alert && !downAlerted) { + if (maybe_alert && !downAlerted) { downAlertedSet(true); upAlertedSet(false); notifications.create({ autoRemove: false, title: "Server down for maintenence", - content: "Please check back shortly" + content: "Please check back shortly", }); - } else if (!serverStatus.maybe_alert && downAlerted && !upAlerted) { + } else if (!maybe_alert && downAlerted && !upAlerted) { upAlertedSet(true); downAlertedSet(false); - notifications.create({ autoRemove: false, title: "Server is back online", }); + notifications.create({ + autoRemove: false, + title: "Server is back online", + }); } - },[downAlerted,initialized,notifications,serverStatus,upAlerted]); + }, [downAlerted, notifications, maybe_alert, upAlerted]); - useInterval({ interval, onTick, locked: !initialized, }); - - return serverStatus; -}; \ No newline at end of file + return maybe_alert; +} diff --git a/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx b/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx index 9cdf4b4aa8..c8450e2b3f 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx @@ -1,6 +1,5 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useContext } from "react"; import { GitSha } from "@storyteller/components/src/elements/GitSha"; -import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper"; import { Link, useLocation } from "react-router-dom"; import { ModerationIcon } from "../_icons/ModerationIcon"; import { WebUrl } from "../../../common/WebUrl"; @@ -13,39 +12,20 @@ import { faRedditAlien, } from "@fortawesome/free-brands-svg-icons"; import { ThirdPartyLinks } from "@storyteller/components/src/constants/ThirdPartyLinks"; -import { - GetServerInfo, - GetServerInfoIsOk, - GetServerInfoSuccessResponse, -} from "@storyteller/components/src/api/server/GetServerInfo"; import { useLocalize } from "hooks"; import { Container } from "components/common"; import { useDomainConfig } from "context/DomainConfigContext"; import { GetDiscordLink } from "@storyteller/components/src/env/GetDiscordLink"; +import { AppStateContext } from "components/providers/AppStateProvider"; -interface Props { - sessionWrapper: SessionWrapper; -} - -function FooterNav(props: Props) { +function FooterNav() { + const { + appState: { server_info }, + sessionWrapper, + } = useContext(AppStateContext); const domain = useDomainConfig(); const { t } = useLocalize("Footer"); - const [serverInfo, setServerInfo] = useState< - GetServerInfoSuccessResponse | undefined - >(undefined); const location = useLocation(); - - const getServerInfo = useCallback(async () => { - const response = await GetServerInfo(); - if (GetServerInfoIsOk(response)) { - setServerInfo(response); - } - }, []); - - useEffect(() => { - getServerInfo(); - }, [getServerInfo]); - //let myDataLink = WebUrl.signupPage(); //if (props.sessionWrapper.isLoggedIn()) { @@ -55,7 +35,7 @@ function FooterNav(props: Props) { let moderationLink = ; - if (props.sessionWrapper.canBanUsers()) { + if (sessionWrapper.canBanUsers()) { moderationLink = (
@@ -69,15 +49,13 @@ function FooterNav(props: Props) { let serverGitSha = <>; if ( - serverInfo !== undefined && - !!serverInfo.server_build_sha && - serverInfo.server_build_sha !== "undefined" + server_info !== undefined && + !!server_info.build_sha_short && + server_info.build_sha_short !== "undefined" ) { serverGitSha = (
-
- API: {serverInfo.server_build_sha.substring(0, 8)} -
+
API: {server_info.build_sha_short}
); } diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx index 60170b7bcb..2ee1dd55c3 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/pages/login/LoginPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; import { CreateSession, @@ -18,7 +18,7 @@ import Panel from "components/common/Panel"; import { useDomainConfig } from "context/DomainConfigContext"; import ScrollingSceneCarousel from "../landing/storyteller/PostlaunchLanding/ScrollingSceneCarousel"; import { InjectScript } from "common/InjectScript"; -import { useSession } from "hooks"; +import { AppStateContext } from "components/providers/AppStateProvider"; // NB: Google Sign In requires a global javascript function declare global { @@ -28,7 +28,7 @@ declare global { function LoginPage() { let history = useHistory(); const domain = useDomainConfig(); - const { querySession, querySubscriptions, sessionWrapper } = useSession(); + const { sessionWrapper, queryAppState } = useContext(AppStateContext); const [password, setPassword] = useState(""); const [usernameOrEmail, setUsernameOrEmail] = useState(""); const [errorMessage, setErrorMessage] = useState(""); @@ -80,8 +80,7 @@ function LoginPage() { if (CreateSessionIsError(response)) { setErrorMessage(response.error_message); } else if (CreateSessionIsSuccess(response)) { - querySession(); - querySubscriptions(); + queryAppState(); Analytics.accountLoginSuccess(); history.push(redirectUrl); } diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx index 29322680f5..948be15415 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/pages/password_reset/PasswordResetVerificationPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useHistory } from "react-router-dom"; import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle"; import Container from "components/common/Container"; @@ -11,7 +11,7 @@ import { RedeemResetPassword, RedeemResetPasswordIsSuccess, } from "@storyteller/components/src/api/user/RedeemResetPassword"; -import { useSession } from "hooks"; +import { AppStateContext } from "components/providers/AppStateProvider"; // TODO(bt,2023-11-12): Localize error messages const ERR_CODE_NOT_SET = "password reset code is not set"; @@ -23,7 +23,7 @@ const ERR_BACKEND = function PasswordResetVerificationPage() { const history = useHistory(); - const { querySession, querySubscriptions, sessionWrapper } = useSession(); + const { sessionWrapper, queryAppState } = useContext(AppStateContext); usePrefixedDocumentTitle("Password Reset Verification"); @@ -130,8 +130,7 @@ function PasswordResetVerificationPage() { if (RedeemResetPasswordIsSuccess(response)) { setBackendError(undefined); - querySession(); - querySubscriptions(); + queryAppState(); history.push("/"); } else { setBackendError(ERR_BACKEND); diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/signup/SignupPage.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/signup/SignupPage.tsx index 8ca40cacee..eecff9b774 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/pages/signup/SignupPage.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/pages/signup/SignupPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { t } from "i18next"; import { Trans } from "react-i18next"; import { @@ -19,7 +19,7 @@ import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClie import Panel from "components/common/Panel"; import { useDomainConfig } from "context/DomainConfigContext"; import ScrollingSceneCarousel from "../landing/storyteller/PostlaunchLanding/ScrollingSceneCarousel"; -import { useSession } from "hooks"; +import { AppStateContext } from "components/providers/AppStateProvider"; enum FieldTriState { EMPTY_FALSE, @@ -31,7 +31,7 @@ function SignupPage() { let history = useHistory(); const domain = useDomainConfig(); let location = useLocation(); - const { querySession, querySubscriptions, sessionWrapper } = useSession(); + const { sessionWrapper, queryAppState } = useContext(AppStateContext); const queryParams = new URLSearchParams(location.search); const parsedQueryString = queryString.parse(window.location.search); @@ -242,8 +242,7 @@ function SignupPage() { setUsernameInvalidReason(reason); } } else if (CreateAccountIsSuccess(response)) { - querySession(); - querySubscriptions(); + queryAppState(); Analytics.accountSignupComplete(); From 278e423117f1a02bb959156603cb72829b3aacd0 Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 24 Sep 2024 14:51:37 -0700 Subject: [PATCH 2/2] fixed comment form crash from undefined user "core info" object. Will replace --- .../src/v2/view/_common/comments/CreateCommentForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/website/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentForm.tsx b/src/website/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentForm.tsx index b83bb2bbbd..4131788afa 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentForm.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentForm.tsx @@ -92,8 +92,8 @@ function CreateCommentForm(props: Props) { let gravatar = ; let username = user?.username; let emailHash = user?.email_gravatar_hash; - let avatarIndex = user?.core_info.default_avatar.image_index; - let backgroundColorIndex = user?.core_info.default_avatar.color_index; + let avatarIndex = user?.core_info?.default_avatar.image_index || 0; + let backgroundColorIndex = user?.core_info?.default_avatar.color_index || 0; if (gravatarHash !== undefined) { gravatar = (