mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(privacy): let minors block media in DMs from others (#3120)
This commit is contained in:
@@ -226,11 +226,17 @@ export class UserAccountSettingsService {
|
|||||||
updatedRowData.sensitive_content_friend_dm_filter = data.sensitive_content_friend_dm_filter;
|
updatedRowData.sensitive_content_friend_dm_filter = data.sensitive_content_friend_dm_filter;
|
||||||
}
|
}
|
||||||
if (data.sensitive_content_non_friend_dm_filter !== undefined) {
|
if (data.sensitive_content_non_friend_dm_filter !== undefined) {
|
||||||
throw ValidationError.fromPath(
|
const allowed =
|
||||||
'sensitive_content_non_friend_dm_filter',
|
data.sensitive_content_non_friend_dm_filter === SensitiveMediaFilterLevel.BLUR ||
|
||||||
'AGE_RESTRICTED',
|
data.sensitive_content_non_friend_dm_filter === SensitiveMediaFilterLevel.BLOCK;
|
||||||
'Non-adult users cannot modify the non-friend DM content filter',
|
if (!allowed) {
|
||||||
);
|
throw ValidationError.fromPath(
|
||||||
|
'sensitive_content_non_friend_dm_filter',
|
||||||
|
'AGE_RESTRICTED',
|
||||||
|
'Non-adult users can only set non-friend DM filter to blur or block',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updatedRowData.sensitive_content_non_friend_dm_filter = data.sensitive_content_non_friend_dm_filter;
|
||||||
}
|
}
|
||||||
if (data.sensitive_content_guild_filter !== undefined) {
|
if (data.sensitive_content_guild_filter !== undefined) {
|
||||||
throw ValidationError.fromPath(
|
throw ValidationError.fromPath(
|
||||||
|
|||||||
@@ -100,6 +100,35 @@ describe('User Settings - Sensitive Content Filters', () => {
|
|||||||
expect(response.errors[0]?.path).toBe('sensitive_content_friend_dm_filter');
|
expect(response.errors[0]?.path).toBe('sensitive_content_friend_dm_filter');
|
||||||
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.AGE_RESTRICTED);
|
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.AGE_RESTRICTED);
|
||||||
});
|
});
|
||||||
|
test('lets a non-adult account tighten the non-friend DM filter', async () => {
|
||||||
|
const account = await createTestAccount(harness, {dateOfBirth: '2010-01-01'});
|
||||||
|
const {json} = await updateUserSettings(harness, account.token, {
|
||||||
|
sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.BLUR,
|
||||||
|
});
|
||||||
|
expect(json.sensitive_content_non_friend_dm_filter).toBe(SensitiveMediaFilterLevel.BLUR);
|
||||||
|
const {json: blocked} = await updateUserSettings(harness, account.token, {
|
||||||
|
sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.BLOCK,
|
||||||
|
});
|
||||||
|
expect(blocked.sensitive_content_non_friend_dm_filter).toBe(SensitiveMediaFilterLevel.BLOCK);
|
||||||
|
});
|
||||||
|
test('rejects a non-adult account relaxing the non-friend DM filter', async () => {
|
||||||
|
const account = await createTestAccount(harness, {dateOfBirth: '2010-01-01'});
|
||||||
|
const response = await createBuilder<{
|
||||||
|
code: string;
|
||||||
|
errors: Array<{
|
||||||
|
path: string;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}>(harness, account.token)
|
||||||
|
.patch('/users/@me/settings')
|
||||||
|
.body({sensitive_content_non_friend_dm_filter: SensitiveMediaFilterLevel.SHOW})
|
||||||
|
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||||
|
.execute();
|
||||||
|
expect(response.code).toBe('VALIDATION_ERROR');
|
||||||
|
expect(response.errors[0]?.path).toBe('sensitive_content_non_friend_dm_filter');
|
||||||
|
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.AGE_RESTRICTED);
|
||||||
|
});
|
||||||
test('allows an account without a birth date to relax every filter when none is collected', async () => {
|
test('allows an account without a birth date to relax every filter when none is collected', async () => {
|
||||||
const repository = getInstanceConfigRepository();
|
const repository = getInstanceConfigRepository();
|
||||||
const previous = (await repository.getAppPublicConfig()).registration.collect_date_of_birth;
|
const previous = (await repository.getAppPublicConfig()).registration.collect_date_of_birth;
|
||||||
|
|||||||
+18
-16
@@ -51,6 +51,7 @@ const SENSITIVE_CONTENT_TAB_ID = 'privacy_safety';
|
|||||||
interface SensitiveContentOption {
|
interface SensitiveContentOption {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SensitiveContentChoiceRowProps {
|
interface SensitiveContentChoiceRowProps {
|
||||||
@@ -73,15 +74,16 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
|||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
const optionRefs = useRef(new Map<number, HTMLButtonElement>());
|
const optionRefs = useRef(new Map<number, HTMLButtonElement>());
|
||||||
const selectedIndex = options.findIndex((option) => option.value === value);
|
const selectedIndex = options.findIndex((option) => option.value === value);
|
||||||
const focusedIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
const enabledOptions = options.filter((option) => !option.disabled);
|
||||||
|
const focusedValue = enabledOptions.some((option) => option.value === value) ? value : enabledOptions[0]?.value;
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, optionValue: number) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, optionValue: number) => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
const currentIndex = options.findIndex((option) => option.value === optionValue);
|
const currentIndex = enabledOptions.findIndex((option) => option.value === optionValue);
|
||||||
if (currentIndex < 0) return;
|
if (currentIndex < 0) return;
|
||||||
const direction = getTabNavigationDirection(event.key, 'horizontal');
|
const direction = getTabNavigationDirection(event.key, 'horizontal');
|
||||||
if (!direction) return;
|
if (!direction) return;
|
||||||
const nextIndex = getNextTabIndex(currentIndex, options.length, direction);
|
const nextIndex = getNextTabIndex(currentIndex, enabledOptions.length, direction);
|
||||||
const nextOption = nextIndex == null ? null : options[nextIndex];
|
const nextOption = nextIndex == null ? null : enabledOptions[nextIndex];
|
||||||
if (!nextOption) return;
|
if (!nextOption) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -101,7 +103,7 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
|||||||
aria-disabled={disabled || undefined}
|
aria-disabled={disabled || undefined}
|
||||||
data-flx={dataFlx}
|
data-flx={dataFlx}
|
||||||
>
|
>
|
||||||
{options.map((option, index) => {
|
{options.map((option) => {
|
||||||
const isSelected = option.value === value;
|
const isSelected = option.value === value;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -116,8 +118,8 @@ const SensitiveContentChoiceRow: React.FC<SensitiveContentChoiceRowProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={isSelected}
|
aria-checked={isSelected}
|
||||||
tabIndex={!disabled && index === focusedIndex ? 0 : -1}
|
tabIndex={!disabled && option.value === focusedValue ? 0 : -1}
|
||||||
disabled={disabled}
|
disabled={disabled || option.disabled}
|
||||||
className={clsx(styles.choiceButton, isSelected && styles.choiceButtonActive)}
|
className={clsx(styles.choiceButton, isSelected && styles.choiceButtonActive)}
|
||||||
onClick={() => onChange(option.value)}
|
onClick={() => onChange(option.value)}
|
||||||
onKeyDown={(event) => handleKeyDown(event, option.value)}
|
onKeyDown={(event) => handleKeyDown(event, option.value)}
|
||||||
@@ -161,11 +163,10 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
|||||||
const [nonFriendDmFilter, setNonFriendDmFilter] = useState(UserSettings.sensitiveContentNonFriendDmFilter);
|
const [nonFriendDmFilter, setNonFriendDmFilter] = useState(UserSettings.sensitiveContentNonFriendDmFilter);
|
||||||
const [guildFilter, setGuildFilter] = useState(UserSettings.sensitiveContentGuildFilter);
|
const [guildFilter, setGuildFilter] = useState(UserSettings.sensitiveContentGuildFilter);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const hasUnsavedChanges = isMatureContentAllowed
|
const hasUnsavedChanges =
|
||||||
? friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter ||
|
friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter ||
|
||||||
nonFriendDmFilter !== UserSettings.sensitiveContentNonFriendDmFilter ||
|
nonFriendDmFilter !== UserSettings.sensitiveContentNonFriendDmFilter ||
|
||||||
guildFilter !== UserSettings.sensitiveContentGuildFilter
|
(isMatureContentAllowed && guildFilter !== UserSettings.sensitiveContentGuildFilter);
|
||||||
: friendDmFilter !== UserSettings.sensitiveContentFriendDmFilter;
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setFriendDmFilter(UserSettings.sensitiveContentFriendDmFilter);
|
setFriendDmFilter(UserSettings.sensitiveContentFriendDmFilter);
|
||||||
setNonFriendDmFilter(UserSettings.sensitiveContentNonFriendDmFilter);
|
setNonFriendDmFilter(UserSettings.sensitiveContentNonFriendDmFilter);
|
||||||
@@ -183,6 +184,7 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
|||||||
} else {
|
} else {
|
||||||
await UserSettingsCommands.update({
|
await UserSettingsCommands.update({
|
||||||
sensitiveContentFriendDmFilter: friendDmFilter,
|
sensitiveContentFriendDmFilter: friendDmFilter,
|
||||||
|
sensitiveContentNonFriendDmFilter: nonFriendDmFilter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -212,8 +214,9 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
|||||||
],
|
],
|
||||||
[i18n.locale],
|
[i18n.locale],
|
||||||
);
|
);
|
||||||
const teenFriendDmOptions = useMemo(
|
const teenDmOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
{value: SensitiveMediaFilterLevel.SHOW, label: i18n._(SHOW_DESCRIPTOR), disabled: true},
|
||||||
{value: SensitiveMediaFilterLevel.BLUR, label: i18n._(BLUR_DESCRIPTOR)},
|
{value: SensitiveMediaFilterLevel.BLUR, label: i18n._(BLUR_DESCRIPTOR)},
|
||||||
{value: SensitiveMediaFilterLevel.BLOCK, label: i18n._(BLOCK_DESCRIPTOR)},
|
{value: SensitiveMediaFilterLevel.BLOCK, label: i18n._(BLOCK_DESCRIPTOR)},
|
||||||
],
|
],
|
||||||
@@ -234,7 +237,7 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
|||||||
<SensitiveContentChoiceRow
|
<SensitiveContentChoiceRow
|
||||||
label={i18n._(DIRECT_MESSAGES_FROM_FRIENDS_DESCRIPTOR)}
|
label={i18n._(DIRECT_MESSAGES_FROM_FRIENDS_DESCRIPTOR)}
|
||||||
value={friendDmFilter}
|
value={friendDmFilter}
|
||||||
options={isMatureContentAllowed ? filterOptions : teenFriendDmOptions}
|
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
|
||||||
onChange={setFriendDmFilter}
|
onChange={setFriendDmFilter}
|
||||||
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-friend-dm-filter"
|
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"
|
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-friend-dm-filter"
|
||||||
@@ -242,9 +245,8 @@ export const SensitiveContentTabContent: React.FC = observer(() => {
|
|||||||
<SensitiveContentChoiceRow
|
<SensitiveContentChoiceRow
|
||||||
label={i18n._(DIRECT_MESSAGES_FROM_OTHERS_DESCRIPTOR)}
|
label={i18n._(DIRECT_MESSAGES_FROM_OTHERS_DESCRIPTOR)}
|
||||||
value={nonFriendDmFilter}
|
value={nonFriendDmFilter}
|
||||||
options={filterOptions}
|
options={isMatureContentAllowed ? filterOptions : teenDmOptions}
|
||||||
onChange={setNonFriendDmFilter}
|
onChange={setNonFriendDmFilter}
|
||||||
disabled={!isMatureContentAllowed}
|
|
||||||
dataFlx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.select.set-non-friend-dm-filter"
|
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"
|
data-flx="user.privacy-safety-tab.sensitive-content-tab.sensitive-content-tab-content.sensitive-content-choice-row.set-non-friend-dm-filter"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user