fix(privacy): clarify the privacy settings page (#3358)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 00:58:55 +02:00
1 parent 12d503d4c5
commit c8755885d4
40 files changed
+2953 -497

No files matched your search

@@ -69,9 +69,10 @@ const TOAST_ERROR_DESCRIPTOR = msg({
message: "Couldn't update voice activity sharing right now",
comment: 'Privacy > Active now: error modal title shown when the mass-update request fails.',
});
const SECTION_TITLE_DESCRIPTOR = msg({
message: 'Voice activity on active now',
comment: 'Privacy > Active now: settings subsection title for voice activity sharing.',
const SHARE_DESCRIPTION_DESCRIPTOR = msg({
message:
"Friends see which voice channel you're in on their Active now list. You can change this once every 24 hours.",
comment: 'Privacy > Active now: helper text under the switch controlling whether friends see voice activity.',
});
function formatRemaining(i18n: I18n, ms: number): string {
@@ -143,12 +144,10 @@ export const ActiveNowTabContent: React.FC = observer(() => {
);
};
return (
<SettingsTabSection
title={i18n._(SECTION_TITLE_DESCRIPTOR)}
data-flx="user.privacy-safety-tab.active-now-tab.settings-tab-section"
>
<SettingsTabSection data-flx="user.privacy-safety-tab.active-now-tab.settings-tab-section">
<Switch
label={i18n._(TITLE_DESCRIPTOR)}
description={i18n._(SHARE_DESCRIPTION_DESCRIPTOR)}
value={sharing}
disabled={onCooldown || submitting}
onChange={(value) => openConfirm(value)}
@@ -180,6 +180,7 @@ export const CommunicationTabContent: React.FC<CommunicationTabContentProps> = o
<>
<SettingsTabSection
title={<Trans>Incoming calls</Trans>}
description={<Trans>Choose who can start a call with you.</Trans>}
data-flx="user.privacy-safety-tab.communication-tab.communication-tab-content.settings-tab-section"
>
<CompactComboboxRow<CommunicationPermissionValue>
@@ -214,7 +215,8 @@ export const CommunicationTabContent: React.FC<CommunicationTabContentProps> = o
)}
{incomingCallBaseValue !== CommunicationPermissionValue.NOBODY && (
<Switch
label={<Trans>Silent calls from everyone</Trans>}
label={<Trans>Receive calls silently</Trans>}
description={<Trans>Calls you allow still come through, but they don't ring.</Trans>}
value={hasCallFlag(IncomingCallFlags.SILENT_EVERYONE)}
onChange={(value) =>
handleIncomingCallModifierToggle({flag: IncomingCallFlags.SILENT_EVERYONE, enabled: value})
@@ -224,7 +226,8 @@ export const CommunicationTabContent: React.FC<CommunicationTabContentProps> = o
)}
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Who can add you to group chats</Trans>}
title={<Trans>Group chats</Trans>}
description={<Trans>Choose who can add you to a group chat without asking first.</Trans>}
data-flx="user.privacy-safety-tab.communication-tab.communication-tab-content.settings-tab-section--2"
>
<CompactComboboxRow<CommunicationPermissionValue>
@@ -277,16 +277,19 @@ export const ConnectionsTabContent: React.FC = observer(() => {
<>
<SettingsTabSection
title={<Trans>Friend requests</Trans>}
description={<Trans>Choose who can send you a friend request.</Trans>}
data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.settings-tab-section"
>
<Switch
label={<Trans>Everyone</Trans>}
description={<Trans>Anyone who finds you can send a request. Turn this off to pick groups below.</Trans>}
value={hasFriendFlag(FriendSourceFlags.NO_RELATION)}
onChange={(value) => handleFriendRequestToggle(FriendSourceFlags.NO_RELATION, value)}
data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.switch.friend-request-toggle"
/>
<Switch
label={i18n._(FRIENDS_OF_FRIENDS_DESCRIPTOR)}
description={<Trans>People who share at least one friend with you.</Trans>}
value={everyoneEnabled || hasFriendFlag(FriendSourceFlags.MUTUAL_FRIENDS)}
onChange={(value) => handleFriendRequestToggle(FriendSourceFlags.MUTUAL_FRIENDS, value)}
disabled={everyoneEnabled}
@@ -294,6 +297,7 @@ export const ConnectionsTabContent: React.FC = observer(() => {
/>
<Switch
label={i18n._(COMMUNITY_MEMBERS_DESCRIPTOR)}
description={<Trans>People who share a community with you.</Trans>}
value={everyoneEnabled || hasFriendFlag(FriendSourceFlags.MUTUAL_GUILDS)}
onChange={(value) => handleFriendRequestToggle(FriendSourceFlags.MUTUAL_GUILDS, value)}
disabled={everyoneEnabled}
@@ -303,16 +307,29 @@ export const ConnectionsTabContent: React.FC = observer(() => {
<PrivacyReviewAlert data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.privacy-review-alert" />
<SettingsTabSection
title={<Trans>Direct messages</Trans>}
description={
<Trans>
Friends can always message you. These settings cover people and bots you only share a community with.
</Trans>
}
data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.settings-tab-section--2"
>
<Switch
label={<Trans>Allow direct messages from community members</Trans>}
description={
<Trans>Applies to communities you join from now on. You can also change it for each community.</Trans>
}
value={!defaultGuildsRestricted}
onChange={handleDirectMessagesToggle}
data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.switch.direct-messages-toggle"
/>
<Switch
label={<Trans>Allow direct messages from community bots</Trans>}
description={
<Trans>
Applies to bots in communities you join from now on. You can also change it for each community.
</Trans>
}
value={!botDefaultGuildsRestricted}
onChange={handleBotDirectMessagesToggle}
data-flx="user.privacy-safety-tab.connections-tab.connections-tab-content.switch.bot-direct-messages-toggle"
@@ -1,55 +1,74 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.container {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(14rem, min(max-content, 55%));
align-items: center;
column-gap: 1rem;
row-gap: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
container-type: inline-size;
}
.intro {
margin: 0;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--text-primary-muted);
}
.rows {
display: flex;
flex-direction: column;
}
.row {
display: contents;
display: flex;
min-height: 2.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0;
}
.text {
display: flex;
min-width: 0;
flex: 1 1 0%;
flex-direction: column;
gap: 0.25rem;
}
.textDisabled {
cursor: not-allowed;
opacity: 0.5;
}
.label {
display: flex;
min-height: 2.25rem;
min-width: 0;
max-width: 100%;
align-items: center;
gap: 0.5rem;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--text-primary);
}
.labelDisabled {
cursor: not-allowed;
opacity: 0.5;
}
.choiceWrap {
width: 100%;
.description {
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--text-primary-muted);
}
.choiceGroup {
position: relative;
display: grid;
display: inline-grid;
flex-shrink: 0;
grid-auto-flow: column;
grid-auto-columns: minmax(0, 1fr);
width: 100%;
height: 2.125rem;
grid-auto-columns: minmax(4.75rem, 1fr);
height: 2.25rem;
border-radius: 0.5rem;
border: 0.0625rem solid var(--background-modifier-accent);
background: var(--background-tertiary);
padding: 0.1875rem;
overflow: hidden;
}
.choiceGroupDisabled {
opacity: 0.55;
opacity: 0.5;
}
.choiceButton {
@@ -59,24 +78,17 @@
border: 0;
border-radius: 0.375rem;
background: transparent;
padding: 0 0.5rem;
padding: 0 0.875rem;
color: var(--text-secondary);
font-weight: 600;
font-size: 0.8125rem;
line-height: 1rem;
font-size: 0.875rem;
line-height: 1.125rem;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: color 150ms ease;
}
.choiceButton:focus-visible {
outline: 0.125rem solid var(--background-modifier-accent-focus);
outline-offset: -0.125rem;
}
.choiceButton:enabled:hover {
color: var(--text-primary);
}
@@ -85,8 +97,13 @@
cursor: not-allowed;
}
.choiceButtonActive {
color: var(--text-primary);
.choiceButton:disabled:not(.choiceButtonActive) {
opacity: 0.5;
}
.choiceButtonActive,
.choiceButtonActive:enabled:hover {
color: var(--text-on-brand-primary);
}
.choiceIndicator {
@@ -95,27 +112,25 @@
bottom: 0.1875rem;
z-index: 0;
border-radius: 0.375rem;
background: var(--background-secondary);
box-shadow: 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.08);
background: var(--brand-primary);
box-shadow: 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.16);
}
@media (max-width: 520px) {
.container {
display: flex;
flex-direction: column;
gap: 1rem;
@media (prefers-reduced-motion: reduce) {
.choiceButton {
transition: none;
}
}
@container (max-width: 34rem) {
.row {
display: flex;
min-height: 2.75rem;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.25rem 0;
align-items: stretch;
gap: 0.625rem;
}
.label {
min-height: 0;
.choiceGroup {
display: grid;
width: 100%;
}
}
@@ -2,6 +2,7 @@
import Accessibility from '@app/features/accessibility/state/Accessibility';
import * as UnsavedChangesCommands from '@app/features/ui/commands/UnsavedChangesCommands';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {getNextTabIndex, getTabNavigationDirection} from '@app/features/ui/tabs/TabKeyboardNavigation';
import * as UserSettingsCommands from '@app/features/user/commands/UserSettingsCommands';
import styles from '@app/features/user/components/modals/tabs/privacy_safety_tab/SensitiveContentTab.module.css';
@@ -46,6 +47,24 @@ const MESSAGES_IN_COMMUNITY_CHANNELS_DESCRIPTOR = msg({
message: 'Messages in community channels',
comment: 'Label in the sensitive content tab.',
});
const INTRO_DESCRIPTOR = msg({
message:
'Choose what happens to images and videos flagged as sensitive. Show displays them as usual, Blur hides them until you click, and Block hides them completely.',
comment:
'Introduction at the top of the sensitive content settings. Show, Blur and Block are the option names used below and must match their translations.',
});
const FRIENDS_DESCRIPTION_DESCRIPTOR = msg({
message: 'Media your friends send you in direct messages.',
comment: 'Helper text under "Direct messages from friends" in the sensitive content settings.',
});
const OTHERS_DESCRIPTION_DESCRIPTOR = msg({
message: "Media in group chats and in direct messages from people who aren't your friends.",
comment: 'Helper text under "Direct messages from others" in the sensitive content settings.',
});
const COMMUNITY_DESCRIPTION_DESCRIPTOR = msg({
message: 'Media in channels that are not marked 18+. Channels marked 18+ always show it.',
comment: 'Helper text under "Messages in community channels" in the sensitive content settings.',
});
const SENSITIVE_CONTENT_TAB_ID = 'privacy_safety';
interface SensitiveContentOption {
@@ -56,6 +75,7 @@ interface SensitiveContentOption {
interface SensitiveContentChoiceRowProps {
label: string;
description: string;
value: number;
options: ReadonlyArray<SensitiveContentOption>;
onChange: (value: number) => void;
@@ -65,6 +85,7 @@ interface SensitiveContentChoiceRowProps {
const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
label,
description,
value,
options,
onChange,
@@ -72,6 +93,7 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
dataFlx,
}) => {
const labelId = useId();
const descriptionId = useId();
const optionRefs = useRef(new Map<number, HTMLButtonElement>());
const selectedIndex = options.findIndex((option) => option.value === value);
const enabledOptions = options.filter((option) => !option.disabled);
@@ -92,22 +114,27 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
};
return (
<div className={styles.row} data-flx={`${dataFlx}.row`}>
<span id={labelId} className={clsx(styles.label, disabled && styles.labelDisabled)} data-flx={`${dataFlx}.label`}>
{label}
</span>
<div className={styles.choiceWrap} data-flx={`${dataFlx}.choice-wrap`}>
<div
className={clsx(styles.choiceGroup, disabled && styles.choiceGroupDisabled)}
role="radiogroup"
aria-labelledby={labelId}
aria-disabled={disabled || undefined}
data-flx={dataFlx}
>
{options.map((option) => {
const isSelected = option.value === value;
return (
<div className={clsx(styles.text, disabled && styles.textDisabled)} data-flx={`${dataFlx}.text`}>
<span id={labelId} className={styles.label} data-flx={`${dataFlx}.label`}>
{label}
</span>
<span id={descriptionId} className={styles.description} data-flx={`${dataFlx}.description`}>
{description}
</span>
</div>
<div
className={clsx(styles.choiceGroup, disabled && styles.choiceGroupDisabled)}
role="radiogroup"
aria-labelledby={labelId}
aria-describedby={descriptionId}
aria-disabled={disabled || undefined}
data-flx={dataFlx}
>
{options.map((option) => {
const isSelected = option.value === value;
return (
<FocusRing key={option.value} offset={-2} data-flx={`${dataFlx}.focus-ring`}>
<button
key={option.value}
ref={(element) => {
if (element) {
optionRefs.current.set(option.value, element);
@@ -127,29 +154,30 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
>
{option.label}
</button>
);
})}
{selectedIndex >= 0 && (
<motion.div
className={styles.choiceIndicator}
layout={true}
transition={
Accessibility.useReducedMotion
? {duration: 0}
: {
type: 'spring',
stiffness: 500,
damping: 35,
}
}
style={{
width: `calc((100% - 0.375rem) / ${options.length})`,
left: `calc(0.1875rem + (100% - 0.375rem) * ${selectedIndex} / ${options.length})`,
}}
data-flx={`${dataFlx}.indicator`}
/>
)}
</div>
</FocusRing>
);
})}
{selectedIndex >= 0 && (
<motion.div
className={styles.choiceIndicator}
layout={true}
aria-hidden={true}
transition={
Accessibility.useReducedMotion
? {duration: 0}
: {
type: 'spring',
stiffness: 500,
damping: 35,
}
}
style={{
width: `calc((100% - 0.375rem) / ${options.length})`,
left: `calc(0.1875rem + (100% - 0.375rem) * ${selectedIndex} / ${options.length})`,
}}
data-flx={`${dataFlx}.indicator`}
/>
)}
</div>
</div>
);
@@ -234,31 +262,45 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
className={styles.container}
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.settings-tab-section"
>
<SensitiveContentChoiceRow
label={i18n._(DIRECT_MESSAGES_FROM_FRIENDS_DESCRIPTOR)}
value={friendDmFilter}
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
onChange={setFriendDmFilter}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-friend-dm-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-friend-dm-filter"
/>
<SensitiveContentChoiceRow
label={i18n._(DIRECT_MESSAGES_FROM_OTHERS_DESCRIPTOR)}
value={nonFriendDmFilter}
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
onChange={setNonFriendDmFilter}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-non-friend-dm-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-non-friend-dm-filter"
/>
<SensitiveContentChoiceRow
label={i18n._(MESSAGES_IN_COMMUNITY_CHANNELS_DESCRIPTOR)}
value={guildFilter}
options={guildFilterOptions}
onChange={setGuildFilter}
disabled={!isMatureContentAllowed}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-guild-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-guild-filter"
/>
<p
className={styles.intro}
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.intro"
>
{i18n._(INTRO_DESCRIPTOR)}
</p>
<div
className={styles.rows}
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.rows"
>
<SensitiveContentChoiceRow
label={i18n._(DIRECT_MESSAGES_FROM_FRIENDS_DESCRIPTOR)}
description={i18n._(FRIENDS_DESCRIPTION_DESCRIPTOR)}
value={friendDmFilter}
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
onChange={setFriendDmFilter}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-friend-dm-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-friend-dm-filter"
/>
<SensitiveContentChoiceRow
label={i18n._(DIRECT_MESSAGES_FROM_OTHERS_DESCRIPTOR)}
description={i18n._(OTHERS_DESCRIPTION_DESCRIPTOR)}
value={nonFriendDmFilter}
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
onChange={setNonFriendDmFilter}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-non-friend-dm-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-non-friend-dm-filter"
/>
<SensitiveContentChoiceRow
label={i18n._(MESSAGES_IN_COMMUNITY_CHANNELS_DESCRIPTOR)}
description={i18n._(COMMUNITY_DESCRIPTION_DESCRIPTOR)}
value={guildFilter}
options={guildFilterOptions}
onChange={setGuildFilter}
disabled={!isMatureContentAllowed}
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-guild-filter"
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-guild-filter"
/>
</div>
</div>
);
});