From 75fc3e3485915826145b02dbd55b682151a5e308 Mon Sep 17 00:00:00 2001
From: Kyle Pope
Date: Wed, 4 Mar 2026 07:34:13 +0800
Subject: [PATCH] Fix notification background polling, add first/last name
sharing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Notifications: enable refetchIntervalInBackground on unread count
query so notifications appear in background tabs without requiring
a tab switch to trigger refetchOnWindowFocus.
Name sharing: add share_first_name and share_last_name to the full
sharing pipeline — migration 045, Settings model/schema, SHAREABLE_FIELDS,
resolve_shared_profile, create_person_from_connection (now populates
first_name + last_name + computed display name), SharingOverrideUpdate,
frontend types and SettingsPage toggles.
Co-Authored-By: Claude Opus 4.6
---
.../versions/045_add_share_name_fields.py | 28 +++++++++++++++++++
backend/app/models/settings.py | 2 ++
backend/app/schemas/connection.py | 2 ++
backend/app/schemas/settings.py | 4 +++
backend/app/services/connection.py | 23 ++++++++++-----
.../src/components/settings/SettingsPage.tsx | 6 ++++
frontend/src/hooks/useNotifications.ts | 3 +-
frontend/src/types/index.ts | 2 ++
8 files changed, 62 insertions(+), 8 deletions(-)
create mode 100644 backend/alembic/versions/045_add_share_name_fields.py
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;