diff --git a/backend/alembic/versions/045_add_share_name_fields.py b/backend/alembic/versions/045_add_share_name_fields.py new file mode 100644 index 0000000..674060a --- /dev/null +++ b/backend/alembic/versions/045_add_share_name_fields.py @@ -0,0 +1,28 @@ +"""Add share_first_name and share_last_name to settings. + +Revision ID: 045 +Revises: 044 +""" +from alembic import op +import sqlalchemy as sa + +revision = "045" +down_revision = "044" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "settings", + sa.Column("share_first_name", sa.Boolean, nullable=False, server_default="false"), + ) + op.add_column( + "settings", + sa.Column("share_last_name", sa.Boolean, nullable=False, server_default="false"), + ) + + +def downgrade() -> None: + op.drop_column("settings", "share_last_name") + op.drop_column("settings", "share_first_name") diff --git a/backend/app/models/settings.py b/backend/app/models/settings.py index dac766a..2f3128a 100644 --- a/backend/app/models/settings.py +++ b/backend/app/models/settings.py @@ -57,6 +57,8 @@ class Settings(Base): accept_connections: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # Sharing defaults (what fields are shared with connections by default) + share_first_name: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") + share_last_name: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") share_preferred_name: Mapped[bool] = mapped_column(Boolean, default=True, server_default="true") share_email: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") share_phone: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") diff --git a/backend/app/schemas/connection.py b/backend/app/schemas/connection.py index c313e72..d293290 100644 --- a/backend/app/schemas/connection.py +++ b/backend/app/schemas/connection.py @@ -78,6 +78,8 @@ class CancelResponse(BaseModel): class SharingOverrideUpdate(BaseModel): model_config = ConfigDict(extra="forbid") + first_name: Optional[bool] = None + last_name: Optional[bool] = None preferred_name: Optional[bool] = None email: Optional[bool] = None phone: Optional[bool] = None diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index cfe09e3..f78dfc6 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -48,6 +48,8 @@ class SettingsUpdate(BaseModel): accept_connections: Optional[bool] = None # Sharing defaults + share_first_name: Optional[bool] = None + share_last_name: Optional[bool] = None share_preferred_name: Optional[bool] = None share_email: Optional[bool] = None share_phone: Optional[bool] = None @@ -185,6 +187,8 @@ class SettingsResponse(BaseModel): accept_connections: bool = False # Sharing defaults + share_first_name: bool = False + share_last_name: bool = False share_preferred_name: bool = True share_email: bool = False share_phone: bool = False diff --git a/backend/app/services/connection.py b/backend/app/services/connection.py index 4aa1fb2..33eeb31 100644 --- a/backend/app/services/connection.py +++ b/backend/app/services/connection.py @@ -20,17 +20,19 @@ logger = logging.getLogger(__name__) # Single source of truth — only these fields can be shared via connections SHAREABLE_FIELDS = frozenset({ - "preferred_name", "email", "phone", "mobile", + "first_name", "last_name", "preferred_name", "email", "phone", "mobile", "birthday", "address", "company", "job_title", }) # Maps shareable field names to their Settings model column names _SETTINGS_FIELD_MAP = { + "first_name": None, # first_name comes from User model + "last_name": None, # last_name comes from User model "preferred_name": "preferred_name", - "email": None, # email comes from User model, not Settings + "email": None, # email comes from User model "phone": "phone", "mobile": "mobile", - "birthday": None, # birthday comes from User model (date_of_birth) + "birthday": None, # birthday comes from User model (date_of_birth) "address": "address", "company": "company", "job_title": "job_title", @@ -60,7 +62,11 @@ def resolve_shared_profile( continue # Resolve the actual value - if field == "preferred_name": + if field == "first_name": + result[field] = user.first_name + elif field == "last_name": + result[field] = user.last_name + elif field == "preferred_name": result[field] = settings.preferred_name elif field == "email": result[field] = user.email @@ -79,8 +85,9 @@ def create_person_from_connection( shared_profile: dict, ) -> Person: """Create a Person record for a new connection. Does NOT add to session — caller does.""" - # Use shared preferred_name for display, fall back to umbral_name - first_name = shared_profile.get("preferred_name") or connected_user.umbral_name + # Use shared first_name, fall back to preferred_name, then umbral_name + first_name = shared_profile.get("first_name") or shared_profile.get("preferred_name") or connected_user.umbral_name + last_name = shared_profile.get("last_name") email = shared_profile.get("email") phone = shared_profile.get("phone") mobile = shared_profile.get("mobile") @@ -97,12 +104,14 @@ def create_person_from_connection( pass # Compute display name - display_name = first_name or connected_user.umbral_name + full = ((first_name or '') + ' ' + (last_name or '')).strip() + display_name = full or connected_user.umbral_name return Person( user_id=owner_user_id, name=display_name, first_name=first_name, + last_name=last_name, email=email, phone=phone, mobile=mobile, diff --git a/frontend/src/components/settings/SettingsPage.tsx b/frontend/src/components/settings/SettingsPage.tsx index dda8c69..e76e550 100644 --- a/frontend/src/components/settings/SettingsPage.tsx +++ b/frontend/src/components/settings/SettingsPage.tsx @@ -66,6 +66,8 @@ export default function SettingsPage() { // Social settings const [acceptConnections, setAcceptConnections] = useState(settings?.accept_connections ?? false); + const [shareFirstName, setShareFirstName] = useState(settings?.share_first_name ?? false); + const [shareLastName, setShareLastName] = useState(settings?.share_last_name ?? false); const [sharePreferredName, setSharePreferredName] = useState(settings?.share_preferred_name ?? true); const [shareEmail, setShareEmail] = useState(settings?.share_email ?? false); const [sharePhone, setSharePhone] = useState(settings?.share_phone ?? false); @@ -116,6 +118,8 @@ export default function SettingsPage() { setSettingsCompany(settings.company ?? ''); setSettingsJobTitle(settings.job_title ?? ''); setAcceptConnections(settings.accept_connections); + setShareFirstName(settings.share_first_name); + setShareLastName(settings.share_last_name); setSharePreferredName(settings.share_preferred_name); setShareEmail(settings.share_email); setSharePhone(settings.share_phone); @@ -788,6 +792,8 @@ export default function SettingsPage() {

{[ + { field: 'share_first_name', label: 'First Name', state: shareFirstName, setter: setShareFirstName }, + { field: 'share_last_name', label: 'Last Name', state: shareLastName, setter: setShareLastName }, { field: 'share_preferred_name', label: 'Preferred Name', state: sharePreferredName, setter: setSharePreferredName }, { field: 'share_email', label: 'Email', state: shareEmail, setter: setShareEmail }, { field: 'share_phone', label: 'Phone', state: sharePhone, setter: setSharePhone }, diff --git a/frontend/src/hooks/useNotifications.ts b/frontend/src/hooks/useNotifications.ts index e2663ad..0681773 100644 --- a/frontend/src/hooks/useNotifications.ts +++ b/frontend/src/hooks/useNotifications.ts @@ -48,7 +48,8 @@ export function NotificationProvider({ children }: { children: ReactNode }) { const { data } = await api.get<{ count: number }>('/notifications/unread-count'); return data.count; }, - refetchInterval: () => (visibleRef.current ? 15_000 : false), + refetchInterval: 15_000, + refetchIntervalInBackground: true, staleTime: 10_000, }); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9fb7552..4476907 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -32,6 +32,8 @@ export interface Settings { // Social settings accept_connections: boolean; // Sharing defaults + share_first_name: boolean; + share_last_name: boolean; share_preferred_name: boolean; share_email: boolean; share_phone: boolean;