Skip to content

Commit b13a40f

Browse files
committed
Use persistent in storage for hidden profiles and allow toggle hidden profile
1 parent a86f841 commit b13a40f

4 files changed

Lines changed: 137 additions & 113 deletions

File tree

web/components/profile/profile-header.tsx

Lines changed: 65 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export default function ProfileHeader(props: {
5959
<Row className="items-center gap-1">
6060
<Col className="gap-1">
6161
{currentUser && isCurrentUser && disabled &&
62-
<div className="text-red-500">{t('profile.header.disabled_notice', 'You disabled your profile, so no one else can access it.')}</div>}
62+
<div
63+
className="text-red-500">{t('profile.header.disabled_notice', 'You disabled your profile, so no one else can access it.')}</div>}
6364
<Row className="items-center gap-1 text-xl">
6465
{/*{!isCurrentUser && <OnlineIcon last_online_time={userActivity?.last_online_time}/>}*/}
6566
<span>
@@ -83,73 +84,73 @@ export default function ProfileHeader(props: {
8384
username={user.username}
8485
/>
8586
<Tooltip text={t('more_options_user.edit_profile', 'Edit profile')} noTap>
86-
<Button
87-
color={'gray-outline'}
88-
onClick={() => {
89-
track('editprofile')
90-
Router.push('profile')
91-
}}
92-
size="sm"
93-
>
94-
<PencilIcon className=" h-4 w-4"/>
95-
</Button>
87+
<Button
88+
color={'gray-outline'}
89+
onClick={() => {
90+
track('editprofile', {userId: user.id})
91+
Router.push('profile')
92+
}}
93+
size="sm"
94+
>
95+
<PencilIcon className=" h-4 w-4"/>
96+
</Button>
9697
</Tooltip>
9798

9899
<Tooltip text={t('more_options_user.profile_options', 'Profile options')} noTap>
99-
<DropdownMenu
100-
menuWidth={'w-52'}
101-
icon={
102-
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true"/>
103-
}
104-
items={[
105-
{
106-
name:
107-
profile.visibility === 'member'
108-
? t('profile.header.menu.list_public', 'List Profile Publicly')
109-
: t('profile.header.menu.limit_members', 'Limit to Members Only'),
110-
icon:
111-
profile.visibility === 'member' ? (
112-
<EyeIcon className="h-4 w-4"/>
113-
) : (
114-
<LockClosedIcon className="h-4 w-4"/>
115-
),
116-
onClick: () => setShowVisibilityModal(true),
117-
},
118-
{
119-
name: disabled ? t('profile.header.menu.enable_profile', 'Enable profile') : t('profile.header.menu.disable_profile', 'Disable profile'),
120-
icon: null,
121-
onClick: async () => {
122-
const confirmed = true // confirm(
123-
// 'Are you sure you want to disable your profile? This will hide your profile from searches and listings..'
124-
// )
125-
if (confirmed) {
126-
toast
127-
.promise(disableProfile(!disabled), {
128-
loading: disabled
129-
? t('profile.header.toast.enabling', 'Enabling profile...')
130-
: t('profile.header.toast.disabling', 'Disabling profile...'),
131-
success: () => {
132-
return disabled
133-
? t('profile.header.toast.enabled', 'Profile enabled')
134-
: t('profile.header.toast.disabled', 'Profile disabled')
135-
},
136-
error: () => {
137-
return disabled
138-
? t('profile.header.toast.failed_enable', 'Failed to enable profile')
139-
: t('profile.header.toast.failed_disable', 'Failed to disable profile')
140-
},
141-
})
142-
.then(() => {
143-
refreshProfile()
144-
})
145-
.catch(() => {
146-
// return false
147-
})
148-
}
100+
<DropdownMenu
101+
menuWidth={'w-52'}
102+
icon={
103+
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true"/>
104+
}
105+
items={[
106+
{
107+
name:
108+
profile.visibility === 'member'
109+
? t('profile.header.menu.list_public', 'List Profile Publicly')
110+
: t('profile.header.menu.limit_members', 'Limit to Members Only'),
111+
icon:
112+
profile.visibility === 'member' ? (
113+
<EyeIcon className="h-4 w-4"/>
114+
) : (
115+
<LockClosedIcon className="h-4 w-4"/>
116+
),
117+
onClick: () => setShowVisibilityModal(true),
149118
},
150-
},
151-
]}
152-
/>
119+
{
120+
name: disabled ? t('profile.header.menu.enable_profile', 'Enable profile') : t('profile.header.menu.disable_profile', 'Disable profile'),
121+
icon: null,
122+
onClick: async () => {
123+
const confirmed = true // confirm(
124+
// 'Are you sure you want to disable your profile? This will hide your profile from searches and listings..'
125+
// )
126+
if (confirmed) {
127+
toast
128+
.promise(disableProfile(!disabled), {
129+
loading: disabled
130+
? t('profile.header.toast.enabling', 'Enabling profile...')
131+
: t('profile.header.toast.disabling', 'Disabling profile...'),
132+
success: () => {
133+
return disabled
134+
? t('profile.header.toast.enabled', 'Profile enabled')
135+
: t('profile.header.toast.disabled', 'Profile disabled')
136+
},
137+
error: () => {
138+
return disabled
139+
? t('profile.header.toast.failed_enable', 'Failed to enable profile')
140+
: t('profile.header.toast.failed_disable', 'Failed to disable profile')
141+
},
142+
})
143+
.then(() => {
144+
refreshProfile()
145+
})
146+
.catch(() => {
147+
// return false
148+
})
149+
}
150+
},
151+
},
152+
]}
153+
/>
153154
</Tooltip>
154155
</Row>
155156
) : (

web/components/settings/hidden-profiles-modal.tsx

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,35 @@ import toast from 'react-hot-toast'
1010
import {useT} from 'web/lib/locale'
1111
import clsx from "clsx";
1212
import Link from "next/link";
13-
import {CompassLoadingIndicator} from "web/components/widgets/loading-indicator";
14-
15-
type HiddenUser = {
16-
id: string
17-
name: string
18-
username: string
19-
avatarUrl?: string | null
20-
createdTime?: string
21-
}
13+
import {useHiddenProfiles} from "web/hooks/use-hidden-profiles";
2214

2315
export function HiddenProfilesModal(props: {
2416
open: boolean
2517
setOpen: (open: boolean) => void
2618
}) {
2719
const {open, setOpen} = props
2820
const t = useT()
29-
const [loading, setLoading] = useState(false)
30-
const [error, setError] = useState<string | null>(null)
31-
const [hidden, setHidden] = useState<HiddenUser[] | null>(null)
3221
const [busyIds, setBusyIds] = useState<Record<string, boolean>>({})
22+
const {hiddenProfiles, refreshHiddenProfiles} = useHiddenProfiles()
3323

34-
const empty = useMemo(() => (hidden ? hidden.length === 0 : false), [hidden])
24+
const empty = useMemo(() => (hiddenProfiles ? hiddenProfiles.length === 0 : false), [hiddenProfiles])
3525

3626
useEffect(() => {
3727
if (!open) return
38-
let alive = true
39-
setLoading(true)
40-
setError(null)
41-
api('get-hidden-profiles', {limit: 200, offset: 0})
42-
.then((res) => {
43-
if (!alive) return
44-
setHidden(res.hidden)
45-
})
46-
.catch((e) => {
47-
console.error('Failed to load hidden profiles', e)
48-
if (!alive) return
49-
setError(t('settings.hidden_profiles.load_error', 'Failed to load hidden profiles.'))
50-
})
51-
.finally(() => alive && setLoading(false))
52-
return () => {
53-
alive = false
54-
}
28+
refreshHiddenProfiles()
5529
}, [open])
5630

5731
const unhide = async (userId: string) => {
5832
if (busyIds[userId]) return
5933
setBusyIds((b) => ({...b, [userId]: true}))
6034
try {
6135
await api('unhide-profile', {hiddenUserId: userId})
62-
setHidden((list) => (list ? list.filter((u) => u.id !== userId) : list))
36+
refreshHiddenProfiles()
6337
} catch (e) {
6438
console.error('Failed to unhide profile', e)
6539
toast.error(t('settings.hidden_profiles.unhide_failed', 'Failed to unhide'))
6640
} finally {
67-
setBusyIds((b) => ({...b, [userId]: false}))
41+
// setBusyIds((b) => ({...b, [userId]: false}))
6842
}
6943
}
7044

@@ -74,14 +48,9 @@ export function HiddenProfilesModal(props: {
7448
<Title className="mb-2">
7549
{t('settings.hidden_profiles.title', "Profiles you've hidden")}
7650
</Title>
77-
{!loading && hidden && hidden.length > 0 && <p>
78-
{t('settings.hidden_profiles.description', "These people don't appear in your search results.")}
79-
</p>}
80-
{loading && <CompassLoadingIndicator/>}
81-
{error && <div className="text-red-500 py-2">{error}</div>}
82-
{!loading && hidden && hidden.length > 0 && (
51+
{hiddenProfiles && hiddenProfiles.length > 0 && (
8352
<Col className={clsx("divide-y divide-canvas-300 w-full pr-4", SCROLLABLE_MODAL_CLASS)}>
84-
{hidden.map((u) => (
53+
{hiddenProfiles.map((u) => (
8554
<Row key={u.id} className="items-center justify-between py-2 gap-2">
8655
<Link
8756
className="w-full rounded-md hover:bg-canvas-100 p-2"
@@ -109,7 +78,7 @@ export function HiddenProfilesModal(props: {
10978
))}
11079
</Col>
11180
)}
112-
{!loading && empty && (
81+
{empty && (
11382
<div className="text-ink-500 py-6 text-center">
11483
{t(
11584
'settings.hidden_profiles.empty',

web/components/widgets/hide-profile-button.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import clsx from 'clsx'
2-
import {useState} from 'react'
2+
import {useMemo, useState} from 'react'
33
import {EyeIcon, EyeOffIcon} from '@heroicons/react/outline'
44
import {Tooltip} from 'web/components/widgets/tooltip'
55
import {api} from 'web/lib/api'
66
import {useT} from 'web/lib/locale'
7+
import {useHiddenProfiles} from "web/hooks/use-hidden-profiles";
78

89
export type HideProfileButtonProps = {
910
hiddenUserId: string
@@ -28,39 +29,55 @@ export function HideProfileButton(props: HideProfileButtonProps) {
2829

2930
const t = useT()
3031
const [submitting, setSubmitting] = useState(false)
31-
const [clicked, setClicked] = useState(false)
32+
const {hiddenProfiles, refreshHiddenProfiles} = useHiddenProfiles()
33+
const [optimisticHidden, setOptimisticHidden] = useState<boolean | undefined>(undefined)
34+
const hidden = useMemo(() => {
35+
if (optimisticHidden !== undefined) return optimisticHidden
36+
return hiddenProfiles?.some((u) => u.id === hiddenUserId) ?? false
37+
}, [hiddenProfiles, hiddenUserId, optimisticHidden])
3238

3339
const onClick = async (e: React.MouseEvent) => {
3440
e.preventDefault()
3541
if (stopPropagation) e.stopPropagation()
3642
if (submitting) return
3743
setSubmitting(true)
38-
setClicked(true)
44+
45+
// Optimistically update hidden state
46+
setOptimisticHidden(!hidden)
47+
3948
try {
40-
await api('hide-profile', {hiddenUserId})
49+
if (hidden) {
50+
await api('unhide-profile', {hiddenUserId})
51+
} else {
52+
await api('hide-profile', {hiddenUserId})
53+
}
54+
refreshHiddenProfiles()
4155
onHidden?.(hiddenUserId)
56+
} catch (e) {
57+
console.error('Failed to toggle hide profile', e)
58+
// Revert optimistic update on failure
59+
setOptimisticHidden(hidden)
4260
} finally {
4361
setSubmitting(false)
4462
}
4563
}
4664

4765
return (
4866
<Tooltip
49-
text={!clicked ? (tooltip ?? t('profile_grid.hide_profile', "Don't show again in search results")) : t('profile_grid.unhide_profile', "Show again in search results")}
67+
text={hidden ? t('profile_grid.unhide_profile', "Show again in search results") : (tooltip ?? t('profile_grid.hide_profile', "Don't show again in search results"))}
5068
noTap>
5169
<button
5270
className={clsx(
5371
'rounded-full p-1 hover:bg-canvas-200 shadow focus:outline-none',
5472
className
5573
)}
74+
disabled={submitting}
5675
onClick={onClick}
5776
aria-label={
58-
ariaLabel ?? (!clicked
59-
? t('profile_grid.hide_profile', 'Hide this profile')
60-
: t('profile_grid.unhide_profile', 'Unhide this profile'))
77+
ariaLabel ?? (hidden ? t('profile_grid.unhide_profile', 'Unhide this profile') : t('profile_grid.hide_profile', 'Hide this profile'))
6178
}
6279
>
63-
{clicked || submitting ? <EyeIcon className={clsx('h-5 w-5 guidance', iconClassName)}/> :
80+
{hidden ? <EyeIcon className={clsx('h-5 w-5 guidance', iconClassName)}/> :
6481
<EyeOffIcon className={clsx('h-5 w-5 guidance', iconClassName)}/>}
6582
</button>
6683
</Tooltip>

web/hooks/use-hidden-profiles.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {useUser} from 'web/hooks/use-user'
2+
import {useEffect} from 'react'
3+
import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
4+
import {api} from "web/lib/api";
5+
6+
type HiddenUser = {
7+
id: string
8+
name: string
9+
username: string
10+
avatarUrl?: string | null
11+
createdTime?: string
12+
}
13+
14+
export const useHiddenProfiles = () => {
15+
const user = useUser()
16+
const [hiddenProfiles, setHiddenProfiles] = usePersistentLocalState<
17+
HiddenUser[] | undefined | null
18+
>(undefined, `hidden-ids-${user?.id}`)
19+
20+
const refreshHiddenProfiles = () => {
21+
if (user) {
22+
api('get-hidden-profiles', {limit: 200, offset: 0})
23+
.then((res) => {
24+
setHiddenProfiles(res.hidden)
25+
})
26+
.catch((e) => {
27+
console.error('Failed to load hidden profiles', e)
28+
})
29+
}
30+
}
31+
32+
useEffect(() => {
33+
refreshHiddenProfiles()
34+
}, [user?.id])
35+
36+
return {hiddenProfiles, refreshHiddenProfiles}
37+
}

0 commit comments

Comments
 (0)