Refactor user profile handling by removing deprecated fields and improving link management

This commit is contained in:
MartinBraquet
2026-02-17 22:02:47 +01:00
parent c8b7b85391
commit 07ce2780c6
5 changed files with 42 additions and 86 deletions

View File

@@ -1,14 +1,14 @@
import { toUserAPIResponse } from 'common/api/user-types'
import { RESERVED_PATHS } from 'common/envs/constants'
import { cleanDisplayName, cleanUsername } from 'common/util/clean-username'
import { removeUndefinedProps } from 'common/util/object'
import { cloneDeep, mapValues } from 'lodash'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { getUser, getUserByUsername } from 'shared/utils'
import { APIError, APIHandler } from './helpers/endpoint'
import { updateUser } from 'shared/supabase/users'
import { broadcastUpdatedUser } from 'shared/websockets/helpers'
import { strip } from 'common/socials'
import {toUserAPIResponse} from 'common/api/user-types'
import {RESERVED_PATHS} from 'common/envs/constants'
import {cleanDisplayName, cleanUsername} from 'common/util/clean-username'
import {removeUndefinedProps} from 'common/util/object'
import {cloneDeep, mapValues} from 'lodash'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {getUser, getUserByUsername} from 'shared/utils'
import {APIError, APIHandler} from './helpers/endpoint'
import {updateUser} from 'shared/supabase/users'
import {broadcastUpdatedUser} from 'shared/websockets/helpers'
import {strip} from 'common/socials'
export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
const update = cloneDeep(props)
@@ -35,10 +35,6 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
const { name, username, avatarUrl, link = {}, ...rest } = update
await updateUser(pg, auth.uid, removeUndefinedProps(rest))
if (update.website != undefined) link.site = update.website
if (update.twitterHandle != undefined) link.x = update.twitterHandle
if (update.discordHandle != undefined) link.discord = update.discordHandle
const stripped = mapValues(
link,
(value, site) => value && strip(site as any, value)
@@ -69,23 +65,26 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
newLinks = data?.link
}
if (name || username || avatarUrl) {
if (name) {
await pg.none(`update users set name = $1 where id = $2`, [
name,
auth.uid,
])
}
if (username) {
await pg.none(`update users set username = $1 where id = $2`, [
username,
auth.uid,
])
}
if (avatarUrl) {
await updateUser(pg, auth.uid, { avatarUrl })
}
if (name) {
await pg.none(`update users
set name = $1
where id = $2`, [name, auth.uid])
}
if (username) {
await pg.none(`update users
set username = $1
where id = $2`, [
username,
auth.uid,
])
}
if (avatarUrl) {
await updateUser(pg, auth.uid, {avatarUrl})
}
// Ensure clients listening on `user/{id}` (e.g. AuthContext via useWebsocketUser)
// get notified about link-only changes as well.
if (name || username || avatarUrl || newLinks != null) {
broadcastUpdatedUser(
removeUndefinedProps({
id: auth.uid,

View File

@@ -8,8 +8,8 @@ jest.mock('shared/supabase/users');
jest.mock('shared/websockets/helpers');
jest.mock('common/envs/constants');
import { updateMe } from "api/update-me";
import { toUserAPIResponse } from "common/api/user-types";
import {updateMe} from "api/update-me";
import {toUserAPIResponse} from "common/api/user-types";
import * as cleanUsernameModules from "common/util/clean-username";
import * as supabaseInit from "shared/supabase/init";
import * as objectUtils from "common/util/object";
@@ -17,7 +17,7 @@ import * as lodashModules from "lodash";
import * as sharedUtils from "shared/utils";
import * as supabaseUsers from "shared/supabase/users";
import * as websocketHelperModules from "shared/websockets/helpers";
import { AuthedUser } from "api/helpers/endpoint";
import {AuthedUser} from "api/helpers/endpoint";
describe('updateMe', () => {
let mockPg = {} as any;
@@ -44,12 +44,7 @@ describe('updateMe', () => {
name: "mockName",
username: "mockUsername",
avatarUrl: "mockAvatarUrl",
bio: "mockBio",
link: {"mockLink" : "mockLinkValue"},
optOutBetWarnings:true,
website: "mockWebsite",
twitterHandle: "mockTwitterHandle",
discordHandle: "mockDiscordHandle",
};
const mockStripped = {
bio: "mockBio"
@@ -152,12 +147,7 @@ describe('updateMe', () => {
name: "mockName",
username: "mockUsername",
avatarUrl: "mockAvatarUrl",
bio: "mockBio",
link: {"mockLink" : "mockLinkValue"},
optOutBetWarnings:true,
website: "mockWebsite",
twitterHandle: "mockTwitterHandle",
discordHandle: "mockDiscordHandle",
};
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
@@ -176,12 +166,7 @@ describe('updateMe', () => {
name: "mockName",
username: "mockUsername",
avatarUrl: "mockAvatarUrl",
bio: "mockBio",
link: {"mockLink" : "mockLinkValue"},
optOutBetWarnings:true,
website: "mockWebsite",
twitterHandle: "mockTwitterHandle",
discordHandle: "mockDiscordHandle",
};
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
@@ -202,12 +187,7 @@ describe('updateMe', () => {
name: "mockName",
username: "mockUsername",
avatarUrl: "mockAvatarUrl",
bio: "mockBio",
link: {"mockLink" : "mockLinkValue"},
optOutBetWarnings:true,
website: "mockWebsite",
twitterHandle: "mockTwitterHandle",
discordHandle: "mockDiscordHandle",
};
const arrySpy = jest.spyOn(Array.prototype, 'includes');
@@ -231,12 +211,7 @@ describe('updateMe', () => {
name: "mockName",
username: "mockUsername",
avatarUrl: "mockAvatarUrl",
bio: "mockBio",
link: {"mockLink" : "mockLinkValue"},
optOutBetWarnings:true,
website: "mockWebsite",
twitterHandle: "mockTwitterHandle",
discordHandle: "mockDiscordHandle",
};
const arrySpy = jest.spyOn(Array.prototype, 'includes');

View File

@@ -208,23 +208,7 @@ export const API = (_apiTypeCheck = {
name: z.string().trim().min(1).optional(),
username: z.string().trim().min(1).optional(),
avatarUrl: z.string().optional(),
bio: z.string().optional(),
link: z.record(z.string().nullable()).optional(),
// settings
optOutBetWarnings: z.boolean().optional(),
isAdvancedTrader: z.boolean().optional(),
//internal
shouldShowWelcome: z.boolean().optional(),
hasSeenContractFollowModal: z.boolean().optional(),
hasSeenLoanModal: z.boolean().optional(),
// Legacy fields (deprecated)
/** @deprecated Use links.site instead */
website: z.string().optional(),
/** @deprecated Use links.x instead */
twitterHandle: z.string().optional(),
/** @deprecated Use links.discord instead */
discordHandle: z.string().optional(),
}),
returns: {} as FullUser,
summary: 'Update authenticated user profile and settings',

View File

@@ -90,9 +90,7 @@ export const OptionalProfileUserForm = (props: {
setHeightInches(Math.round(h % 12))
}
const [newLinks, setNewLinks] = useState<Record<string, string | null>>(
user.link
)
const [newLinks, setNewLinks] = useState<Record<string, string | null>>(user.link)
const [newLinkPlatform, setNewLinkPlatform] = useState('')
const [newLinkValue, setNewLinkValue] = useState('')
@@ -201,7 +199,7 @@ export const OptionalProfileUserForm = (props: {
i++
}
if (profile) {
await sleep(5000)
await sleep(5000) // attempt to mitigate profile not found at /${username} upon creation
router.push(`/${user.username}${fromSignup ? '?fromSignup=true' : ''}`)
} else {
console.log("Profile not found after fetching, going back home...")

View File

@@ -1,10 +1,10 @@
'use client'
import { PrivateUser, User } from 'common/user'
import { useContext, useEffect, useState } from 'react'
import { AuthContext } from 'web/components/auth-context'
import { useIsPageVisible } from './use-page-visible'
import { useApiSubscription } from './use-api-subscription'
import { getFullUserById, getPrivateUserSafe } from 'web/lib/supabase/users'
import {PrivateUser, User} from 'common/user'
import {useContext, useEffect, useState} from 'react'
import {AuthContext} from 'web/components/auth-context'
import {useIsPageVisible} from './use-page-visible'
import {useApiSubscription} from './use-api-subscription'
import {getFullUserById, getPrivateUserSafe} from 'web/lib/supabase/users'
export const useUser = () => {
const authUser = useContext(AuthContext)
@@ -29,7 +29,7 @@ export const useWebsocketUser = (userId: string | undefined) => {
useApiSubscription({
topics: [`user/${userId ?? '_'}`],
onBroadcast: ({ data }) => {
console.debug(data)
console.debug('User broadcast', {data})
setUser((user) => {
if (!user || !data.user) {
return user