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:
parent
820ff46efa
commit
75fc3e3485
28
backend/alembic/versions/045_add_share_name_fields.py
Normal file
28
backend/alembic/versions/045_add_share_name_fields.py
Normal 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")
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -20,17 +20,19 @@ 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)
|
||||||
"address": "address",
|
"address": "address",
|
||||||
"company": "company",
|
"company": "company",
|
||||||
"job_title": "job_title",
|
"job_title": "job_title",
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user