mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Hide feature to hide per-user profiles
This commit is contained in:
@@ -8,7 +8,7 @@ import {OptionTableKey} from "common/profiles/constants";
|
||||
export type profileQueryType = {
|
||||
limit?: number | undefined,
|
||||
after?: string | undefined,
|
||||
// Search and filter parameters
|
||||
userId?: string | undefined,
|
||||
name?: string | undefined,
|
||||
genders?: string[] | undefined,
|
||||
education_levels?: string[] | undefined,
|
||||
@@ -52,6 +52,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
limit: limitParam,
|
||||
after,
|
||||
name,
|
||||
userId,
|
||||
genders,
|
||||
education_levels,
|
||||
pref_gender,
|
||||
@@ -300,6 +301,15 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
`),
|
||||
|
||||
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
|
||||
|
||||
// Exclude profiles that the requester has chosen to hide
|
||||
userId && where(
|
||||
`NOT EXISTS (
|
||||
SELECT 1 FROM hidden_profiles hp
|
||||
WHERE hp.hider_user_id = $(userId)
|
||||
AND hp.hidden_user_id = profiles.user_id
|
||||
)`, {userId}
|
||||
),
|
||||
]
|
||||
|
||||
let selectCols = 'profiles.*, users.name, users.username, users.data as user'
|
||||
@@ -341,7 +351,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
export const getProfiles: APIHandler<'get-profiles'> = async (props, auth) => {
|
||||
try {
|
||||
if (!props.skipId) props.skipId = auth.uid
|
||||
const {profiles, count} = await loadProfiles(props)
|
||||
const {profiles, count} = await loadProfiles({...props, userId: auth.uid})
|
||||
return {status: 'success', profiles: profiles, count: count}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
24
backend/api/src/hide-profile.ts
Normal file
24
backend/api/src/hide-profile.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {APIError, APIHandler} from './helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
// Hide a profile for the requesting user by inserting a row into hidden_profiles.
|
||||
// Idempotent: if the pair already exists, succeed silently.
|
||||
export const hideProfile: APIHandler<'hide-profile'> = async (
|
||||
{hiddenUserId},
|
||||
auth
|
||||
) => {
|
||||
if (auth.uid === hiddenUserId)
|
||||
throw new APIError(400, 'You cannot hide yourself')
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
// Insert idempotently: do nothing on conflict
|
||||
await pg.none(
|
||||
`insert into hidden_profiles (hider_user_id, hidden_user_id)
|
||||
values ($1, $2)
|
||||
on conflict (hider_user_id, hidden_user_id) do nothing`,
|
||||
[auth.uid, hiddenUserId]
|
||||
)
|
||||
|
||||
return {status: 'success'}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ export const sendSearchNotifications = async () => {
|
||||
const props = {
|
||||
...filters,
|
||||
skipId: row.creator_id,
|
||||
userId: row.creator_id,
|
||||
lastModificationWithin: '24 hours',
|
||||
shortBio: true,
|
||||
}
|
||||
|
||||
27
backend/supabase/hidden_profiles.sql
Normal file
27
backend/supabase/hidden_profiles.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
create table if not exists hidden_profiles
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
created_time timestamptz not null default now(),
|
||||
hider_user_id TEXT not null,
|
||||
hidden_user_id TEXT not null
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE hidden_profiles
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Ensure a user can only hide another user once (idempotent)
|
||||
create unique index if not exists hidden_profiles_unique_hider_hidden
|
||||
on hidden_profiles (hider_user_id, hidden_user_id);
|
||||
|
||||
-- Helpful indexes
|
||||
create index if not exists hidden_profiles_hider_idx on hidden_profiles (hider_user_id);
|
||||
create index if not exists hidden_profiles_hidden_idx on hidden_profiles (hidden_user_id);
|
||||
|
||||
-- Optional FKs to users table if present in schema
|
||||
alter table hidden_profiles
|
||||
add constraint fk_hidden_profiles_hider
|
||||
foreign key (hider_user_id) references users (id) on delete cascade;
|
||||
alter table hidden_profiles
|
||||
add constraint fk_hidden_profiles_hidden
|
||||
foreign key (hidden_user_id) references users (id) on delete cascade;
|
||||
@@ -444,6 +444,19 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Star or unstar a profile',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'hide-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
hiddenUserId: z.string(),
|
||||
}),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
},
|
||||
summary: 'Hide a profile for the current user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'get-profiles': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
|
||||
@@ -13,6 +13,10 @@ import {useUser} from "web/hooks/use-user";
|
||||
import {useT} from "web/lib/locale";
|
||||
import {useAllChoices} from "web/hooks/use-choices";
|
||||
import {getSeekingGenderText} from "web/lib/profile/seeking";
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
import {EyeOffIcon} from '@heroicons/react/outline'
|
||||
import {useState} from 'react'
|
||||
import {api} from 'web/lib/api'
|
||||
|
||||
export const ProfileGrid = (props: {
|
||||
profiles: Profile[]
|
||||
@@ -22,6 +26,7 @@ export const ProfileGrid = (props: {
|
||||
compatibilityScores: Record<string, CompatibilityScore> | undefined
|
||||
starredUserIds: string[] | undefined
|
||||
refreshStars: () => Promise<void>
|
||||
onHide?: (userId: string) => void
|
||||
}) => {
|
||||
const {
|
||||
profiles,
|
||||
@@ -31,6 +36,7 @@ export const ProfileGrid = (props: {
|
||||
compatibilityScores,
|
||||
starredUserIds,
|
||||
refreshStars,
|
||||
onHide,
|
||||
} = props
|
||||
|
||||
const user = useUser()
|
||||
@@ -54,6 +60,7 @@ export const ProfileGrid = (props: {
|
||||
compatibilityScore={compatibilityScores?.[profile.user_id]}
|
||||
hasStar={starredUserIds?.includes(profile.user_id) ?? false}
|
||||
refreshStars={refreshStars}
|
||||
onHide={onHide}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -81,12 +88,14 @@ function ProfilePreview(props: {
|
||||
compatibilityScore: CompatibilityScore | undefined
|
||||
hasStar: boolean
|
||||
refreshStars: () => Promise<void>
|
||||
onHide?: (userId: string) => void
|
||||
}) {
|
||||
const {profile, compatibilityScore} = props
|
||||
const {profile, compatibilityScore, onHide} = props
|
||||
const {user} = profile
|
||||
const choicesIdsToLabels = useAllChoices()
|
||||
const t = useT()
|
||||
// const currentUser = useUser()
|
||||
const [hiding, setHiding] = useState(false)
|
||||
|
||||
const bio = profile.bio as JSONContent;
|
||||
|
||||
@@ -157,7 +166,30 @@ function ProfilePreview(props: {
|
||||
{/* <div />*/}
|
||||
{/* )}*/}
|
||||
{compatibilityScore && (
|
||||
<CompatibleBadge compatibility={compatibilityScore}/>
|
||||
<CompatibleBadge compatibility={compatibilityScore} className={'pt-1'}/>
|
||||
)}
|
||||
{/* Hide profile button */}
|
||||
{onHide && (
|
||||
<Tooltip text={t('profile_grid.hide_profile', "Don't show again")} noTap>
|
||||
<button
|
||||
className="ml-2 rounded-full p-1 hover:bg-canvas-300 shadow focus:outline-none"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (hiding) return
|
||||
setHiding(true)
|
||||
try {
|
||||
await api('hide-profile', {hiddenUserId: profile.user_id})
|
||||
onHide(profile.user_id)
|
||||
} finally {
|
||||
setHiding(false)
|
||||
}
|
||||
}}
|
||||
aria-label={t('profile_grid.hide_profile', 'Hide this profile')}
|
||||
>
|
||||
<EyeOffIcon className="h-5 w-5 guidance"/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -138,6 +138,12 @@ export function ProfilesHome() {
|
||||
}
|
||||
}, [profiles, filters, isLoadingMore, setProfiles])
|
||||
|
||||
const onHide = useCallback((userId: string) => {
|
||||
setProfiles((prev) => prev?.filter((p) => p.user_id !== userId))
|
||||
setProfileCount((prev) => prev ? prev - 1 : 0)
|
||||
toast.success(t('profiles.hidden_success', 'Profile hidden. You will no longer see this person in search results.'))
|
||||
}, [setProfiles, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBanner && fromSignup && (
|
||||
@@ -237,6 +243,7 @@ export function ProfilesHome() {
|
||||
compatibilityScores={compatibleProfiles?.profileCompatibilityScores}
|
||||
starredUserIds={starredUserIds}
|
||||
refreshStars={refreshStars}
|
||||
onHide={onHide}
|
||||
/>
|
||||
</>)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user