Fix notification background polling, add first/last name sharing

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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-04 07:34:13 +08:00
parent 820ff46efa
commit 75fc3e3485
8 changed files with 62 additions and 8 deletions

View File

@ -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")

View File

@ -57,6 +57,8 @@ class Settings(Base):
accept_connections: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") accept_connections: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
# Sharing defaults (what fields are shared with connections by default) # 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_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_email: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
share_phone: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") share_phone: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")

View File

@ -78,6 +78,8 @@ class CancelResponse(BaseModel):
class SharingOverrideUpdate(BaseModel): class SharingOverrideUpdate(BaseModel):
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
first_name: Optional[bool] = None
last_name: Optional[bool] = None
preferred_name: Optional[bool] = None preferred_name: Optional[bool] = None
email: Optional[bool] = None email: Optional[bool] = None
phone: Optional[bool] = None phone: Optional[bool] = None

View File

@ -48,6 +48,8 @@ class SettingsUpdate(BaseModel):
accept_connections: Optional[bool] = None accept_connections: Optional[bool] = None
# Sharing defaults # Sharing defaults
share_first_name: Optional[bool] = None
share_last_name: Optional[bool] = None
share_preferred_name: Optional[bool] = None share_preferred_name: Optional[bool] = None
share_email: Optional[bool] = None share_email: Optional[bool] = None
share_phone: Optional[bool] = None share_phone: Optional[bool] = None
@ -185,6 +187,8 @@ class SettingsResponse(BaseModel):
accept_connections: bool = False accept_connections: bool = False
# Sharing defaults # Sharing defaults
share_first_name: bool = False
share_last_name: bool = False
share_preferred_name: bool = True share_preferred_name: bool = True
share_email: bool = False share_email: bool = False
share_phone: bool = False share_phone: bool = False

View File

@ -20,14 +20,16 @@ logger = logging.getLogger(__name__)
# Single source of truth — only these fields can be shared via connections # Single source of truth — only these fields can be shared via connections
SHAREABLE_FIELDS = frozenset({ SHAREABLE_FIELDS = frozenset({
"preferred_name", "email", "phone", "mobile", "first_name", "last_name", "preferred_name", "email", "phone", "mobile",
"birthday", "address", "company", "job_title", "birthday", "address", "company", "job_title",
}) })
# Maps shareable field names to their Settings model column names # Maps shareable field names to their Settings model column names
_SETTINGS_FIELD_MAP = { _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", "preferred_name": "preferred_name",
"email": None, # email comes from User model, not Settings "email": None, # email comes from User model
"phone": "phone", "phone": "phone",
"mobile": "mobile", "mobile": "mobile",
"birthday": None, # birthday comes from User model (date_of_birth) "birthday": None, # birthday comes from User model (date_of_birth)
@ -60,7 +62,11 @@ def resolve_shared_profile(
continue continue
# Resolve the actual value # 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 result[field] = settings.preferred_name
elif field == "email": elif field == "email":
result[field] = user.email result[field] = user.email
@ -79,8 +85,9 @@ def create_person_from_connection(
shared_profile: dict, shared_profile: dict,
) -> Person: ) -> Person:
"""Create a Person record for a new connection. Does NOT add to session — caller does.""" """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 # Use shared first_name, fall back to preferred_name, then umbral_name
first_name = shared_profile.get("preferred_name") or connected_user.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") email = shared_profile.get("email")
phone = shared_profile.get("phone") phone = shared_profile.get("phone")
mobile = shared_profile.get("mobile") mobile = shared_profile.get("mobile")
@ -97,12 +104,14 @@ def create_person_from_connection(
pass pass
# Compute display name # 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( return Person(
user_id=owner_user_id, user_id=owner_user_id,
name=display_name, name=display_name,
first_name=first_name, first_name=first_name,
last_name=last_name,
email=email, email=email,
phone=phone, phone=phone,
mobile=mobile, mobile=mobile,

View File

@ -66,6 +66,8 @@ export default function SettingsPage() {
// Social settings // Social settings
const [acceptConnections, setAcceptConnections] = useState(settings?.accept_connections ?? false); 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 [sharePreferredName, setSharePreferredName] = useState(settings?.share_preferred_name ?? true);
const [shareEmail, setShareEmail] = useState(settings?.share_email ?? false); const [shareEmail, setShareEmail] = useState(settings?.share_email ?? false);
const [sharePhone, setSharePhone] = useState(settings?.share_phone ?? false); const [sharePhone, setSharePhone] = useState(settings?.share_phone ?? false);
@ -116,6 +118,8 @@ export default function SettingsPage() {
setSettingsCompany(settings.company ?? ''); setSettingsCompany(settings.company ?? '');
setSettingsJobTitle(settings.job_title ?? ''); setSettingsJobTitle(settings.job_title ?? '');
setAcceptConnections(settings.accept_connections); setAcceptConnections(settings.accept_connections);
setShareFirstName(settings.share_first_name);
setShareLastName(settings.share_last_name);
setSharePreferredName(settings.share_preferred_name); setSharePreferredName(settings.share_preferred_name);
setShareEmail(settings.share_email); setShareEmail(settings.share_email);
setSharePhone(settings.share_phone); setSharePhone(settings.share_phone);
@ -788,6 +792,8 @@ export default function SettingsPage() {
</p> </p>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{[ {[
{ 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_preferred_name', label: 'Preferred Name', state: sharePreferredName, setter: setSharePreferredName },
{ field: 'share_email', label: 'Email', state: shareEmail, setter: setShareEmail }, { field: 'share_email', label: 'Email', state: shareEmail, setter: setShareEmail },
{ field: 'share_phone', label: 'Phone', state: sharePhone, setter: setSharePhone }, { field: 'share_phone', label: 'Phone', state: sharePhone, setter: setSharePhone },

View File

@ -48,7 +48,8 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
const { data } = await api.get<{ count: number }>('/notifications/unread-count'); const { data } = await api.get<{ count: number }>('/notifications/unread-count');
return data.count; return data.count;
}, },
refetchInterval: () => (visibleRef.current ? 15_000 : false), refetchInterval: 15_000,
refetchIntervalInBackground: true,
staleTime: 10_000, staleTime: 10_000,
}); });

View File

@ -32,6 +32,8 @@ export interface Settings {
// Social settings // Social settings
accept_connections: boolean; accept_connections: boolean;
// Sharing defaults // Sharing defaults
share_first_name: boolean;
share_last_name: boolean;
share_preferred_name: boolean; share_preferred_name: boolean;
share_email: boolean; share_email: boolean;
share_phone: boolean; share_phone: boolean;