mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-27 19:12:05 -04:00
Add connection preferences feature with API endpoints and UI updates
This commit is contained in:
@@ -22,6 +22,7 @@ import {saveSubscriptionMobile} from 'api/save-subscription-mobile'
|
||||
import {sendSearchNotifications} from 'api/send-search-notifications'
|
||||
import {localSendTestEmail} from 'api/test'
|
||||
import {unhideProfile} from 'api/unhide-profile'
|
||||
import {updateConnectionInterests} from 'api/update-connection-interests'
|
||||
import {updateOptions} from 'api/update-options'
|
||||
import {vote} from 'api/vote'
|
||||
import {API, type APIPath} from 'common/api/schema'
|
||||
@@ -57,6 +58,7 @@ import {deleteBookmarkedSearch} from './delete-bookmarked-search'
|
||||
import {deleteCompatibilityAnswer} from './delete-compatibility-answer'
|
||||
import {deleteMe} from './delete-me'
|
||||
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
||||
import {getConnectionInterests} from './get-connection-interests'
|
||||
import {getCurrentPrivateUser} from './get-current-private-user'
|
||||
import {getEvents} from './get-events'
|
||||
import {getLikesAndShips} from './get-likes-and-ships'
|
||||
@@ -375,6 +377,8 @@ const handlers: {[k in APIPath]: APIHandler<k>} = {
|
||||
'update-user-locale': updateUserLocale,
|
||||
'update-private-user-message-channel': updatePrivateUserMessageChannel,
|
||||
'update-profile': updateProfile,
|
||||
'get-connection-interests': getConnectionInterests,
|
||||
'update-connection-interest': updateConnectionInterests,
|
||||
'user/by-id/:id': getUser,
|
||||
'user/by-id/:id/block': blockUser,
|
||||
'user/by-id/:id/unblock': unblockUser,
|
||||
|
||||
44
backend/api/src/get-connection-interests.ts
Normal file
44
backend/api/src/get-connection-interests.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
export const getConnectionInterests: APIHandler<'get-connection-interests'> = async (
|
||||
props,
|
||||
auth,
|
||||
) => {
|
||||
const {targetUserId} = props
|
||||
const userId = auth.uid
|
||||
|
||||
if (!targetUserId) {
|
||||
throw new Error('Missing target user ID')
|
||||
}
|
||||
|
||||
if (targetUserId === userId) {
|
||||
throw new Error('Cannot get connection interests for yourself')
|
||||
}
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
// Get what connection interest I have with them
|
||||
const _interests = await pg.query(
|
||||
'SELECT connection_type FROM connection_interests WHERE user_id = $1 AND target_user_id = $2',
|
||||
[userId, targetUserId],
|
||||
)
|
||||
const interests = _interests.map((i: {connection_type: string}) => i.connection_type) ?? []
|
||||
console.log({_interests, interests})
|
||||
|
||||
// Get what connection interest they have with me (filtering out the ones I haven't expressed interest in
|
||||
// so it's risk-free to express interest in them)
|
||||
const _targetInterests = await pg.query(
|
||||
'SELECT connection_type FROM connection_interests WHERE user_id = $1 AND target_user_id = $2',
|
||||
[targetUserId, userId],
|
||||
)
|
||||
const targetInterests =
|
||||
_targetInterests
|
||||
?.map((i: {connection_type: string}) => i.connection_type)
|
||||
?.filter((i: string) => interests.includes(i)) ?? []
|
||||
|
||||
return {
|
||||
interests,
|
||||
targetInterests,
|
||||
}
|
||||
}
|
||||
32
backend/api/src/update-connection-interests.ts
Normal file
32
backend/api/src/update-connection-interests.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
|
||||
export const updateConnectionInterests: APIHandler<'update-connection-interest'> = async (
|
||||
props,
|
||||
auth,
|
||||
) => {
|
||||
const {targetUserId, connectionType, seeking} = props
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
if (!connectionType) {
|
||||
throw new Error('Invalid connection type')
|
||||
}
|
||||
|
||||
if (seeking) {
|
||||
// Insert or update the interest
|
||||
await pg.query(
|
||||
`INSERT INTO connection_interests (user_id, target_user_id, connection_type)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, target_user_id, connection_type) DO NOTHING`,
|
||||
[auth.uid, targetUserId, connectionType],
|
||||
)
|
||||
} else {
|
||||
// Remove the interest
|
||||
await pg.query(
|
||||
'DELETE FROM connection_interests WHERE user_id = $1 AND target_user_id = $2 AND connection_type = $3',
|
||||
[auth.uid, targetUserId, connectionType],
|
||||
)
|
||||
}
|
||||
|
||||
return {success: true}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ export type PartialProfile = Pick<
|
||||
| 'last_modification_time'
|
||||
| 'disabled'
|
||||
| 'looking_for_matches'
|
||||
| 'messaging_status'
|
||||
| 'comments_enabled'
|
||||
| 'visibility'
|
||||
> &
|
||||
@@ -48,7 +47,6 @@ export const jamesProfile: PartialProfile = {
|
||||
last_modification_time: '2024-05-17T02:11:48.83+00:00',
|
||||
disabled: false,
|
||||
looking_for_matches: true,
|
||||
messaging_status: 'open',
|
||||
comments_enabled: true,
|
||||
visibility: 'public',
|
||||
city: 'San Francisco',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const removePinnedUrlFromPhotoUrls = async (parsedBody: {
|
||||
pinned_url?: string
|
||||
photo_urls?: string[] | null
|
||||
pinned_url?: string | null | undefined
|
||||
photo_urls?: string[] | null | undefined
|
||||
}) => {
|
||||
if (parsedBody.photo_urls && parsedBody.pinned_url) {
|
||||
parsedBody.photo_urls = parsedBody.photo_urls.filter(
|
||||
|
||||
@@ -50,4 +50,5 @@ BEGIN;
|
||||
\i backend/supabase/migrations/20260218_add_notification_templates.sql
|
||||
\i backend/supabase/migrations/20260222_add_deleted_users.sql
|
||||
\i backend/supabase/migrations/20260223_add_notification_template_translations.sql
|
||||
\i backend/supabase/migrations/20260224_add_connection_preferences.sql
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS allow_direct_messaging BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS allow_interest_indicating BOOLEAN DEFAULT TRUE;
|
||||
|
||||
CREATE TABLE connection_interests
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
|
||||
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
target_user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
|
||||
connection_type TEXT NOT NULL CHECK (connection_type IN ('relationship', 'friendship', 'collaboration')),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
CHECK (user_id != target_user_id),
|
||||
|
||||
UNIQUE (user_id, target_user_id, connection_type)
|
||||
);
|
||||
|
||||
-- Row Level Security for connection_interests
|
||||
ALTER TABLE connection_interests
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_ci_user_target_type
|
||||
ON connection_interests (user_id, target_user_id, connection_type);
|
||||
|
||||
CREATE INDEX idx_ci_target_user_type
|
||||
ON connection_interests (target_user_id, user_id, connection_type);
|
||||
@@ -1,90 +1,95 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'profile_visibility') THEN
|
||||
CREATE TYPE profile_visibility AS ENUM ('public', 'member');
|
||||
END IF;
|
||||
END$$;
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'profile_visibility') THEN
|
||||
CREATE TYPE profile_visibility AS ENUM ('public', 'member');
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
age INTEGER NULL,
|
||||
bio JSONB,
|
||||
bio_length integer null,
|
||||
born_in_location TEXT,
|
||||
city TEXT,
|
||||
city_latitude NUMERIC(9, 6),
|
||||
city_longitude NUMERIC(9, 6),
|
||||
comments_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
company TEXT,
|
||||
country TEXT,
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
diet TEXT[],
|
||||
disabled BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
drinks_per_month INTEGER,
|
||||
education_level TEXT,
|
||||
ethnicity TEXT[],
|
||||
gender TEXT,
|
||||
geodb_city_id TEXT,
|
||||
has_kids INTEGER,
|
||||
height_in_inches float4,
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
image_descriptions jsonb,
|
||||
is_smoker BOOLEAN,
|
||||
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL,
|
||||
occupation TEXT,
|
||||
occupation_title TEXT,
|
||||
photo_urls TEXT[],
|
||||
pinned_url TEXT,
|
||||
political_beliefs TEXT[],
|
||||
political_details TEXT,
|
||||
pref_age_max INTEGER NULL,
|
||||
pref_age_min INTEGER NULL,
|
||||
pref_gender TEXT[],
|
||||
pref_relation_styles TEXT[],
|
||||
pref_romantic_styles TEXT[],
|
||||
raised_in_city TEXT,
|
||||
raised_in_country TEXT,
|
||||
raised_in_geodb_city_id TEXT,
|
||||
raised_in_lat NUMERIC(9, 6),
|
||||
raised_in_lon NUMERIC(9, 6),
|
||||
raised_in_radius INTEGER,
|
||||
raised_in_region_code TEXT,
|
||||
referred_by_username TEXT,
|
||||
region_code TEXT,
|
||||
relationship_status TEXT[],
|
||||
religion TEXT[],
|
||||
CREATE TABLE IF NOT EXISTS profiles
|
||||
(
|
||||
age INTEGER NULL,
|
||||
bio JSONB,
|
||||
bio_length integer null,
|
||||
born_in_location TEXT,
|
||||
city TEXT,
|
||||
city_latitude NUMERIC(9, 6),
|
||||
city_longitude NUMERIC(9, 6),
|
||||
comments_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
company TEXT,
|
||||
country TEXT,
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
diet TEXT[],
|
||||
disabled BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
drinks_per_month INTEGER,
|
||||
education_level TEXT,
|
||||
ethnicity TEXT[],
|
||||
gender TEXT,
|
||||
geodb_city_id TEXT,
|
||||
has_kids INTEGER,
|
||||
height_in_inches float4,
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
image_descriptions jsonb,
|
||||
is_smoker BOOLEAN,
|
||||
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
allow_direct_messaging BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
allow_interest_indicating BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
occupation TEXT,
|
||||
occupation_title TEXT,
|
||||
photo_urls TEXT[],
|
||||
pinned_url TEXT,
|
||||
political_beliefs TEXT[],
|
||||
political_details TEXT,
|
||||
pref_age_max INTEGER NULL,
|
||||
pref_age_min INTEGER NULL,
|
||||
pref_gender TEXT[],
|
||||
pref_relation_styles TEXT[],
|
||||
pref_romantic_styles TEXT[],
|
||||
raised_in_city TEXT,
|
||||
raised_in_country TEXT,
|
||||
raised_in_geodb_city_id TEXT,
|
||||
raised_in_lat NUMERIC(9, 6),
|
||||
raised_in_lon NUMERIC(9, 6),
|
||||
raised_in_radius INTEGER,
|
||||
raised_in_region_code TEXT,
|
||||
referred_by_username TEXT,
|
||||
region_code TEXT,
|
||||
relationship_status TEXT[],
|
||||
religion TEXT[],
|
||||
religious_belief_strength INTEGER,
|
||||
religious_beliefs TEXT,
|
||||
twitter TEXT,
|
||||
university TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
||||
wants_kids_strength INTEGER DEFAULT 0,
|
||||
website TEXT,
|
||||
religious_beliefs TEXT,
|
||||
twitter TEXT,
|
||||
university TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
||||
wants_kids_strength INTEGER DEFAULT 0,
|
||||
website TEXT,
|
||||
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE profiles
|
||||
ADD CONSTRAINT profiles_user_id_fkey
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
REFERENCES users (id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE profiles
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies
|
||||
DROP POLICY IF EXISTS "public read" ON profiles;
|
||||
CREATE POLICY "public read" ON profiles
|
||||
FOR SELECT USING (true);
|
||||
FOR SELECT USING (true);
|
||||
|
||||
DROP POLICY IF EXISTS "self update" ON profiles;
|
||||
|
||||
CREATE POLICY "self update" ON profiles
|
||||
FOR UPDATE
|
||||
WITH CHECK ((user_id = firebase_uid()));
|
||||
FOR UPDATE
|
||||
WITH CHECK ((user_id = firebase_uid()));
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON public.profiles USING btree (user_id);
|
||||
@@ -140,8 +145,9 @@ CREATE INDEX users_name_trgm_idx
|
||||
|
||||
-- Functions and Triggers
|
||||
CREATE
|
||||
OR REPLACE FUNCTION update_last_modification_time()
|
||||
RETURNS TRIGGER AS $$
|
||||
OR REPLACE FUNCTION update_last_modification_time()
|
||||
RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
NEW.last_modification_time = now();
|
||||
RETURN NEW;
|
||||
@@ -153,7 +159,7 @@ CREATE TRIGGER trigger_update_last_mod_time
|
||||
BEFORE UPDATE
|
||||
ON profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_last_modification_time();
|
||||
EXECUTE FUNCTION update_last_modification_time();
|
||||
|
||||
-- pg_trgm
|
||||
create extension if not exists pg_trgm;
|
||||
@@ -163,10 +169,12 @@ CREATE INDEX profiles_bio_trgm_idx
|
||||
|
||||
|
||||
--- bio_text
|
||||
ALTER TABLE profiles ADD COLUMN bio_text TEXT;
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN bio_text TEXT;
|
||||
|
||||
ALTER TABLE profiles ADD COLUMN bio_tsv tsvector
|
||||
GENERATED ALWAYS AS (to_tsvector('english', coalesce(bio_text, ''))) STORED;
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN bio_tsv tsvector
|
||||
GENERATED ALWAYS AS (to_tsvector('english', coalesce(bio_text, ''))) STORED;
|
||||
|
||||
CREATE INDEX profiles_bio_tsv_idx ON profiles USING GIN (bio_tsv);
|
||||
|
||||
@@ -176,7 +184,8 @@ ALTER TABLE profiles
|
||||
|
||||
-- Rebuild search (search_txt and search_tsv)
|
||||
CREATE OR REPLACE FUNCTION trg_profiles_rebuild_search()
|
||||
RETURNS trigger AS $$
|
||||
RETURNS trigger AS
|
||||
$$
|
||||
BEGIN
|
||||
PERFORM rebuild_profile_search(NEW.id);
|
||||
RETURN NEW;
|
||||
|
||||
@@ -526,6 +526,11 @@
|
||||
"onboarding.step2.title": "Suchen, nicht scrollen.",
|
||||
"onboarding.step3.body1": "Matches sind weder magisch noch mysteriös.",
|
||||
"onboarding.step3.body2": "Ihr Kompatibilitäts-Score kommt von expliziten Fragen:",
|
||||
"profile.turned_off_interest_indicators": "{user} hat Interessenindikatoren deaktiviert",
|
||||
"profile.mutual_interests": "Sie haben beide gemeinsame Interessen: {matches}",
|
||||
"profile.mutual": "Gegenseitig",
|
||||
"profile.not_both_open_to_type": "Sie sind nicht beide für eine(n) {type} offen",
|
||||
"profile.wont_be_notified_unless_mutual": "Sie werden nicht benachrichtigt, es sei denn, das Interesse ist gegenseitig.",
|
||||
"onboarding.step3.body3": "Sie können das System inspizieren, in Frage stellen und verbessern. Die vollständige Mathematik ist Open Source.",
|
||||
"onboarding.step3.continue": "Loslegen",
|
||||
"onboarding.step3.footer": "Wenn Sie nicht mit der Funktionsweise einverstanden sind, können Sie helfen, sie zu ändern.",
|
||||
@@ -1004,6 +1009,7 @@
|
||||
"security.title": "Sicherheit",
|
||||
"send_message.button_label": "Nachricht",
|
||||
"send_message.title": "Kontakt",
|
||||
"messaging.send_thoughtful_message": "Sende ihnen eine durchdachte Nachricht",
|
||||
"settings.action.cancel": "Abbrechen",
|
||||
"settings.action.save": "Speichern",
|
||||
"settings.danger_zone": "Gefahrenzone",
|
||||
@@ -1033,6 +1039,8 @@
|
||||
"settings.email.verification_failed": "Senden der Bestätigungs-E-Mail fehlgeschlagen.",
|
||||
"settings.email.verification_sent": "Bestätigungs-E-Mail gesendet – prüfen Sie Ihren Posteingang und Spam-Ordner.",
|
||||
"settings.email.verified": "E-Mail bestätigt ✔️",
|
||||
"settings.email.just_verified": "Ich habe meine E-Mail bestätigt",
|
||||
"settings.email.not_verified": "E-Mail immer noch nicht bestätigt...",
|
||||
"settings.general.account": "Konto",
|
||||
"settings.general.email": "E-Mail",
|
||||
"settings.general.font": "Schriftart",
|
||||
@@ -1204,5 +1212,27 @@
|
||||
"vote.toast.created": "Vorschlag erstellt",
|
||||
"vote.urgent": "Dringend",
|
||||
"vote.voted": "Stimme gespeichert",
|
||||
"vote.with_priority": "mit Priorität"
|
||||
"vote.with_priority": "mit Priorität",
|
||||
"settings.connection_preferences.title": "Verbindungseinstellungen",
|
||||
"settings.connection_preferences.description": "Kontrollieren Sie, wie andere sich mit Ihnen verbinden können.",
|
||||
"settings.connection_preferences.direct_messaging": "Direktnachrichten",
|
||||
"settings.connection_preferences.dm_description": "Lassen Sie jeden sofort ein Gespräch mit Ihnen beginnen.",
|
||||
"settings.connection_preferences.interest_indicator": "Private Interessenssignale",
|
||||
"settings.connection_preferences.indicator_description": "Erlauben Sie Menschen, privat Interesse zu signalisieren. Sie werden nur benachrichtigt, wenn das Interesse gegenseitig ist.",
|
||||
"settings.connection_preferences.updated": "Einstellungen aktualisiert",
|
||||
"settings.connection_preferences.update_failed": "Fehler beim Aktualisieren der Einstellungen",
|
||||
"profile.connect.load_preferences_failed": "Fehler beim Laden der Einstellungen",
|
||||
"profile.connect.update_preference_failed": "Fehler beim Aktualisieren der Einstellung",
|
||||
"profile.connect.title": "Verbinden",
|
||||
"profile.connect.send_message": "Nachricht senden",
|
||||
"profile.connect.direct_messaging_disabled": "{user} hat Direktnachrichten deaktiviert",
|
||||
"profile.header.tooltip.direct_messaging_off": "{name} hat Direktnachrichten deaktiviert",
|
||||
"profile.header.tooltip.can_express_interest": ", aber Sie können am Ende des Profils immer noch Interesse zeigen",
|
||||
"profile.connect.private_connection_signal": "Privates Verbindungssignal",
|
||||
"profile.connect.mutual_notification_description": "Sie werden nicht benachrichtigt, es sei denn, das Interesse ist gegenseitig.",
|
||||
"profile.connect.how_this_works": "Wie es funktioniert",
|
||||
"profile.connect.not_both_open": "Sie sind nicht beide für einen {type} offen",
|
||||
"profile.connect.mutual": "Gegenseitig",
|
||||
"profile.connect.interest_indicators_disabled": "{user} hat Interessenssignale deaktiviert",
|
||||
"profile.connect.tips": "- Wählen Sie den Verbindungstyp, für den Sie offen sind.\n- Sie sehen dies nicht, es sei denn, sie wählen denselben Typ wie Sie.\n- Wenn Sie beide denselben Typ wählen, werden Sie beide benachrichtigt."
|
||||
}
|
||||
|
||||
@@ -524,6 +524,11 @@
|
||||
"onboarding.step2.item4": "Lieu et intention",
|
||||
"onboarding.step2.title": "Cherchez, sans swipe.",
|
||||
"onboarding.step3.body1": "Les correspondances ne sont ni magiques ni mystérieuses.",
|
||||
"profile.turned_off_interest_indicators": "{user} a désactivé les indicateurs d'intérêt",
|
||||
"profile.mutual_interests": "Vous avez tous les deux des intérêts communs : {matches}. Contactez-les ci-dessus!",
|
||||
"profile.mutual": "Mutuel",
|
||||
"profile.not_both_open_to_type": "Vous n'êtes pas tous les deux ouverts à un(e) {type}",
|
||||
"profile.wont_be_notified_unless_mutual": "Il/elle ne sera pas notifié(e) à moins que l'intérêt soit mutuel.",
|
||||
"onboarding.step3.body2": "Votre score de compatibilité provient de questions explicites :",
|
||||
"onboarding.step3.body3": "Vous pouvez inspecter, questionner et améliorer le système. Les maths derrière sont open source.",
|
||||
"onboarding.step3.continue": "Commencer",
|
||||
@@ -1003,6 +1008,7 @@
|
||||
"security.title": "Sécurité",
|
||||
"send_message.button_label": "Contacter",
|
||||
"send_message.title": "Contactez",
|
||||
"messaging.send_thoughtful_message": "Envoyez-leur un message réfléchi",
|
||||
"settings.action.cancel": "Annuler",
|
||||
"settings.action.save": "Enregistrer",
|
||||
"settings.danger_zone": "Zone dangereuse",
|
||||
@@ -1032,6 +1038,8 @@
|
||||
"settings.email.verification_failed": "Échec de l'envoi de l'e-mail de vérification.",
|
||||
"settings.email.verification_sent": "E-mail de vérification envoyé — vérifiez votre boîte de réception et les spams.",
|
||||
"settings.email.verified": "E-mail vérifié ✔️",
|
||||
"settings.email.just_verified": "J'ai vérifié mon e-mail",
|
||||
"settings.email.not_verified": "E-mail toujours non vérifié...",
|
||||
"settings.general.account": "Compte",
|
||||
"settings.general.email": "E-mail",
|
||||
"settings.general.font": "Police",
|
||||
@@ -1203,5 +1211,27 @@
|
||||
"vote.toast.created": "Proposition créée",
|
||||
"vote.urgent": "Urgente",
|
||||
"vote.voted": "Vote enregistré",
|
||||
"vote.with_priority": "avec priorité"
|
||||
"vote.with_priority": "avec priorité",
|
||||
"settings.connection_preferences.title": "Préférences de connexion",
|
||||
"settings.connection_preferences.description": "Contrôlez comment les autres peuvent se connecter avec vous.",
|
||||
"settings.connection_preferences.direct_messaging": "Messagerie directe",
|
||||
"settings.connection_preferences.dm_description": "Laissez n'importe qui commencer une conversation avec vous immédiatement.",
|
||||
"settings.connection_preferences.interest_indicator": "Signaux d'intérêt privés",
|
||||
"settings.connection_preferences.indicator_description": "Autorisez les gens à signaler leur intérêt en privé. Vous n'êtes notifié que si l'intérêt est mutuel.",
|
||||
"settings.connection_preferences.updated": "Préférences mises à jour",
|
||||
"settings.connection_preferences.update_failed": "Échec de la mise à jour des préférences",
|
||||
"profile.connect.load_preferences_failed": "Échec du chargement des préférences",
|
||||
"profile.connect.update_preference_failed": "Échec de la mise à jour de la préférence",
|
||||
"profile.connect.title": "Connecter",
|
||||
"profile.connect.send_message": "Envoyer un message",
|
||||
"profile.connect.direct_messaging_disabled": "{user} a désactivé la messagerie directe",
|
||||
"profile.header.tooltip.direct_messaging_off": "{name} a désactivé la messagerie directe",
|
||||
"profile.header.tooltip.can_express_interest": ", mais vous pouvez toujours exprimer votre intérêt à la fin du profil",
|
||||
"profile.connect.private_connection_signal": "Signal de connexion privé",
|
||||
"profile.connect.mutual_notification_description": "Ils ne seront pas notifiés à moins que l'intérêt soit mutuel.",
|
||||
"profile.connect.how_this_works": "Comment ça fonctionne",
|
||||
"profile.connect.not_both_open": "Vous n'êtes pas tous les deux ouverts à un {type}",
|
||||
"profile.connect.mutual": "Mutuel",
|
||||
"profile.connect.interest_indicators_disabled": "{user} a désactivé les indicateurs d'intérêt",
|
||||
"profile.connect.tips": "- Vous choisissez le type de connexion auquel vous êtes ouvert.\n- Ils ne verront pas ceci à moins qu'ils ne choisissent le même type que vous.\n- Si vous choisissez tous les deux le même type, vous serez tous les deux notifiés."
|
||||
}
|
||||
|
||||
@@ -235,6 +235,33 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Update profile fields for the authenticated user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'get-connection-interests': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: false,
|
||||
props: z.object({
|
||||
targetUserId: z.string(),
|
||||
}),
|
||||
returns: {} as {
|
||||
interests: string[]
|
||||
targetInterests: string[]
|
||||
},
|
||||
summary: 'Get connection preferences for a user or another user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'update-connection-interest': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
targetUserId: z.string(),
|
||||
connectionType: z.string(),
|
||||
seeking: z.boolean(),
|
||||
}),
|
||||
returns: {} as {success: boolean},
|
||||
summary: 'Update connection preference for the authenticated user',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'update-notif-settings': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
|
||||
@@ -46,14 +46,16 @@ export const baseProfilesSchema = z.object({
|
||||
age: z.number().min(18).max(100).optional().nullable(),
|
||||
bio: contentSchema.optional().nullable(),
|
||||
bio_length: z.number().optional().nullable(),
|
||||
city: z.string(),
|
||||
city: z.string().optional().nullable(),
|
||||
city_latitude: z.number().optional().nullable(),
|
||||
city_longitude: z.number().optional().nullable(),
|
||||
country: z.string().optional().nullable(),
|
||||
gender: genderType,
|
||||
gender: genderType.optional().nullable(),
|
||||
geodb_city_id: z.string().optional().nullable(),
|
||||
languages: z.array(z.string()).optional().nullable(),
|
||||
looking_for_matches: zBoolean,
|
||||
looking_for_matches: zBoolean.optional(),
|
||||
allow_direct_messaging: zBoolean.optional(),
|
||||
allow_interest_indicating: zBoolean.optional(),
|
||||
photo_urls: z.array(z.string()).nullable(),
|
||||
pinned_url: z.string(),
|
||||
pref_age_max: z.number().min(18).max(100).optional().nullable(),
|
||||
|
||||
@@ -4,7 +4,7 @@ export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: '13.0.4'
|
||||
PostgrestVersion: '13.0.5'
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
@@ -292,6 +292,45 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
connection_interests: {
|
||||
Row: {
|
||||
connection_type: string
|
||||
created_at: string
|
||||
id: number
|
||||
target_user_id: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
connection_type: string
|
||||
created_at?: string
|
||||
id?: never
|
||||
target_user_id: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
connection_type?: string
|
||||
created_at?: string
|
||||
id?: never
|
||||
target_user_id?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'connection_interests_target_user_id_fkey'
|
||||
columns: ['target_user_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'connection_interests_user_id_fkey'
|
||||
columns: ['user_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
contact: {
|
||||
Row: {
|
||||
content: Json | null
|
||||
@@ -528,6 +567,38 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
notification_template_translations: {
|
||||
Row: {
|
||||
created_time: string
|
||||
locale: string
|
||||
source_text: string
|
||||
template_id: string
|
||||
title: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_time?: string
|
||||
locale: string
|
||||
source_text: string
|
||||
template_id: string
|
||||
title?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_time?: string
|
||||
locale?: string
|
||||
source_text?: string
|
||||
template_id?: string
|
||||
title?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'notification_template_translations_template_id_fkey'
|
||||
columns: ['template_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'notification_templates'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
notification_templates: {
|
||||
Row: {
|
||||
created_time: number
|
||||
@@ -564,37 +635,6 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
notification_template_translations: {
|
||||
Row: {
|
||||
template_id: string
|
||||
locale: string
|
||||
title: string | null
|
||||
source_text: string
|
||||
created_time: number
|
||||
}
|
||||
Insert: {
|
||||
template_id: string
|
||||
locale: string
|
||||
title?: string | null
|
||||
source_text: string
|
||||
created_time?: number
|
||||
}
|
||||
Update: {
|
||||
template_id?: string
|
||||
locale?: string
|
||||
title?: string | null
|
||||
source_text?: string
|
||||
created_time?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'notification_template_translations_template_id_fkey'
|
||||
columns: ['template_id']
|
||||
referencedRelation: 'notification_templates'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
private_user_message_channel_members: {
|
||||
Row: {
|
||||
channel_id: number
|
||||
@@ -764,14 +804,17 @@ export type Database = {
|
||||
Row: {
|
||||
data: Json
|
||||
id: string
|
||||
locale: string | null
|
||||
}
|
||||
Insert: {
|
||||
data: Json
|
||||
id: string
|
||||
locale?: string | null
|
||||
}
|
||||
Update: {
|
||||
data?: Json
|
||||
id?: string
|
||||
locale?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
@@ -1036,6 +1079,8 @@ export type Database = {
|
||||
profiles: {
|
||||
Row: {
|
||||
age: number | null
|
||||
allow_direct_messaging: boolean | null
|
||||
allow_interest_indicating: boolean | null
|
||||
big5_agreeableness: number | null
|
||||
big5_conscientiousness: number | null
|
||||
big5_extraversion: number | null
|
||||
@@ -1069,7 +1114,6 @@ export type Database = {
|
||||
last_modification_time: string
|
||||
looking_for_matches: boolean
|
||||
mbti: string | null
|
||||
messaging_status: string
|
||||
occupation: string | null
|
||||
occupation_title: string | null
|
||||
photo_urls: string[] | null
|
||||
@@ -1105,6 +1149,8 @@ export type Database = {
|
||||
}
|
||||
Insert: {
|
||||
age?: number | null
|
||||
allow_direct_messaging?: boolean | null
|
||||
allow_interest_indicating?: boolean | null
|
||||
big5_agreeableness?: number | null
|
||||
big5_conscientiousness?: number | null
|
||||
big5_extraversion?: number | null
|
||||
@@ -1138,7 +1184,6 @@ export type Database = {
|
||||
last_modification_time?: string
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
@@ -1174,6 +1219,8 @@ export type Database = {
|
||||
}
|
||||
Update: {
|
||||
age?: number | null
|
||||
allow_direct_messaging?: boolean | null
|
||||
allow_interest_indicating?: boolean | null
|
||||
big5_agreeableness?: number | null
|
||||
big5_conscientiousness?: number | null
|
||||
big5_extraversion?: number | null
|
||||
@@ -1207,7 +1254,6 @@ export type Database = {
|
||||
last_modification_time?: string
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
@@ -1420,15 +1466,7 @@ export type Database = {
|
||||
ts?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'user_events_user_id_fkey'
|
||||
columns: ['user_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
Relationships: []
|
||||
}
|
||||
user_notifications: {
|
||||
Row: {
|
||||
|
||||
@@ -10,6 +10,8 @@ export type User = {
|
||||
link: Socials // Social links
|
||||
isBannedFromPosting?: boolean
|
||||
userDeleted?: boolean
|
||||
allow_direct_messaging?: boolean
|
||||
allow_interest_indicating?: boolean
|
||||
}
|
||||
|
||||
export type PrivateUser = {
|
||||
|
||||
98
web/components/connection-preferences-settings.tsx
Normal file
98
web/components/connection-preferences-settings.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import {useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {SwitchSetting} from 'web/components/switch-setting'
|
||||
import {useProfile} from 'web/hooks/use-profile'
|
||||
import {updateProfile} from 'web/lib/api'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export function ConnectionPreferencesSettings() {
|
||||
const t = useT()
|
||||
const profile = useProfile()
|
||||
|
||||
const [allowDirectMessaging, setAllowDirectMessaging] = useState(
|
||||
profile?.allow_direct_messaging !== false,
|
||||
)
|
||||
const [allowInterestIndicating, setAllowInterestIndicating] = useState(
|
||||
profile?.allow_interest_indicating !== false,
|
||||
)
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
|
||||
const handleUpdate = async (field: string, value: boolean) => {
|
||||
setIsUpdating(true)
|
||||
try {
|
||||
await updateProfile({[field]: value})
|
||||
// toast.success(t('settings.connection_preferences.updated', 'Preferences updated'))
|
||||
} catch (error) {
|
||||
console.error('Error updating preferences:', error)
|
||||
toast.error(
|
||||
t('settings.connection_preferences.update_failed', 'Failed to update preferences'),
|
||||
)
|
||||
} finally {
|
||||
setIsUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDirectMessagingChange = (checked: boolean) => {
|
||||
setAllowDirectMessaging(checked)
|
||||
handleUpdate('allow_direct_messaging', checked)
|
||||
}
|
||||
|
||||
const handleInterestIndicatingChange = (checked: boolean) => {
|
||||
setAllowInterestIndicating(checked)
|
||||
handleUpdate('allow_interest_indicating', checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<Col className="gap-4">
|
||||
<div className="text-sm text-ink-500">
|
||||
{t(
|
||||
'settings.connection_preferences.description',
|
||||
'Control how others can connect with you.',
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Row className="items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">
|
||||
{t('settings.connection_preferences.direct_messaging', 'Direct Messaging')}
|
||||
</div>
|
||||
<div className="text-ink-500 text-sm">
|
||||
{t(
|
||||
'settings.connection_preferences.dm_description',
|
||||
'Let anyone start a conversation with you immediately.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SwitchSetting
|
||||
checked={allowDirectMessaging}
|
||||
onChange={handleDirectMessagingChange}
|
||||
disabled={isUpdating}
|
||||
colorMode={'primary'}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row className="items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">
|
||||
{t('settings.connection_preferences.interest_indicator', 'Private interest signals')}
|
||||
</div>
|
||||
<div className="text-ink-500 text-sm">
|
||||
{t(
|
||||
'settings.connection_preferences.indicator_description',
|
||||
'Allow people to privately signal interest. You are only notified if the interest is mutual.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SwitchSetting
|
||||
checked={allowInterestIndicating}
|
||||
onChange={handleInterestIndicatingChange}
|
||||
disabled={isUpdating}
|
||||
colorMode={'primary'}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export function EmailVerificationButton() {
|
||||
console.log('User email verified')
|
||||
return true
|
||||
} else {
|
||||
toast.error(t('', 'Email still not verified...'))
|
||||
toast.error(t('settings.email.not_verified', 'Email still not verified...'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,11 @@ export const SendMessageButton = (props: {
|
||||
currentUser: User | undefined | null
|
||||
includeLabel?: boolean
|
||||
circleButton?: boolean
|
||||
text?: string
|
||||
tooltipText?: string
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const {toUser, currentUser, includeLabel, circleButton} = props
|
||||
const {toUser, currentUser, includeLabel, circleButton, text, tooltipText, disabled} = props
|
||||
const firebaseUser = useFirebaseUser()
|
||||
const router = useRouter()
|
||||
const privateUser = usePrivateUser()
|
||||
@@ -40,6 +43,7 @@ export const SendMessageButton = (props: {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const messageButtonClicked = async () => {
|
||||
if (disabled) return
|
||||
if (!currentUser) return firebaseLogin()
|
||||
const previousDirectMessageChannel = findKey(
|
||||
memberIdsByChannelId,
|
||||
@@ -97,11 +101,24 @@ export const SendMessageButton = (props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip text={t('send_message.button_label', 'Message')} noTap>
|
||||
{circleButton ? (
|
||||
<button
|
||||
className="bg-primary-900 hover:bg-primary-600 h-7 w-7 rounded-full transition-colors"
|
||||
<Tooltip text={tooltipText || t('send_message.button_label', 'Message')} noTap>
|
||||
{text ? (
|
||||
<Button
|
||||
className={clsx('h-fit gap-1', disabled && 'opacity-50 cursor-not-allowed')}
|
||||
color={'gray-outline'}
|
||||
onClick={messageButtonClicked}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('messaging.send_thoughtful_message', 'Send them a thoughtful message')}
|
||||
</Button>
|
||||
) : circleButton ? (
|
||||
<button
|
||||
className={clsx(
|
||||
'h-7 w-7 rounded-full transition-colors',
|
||||
disabled ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary-900 hover:bg-primary-600',
|
||||
)}
|
||||
onClick={messageButtonClicked}
|
||||
disabled={disabled}
|
||||
>
|
||||
<BiEnvelope
|
||||
className={clsx('m-auto h-5 w-5 text-white drop-shadow', includeLabel && 'mr-2')}
|
||||
@@ -112,7 +129,11 @@ export const SendMessageButton = (props: {
|
||||
size={'sm'}
|
||||
onClick={messageButtonClicked}
|
||||
color={'none'}
|
||||
className="bg-canvas-200 hover:bg-canvas-300"
|
||||
className={clsx(
|
||||
'bg-canvas-200 hover:bg-canvas-300',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<BiEnvelope className={clsx('h-5 w-5', includeLabel && 'mr-2')} />{' '}
|
||||
{includeLabel && <>{t('send_message.button_label', 'Message')}</>}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ProfileCommentSection = (props: {
|
||||
if (!currentUser && (!profile.comments_enabled || parentComments.length == 0)) return null
|
||||
|
||||
return (
|
||||
<Col className={'mt-4 rounded py-2'}>
|
||||
<Col className={'rounded'}>
|
||||
<Row className={'mb-4 justify-between'}>
|
||||
<Subtitle>{t('profile.comments.section_title', 'Endorsements')}</Subtitle>
|
||||
{isCurrentUser && !simpleView && (
|
||||
|
||||
204
web/components/profile/connect-actions.tsx
Normal file
204
web/components/profile/connect-actions.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import {INVERTED_RELATIONSHIP_CHOICES, RELATIONSHIP_CHOICES} from 'common/choices'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {useEffect, useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {SendMessageButton} from 'web/components/messaging/send-message-button'
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
import {useProfile} from 'web/hooks/use-profile'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {api} from 'web/lib/api'
|
||||
import {User} from 'web/lib/firebase/users'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export function ConnectActions(props: {profile: Profile; user: User}) {
|
||||
const {profile, user} = props
|
||||
const currentUser = useUser()
|
||||
const currentProfile = useProfile()
|
||||
const t = useT()
|
||||
const isCurrentUser = currentUser?.id === user.id
|
||||
|
||||
const soughtConnections = currentProfile?.pref_relation_styles
|
||||
const targetSoughtConnections = profile.pref_relation_styles
|
||||
|
||||
const [interests, setInterests] = useState<string[]>([])
|
||||
const [targetInterests, setTargetInterests] = useState<string[]>([])
|
||||
|
||||
const matches = interests.filter((interest) => targetInterests.includes(interest))
|
||||
|
||||
const tips = t(
|
||||
'profile.connect.tips',
|
||||
`
|
||||
- You choose the type of connection you're open to.
|
||||
- They won’t see this unless they choose the same type with you.
|
||||
- If you both choose the same type, you’re both notified.
|
||||
`,
|
||||
)
|
||||
const [showHelp, setShowHelp] = useState<boolean>(false)
|
||||
|
||||
const loadPreferences = async () => {
|
||||
try {
|
||||
const result = await api('get-connection-interests', {
|
||||
targetUserId: user.id,
|
||||
})
|
||||
console.log('Preferences:', result)
|
||||
setInterests(result.interests)
|
||||
setTargetInterests(result.targetInterests)
|
||||
} catch (e) {
|
||||
console.error('Error loading preferences:', e)
|
||||
toast.error(t('profile.connect.load_preferences_failed', 'Failed to load preferences'))
|
||||
}
|
||||
}
|
||||
|
||||
// Load user preferences
|
||||
useEffect(() => {
|
||||
if (!currentUser || isCurrentUser) return
|
||||
loadPreferences()
|
||||
}, [currentUser, user.id, isCurrentUser])
|
||||
|
||||
const handleInterestChange = async (connectionType: string, checked: boolean) => {
|
||||
if (!currentUser || isCurrentUser) return
|
||||
|
||||
try {
|
||||
await api('update-connection-interest', {
|
||||
targetUserId: user.id,
|
||||
connectionType,
|
||||
seeking: checked,
|
||||
})
|
||||
|
||||
// Reload preferences to get updated data
|
||||
setInterests((prev) =>
|
||||
checked ? [...prev, connectionType] : prev.filter((e) => e !== connectionType),
|
||||
)
|
||||
if (checked) await loadPreferences()
|
||||
} catch (error) {
|
||||
console.error('Error updating preference:', error)
|
||||
toast.error(t('profile.connect.update_preference_failed', 'Failed to update preference'))
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrentUser || !currentUser) return null
|
||||
|
||||
return (
|
||||
<Col className="bg-canvas-0 w-full gap-6 rounded-xl px-2 shadow-sm">
|
||||
<div className="border-t border-ink-200">
|
||||
<h3 className="text-xl font-semibold mb-4">{t('profile.connect.title', 'Connect')}</h3>
|
||||
|
||||
{/* Primary Action */}
|
||||
<div className="mb-6">
|
||||
{profile.allow_direct_messaging || matches.length > 0 ? (
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
currentUser={currentUser}
|
||||
text={t('profile.connect.send_message', 'Send Message')}
|
||||
/>
|
||||
) : (
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'profile.connect.direct_messaging_disabled',
|
||||
'{user} turned off direct messaging',
|
||||
{user: user.name},
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Interest Section */}
|
||||
<div className="prose prose-neutral dark:prose-invert">
|
||||
<div className="text-ink-700 font-medium text-lg">
|
||||
{t('profile.connect.private_connection_signal', 'Private connection signal')}
|
||||
</div>
|
||||
|
||||
{profile.allow_interest_indicating ? (
|
||||
<>
|
||||
<Row className={'gap-2 my-4'}>
|
||||
<div className="text-ink-500">
|
||||
{t(
|
||||
'profile.wont_be_notified_unless_mutual',
|
||||
'They won’t be notified unless the interest is mutual.',
|
||||
)}
|
||||
</div>
|
||||
<button className="text-primary-600" onClick={() => setShowHelp(!showHelp)}>
|
||||
{t('profile.connect.how_this_works', 'How this works')}
|
||||
</button>
|
||||
</Row>
|
||||
{showHelp && <ReactMarkdown>{tips}</ReactMarkdown>}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Object.entries(RELATIONSHIP_CHOICES).map(([label, type]) => {
|
||||
const isSelected = interests.includes(type)
|
||||
const isMutual = interests.includes(type) && targetInterests.includes(type)
|
||||
|
||||
const isDisabled =
|
||||
!(
|
||||
targetSoughtConnections?.includes(type) ||
|
||||
(targetSoughtConnections ?? []).length === 0
|
||||
) || !soughtConnections?.includes(type)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={
|
||||
isDisabled &&
|
||||
t('profile.not_both_open_to_type', 'You are not both open to a {type}', {
|
||||
type: t(`profile.relationship.${type}`, label).toLowerCase(),
|
||||
})
|
||||
}
|
||||
>
|
||||
<button
|
||||
key={type}
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleInterestChange(type, !isSelected)}
|
||||
className={`
|
||||
px-4 py-2 rounded-full text-sm font-medium transition
|
||||
border
|
||||
${
|
||||
isMutual
|
||||
? 'bg-primary-500 text-white border-primary-500'
|
||||
: isSelected
|
||||
? 'bg-primary-100 text-primary-800 border-primary-300'
|
||||
: 'bg-canvas-50 text-ink-700 border-ink-300 hover:border-primary-400'
|
||||
}
|
||||
${isDisabled ? 'opacity-40 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
{t(`profile.relationship.${type}`, label)}
|
||||
{isMutual && ` • ${t('profile.mutual', 'Mutual')}`}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{matches.length > 0 && (
|
||||
<div className="my-4">
|
||||
{t(
|
||||
'profile.mutual_interests',
|
||||
'You both have mutual interests: {matches}. Contact them above to connect!',
|
||||
{
|
||||
matches: matches
|
||||
.map((m) =>
|
||||
t(
|
||||
`profile.relationship.${m}`,
|
||||
INVERTED_RELATIONSHIP_CHOICES[m],
|
||||
).toLowerCase(),
|
||||
)
|
||||
.join(', '),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'profile.turned_off_interest_indicators',
|
||||
'{user} turned off interest indicators',
|
||||
{user: user.name},
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
@@ -61,6 +61,24 @@ export default function ProfileHeader(props: {
|
||||
currentUser,
|
||||
})
|
||||
|
||||
let tooltipText = undefined
|
||||
if (!profile.allow_direct_messaging) {
|
||||
tooltipText = t(
|
||||
'profile.header.tooltip.direct_messaging_off',
|
||||
'{name} turned off direct messaging',
|
||||
{
|
||||
name: user.name,
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!profile.allow_direct_messaging && profile.allow_interest_indicating) {
|
||||
tooltipText =
|
||||
tooltipText +
|
||||
t(
|
||||
'profile.header.tooltip.can_express_interest',
|
||||
', but you can still express interest at the end of the profile',
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Col className="w-full">
|
||||
{currentUser && !isCurrentUser && isHiddenFromMe && (
|
||||
@@ -194,7 +212,12 @@ export default function ProfileHeader(props: {
|
||||
/>
|
||||
)}
|
||||
{currentUser && showMessageButton && (
|
||||
<SendMessageButton toUser={user} currentUser={currentUser} />
|
||||
<SendMessageButton
|
||||
toUser={user}
|
||||
currentUser={currentUser}
|
||||
tooltipText={tooltipText}
|
||||
disabled={!profile.allow_direct_messaging}
|
||||
/>
|
||||
)}
|
||||
<MoreOptionsUserButton user={user} />
|
||||
</Row>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ProfileHeader from 'web/components/profile/profile-header'
|
||||
import ProfileAbout from 'web/components/profile-about'
|
||||
import ProfileCarousel from 'web/components/profile-carousel'
|
||||
import {ProfileCommentSection} from 'web/components/profile-comment-section'
|
||||
import {ConnectActions} from 'web/components/profile/connect-actions'
|
||||
import {Content} from 'web/components/widgets/editor'
|
||||
import {useGetter} from 'web/hooks/use-getter'
|
||||
import {useHiddenProfiles} from 'web/hooks/use-hidden-profiles'
|
||||
@@ -180,6 +181,7 @@ function ProfileContent(props: {
|
||||
fromProfilePage={fromProfilePage}
|
||||
profile={profile}
|
||||
/>
|
||||
<ConnectActions user={user} profile={profile} />
|
||||
<ProfileCommentSection
|
||||
onUser={user}
|
||||
profile={profile}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {Input} from 'web/components/widgets/input'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {useEditableUserInfo} from 'web/hooks/use-editable-user-info'
|
||||
import {useProfileDraft} from 'web/hooks/use-profile-draft'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {labelClassName} from 'web/pages/signup'
|
||||
|
||||
@@ -24,7 +23,6 @@ export const initialRequiredState = {
|
||||
pref_relation_styles: [],
|
||||
wants_kids_strength: -1,
|
||||
looking_for_matches: true,
|
||||
messaging_status: 'open',
|
||||
visibility: 'member',
|
||||
city: '',
|
||||
pinned_url: '',
|
||||
@@ -51,20 +49,12 @@ export const RequiredProfileUserForm = (props: {
|
||||
onSubmit?: () => void
|
||||
profileCreatedAlready?: boolean
|
||||
}) => {
|
||||
const {user, onSubmit, profileCreatedAlready, setProfile, profile, isSubmitting} = props
|
||||
const {user, onSubmit, profileCreatedAlready, isSubmitting} = props
|
||||
const {updateDisplayName, userInfo, updateUserState, updateUsername} = useEditableUserInfo(user)
|
||||
|
||||
const [step, setStep] = useState<number>(0)
|
||||
const t = useT()
|
||||
|
||||
const {draftLoaded} = useProfileDraft(user.id, profile, setProfile)
|
||||
|
||||
useEffect(() => {
|
||||
if (draftLoaded) {
|
||||
setStep(1)
|
||||
}
|
||||
}, [draftLoaded])
|
||||
|
||||
const {name, username, errorUsername, loadingUsername, loadingName, errorName} = userInfo
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import ShortToggle, {ToggleColorMode} from './widgets/short-toggle'
|
||||
export const SwitchSetting = (props: {
|
||||
checked: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
label: 'Web' | 'Email' | 'Mobile'
|
||||
label?: 'Web' | 'Email' | 'Mobile'
|
||||
disabled: boolean
|
||||
colorMode?: ToggleColorMode
|
||||
}) => {
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ProfilePage() {
|
||||
if (!user || !profile) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowLoading(true)
|
||||
}, 3000)
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
} else {
|
||||
setShowLoading(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {useForm} from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import {AboutSettings} from 'web/components/about-settings'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {ConnectionPreferencesSettings} from 'web/components/connection-preferences-settings'
|
||||
import {EmailVerificationButton} from 'web/components/email-verification-button'
|
||||
import {FontPicker} from 'web/components/font-picker'
|
||||
import {LanguagePicker} from 'web/components/language/language-picker'
|
||||
@@ -130,6 +131,9 @@ const LoadedGeneralSettings = (props: {privateUser: PrivateUser}) => {
|
||||
{t('settings.hidden_profiles.manage', 'Manage hidden profiles')}
|
||||
</Button>
|
||||
|
||||
<h3>{t('settings.connection_preferences.title', 'Connection Preferences')}</h3>
|
||||
<ConnectionPreferencesSettings />
|
||||
|
||||
<h3>{t('settings.general.account', 'Account')}</h3>
|
||||
<h5>{t('settings.general.email', 'Email')}</h5>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user