feat(privacy): support nobody option for group invites
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
2026-03-08 20:32:29 +03:00
parent 362098b954
commit 4122882b7e
6 changed files with 8 additions and 6 deletions

View File

@@ -5,7 +5,7 @@ from typing import Literal
PrivacyLevel = Literal["everyone", "contacts", "nobody"] PrivacyLevel = Literal["everyone", "contacts", "nobody"]
GroupInvitePrivacyLevel = Literal["everyone", "contacts"] GroupInvitePrivacyLevel = Literal["everyone", "contacts", "nobody"]
PrivateMessagesPrivacyLevel = Literal["everyone", "contacts", "nobody"] PrivateMessagesPrivacyLevel = Literal["everyone", "contacts", "nobody"]

View File

@@ -211,6 +211,7 @@ Server behavior: when a user disconnects, active typing/recording indicators are
All fields are optional. All fields are optional.
`privacy_private_messages`: `everyone | contacts | nobody`. `privacy_private_messages`: `everyone | contacts | nobody`.
`privacy_group_invites`: `everyone | contacts | nobody`.
## 3.3 Chats ## 3.3 Chats

View File

@@ -37,7 +37,7 @@ Legend:
28. Notifications - `PARTIAL` (browser notifications + mute/settings; no mobile push infra) 28. Notifications - `PARTIAL` (browser notifications + mute/settings; no mobile push infra)
29. Archive - `DONE` 29. Archive - `DONE`
30. Blacklist - `DONE` 30. Blacklist - `DONE`
31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; integration tests cover PM policy matrix (`everyone/contacts/nobody`), group-invite policy matrix (`everyone/contacts/nobody`), and private chat counterpart visibility for `nobody/contacts`, remaining UX/matrix hardening) 31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; group-invite `nobody` is available in API and web settings; integration tests cover PM policy matrix (`everyone/contacts/nobody`), group-invite policy matrix (`everyone/contacts/nobody`), and private chat counterpart visibility for `nobody/contacts`, remaining UX/matrix hardening)
32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; integration tests cover single-session revoke and revoke-all invalidation/force-disconnect; 2FA setup now blocked after enable to prevent secret re-issuance; one-time recovery codes added; UX polish ongoing) 32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; integration tests cover single-session revoke and revoke-all invalidation/force-disconnect; 2FA setup now blocked after enable to prevent secret re-issuance; one-time recovery codes added; UX polish ongoing)
33. Realtime Events - `DONE` (connect/disconnect/send/receive/typing/read/delivered/online/offline + chat/message updates + chat_deleted) 33. Realtime Events - `DONE` (connect/disconnect/send/receive/typing/read/delivered/online/offline + chat/message updates + chat_deleted)
34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages, chat-info panel hot-refreshes on `chat_updated`, delete/leave updates realtime subscriptions, full-chat delete emits `chat_deleted`) 34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages, chat-info panel hot-refreshes on `chat_updated`, delete/leave updates realtime subscriptions, full-chat delete emits `chat_deleted`)

View File

@@ -17,7 +17,7 @@ interface UserProfileUpdatePayload {
privacy_private_messages?: "everyone" | "contacts" | "nobody"; privacy_private_messages?: "everyone" | "contacts" | "nobody";
privacy_last_seen?: "everyone" | "contacts" | "nobody"; privacy_last_seen?: "everyone" | "contacts" | "nobody";
privacy_avatar?: "everyone" | "contacts" | "nobody"; privacy_avatar?: "everyone" | "contacts" | "nobody";
privacy_group_invites?: "everyone" | "contacts"; privacy_group_invites?: "everyone" | "contacts" | "nobody";
} }
export async function updateMyProfile(payload: UserProfileUpdatePayload): Promise<AuthUser> { export async function updateMyProfile(payload: UserProfileUpdatePayload): Promise<AuthUser> {

View File

@@ -86,7 +86,7 @@ export interface AuthUser {
privacy_private_messages?: "everyone" | "contacts" | "nobody"; privacy_private_messages?: "everyone" | "contacts" | "nobody";
privacy_last_seen?: "everyone" | "contacts" | "nobody"; privacy_last_seen?: "everyone" | "contacts" | "nobody";
privacy_avatar?: "everyone" | "contacts" | "nobody"; privacy_avatar?: "everyone" | "contacts" | "nobody";
privacy_group_invites?: "everyone" | "contacts"; privacy_group_invites?: "everyone" | "contacts" | "nobody";
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }

View File

@@ -47,7 +47,7 @@ export function SettingsPanel({ open, onClose }: Props) {
const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]); const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]);
const [privacyLastSeen, setPrivacyLastSeen] = useState<"everyone" | "contacts" | "nobody">("everyone"); const [privacyLastSeen, setPrivacyLastSeen] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [privacyAvatar, setPrivacyAvatar] = useState<"everyone" | "contacts" | "nobody">("everyone"); const [privacyAvatar, setPrivacyAvatar] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [privacyGroupInvites, setPrivacyGroupInvites] = useState<"everyone" | "contacts">("everyone"); const [privacyGroupInvites, setPrivacyGroupInvites] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [notificationItems, setNotificationItems] = useState<NotificationItem[]>([]); const [notificationItems, setNotificationItems] = useState<NotificationItem[]>([]);
const [notificationItemsLoading, setNotificationItemsLoading] = useState(false); const [notificationItemsLoading, setNotificationItemsLoading] = useState(false);
const [profileDraft, setProfileDraft] = useState({ const [profileDraft, setProfileDraft] = useState({
@@ -501,10 +501,11 @@ export function SettingsPanel({ open, onClose }: Props) {
<select <select
className="w-full rounded bg-slate-800 px-2 py-1 text-xs" className="w-full rounded bg-slate-800 px-2 py-1 text-xs"
value={privacyGroupInvites} value={privacyGroupInvites}
onChange={(e) => setPrivacyGroupInvites(e.target.value as "everyone" | "contacts")} onChange={(e) => setPrivacyGroupInvites(e.target.value as "everyone" | "contacts" | "nobody")}
> >
<option value="everyone">Everybody</option> <option value="everyone">Everybody</option>
<option value="contacts">My contacts</option> <option value="contacts">My contacts</option>
<option value="nobody">Nobody</option>
</select> </select>
</div> </div>
<button <button