mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-15 00:11:07 -05:00
Show the profiles I hid in the settings
This commit is contained in:
@@ -77,6 +77,8 @@ import {deleteMessage} from "api/delete-message";
|
||||
import {updateOptions} from "api/update-options";
|
||||
import {getOptions} from "api/get-options";
|
||||
import {hideProfile} from "api/hide-profile";
|
||||
import {unhideProfile} from "api/unhide-profile";
|
||||
import {getHiddenProfiles} from "api/get-hidden-profiles";
|
||||
|
||||
// const corsOptions: CorsOptions = {
|
||||
// origin: ['*'], // Only allow requests from this domain
|
||||
@@ -341,6 +343,8 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
'has-free-like': hasFreeLike,
|
||||
'hide-comment': hideComment,
|
||||
'hide-profile': hideProfile,
|
||||
'unhide-profile': unhideProfile,
|
||||
'get-hidden-profiles': getHiddenProfiles,
|
||||
'leave-private-user-message-channel': leavePrivateUserMessageChannel,
|
||||
'like-profile': likeProfile,
|
||||
'mark-all-notifs-read': markAllNotifsRead,
|
||||
|
||||
38
backend/api/src/get-hidden-profiles.ts
Normal file
38
backend/api/src/get-hidden-profiles.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
export const getHiddenProfiles: APIHandler<'get-hidden-profiles'> = async (
|
||||
{limit = 100, offset = 0},
|
||||
auth
|
||||
) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
// Count total hidden for pagination info
|
||||
const countRes = await pg.one<{ count: string }>(
|
||||
`select count(*)::text as count
|
||||
from hidden_profiles
|
||||
where hider_user_id = $1`,
|
||||
[auth.uid]
|
||||
)
|
||||
const count = Number(countRes.count) || 0
|
||||
|
||||
// Fetch hidden users joined with users table for display
|
||||
const rows = await pg.map(
|
||||
`select u.id, u.name, u.username, u.data ->> 'avatarUrl' as "avatarUrl", hp.created_time as "createdTime"
|
||||
from hidden_profiles hp
|
||||
join users u on u.id = hp.hidden_user_id
|
||||
where hp.hider_user_id = $1
|
||||
order by hp.created_time desc
|
||||
limit $2 offset $3`,
|
||||
[auth.uid, limit, offset],
|
||||
(r: any) => ({
|
||||
id: r.id as string,
|
||||
name: r.name as string,
|
||||
username: r.username as string,
|
||||
avatarUrl: r.avatarUrl as string | null | undefined,
|
||||
createdTime: r.createdTime as string | undefined,
|
||||
})
|
||||
)
|
||||
|
||||
return {status: 'success', hidden: rows, count}
|
||||
}
|
||||
21
backend/api/src/unhide-profile.ts
Normal file
21
backend/api/src/unhide-profile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {APIHandler} from './helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
// Unhide a profile for the requesting user by deleting from hidden_profiles.
|
||||
// Idempotent: if the pair does not exist, succeed silently.
|
||||
export const unhideProfile: APIHandler<'unhide-profile'> = async (
|
||||
{hiddenUserId},
|
||||
auth
|
||||
) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
await pg.none(
|
||||
`delete
|
||||
from hidden_profiles
|
||||
where hider_user_id = $1
|
||||
and hidden_user_id = $2`,
|
||||
[auth.uid, hiddenUserId]
|
||||
)
|
||||
|
||||
return {status: 'success'}
|
||||
}
|
||||
@@ -457,6 +457,41 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Hide a profile for the current user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'unhide-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
hiddenUserId: z.string(),
|
||||
}),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
},
|
||||
summary: 'Unhide a previously hidden profile for the current user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'get-hidden-profiles': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
limit: z.coerce.number().min(1).max(200).optional(),
|
||||
offset: z.coerce.number().min(0).optional(),
|
||||
}).strict(),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
hidden: {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
createdTime?: string
|
||||
}[]
|
||||
count: number
|
||||
},
|
||||
summary: 'Get the list of profiles the current user has hidden',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'get-profiles': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
|
||||
139
web/components/settings/hidden-profiles-modal.tsx
Normal file
139
web/components/settings/hidden-profiles-modal.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {Avatar} from 'web/components/widgets/avatar'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {api} from 'web/lib/api'
|
||||
import toast from 'react-hot-toast'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
type HiddenUser = {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
createdTime?: string
|
||||
}
|
||||
|
||||
export function HiddenProfilesModal(props: {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) {
|
||||
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 empty = useMemo(() => (hidden ? hidden.length === 0 : false), [hidden])
|
||||
|
||||
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
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const unhide = async (userId: string) => {
|
||||
if (busyIds[userId]) return
|
||||
setBusyIds((b) => ({...b, [userId]: true}))
|
||||
try {
|
||||
await api('unhide-profile', {hiddenUserId: userId})
|
||||
setHidden((list) => (list ? list.filter((u) => u.id !== userId) : list))
|
||||
toast.success(
|
||||
t(
|
||||
'settings.hidden_profiles.unhidden_success',
|
||||
'Profile unhidden. You will start seeing this person again in results.'
|
||||
)
|
||||
)
|
||||
} 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}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} size="lg">
|
||||
<Col className={clsx(MODAL_CLASS, 'mx-0 sm:mx-24')}>
|
||||
<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 && (
|
||||
<div className="text-ink-500 py-4">
|
||||
{t('settings.hidden_profiles.loading', 'Loading hidden profiles...')}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-red-500 py-2">{error}</div>}
|
||||
{!loading && hidden && hidden.length > 0 && (
|
||||
<Col className={clsx("divide-y divide-canvas-300 w-full pr-4", SCROLLABLE_MODAL_CLASS)}>
|
||||
{hidden.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"
|
||||
href={'/' + u.username}
|
||||
>
|
||||
<Row className="items-center gap-3">
|
||||
<Avatar size="md" username={u.username} avatarUrl={u.avatarUrl ?? undefined}/>
|
||||
<Col>
|
||||
<div className="font-medium">{u.name}</div>
|
||||
<div className="text-ink-500 text-sm">@{u.username}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Link>
|
||||
<Button
|
||||
size="sm"
|
||||
color="gray-outline"
|
||||
onClick={() => unhide(u.id)}
|
||||
disabled={busyIds[u.id]}
|
||||
>
|
||||
{busyIds[u.id]
|
||||
? t('settings.hidden_profiles.unhiding', 'Unhiding...')
|
||||
: t('settings.hidden_profiles.unhide', 'Unhide')}
|
||||
</Button>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
)}
|
||||
{!loading && empty && (
|
||||
<div className="text-ink-500 py-6 text-center">
|
||||
{t(
|
||||
'settings.hidden_profiles.empty',
|
||||
"You haven't hidden any profiles."
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default HiddenProfilesModal
|
||||
@@ -56,7 +56,7 @@ export function HideProfileButton(props: HideProfileButtonProps) {
|
||||
<Tooltip text={tooltip ?? t('profile_grid.hide_profile', "Don't show again in search results")} noTap>
|
||||
<button
|
||||
className={clsx(
|
||||
'rounded-full p-1 hover:bg-canvas-300 shadow focus:outline-none',
|
||||
'rounded-full p-1 hover:bg-canvas-200 shadow focus:outline-none',
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -469,6 +469,8 @@
|
||||
"profile.has_kids.no_preference": "Egal",
|
||||
"profile.has_kids_many": "Hat {count} Kinder",
|
||||
"profile.has_kids_one": "Hat {count} Kind",
|
||||
"profiles.hidden_success": "Profil ausgeblendet. Sie werden diese Person nicht mehr in den Suchergebnissen sehen.",
|
||||
"profile_grid.hide_profile": "Nicht mehr in den Suchergebnissen anzeigen",
|
||||
"profile.header.disabled_notice": "Sie haben Ihr Profil deaktiviert, niemand sonst kann darauf zugreifen.",
|
||||
"profile.header.menu.disable_profile": "Profil deaktivieren",
|
||||
"profile.header.menu.enable_profile": "Profil aktivieren",
|
||||
@@ -820,12 +822,23 @@
|
||||
"settings.general.email": "E-Mail",
|
||||
"settings.general.language": "Sprache",
|
||||
"settings.general.password": "Passwort",
|
||||
"settings.general.people": "Personen",
|
||||
"settings.general.theme": "Design",
|
||||
"settings.password.send_reset": "Passwort-Zurücksetzungs-E-Mail senden",
|
||||
"settings.tabs.about": "Über",
|
||||
"settings.tabs.general": "Allgemein",
|
||||
"settings.tabs.notifications": "Benachrichtigungen",
|
||||
"settings.title": "Einstellungen",
|
||||
"settings.hidden_profiles.title": "Profile, die Sie ausgeblendet haben",
|
||||
"settings.hidden_profiles.manage": "Ausgeblendete Profile verwalten",
|
||||
"settings.hidden_profiles.load_error": "Fehler beim Laden ausgeblendeter Profile.",
|
||||
"settings.hidden_profiles.unhidden_success": "Profil eingeblendet. Sie werden diese Person wieder in den Ergebnissen sehen.",
|
||||
"settings.hidden_profiles.unhide_failed": "Fehler beim Einblenden",
|
||||
"settings.hidden_profiles.loading": "Ausgeblendete Profile werden geladen...",
|
||||
"settings.hidden_profiles.unhiding": "Einblenden...",
|
||||
"settings.hidden_profiles.unhide": "Einblenden",
|
||||
"settings.hidden_profiles.empty": "Sie haben keine Profile ausgeblendet.",
|
||||
"settings.hidden_profiles.description": "Diese Personen erscheinen nicht in Ihren Suchergebnissen.",
|
||||
"signin.continue": "Oder fortfahren mit",
|
||||
"signin.email": "E-Mail",
|
||||
"signin.enter_email": "Bitte geben Sie Ihre E-Mail ein",
|
||||
|
||||
@@ -469,6 +469,8 @@
|
||||
"profile.has_kids.no_preference": "Tout enfant",
|
||||
"profile.has_kids_many": "A {count} enfants",
|
||||
"profile.has_kids_one": "A {count} enfant",
|
||||
"profiles.hidden_success": "Profil masqué. Vous ne verrez plus cette personne dans les résultats de recherche.",
|
||||
"profile_grid.hide_profile": "Ne plus afficher dans les résultats de recherche",
|
||||
"profile.header.disabled_notice": "Vous avez désactivé votre profil, personne d'autre ne peut y accéder.",
|
||||
"profile.header.menu.disable_profile": "Désactiver le profil",
|
||||
"profile.header.menu.enable_profile": "Activer le profil",
|
||||
@@ -820,12 +822,23 @@
|
||||
"settings.general.email": "E-mail",
|
||||
"settings.general.language": "Langue",
|
||||
"settings.general.password": "Mot de passe",
|
||||
"settings.general.people": "Personnes",
|
||||
"settings.general.theme": "Thème",
|
||||
"settings.password.send_reset": "Envoyer un e‑mail de réinitialisation de mot de passe",
|
||||
"settings.tabs.about": "À propos",
|
||||
"settings.tabs.general": "Général",
|
||||
"settings.tabs.notifications": "Notifications",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.hidden_profiles.title": "Profils que vous avez masqués",
|
||||
"settings.hidden_profiles.manage": "Gérer les profils masqués",
|
||||
"settings.hidden_profiles.load_error": "Échec du chargement des profils masqués.",
|
||||
"settings.hidden_profiles.unhidden_success": "Profil affiché. Vous commencerez à voir cette personne à nouveau dans les résultats.",
|
||||
"settings.hidden_profiles.unhide_failed": "Échec de l'affichage",
|
||||
"settings.hidden_profiles.loading": "Chargement des profils masqués...",
|
||||
"settings.hidden_profiles.unhiding": "Affichage...",
|
||||
"settings.hidden_profiles.unhide": "Afficher",
|
||||
"settings.hidden_profiles.empty": "Vous n'avez masqué aucun profil.",
|
||||
"settings.hidden_profiles.description": "Ces personnes n'apparaissent pas dans vos résultats de recherche.",
|
||||
"signin.continue": "Ou continuez avec",
|
||||
"signin.email": "E-mail",
|
||||
"signin.enter_email": "Veuillez taper votre email",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {sendPasswordReset} from "web/lib/firebase/password";
|
||||
import {AboutSettings} from "web/components/about-settings";
|
||||
import {LanguagePicker} from "web/components/language/language-picker";
|
||||
import {useT} from "web/lib/locale";
|
||||
import HiddenProfilesModal from 'web/components/settings/hidden-profiles-modal'
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const t = useT()
|
||||
@@ -54,6 +55,7 @@ const LoadedGeneralSettings = (props: {
|
||||
const {privateUser} = props
|
||||
|
||||
const [isChangingEmail, setIsChangingEmail] = useState(false)
|
||||
const [showHiddenProfiles, setShowHiddenProfiles] = useState(false)
|
||||
const {register, handleSubmit, formState: {errors}, reset} = useForm<{ newEmail: string }>()
|
||||
const t = useT()
|
||||
|
||||
@@ -136,15 +138,21 @@ const LoadedGeneralSettings = (props: {
|
||||
<h3>{t('settings.general.language', 'Language')}</h3>
|
||||
<LanguagePicker className={'w-fit min-w-[120px]'}/>
|
||||
|
||||
<h3>{t('settings.general.people', 'People')}</h3>
|
||||
{/*<h5>{t('settings.hidden_profiles.title', 'Hidden profiles')}</h5>*/}
|
||||
<Button color={'gray-outline'} onClick={() => setShowHiddenProfiles(true)}>
|
||||
{t('settings.hidden_profiles.manage', 'Manage hidden profiles')}
|
||||
</Button>
|
||||
|
||||
<h3>{t('settings.general.account', 'Account')}</h3>
|
||||
<h5>{t('settings.general.email', 'Email')}</h5>
|
||||
|
||||
<Button onClick={sendVerificationEmail} disabled={!privateUser?.email || isEmailVerified}>
|
||||
<Button color={'gray-outline'} onClick={sendVerificationEmail} disabled={!privateUser?.email || isEmailVerified}>
|
||||
{isEmailVerified ? t('settings.email.verified', 'Email Verified ✔️') : t('settings.email.send_verification', 'Send verification email')}
|
||||
</Button>
|
||||
|
||||
{!isChangingEmail ? (
|
||||
<Button onClick={() => setIsChangingEmail(true)}>
|
||||
<Button color={'gray-outline'} onClick={() => setIsChangingEmail(true)}>
|
||||
{t('settings.email.change', 'Change email address')}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -191,7 +199,8 @@ const LoadedGeneralSettings = (props: {
|
||||
<h5>{t('settings.general.password', 'Password')}</h5>
|
||||
<Button
|
||||
onClick={() => sendPasswordReset(privateUser?.email)}
|
||||
className="mb-2"
|
||||
className="mb-2 max-w-[250px]"
|
||||
color={'gray-outline'}
|
||||
>
|
||||
{t('settings.password.send_reset', 'Send password reset email')}
|
||||
</Button>
|
||||
@@ -201,5 +210,8 @@ const LoadedGeneralSettings = (props: {
|
||||
{t('settings.delete_account', 'Delete Account')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Hidden profiles modal */}
|
||||
<HiddenProfilesModal open={showHiddenProfiles} setOpen={setShowHiddenProfiles}/>
|
||||
</>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user