mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-16 00:41:13 -05:00
Use persistent in storage for hidden profiles and allow toggle hidden profile
This commit is contained in:
@@ -59,7 +59,8 @@ export default function ProfileHeader(props: {
|
||||
<Row className="items-center gap-1">
|
||||
<Col className="gap-1">
|
||||
{currentUser && isCurrentUser && disabled &&
|
||||
<div className="text-red-500">{t('profile.header.disabled_notice', 'You disabled your profile, so no one else can access it.')}</div>}
|
||||
<div
|
||||
className="text-red-500">{t('profile.header.disabled_notice', 'You disabled your profile, so no one else can access it.')}</div>}
|
||||
<Row className="items-center gap-1 text-xl">
|
||||
{/*{!isCurrentUser && <OnlineIcon last_online_time={userActivity?.last_online_time}/>}*/}
|
||||
<span>
|
||||
@@ -83,73 +84,73 @@ export default function ProfileHeader(props: {
|
||||
username={user.username}
|
||||
/>
|
||||
<Tooltip text={t('more_options_user.edit_profile', 'Edit profile')} noTap>
|
||||
<Button
|
||||
color={'gray-outline'}
|
||||
onClick={() => {
|
||||
track('editprofile')
|
||||
Router.push('profile')
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<PencilIcon className=" h-4 w-4"/>
|
||||
</Button>
|
||||
<Button
|
||||
color={'gray-outline'}
|
||||
onClick={() => {
|
||||
track('editprofile', {userId: user.id})
|
||||
Router.push('profile')
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<PencilIcon className=" h-4 w-4"/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip text={t('more_options_user.profile_options', 'Profile options')} noTap>
|
||||
<DropdownMenu
|
||||
menuWidth={'w-52'}
|
||||
icon={
|
||||
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true"/>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
name:
|
||||
profile.visibility === 'member'
|
||||
? t('profile.header.menu.list_public', 'List Profile Publicly')
|
||||
: t('profile.header.menu.limit_members', 'Limit to Members Only'),
|
||||
icon:
|
||||
profile.visibility === 'member' ? (
|
||||
<EyeIcon className="h-4 w-4"/>
|
||||
) : (
|
||||
<LockClosedIcon className="h-4 w-4"/>
|
||||
),
|
||||
onClick: () => setShowVisibilityModal(true),
|
||||
},
|
||||
{
|
||||
name: disabled ? t('profile.header.menu.enable_profile', 'Enable profile') : t('profile.header.menu.disable_profile', 'Disable profile'),
|
||||
icon: null,
|
||||
onClick: async () => {
|
||||
const confirmed = true // confirm(
|
||||
// 'Are you sure you want to disable your profile? This will hide your profile from searches and listings..'
|
||||
// )
|
||||
if (confirmed) {
|
||||
toast
|
||||
.promise(disableProfile(!disabled), {
|
||||
loading: disabled
|
||||
? t('profile.header.toast.enabling', 'Enabling profile...')
|
||||
: t('profile.header.toast.disabling', 'Disabling profile...'),
|
||||
success: () => {
|
||||
return disabled
|
||||
? t('profile.header.toast.enabled', 'Profile enabled')
|
||||
: t('profile.header.toast.disabled', 'Profile disabled')
|
||||
},
|
||||
error: () => {
|
||||
return disabled
|
||||
? t('profile.header.toast.failed_enable', 'Failed to enable profile')
|
||||
: t('profile.header.toast.failed_disable', 'Failed to disable profile')
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
refreshProfile()
|
||||
})
|
||||
.catch(() => {
|
||||
// return false
|
||||
})
|
||||
}
|
||||
<DropdownMenu
|
||||
menuWidth={'w-52'}
|
||||
icon={
|
||||
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true"/>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
name:
|
||||
profile.visibility === 'member'
|
||||
? t('profile.header.menu.list_public', 'List Profile Publicly')
|
||||
: t('profile.header.menu.limit_members', 'Limit to Members Only'),
|
||||
icon:
|
||||
profile.visibility === 'member' ? (
|
||||
<EyeIcon className="h-4 w-4"/>
|
||||
) : (
|
||||
<LockClosedIcon className="h-4 w-4"/>
|
||||
),
|
||||
onClick: () => setShowVisibilityModal(true),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
name: disabled ? t('profile.header.menu.enable_profile', 'Enable profile') : t('profile.header.menu.disable_profile', 'Disable profile'),
|
||||
icon: null,
|
||||
onClick: async () => {
|
||||
const confirmed = true // confirm(
|
||||
// 'Are you sure you want to disable your profile? This will hide your profile from searches and listings..'
|
||||
// )
|
||||
if (confirmed) {
|
||||
toast
|
||||
.promise(disableProfile(!disabled), {
|
||||
loading: disabled
|
||||
? t('profile.header.toast.enabling', 'Enabling profile...')
|
||||
: t('profile.header.toast.disabling', 'Disabling profile...'),
|
||||
success: () => {
|
||||
return disabled
|
||||
? t('profile.header.toast.enabled', 'Profile enabled')
|
||||
: t('profile.header.toast.disabled', 'Profile disabled')
|
||||
},
|
||||
error: () => {
|
||||
return disabled
|
||||
? t('profile.header.toast.failed_enable', 'Failed to enable profile')
|
||||
: t('profile.header.toast.failed_disable', 'Failed to disable profile')
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
refreshProfile()
|
||||
})
|
||||
.catch(() => {
|
||||
// return false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Row>
|
||||
) : (
|
||||
|
||||
@@ -10,15 +10,7 @@ import toast from 'react-hot-toast'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import {CompassLoadingIndicator} from "web/components/widgets/loading-indicator";
|
||||
|
||||
type HiddenUser = {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
createdTime?: string
|
||||
}
|
||||
import {useHiddenProfiles} from "web/hooks/use-hidden-profiles";
|
||||
|
||||
export function HiddenProfilesModal(props: {
|
||||
open: boolean
|
||||
@@ -26,32 +18,14 @@ export function HiddenProfilesModal(props: {
|
||||
}) {
|
||||
const {open, setOpen} = props
|
||||
const t = useT()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [hidden, setHidden] = useState<HiddenUser[] | null>(null)
|
||||
const [busyIds, setBusyIds] = useState<Record<string, boolean>>({})
|
||||
const {hiddenProfiles, refreshHiddenProfiles} = useHiddenProfiles()
|
||||
|
||||
const empty = useMemo(() => (hidden ? hidden.length === 0 : false), [hidden])
|
||||
const empty = useMemo(() => (hiddenProfiles ? hiddenProfiles.length === 0 : false), [hiddenProfiles])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
let alive = true
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
api('get-hidden-profiles', {limit: 200, offset: 0})
|
||||
.then((res) => {
|
||||
if (!alive) return
|
||||
setHidden(res.hidden)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to load hidden profiles', e)
|
||||
if (!alive) return
|
||||
setError(t('settings.hidden_profiles.load_error', 'Failed to load hidden profiles.'))
|
||||
})
|
||||
.finally(() => alive && setLoading(false))
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
refreshHiddenProfiles()
|
||||
}, [open])
|
||||
|
||||
const unhide = async (userId: string) => {
|
||||
@@ -59,12 +33,12 @@ export function HiddenProfilesModal(props: {
|
||||
setBusyIds((b) => ({...b, [userId]: true}))
|
||||
try {
|
||||
await api('unhide-profile', {hiddenUserId: userId})
|
||||
setHidden((list) => (list ? list.filter((u) => u.id !== userId) : list))
|
||||
refreshHiddenProfiles()
|
||||
} catch (e) {
|
||||
console.error('Failed to unhide profile', e)
|
||||
toast.error(t('settings.hidden_profiles.unhide_failed', 'Failed to unhide'))
|
||||
} finally {
|
||||
setBusyIds((b) => ({...b, [userId]: false}))
|
||||
// setBusyIds((b) => ({...b, [userId]: false}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,14 +48,9 @@ export function HiddenProfilesModal(props: {
|
||||
<Title className="mb-2">
|
||||
{t('settings.hidden_profiles.title', "Profiles you've hidden")}
|
||||
</Title>
|
||||
{!loading && hidden && hidden.length > 0 && <p>
|
||||
{t('settings.hidden_profiles.description', "These people don't appear in your search results.")}
|
||||
</p>}
|
||||
{loading && <CompassLoadingIndicator/>}
|
||||
{error && <div className="text-red-500 py-2">{error}</div>}
|
||||
{!loading && hidden && hidden.length > 0 && (
|
||||
{hiddenProfiles && hiddenProfiles.length > 0 && (
|
||||
<Col className={clsx("divide-y divide-canvas-300 w-full pr-4", SCROLLABLE_MODAL_CLASS)}>
|
||||
{hidden.map((u) => (
|
||||
{hiddenProfiles.map((u) => (
|
||||
<Row key={u.id} className="items-center justify-between py-2 gap-2">
|
||||
<Link
|
||||
className="w-full rounded-md hover:bg-canvas-100 p-2"
|
||||
@@ -109,7 +78,7 @@ export function HiddenProfilesModal(props: {
|
||||
))}
|
||||
</Col>
|
||||
)}
|
||||
{!loading && empty && (
|
||||
{empty && (
|
||||
<div className="text-ink-500 py-6 text-center">
|
||||
{t(
|
||||
'settings.hidden_profiles.empty',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import clsx from 'clsx'
|
||||
import {useState} from 'react'
|
||||
import {useMemo, useState} from 'react'
|
||||
import {EyeIcon, EyeOffIcon} from '@heroicons/react/outline'
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {useHiddenProfiles} from "web/hooks/use-hidden-profiles";
|
||||
|
||||
export type HideProfileButtonProps = {
|
||||
hiddenUserId: string
|
||||
@@ -28,17 +29,34 @@ export function HideProfileButton(props: HideProfileButtonProps) {
|
||||
|
||||
const t = useT()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [clicked, setClicked] = useState(false)
|
||||
const {hiddenProfiles, refreshHiddenProfiles} = useHiddenProfiles()
|
||||
const [optimisticHidden, setOptimisticHidden] = useState<boolean | undefined>(undefined)
|
||||
const hidden = useMemo(() => {
|
||||
if (optimisticHidden !== undefined) return optimisticHidden
|
||||
return hiddenProfiles?.some((u) => u.id === hiddenUserId) ?? false
|
||||
}, [hiddenProfiles, hiddenUserId, optimisticHidden])
|
||||
|
||||
const onClick = async (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (stopPropagation) e.stopPropagation()
|
||||
if (submitting) return
|
||||
setSubmitting(true)
|
||||
setClicked(true)
|
||||
|
||||
// Optimistically update hidden state
|
||||
setOptimisticHidden(!hidden)
|
||||
|
||||
try {
|
||||
await api('hide-profile', {hiddenUserId})
|
||||
if (hidden) {
|
||||
await api('unhide-profile', {hiddenUserId})
|
||||
} else {
|
||||
await api('hide-profile', {hiddenUserId})
|
||||
}
|
||||
refreshHiddenProfiles()
|
||||
onHidden?.(hiddenUserId)
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle hide profile', e)
|
||||
// Revert optimistic update on failure
|
||||
setOptimisticHidden(hidden)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -46,21 +64,20 @@ export function HideProfileButton(props: HideProfileButtonProps) {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
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")}
|
||||
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"))}
|
||||
noTap>
|
||||
<button
|
||||
className={clsx(
|
||||
'rounded-full p-1 hover:bg-canvas-200 shadow focus:outline-none',
|
||||
className
|
||||
)}
|
||||
disabled={submitting}
|
||||
onClick={onClick}
|
||||
aria-label={
|
||||
ariaLabel ?? (!clicked
|
||||
? t('profile_grid.hide_profile', 'Hide this profile')
|
||||
: t('profile_grid.unhide_profile', 'Unhide this profile'))
|
||||
ariaLabel ?? (hidden ? t('profile_grid.unhide_profile', 'Unhide this profile') : t('profile_grid.hide_profile', 'Hide this profile'))
|
||||
}
|
||||
>
|
||||
{clicked || submitting ? <EyeIcon className={clsx('h-5 w-5 guidance', iconClassName)}/> :
|
||||
{hidden ? <EyeIcon className={clsx('h-5 w-5 guidance', iconClassName)}/> :
|
||||
<EyeOffIcon className={clsx('h-5 w-5 guidance', iconClassName)}/>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
37
web/hooks/use-hidden-profiles.ts
Normal file
37
web/hooks/use-hidden-profiles.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {useEffect} from 'react'
|
||||
import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
|
||||
import {api} from "web/lib/api";
|
||||
|
||||
type HiddenUser = {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
createdTime?: string
|
||||
}
|
||||
|
||||
export const useHiddenProfiles = () => {
|
||||
const user = useUser()
|
||||
const [hiddenProfiles, setHiddenProfiles] = usePersistentLocalState<
|
||||
HiddenUser[] | undefined | null
|
||||
>(undefined, `hidden-ids-${user?.id}`)
|
||||
|
||||
const refreshHiddenProfiles = () => {
|
||||
if (user) {
|
||||
api('get-hidden-profiles', {limit: 200, offset: 0})
|
||||
.then((res) => {
|
||||
setHiddenProfiles(res.hidden)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to load hidden profiles', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshHiddenProfiles()
|
||||
}, [user?.id])
|
||||
|
||||
return {hiddenProfiles, refreshHiddenProfiles}
|
||||
}
|
||||
Reference in New Issue
Block a user