Compare commits

...
22 Commits
Author SHA1 Message Date
HampusandGitHub a208b56354 feat(app): let Windows users switch back to Fluxer Sans (#3365) 2026-10-10 03:15:30 +02:00
HampusandGitHub 71be6c1de4 build(fonts): restore symmetric smoothing for Plex at every size (#3364) 2026-10-10 03:15:00 +02:00
HampusandGitHub eae905b1da fix(app): use the Windows system font for UI text on Windows (#3363) 2026-10-10 03:14:25 +02:00
HampusandGitHub 10cd4f96a4 fix(ui): let report modal steps scroll when zoomed in (#3362) 2026-10-10 01:46:24 +02:00
HampusandGitHub 9d74daf8ad fix(reports): drop the in-app DSA link, link copyright help (#3361) 2026-10-10 01:46:19 +02:00
HampusandGitHub 6f344def93 test(api): backdate seeded evidence in report deletion tests (#3360) 2026-10-10 01:23:10 +02:00
HampusandGitHub 72f9f9c396 fix(settings): group account security into clear cards (#3359) 2026-10-10 00:59:33 +02:00
HampusandGitHub c8755885d4 fix(privacy): clarify the privacy settings page (#3358) 2026-10-10 00:58:55 +02:00
HampusandGitHub 12d503d4c5 feat(app): show current DM settings and keep them by default (#3357) 2026-10-10 00:58:10 +02:00
HampusandGitHub 2f6201bcbd feat(app): guide privacy review with dots and an inline alert (#3356) 2026-10-10 00:57:26 +02:00
HampusandGitHub ec681e6061 fix(desktop): retry an update that never installed (#3354) 2026-10-09 15:03:36 +02:00
HampusandGitHub 0fc6fad11d fix(ui): report the shown carousel step so report flows react (#3353) 2026-10-09 14:53:08 +02:00
HampusandGitHub eb329f2cd0 fix(app): add missing data-flx attributes (#3351) 2026-10-09 14:18:01 +02:00
HampusandGitHub 9a20821332 feat(app,api): let group DM owners allow mature content (#3350) 2026-10-09 14:12:13 +02:00
HampusandGitHub 4749eb7f86 ci(desktop): use a clean profile for each Windows update check (#3349) 2026-10-09 08:32:45 +02:00
HampusandGitHub 2d83fd2a93 ci(desktop): keep the logs from Windows update checks (#3348) 2026-10-09 08:22:21 +02:00
HampusandGitHub d180af6ba2 fix(desktop): clean up release check runs on Windows (#3347) 2026-10-09 06:53:50 +02:00
HampusandGitHub 4810eb3e24 feat(ci): run the release check on built Windows shells (#3346) 2026-10-09 05:40:01 +02:00
HampusandGitHub 48a19dedd8 fix(desktop): stop requiring the removed win-toast at startup (#3345) 2026-10-09 05:39:56 +02:00
HampusandGitHub cadf7ea532 feat(schema): add double tap action to synced preferences (#3344) 2026-10-09 04:09:24 +02:00
HampusandGitHub fc60d05f17 feat(app): load heavy desktop assets on demand, update in place (#3343) 2026-10-09 03:53:57 +02:00
HampusandGitHub 5072488f1e feat(desktop): prefer the bundled renderer, own update prompts (#3341) 2026-10-09 03:01:15 +02:00
468 changed files with 27148 additions and 12854 deletions
+57 -1
View File
@@ -551,6 +551,16 @@ jobs:
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step verify_windows_unpacked_signatures
- name: Verify the packaged shell starts and boots its bundled renderer (Windows)
if: matrix.platform == 'windows' && matrix.arch == 'x64'
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
env:
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
BUILD_VERSION: ${{ env.VERSION }}
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step verify_bundled_renderer_windows
- name: Create portable ZIP (Windows)
if: matrix.platform == 'windows'
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
@@ -661,13 +671,59 @@ jobs:
retention-days: 1
compression-level: 0
verify_windows_arm64:
name: Verify windows (arm64)
if: ${{ !cancelled() && needs.build.result == 'success' }}
needs:
- meta
- build
runs-on: windows-11-arm
timeout-minutes: 30
permissions:
actions: read
contents: read
env:
CHANNEL: ${{ needs.meta.outputs.build_channel }}
VERSION: ${{ needs.meta.outputs.version }}
steps:
- name: Checkout source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.meta.outputs.source_sha }}
sparse-checkout: fluxer_desktop/scripts
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Download windows arm64 build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: artifacts
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-windows-arm64*
- name: Verify the packaged shell starts and boots its bundled renderer
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$zip = Get-ChildItem -Path artifacts -Recurse -Filter "*-$env:VERSION-portable-win-arm64.zip" | Select-Object -First 1
if ($null -eq $zip) { Write-Host 'This build has no windows arm64 portable zip'; exit 0 }
$app = Join-Path $env:RUNNER_TEMP 'app'
Expand-Archive -Path $zip.FullName -DestinationPath $app
$exe = Get-ChildItem -Path $app -Recurse -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
if ($null -eq $exe) { throw "$($zip.Name) holds no Fluxer executable" }
node fluxer_desktop/scripts/release-check.mjs --app $exe.DirectoryName --channel $env:CHANNEL --expect-renderer $env:VERSION --expect-source bundled --package-origin http://127.0.0.1:9 --app-arg=--disable-gpu --workdir (Join-Path $env:RUNNER_TEMP 'runs') --timeout-seconds 240
if ($LASTEXITCODE -ne 0) { throw "The windows arm64 build fails its release check" }
upload:
name: Assemble desktop release assets
if: ${{ !cancelled() && needs.build.result == 'success' }}
if: ${{ !cancelled() && needs.build.result == 'success' && needs.verify_windows_arm64.result == 'success' }}
needs:
- meta
- shared_assets
- build
- verify_windows_arm64
runs-on: ubuntu-24.04-arm
environment: desktop-releases
timeout-minutes: 180
@@ -0,0 +1,152 @@
name: desktop windows release check
on:
workflow_dispatch:
inputs:
channel:
description: Release channel
type: choice
options:
- canary
- stable
default: canary
version:
description: Published version to check
required: true
type: string
expect_failure:
description: Pass only when the published build fails to start
type: boolean
default: false
update_from:
description: Comma-separated published versions to install with Setup and update to the version above from the live feed
required: false
type: string
default: ""
stale_apply_record:
description: Before each update, leave behind an hour-old record of an update that never installed (passes only when the installed versions retry it)
type: boolean
default: false
pull_request:
paths:
- .github/workflows/desktop-windows-release-check.yaml
- fluxer_desktop/scripts/release-check.mjs
permissions:
contents: read
concurrency:
group: desktop-windows-release-check-${{ github.ref }}-${{ inputs.version || 'pr' }}
cancel-in-progress: false
jobs:
check:
name: Check windows (${{ matrix.arch }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- arch: x64
os: windows-2025
- arch: arm64
os: windows-11-arm
env:
CHANNEL: ${{ inputs.channel || 'canary' }}
VERSION: ${{ inputs.version || '2026.1009.20411' }}
EXPECT_FAILURE: ${{ github.event_name == 'pull_request' && 'true' || inputs.expect_failure }}
UPDATE_FROM: ${{ inputs.update_from || '' }}
STALE_APPLY_RECORD: ${{ inputs.stale_apply_record && 'true' || 'false' }}
ARCH: ${{ matrix.arch }}
steps:
- name: Checkout source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
sparse-checkout: fluxer_desktop/scripts
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Check the portable build
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$work = Join-Path $env:RUNNER_TEMP 'portable'
New-Item -ItemType Directory -Force -Path $work | Out-Null
$zip = Join-Path $work 'portable.zip'
Invoke-WebRequest -Uri "https://pkgs.fluxer.com/desktop/$env:CHANNEL/win32/$env:ARCH/$env:VERSION/portable" -OutFile $zip
Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'app')
$exe = Get-ChildItem -Path (Join-Path $work 'app') -Recurse -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
if ($null -eq $exe) { throw "The portable build holds no Fluxer executable" }
$output = & node fluxer_desktop/scripts/release-check.mjs --app $exe.DirectoryName --channel $env:CHANNEL --expect-renderer $env:VERSION --expect-source bundled --app-arg=--disable-gpu --workdir (Join-Path $work 'runs') --timeout-seconds 240 2>&1
$code = $LASTEXITCODE
$output | ForEach-Object { Write-Host $_ }
if ($env:EXPECT_FAILURE -eq 'true') {
if ($code -eq 0) { throw "Expected $env:VERSION to fail its release check, it passed" }
Write-Host "$env:VERSION fails its release check on windows $env:ARCH as expected"
exit 0
}
if ($code -ne 0) { throw "The release check failed for $env:VERSION on windows $env:ARCH" }
- name: Update installed builds from the live feed
if: env.UPDATE_FROM != ''
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$channelDirs = @{ canary = 'fluxer_desktop_canary'; stable = 'fluxer_desktop' }
$dataDirs = @{ canary = 'fluxercanary'; stable = 'fluxer' }
$installRoot = Join-Path $env:LOCALAPPDATA $channelDirs[$env:CHANNEL]
$logPath = Join-Path (Join-Path $env:APPDATA $dataDirs[$env:CHANNEL]) 'logs\main.log'
$failures = @()
foreach ($from in ($env:UPDATE_FROM -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })) {
Write-Host "== install $from, then update to $env:VERSION"
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
if (Test-Path $installRoot) { Remove-Item -Recurse -Force $installRoot }
Remove-Item -Recurse -Force (Split-Path (Split-Path $logPath -Parent) -Parent) -ErrorAction SilentlyContinue
$setup = Join-Path $env:RUNNER_TEMP "setup-$from.exe"
Invoke-WebRequest -Uri "https://pkgs.fluxer.com/desktop/$env:CHANNEL/win32/$env:ARCH/$from/setup" -OutFile $setup
Start-Process -FilePath $setup -ArgumentList '--silent' -Wait
Start-Sleep -Seconds 5
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
$exe = Get-ChildItem -Path (Join-Path $installRoot 'current') -Filter 'Fluxer*.exe' | Where-Object { $_.Name -notlike '*_ExecutionStub.exe' } | Select-Object -First 1
if ($null -eq $exe) { $failures += "$from did not install"; continue }
if ($env:STALE_APPLY_RECORD -eq 'true') {
$dataRoot = Split-Path (Split-Path $logPath -Parent) -Parent
New-Item -ItemType Directory -Force -Path $dataRoot | Out-Null
$attemptedAt = [DateTimeOffset]::UtcNow.AddHours(-1).ToUnixTimeMilliseconds()
Set-Content -Path (Join-Path $dataRoot 'update-apply-state.json') -Value "{`"version`":`"$env:VERSION`",`"attemptedAt`":$attemptedAt}" -NoNewline
Write-Host "left a stale apply record for $env:VERSION"
}
Start-Process -FilePath $exe.FullName -ArgumentList '--disable-gpu'
$deadline = (Get-Date).AddMinutes(8)
$reported = $null
while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 10
if (Test-Path $logPath) {
$line = Select-String -Path $logPath -Pattern "The renderer reported its build: .*Desktop $([regex]::Escape($env:VERSION)), Web $([regex]::Escape($env:VERSION))" | Select-Object -Last 1
if ($null -ne $line) { $reported = $line.Line; break }
}
}
$installed = Get-Content (Join-Path $installRoot 'current\sq.version') -Raw -ErrorAction SilentlyContinue
Write-Host "installed package after update: $installed"
$logs = Join-Path $env:RUNNER_TEMP "logs\$from"
New-Item -ItemType Directory -Force -Path $logs | Out-Null
if (Test-Path $logPath) { Copy-Item $logPath $logs }
Get-ChildItem -Path $installRoot -Filter '*.log' -ErrorAction SilentlyContinue | Copy-Item -Destination $logs
if (Test-Path $logPath) { Get-Content $logPath | Select-String -Pattern 'Bootstrap|ShellSelfUpdate|Velopack|velopack|NativeModulePreflight|reported its build' | Select-Object -Last 40 | ForEach-Object { Write-Host $_.Line } }
if ($null -eq $reported) { $failures += "$from did not reach Desktop and Web $env:VERSION" } else { Write-Host "$from updated: $reported" }
Get-Process | Where-Object { $_.Path -like "$installRoot*" } | Stop-Process -Force -ErrorAction SilentlyContinue
}
if ($failures.Count -gt 0) { throw ($failures -join "`n") }
- name: Upload update logs
if: always() && env.UPDATE_FROM != ''
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: update-logs-${{ matrix.arch }}
path: ${{ runner.temp }}/logs
if-no-files-found: ignore
retention-days: 7
+2
View File
@@ -17262,6 +17262,7 @@
"MAX_GROUP_DM_RECIPIENTS",
"MAX_GROUP_DMS",
"GROUP_DM_RECIPIENTS_NOT_ADDABLE",
"GROUP_DM_MATURE_CONTENT_INELIGIBLE",
"MAX_GUILD_CHANNELS",
"MAX_GUILD_MEMBERS",
"MAX_GUILD_ROLES",
@@ -17558,6 +17559,7 @@
"You've reached the maximum of {count, plural, one {# group DM recipient} other {# group DM recipients}}.",
"You've reached the maximum of {count, plural, one {# group DM} other {# group DMs}}.",
"One or more selected users can't be added to this group DM.",
"Not everyone in this group is eligible for mature content.",
"You've reached the maximum of {count, plural, one {# community channel} other {# community channels}}.",
"You've reached the maximum of {count, plural, one {# community member} other {# community members}}.",
"You've reached the maximum of {count, plural, one {# community role} other {# community roles}}.",
@@ -130,6 +130,9 @@ describe('DELETE /admin/reports/:report_id', () => {
const profileKey = `reports/${reportId}/profile/user_avatar/abc${id}`;
await harness.storageService.uploadObject({bucket: REPORTS_BUCKET, key: attachmentKey, body: new Uint8Array([1])});
await harness.storageService.uploadObject({bucket: REPORTS_BUCKET, key: profileKey, body: new Uint8Array([2])});
const uploadedAt = new Date(Date.now() - ms('1 hour'));
harness.storageService.setObjectLastModified(REPORTS_BUCKET, attachmentKey, uploadedAt);
harness.storageService.setObjectLastModified(REPORTS_BUCKET, profileKey, uploadedAt);
const report = await repository.createReport({
...Object.fromEntries(IAR_SUBMISSION_COLUMNS.map((column) => [column, null])),
report_id: id,
@@ -165,6 +165,7 @@ function serializeGroupDMChannel(channel: Channel): ChannelResponse {
icon: channel.iconHash ?? null,
owner_id: channel.ownerId ? channel.ownerId.toString() : null,
nicks: nicknameMap.size > 0 ? nicks : undefined,
nsfw: channel.isNsfw,
};
}
@@ -179,6 +179,7 @@ export class ChannelDataService {
icon: groupDmData.icon !== undefined ? groupDmData.icon : undefined,
ownerId: groupDmData.owner_id ? createUserID(groupDmData.owner_id) : undefined,
nicks: groupDmData.nicks,
nsfw: groupDmData.nsfw ?? undefined,
requestCache,
});
}
@@ -12,7 +12,9 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {Channel} from '@app/api/models/Channel';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils';
import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
import {GroupDmMatureContentIneligibleError} from '@fluxer/errors/src/domains/channel/GroupDmMatureContentIneligibleError';
import {InvalidChannelTypeError} from '@fluxer/errors/src/domains/channel/InvalidChannelTypeError';
import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
@@ -37,6 +39,7 @@ export class GroupDmUpdateService {
icon,
ownerId,
nicks,
nsfw,
requestCache,
}: {
userId: UserID;
@@ -45,6 +48,7 @@ export class GroupDmUpdateService {
icon?: string | null;
ownerId?: UserID;
nicks?: Record<string, string | null> | null;
nsfw?: boolean;
requestCache: RequestCache;
}): Promise<Channel> {
const channel = await this.channelRepository.channelData.findUnique(channelId);
@@ -113,6 +117,20 @@ export class GroupDmUpdateService {
updates.nicks = updatedNicknames.size > 0 ? (updatedNicknames as Map<string, string>) : null;
}
}
if (nsfw !== undefined && nsfw !== channel.isNsfw) {
if (channel.ownerId !== userId) {
throw new MissingPermissionsError();
}
if (nsfw) {
const recipients = await Promise.all(
Array.from(channel.recipientIds).map((id) => this.userRepository.findUnique(id)),
);
if (recipients.some((recipient) => !recipient || !canUserAccessNsfwContent(recipient))) {
throw new GroupDmMatureContentIneligibleError();
}
}
updates.nsfw = nsfw;
}
let iconHash: string | null = null;
if (icon !== undefined) {
iconHash = await this.avatarService.uploadAvatar({
@@ -24,6 +24,7 @@ import type {User} from '@app/api/models/User';
import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup';
import {assertAccountNotLimited} from '@app/api/user/AccountLimit';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils';
import {UserPermissionUtils} from '@app/api/utils/UserPermissionUtils';
import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
@@ -36,6 +37,7 @@ import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownCha
import {CannotRemoveOtherRecipientsError} from '@fluxer/errors/src/domains/core/CannotRemoveOtherRecipientsError';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
import {NsfwContentRequiresAgeVerificationError} from '@fluxer/errors/src/domains/moderation/NsfwContentRequiresAgeVerificationError';
import {NotFriendsWithUserError} from '@fluxer/errors/src/domains/user/NotFriendsWithUserError';
export class GroupDmOperationsService {
@@ -84,6 +86,9 @@ export class GroupDmOperationsService {
userId,
targetId: recipientId,
});
if (channel.isNsfw && !(await this.canJoinMatureGroup(recipientId))) {
throw new MissingAccessError();
}
const {channel: updatedChannel} = await this.addRecipientViaInviteWithResult({
channelId,
recipientId,
@@ -122,6 +127,11 @@ export class GroupDmOperationsService {
return updatedChannel;
}
private async canJoinMatureGroup(userId: UserID): Promise<boolean> {
const user = await this.userRepository.findUnique(userId);
return user != null && canUserAccessNsfwContent(user);
}
private async assertInviterNotLimited(userId: UserID): Promise<void> {
const user = await this.userRepository.findUnique(userId);
if (user) assertAccountNotLimited(user);
@@ -169,6 +179,9 @@ export class GroupDmOperationsService {
if (channel.recipientIds.has(recipientId)) {
return {channel, recipientAdded: false};
}
if (channel.isNsfw && !(await this.canJoinMatureGroup(recipientId))) {
throw new NsfwContentRequiresAgeVerificationError();
}
const inviterUser = inviterId ? await this.userRepository.findUnique(inviterId) : null;
const fallbackLimit = MAX_GROUP_DM_RECIPIENTS;
const recipientLimit = this.resolveLimitForUser(inviterUser ?? null, 'max_group_dm_recipients', fallbackLimit);
@@ -9,7 +9,7 @@ import * as EmojiUtils from '@app/api/utils/EmojiUtils';
import {ChannelTypes, GUILD_TEXT_BASED_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants';
import {GuildExplicitContentFilterTypes, GuildFeatures, GuildNSFWLevel} from '@fluxer/constants/src/GuildConstants';
import {THREAD_ONLY_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants';
import {SensitiveMediaFilterLevel} from '@fluxer/constants/src/UserConstants';
import {RelationshipTypes, SensitiveMediaFilterLevel} from '@fluxer/constants/src/UserConstants';
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
@@ -40,6 +40,30 @@ export class MessageContentService {
});
}
async resolveDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
if (channel.type !== ChannelTypes.DM) {
return undefined;
}
const recipientIds = Array.from(channel.recipientIds).filter((id) => id !== senderId);
if (recipientIds.length !== 1) {
return undefined;
}
const recipientId = recipientIds[0];
const [senderSettings, recipientSettings, friendship] = await Promise.all([
this.userRepository.findSettings(senderId),
this.userRepository.findSettings(recipientId),
this.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.FRIEND),
]);
const areFriends = friendship != null;
const senderFilterLevel = areFriends
? (senderSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
: (senderSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
const recipientFilterLevel = areFriends
? (recipientSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
: (recipientSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
return {senderFilterLevel, recipientFilterLevel};
}
isNSFWContentAllowed(params: {
channel?: Channel;
guild?: GuildResponse | null;
@@ -61,6 +85,9 @@ export class MessageContentService {
if (channel?.type === ChannelTypes.DM_PERSONAL_NOTES) {
return true;
}
if (channel?.type === ChannelTypes.GROUP_DM && channel.isNsfw) {
return true;
}
if (!guild) {
if (dmNsfwContext) {
const eitherBlocks =
@@ -175,6 +175,7 @@ export class MessageEditService {
});
}
const isBugHunterBot = !!user?.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
const dmNsfwContext = guild ? undefined : await this.deps.persistenceService.resolveDmNsfwContext(channel, userId);
const updateResult = await this.deps.messageWriteLock.withFreshMessage(channelId, messageId, async (fresh) => {
if (!fresh) throw new UnknownMessageError();
return this.deps.crosspostPropagation.withPublishedEditBudget({fresh, actor: 'author'}, () =>
@@ -190,6 +191,7 @@ export class MessageEditService {
isBot: user?.isBot,
isBugHunterBot,
locale: user?.locale,
dmNsfwContext,
}),
);
});
@@ -158,6 +158,10 @@ export class MessagePersistenceService {
return resolveNsfwScopeChannel(channel, (channelId) => this.channelRepository.channelData.findUnique(channelId));
}
resolveDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
return this.contentService.resolveDmNsfwContext(channel, senderId);
}
getEmbedAttachmentResolver(): MessageEmbedAttachmentResolver {
return this.embedAttachmentResolver;
}
@@ -402,6 +406,7 @@ export class MessagePersistenceService {
isBot?: boolean;
isBugHunterBot?: boolean;
locale?: string | null;
dmNsfwContext?: DmNsfwContext;
}): Promise<UpdateMessageResult> {
const {message, messageId, data, channel, guild, member} = params;
if (message.messageSnapshots && message.messageSnapshots.length > 0) {
@@ -412,6 +417,7 @@ export class MessagePersistenceService {
guild,
member,
isBot: params.isBot,
dmNsfwContext: params.dmNsfwContext,
});
const updatedRowData = {...message.toRow()};
let hasChanges = false;
@@ -19,7 +19,6 @@ import type {AuthenticatedChannel} from '@app/api/channel/services/Authenticated
import type {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation';
import {emitMessageCreated} from '@app/api/channel/services/message/MessageActivity';
import type {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService';
import type {DmNsfwContext} from '@app/api/channel/services/message/MessageContentService';
import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService';
import type {MessageEmbedAttachmentResolver} from '@app/api/channel/services/message/MessageEmbedAttachmentResolver';
import {
@@ -69,12 +68,7 @@ import {
} from '@fluxer/constants/src/ChannelConstants';
import {GuildNSFWLevel, GuildOperations} from '@fluxer/constants/src/GuildConstants';
import {threadWriteBlock} from '@fluxer/constants/src/ThreadPermissionUtils';
import {
DELETED_USER_ID,
RelationshipTypes,
SensitiveMediaFilterLevel,
UserFlags,
} from '@fluxer/constants/src/UserConstants';
import {DELETED_USER_ID, UserFlags} from '@fluxer/constants/src/UserConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError';
import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError';
@@ -193,33 +187,6 @@ export class MessageSendService {
return includeDefault ? {includeDefault} : null;
}
private async buildDmNsfwContext(channel: Channel, senderId: UserID): Promise<DmNsfwContext | undefined> {
if (channel.type === ChannelTypes.GROUP_DM || channel.type === ChannelTypes.DM_PERSONAL_NOTES) {
return undefined;
}
if (channel.type !== ChannelTypes.DM) {
return undefined;
}
const recipientIds = Array.from(channel.recipientIds).filter((id) => id !== senderId);
if (recipientIds.length !== 1) {
return undefined;
}
const recipientId = recipientIds[0];
const [senderSettings, recipientSettings, friendship] = await Promise.all([
this.deps.userRepository.findSettings(senderId),
this.deps.userRepository.findSettings(recipientId),
this.deps.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.FRIEND),
]);
const areFriends = friendship != null;
const senderFilterLevel = areFriends
? (senderSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
: (senderSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
const recipientFilterLevel = areFriends
? (recipientSettings?.sensitiveContentFriendDmFilter ?? SensitiveMediaFilterLevel.SHOW)
: (recipientSettings?.sensitiveContentNonFriendDmFilter ?? SensitiveMediaFilterLevel.BLOCK);
return {senderFilterLevel, recipientFilterLevel};
}
private attachmentsToProcess(attachments?: Array<AttachmentRequestData>): Array<AttachmentToProcess> | undefined {
if (!attachments) return undefined;
const processed = attachments.filter(
@@ -1033,7 +1000,7 @@ export class MessageSendService {
channelId,
data,
});
const dmNsfwContext = guild ? undefined : await this.buildDmNsfwContext(channel, user.id);
const dmNsfwContext = guild ? undefined : await this.deps.persistenceService.resolveDmNsfwContext(channel, user.id);
const messageId = forumStarter ? channelIdToMessageId(channel.id) : await this.generateMessageId(channel);
let mentionData: SendMentionData | undefined;
const shouldExtractMentions =
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {MessageContentService} from '@app/api/channel/services/message/MessageContentService';
import {createChannelInvite, createFriendship, createGroupDmChannel} from '@app/api/channel/tests/ChannelTestUtils';
import {ensureSessionStarted, sendMessage} from '@app/api/message/tests/MessageTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
describe('group dm mature content', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterEach(() => {
vi.restoreAllMocks();
});
afterAll(async () => {
await harness.shutdown();
});
async function setup() {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
const other = await createTestAccount(harness);
await createFriendship(harness, owner, member);
await createFriendship(harness, owner, other);
await ensureSessionStarted(harness, owner.token);
const group = await createGroupDmChannel(harness, owner.token, [member.userId, other.userId]);
return {owner, member, other, groupId: group.id};
}
async function joinAsMinor(groupId: string, owner: TestAccount, status: number, code?: string) {
const minor = await createTestAccount(harness, {dateOfBirth: '2012-01-01'});
const invite = await createChannelInvite(harness, owner.token, groupId);
await createBuilder(harness, minor.token).post(`/invites/${invite.code}`).body({}).expect(status, code).execute();
}
function setNsfw(account: TestAccount, channelId: string, nsfw: boolean, status = 200, code?: string) {
return createBuilder<ChannelResponse>(harness, account.token)
.patch(`/channels/${channelId}`)
.body({type: ChannelTypes.GROUP_DM, nsfw})
.expect(status, code)
.execute();
}
it('lets the owner mark the group and allows mature media once marked', async () => {
const s = await setup();
const scope = vi.spyOn(MessageContentService.prototype, 'isNSFWContentAllowed');
await sendMessage(harness, s.owner.token, s.groupId, 'before');
expect(scope.mock.results.at(-1)?.value).toBe(false);
const updated = await setNsfw(s.owner, s.groupId, true);
expect(updated.nsfw).toBe(true);
await sendMessage(harness, s.owner.token, s.groupId, 'after');
expect(scope.mock.results.at(-1)?.value).toBe(true);
const cleared = await setNsfw(s.owner, s.groupId, false);
expect(cleared.nsfw).toBe(false);
});
it('rejects the toggle from a member who is not the owner', async () => {
const s = await setup();
await setNsfw(s.member, s.groupId, true, 403);
});
it('refuses to mark a group that includes a minor', async () => {
const s = await setup();
await joinAsMinor(s.groupId, s.owner, 200);
await setNsfw(s.owner, s.groupId, true, 400, APIErrorCodes.GROUP_DM_MATURE_CONTENT_INELIGIBLE);
});
it('refuses to let a minor join a marked group', async () => {
const s = await setup();
await setNsfw(s.owner, s.groupId, true);
await joinAsMinor(s.groupId, s.owner, 403, APIErrorCodes.NSFW_CONTENT_AGE_RESTRICTED);
});
});
@@ -47,7 +47,6 @@ export const CONTENT_I18N_MESSAGES = {
'report_flow.label.csam': 'Real or AI-generated images or videos of child sexual abuse',
'report_flow.label.discovery_listing': 'Discovery listing',
'report_flow.label.dislike': "It's just not for me",
'report_flow.label.dsa': "Content that's illegal in the EU (Digital Services Act)",
'report_flow.label.eating_disorder_promotion': 'Encouraging disordered eating',
'report_flow.label.eating_disorder_promotion_profile': 'Their profile promotes eating disorders',
'report_flow.label.email': 'Email address',
@@ -45,7 +45,6 @@ const CONTENT_I18N_AR_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "صور أو مقاطع فيديو حقيقية أو مولدة بالذكاء الاصطناعي تُظهر اعتداءً جنسيًا على الأطفال",
"report_flow.label.discovery_listing": "قائمة الاكتشاف",
"report_flow.label.dislike": "هذا ببساطة لا يناسبني",
"report_flow.label.dsa": "محتوى غير قانوني في الاتحاد الأوروبي (قانون الخدمات الرقمية)",
"report_flow.label.eating_disorder_promotion": "تشجيع سلوكيات الأكل المضطربة",
"report_flow.label.eating_disorder_promotion_profile": "ملفهم الشخصي يروج لاضطرابات الأكل",
"report_flow.label.email": "عنوان البريد الإلكتروني",
@@ -34,10 +34,6 @@
"msgctxt": "report_flow.label.dislike",
"msgid": "It's just not for me"
},
{
"msgctxt": "report_flow.label.dsa",
"msgid": "Content that's illegal in the EU (Digital Services Act)"
},
{
"msgctxt": "report_flow.label.eating_disorder_promotion",
"msgid": "Encouraging disordered eating"
@@ -45,7 +45,6 @@ const CONTENT_I18N_BG_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Реални или генерирани от изкуствен интелект изображения или видеоклипове на сексуално насилие над деца",
"report_flow.label.discovery_listing": "Списък за откриване",
"report_flow.label.dislike": "Просто не е за мен",
"report_flow.label.dsa": "Съдържание, което е незаконно в ЕС (Акт за цифровите услуги)",
"report_flow.label.eating_disorder_promotion": "Насърчаване на хранителни разстройства",
"report_flow.label.eating_disorder_promotion_profile": "Профилът им насърчава хранителни разстройства",
"report_flow.label.email": "Имейл адрес",
@@ -45,7 +45,6 @@ const CONTENT_I18N_CS_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Skutečné nebo pomocí AI vytvořené obrázky či videa sexuálního zneužívání dětí",
"report_flow.label.discovery_listing": "Záznam v seznamu objevování",
"report_flow.label.dislike": "Prostě to není pro mě",
"report_flow.label.dsa": "Obsah, který je v EU nelegální (akt o digitálních službách)",
"report_flow.label.eating_disorder_promotion": "Nabádání k poruchám příjmu potravy",
"report_flow.label.eating_disorder_promotion_profile": "Jejich profil propaguje poruchy příjmu potravy",
"report_flow.label.email": "E-mailová adresa",
@@ -45,7 +45,6 @@ const CONTENT_I18N_DA_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Ægte eller AI-genererede billeder eller videoer af seksuelle overgreb mod børn",
"report_flow.label.discovery_listing": "Opslag i Opdag",
"report_flow.label.dislike": "Det er bare ikke noget for mig",
"report_flow.label.dsa": "Indhold, der er ulovligt i EU (retsakten om digitale tjenester, DSA)",
"report_flow.label.eating_disorder_promotion": "Opfordrer til spiseforstyrrelser",
"report_flow.label.eating_disorder_promotion_profile": "Deres profil fremmer spiseforstyrrelser",
"report_flow.label.email": "E-mailadresse",
@@ -45,7 +45,6 @@ const CONTENT_I18N_DE_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Reale oder KI-generierte Bilder oder Videos von sexuellem Kindesmissbrauch",
"report_flow.label.discovery_listing": "Eintrag in „Entdecken“",
"report_flow.label.dislike": "Das ist einfach nichts für mich",
"report_flow.label.dsa": "Inhalte, die in der EU illegal sind (Gesetz über digitale Dienste)",
"report_flow.label.eating_disorder_promotion": "Zu gestörtem Essverhalten ermutigen",
"report_flow.label.eating_disorder_promotion_profile": "Das Profil der Person fördert Essstörungen",
"report_flow.label.email": "E-Mail-Adresse",
@@ -45,7 +45,6 @@ const CONTENT_I18N_EL_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Πραγματικές ή δημιουργημένες με AI εικόνες ή βίντεο σεξουαλικής κακοποίησης παιδιών",
"report_flow.label.discovery_listing": "Καταχώριση ανακάλυψης",
"report_flow.label.dislike": "Απλά δεν μου ταιριάζει",
"report_flow.label.dsa": "Περιεχόμενο που είναι παράνομο στην ΕΕ (πράξη για τις ψηφιακές υπηρεσίες, DSA)",
"report_flow.label.eating_disorder_promotion": "Ενθάρρυνση διατροφικών διαταραχών",
"report_flow.label.eating_disorder_promotion_profile": "Το προφίλ τους προωθεί διατροφικές διαταραχές",
"report_flow.label.email": "Διεύθυνση email",
@@ -45,7 +45,6 @@ const CONTENT_I18N_EN_GB_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
"report_flow.label.discovery_listing": "Discovery listing",
"report_flow.label.dislike": "It's just not for me",
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
"report_flow.label.email": "Email address",
@@ -45,7 +45,6 @@ const CONTENT_I18N_ES_419_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Imágenes o videos reales o generados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Listado en Descubrir",
"report_flow.label.dislike": "Simplemente no es para mí",
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos alimenticios",
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos alimenticios",
"report_flow.label.email": "Dirección de correo electrónico",
@@ -45,7 +45,6 @@ const CONTENT_I18N_ES_ES_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Imágenes o vídeos reales o generados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Ficha en Descubrir",
"report_flow.label.dislike": "Simplemente no es para mí",
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos de la conducta alimentaria",
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos de la conducta alimentaria",
"report_flow.label.email": "Dirección de correo electrónico",
@@ -45,7 +45,6 @@ const CONTENT_I18N_FI_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Aitoja tai tekoälyn luomia kuvia tai videoita lapsiin kohdistuvasta seksuaalisesta hyväksikäytöstä",
"report_flow.label.discovery_listing": "Löydä-listaus",
"report_flow.label.dislike": "Se ei vain ole minun juttuni",
"report_flow.label.dsa": "Laiton sisältö EU:ssa (digipalvelusäädös, DSA)",
"report_flow.label.eating_disorder_promotion": "Syömishäiriöihin kannustaminen",
"report_flow.label.eating_disorder_promotion_profile": "Hänen profiilinsa edistää syömishäiriöitä",
"report_flow.label.email": "Sähköpostiosoite",
@@ -45,7 +45,6 @@ const CONTENT_I18N_FR_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Images ou vidéos réelles ou générées par IA représentant des abus sexuels sur des enfants",
"report_flow.label.discovery_listing": "Fiche dans Découverte",
"report_flow.label.dislike": "Ce n'est pas pour moi",
"report_flow.label.dsa": "Contenu illégal dans l'UE (règlement sur les services numériques, DSA)",
"report_flow.label.eating_disorder_promotion": "Encourager les troubles du comportement alimentaire",
"report_flow.label.eating_disorder_promotion_profile": "Son profil fait la promotion des troubles du comportement alimentaire",
"report_flow.label.email": "Adresse e-mail",
@@ -45,7 +45,6 @@ const CONTENT_I18N_HE_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "תמונות או סרטונים של פגיעה מינית בילדים, אמיתיים או שנוצרו באמצעות AI",
"report_flow.label.discovery_listing": "רישום בגילוי",
"report_flow.label.dislike": "זה פשוט לא בשבילי",
"report_flow.label.dsa": "תוכן בלתי חוקי באיחוד האירופי (חוק השירותים הדיגיטליים, DSA)",
"report_flow.label.eating_disorder_promotion": "עידוד הפרעות אכילה",
"report_flow.label.eating_disorder_promotion_profile": "הפרופיל שלהם מקדם הפרעות אכילה",
"report_flow.label.email": "כתובת אימייל",
@@ -45,7 +45,6 @@ const CONTENT_I18N_HI_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "बाल यौन शोषण की असली या AI से बनाई गई तस्वीरें या वीडियो",
"report_flow.label.discovery_listing": "डिस्कवरी लिस्टिंग",
"report_flow.label.dislike": "यह बस मेरे लिए नहीं है",
"report_flow.label.dsa": "यूरोपीय संघ में गैर-कानूनी सामग्री (डिजिटल सेवा अधिनियम)",
"report_flow.label.eating_disorder_promotion": "खाने संबंधी विकार वाली आदतों को बढ़ावा देना",
"report_flow.label.eating_disorder_promotion_profile": "उनकी प्रोफ़ाइल खाने संबंधी विकारों को बढ़ावा देती है",
"report_flow.label.email": "ईमेल पता",
@@ -45,7 +45,6 @@ const CONTENT_I18N_HR_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Stvarne ili AI-generirane slike ili videozapisi seksualnog zlostavljanja djece",
"report_flow.label.discovery_listing": "Unos u Otkrivanju",
"report_flow.label.dislike": "Jednostavno mi ne odgovara",
"report_flow.label.dsa": "Sadržaj koji je nezakonit u EU-u (Akt o digitalnim uslugama)",
"report_flow.label.eating_disorder_promotion": "Poticanje poremećaja hranjenja",
"report_flow.label.eating_disorder_promotion_profile": "Njihov profil promiče poremećaje hranjenja",
"report_flow.label.email": "Adresa e-pošte",
@@ -45,7 +45,6 @@ const CONTENT_I18N_HU_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Gyermekek szexuális bántalmazását ábrázoló valódi vagy mesterséges intelligencia által generált képek vagy videók",
"report_flow.label.discovery_listing": "Listázás a Felfedezésben",
"report_flow.label.dislike": "Egyszerűen nem nekem való",
"report_flow.label.dsa": "Az EU-ban illegális tartalom (digitális szolgáltatásokról szóló rendelet, DSA)",
"report_flow.label.eating_disorder_promotion": "Kóros étkezési szokásokra buzdítás",
"report_flow.label.eating_disorder_promotion_profile": "A profilja evészavarokat népszerűsít",
"report_flow.label.email": "E-mail-cím",
@@ -45,7 +45,6 @@ const CONTENT_I18N_ID_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Gambar atau video kekerasan seksual terhadap anak, baik yang asli maupun buatan AI",
"report_flow.label.discovery_listing": "Listing di Discovery",
"report_flow.label.dislike": "Ini tidak cocok untuk saya",
"report_flow.label.dsa": "Konten yang ilegal di UE (Undang-Undang Layanan Digital/DSA)",
"report_flow.label.eating_disorder_promotion": "Mendorong perilaku makan yang terganggu",
"report_flow.label.eating_disorder_promotion_profile": "Profil mereka mempromosikan gangguan makan",
"report_flow.label.email": "Alamat email",
@@ -45,7 +45,6 @@ const CONTENT_I18N_IT_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Immagini o video reali o generati da IA di abusi sessuali su minori",
"report_flow.label.discovery_listing": "Elenco in Scopri",
"report_flow.label.dislike": "Non fa per me",
"report_flow.label.dsa": "Contenuti illegali nell'UE (Regolamento sui servizi digitali, DSA)",
"report_flow.label.eating_disorder_promotion": "Incoraggiare disturbi alimentari",
"report_flow.label.eating_disorder_promotion_profile": "Il suo profilo promuove disturbi alimentari",
"report_flow.label.email": "Indirizzo e-mail",
@@ -45,7 +45,6 @@ const CONTENT_I18N_JA_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "児童性的虐待の画像・動画(実在のもの、またはAI生成のもの)",
"report_flow.label.discovery_listing": "「見つける」の掲載内容",
"report_flow.label.dislike": "自分には合わない",
"report_flow.label.dsa": "EUで違法なコンテンツ(デジタルサービス法、DSA)",
"report_flow.label.eating_disorder_promotion": "摂食障害的な食行動を促している",
"report_flow.label.eating_disorder_promotion_profile": "プロフィールが摂食障害を助長している",
"report_flow.label.email": "メールアドレス",
@@ -45,7 +45,6 @@ const CONTENT_I18N_KO_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "아동 성착취물(실제 또는 AI로 생성된 이미지 또는 동영상)",
"report_flow.label.discovery_listing": "디스커버리 목록",
"report_flow.label.dislike": "그냥 내 취향이 아닙니다",
"report_flow.label.dsa": "EU에서 불법인 콘텐츠(디지털 서비스법, DSA)",
"report_flow.label.eating_disorder_promotion": "섭식 장애 행동 조장",
"report_flow.label.eating_disorder_promotion_profile": "프로필에서 섭식 장애를 조장합니다",
"report_flow.label.email": "이메일 주소",
@@ -45,7 +45,6 @@ const CONTENT_I18N_LT_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Tikri ar dirbtinio intelekto sukurti vaikų seksualinės prievartos vaizdai ar vaizdo įrašai",
"report_flow.label.discovery_listing": "Atradimo sąrašas",
"report_flow.label.dislike": "Tai tiesiog ne man",
"report_flow.label.dsa": "Turinys, kuris neteisėtas ES (Skaitmeninių paslaugų aktas)",
"report_flow.label.eating_disorder_promotion": "Sutrikusių mitybos įpročių skatinimas",
"report_flow.label.eating_disorder_promotion_profile": "Jų profilis propaguoja valgymo sutrikimus",
"report_flow.label.email": "El. pašto adresas",
@@ -45,7 +45,6 @@ const CONTENT_I18N_NL_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Echte of door AI gegenereerde beelden of video's van seksueel misbruik van kinderen",
"report_flow.label.discovery_listing": "Vermelding bij Ontdekken",
"report_flow.label.dislike": "Het is gewoon niets voor mij",
"report_flow.label.dsa": "Inhoud die illegaal is in de EU (digitaledienstenverordening)",
"report_flow.label.eating_disorder_promotion": "Gestoord eetgedrag aanmoedigen",
"report_flow.label.eating_disorder_promotion_profile": "Hun profiel promoot eetstoornissen",
"report_flow.label.email": "E-mailadres",
@@ -45,7 +45,6 @@ const CONTENT_I18N_NO_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Ekte eller AI-genererte bilder eller videoer av seksuelle overgrep mot barn",
"report_flow.label.discovery_listing": "Oppføring i Oppdag",
"report_flow.label.dislike": "Det er bare ikke noe for meg",
"report_flow.label.dsa": "Innhold som er ulovlig i EU (forordningen om digitale tjenester, DSA)",
"report_flow.label.eating_disorder_promotion": "Oppfordrer til spiseforstyrrelser",
"report_flow.label.eating_disorder_promotion_profile": "Profilen deres fremmer spiseforstyrrelser",
"report_flow.label.email": "E-postadresse",
@@ -45,7 +45,6 @@ const CONTENT_I18N_PL_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Prawdziwe lub wygenerowane przez AI obrazy lub filmy przedstawiające wykorzystywanie seksualne dzieci",
"report_flow.label.discovery_listing": "Wpis w sekcji Odkrywanie",
"report_flow.label.dislike": "To po prostu nie jest dla mnie",
"report_flow.label.dsa": "Treści, które są nielegalne w UE (akt o usługach cyfrowych)",
"report_flow.label.eating_disorder_promotion": "Zachęcanie do zaburzeń odżywiania",
"report_flow.label.eating_disorder_promotion_profile": "Ich profil promuje zaburzenia odżywiania",
"report_flow.label.email": "Adres e-mail",
@@ -45,7 +45,6 @@ const CONTENT_I18N_PT_BR_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Imagens ou vídeos reais ou gerados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Listagem em Descobrir",
"report_flow.label.dislike": "Não é para mim",
"report_flow.label.dsa": "Conteúdo ilegal na UE (Regulamento dos Serviços Digitais)",
"report_flow.label.eating_disorder_promotion": "Incentivando transtornos alimentares",
"report_flow.label.eating_disorder_promotion_profile": "O perfil dele(a) promove transtornos alimentares",
"report_flow.label.email": "Endereço de e-mail",
@@ -45,7 +45,6 @@ const CONTENT_I18N_RO_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Imagini sau videoclipuri reale sau generate de AI cu abuz sexual asupra copiilor",
"report_flow.label.discovery_listing": "Listarea în Discovery",
"report_flow.label.dislike": "Pur și simplu nu este pentru mine",
"report_flow.label.dsa": "Conținut ilegal în UE (Regulamentul privind serviciile digitale)",
"report_flow.label.eating_disorder_promotion": "Încurajarea tulburărilor de alimentație",
"report_flow.label.eating_disorder_promotion_profile": "Profilul său promovează tulburările de alimentație",
"report_flow.label.email": "Adresă de e-mail",
@@ -45,7 +45,6 @@ const CONTENT_I18N_RU_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Реальные или созданные ИИ изображения или видео сексуального насилия над детьми",
"report_flow.label.discovery_listing": "Запись в каталоге",
"report_flow.label.dislike": "Мне это не подходит",
"report_flow.label.dsa": "Незаконный в ЕС контент (Закон о цифровых услугах, DSA)",
"report_flow.label.eating_disorder_promotion": "Поощрение расстройств пищевого поведения",
"report_flow.label.eating_disorder_promotion_profile": "Их профиль пропагандирует расстройства пищевого поведения",
"report_flow.label.email": "Адрес электронной почты",
@@ -45,7 +45,6 @@ const CONTENT_I18N_SV_SE_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Verkliga eller AI-genererade bilder eller videor av sexuella övergrepp mot barn",
"report_flow.label.discovery_listing": "Listning i Upptäck",
"report_flow.label.dislike": "Det är bara inget för mig",
"report_flow.label.dsa": "Innehåll som är olagligt i EU (förordningen om digitala tjänster)",
"report_flow.label.eating_disorder_promotion": "Uppmuntrar till ätstörningar",
"report_flow.label.eating_disorder_promotion_profile": "Deras profil främjar ätstörningar",
"report_flow.label.email": "E-postadress",
@@ -45,7 +45,6 @@ const CONTENT_I18N_TH_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "ภาพหรือวิดีโอการล่วงละเมิดทางเพศเด็ก ไม่ว่าจะเป็นของจริงหรือสร้างโดย AI",
"report_flow.label.discovery_listing": "รายการใน Discovery",
"report_flow.label.dislike": "ไม่เหมาะกับฉัน",
"report_flow.label.dsa": "เนื้อหาที่ผิดกฎหมายในสหภาพยุโรป (กฎหมายบริการดิจิทัล หรือ DSA)",
"report_flow.label.eating_disorder_promotion": "ส่งเสริมพฤติกรรมการกินที่ผิดปกติ",
"report_flow.label.eating_disorder_promotion_profile": "โปรไฟล์ของพวกเขาส่งเสริมความผิดปกติในการกิน",
"report_flow.label.email": "ที่อยู่อีเมล",
@@ -45,7 +45,6 @@ const CONTENT_I18N_TR_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Çocuk cinsel istismarına ait gerçek veya yapay zeka ile üretilmiş görseller ya da videolar",
"report_flow.label.discovery_listing": "Keşfet listesi",
"report_flow.label.dislike": "Bana göre değil",
"report_flow.label.dsa": "AB'de yasa dışı içerik (Dijital Hizmetler Yasası)",
"report_flow.label.eating_disorder_promotion": "Düzensiz yeme davranışlarını teşvik etme",
"report_flow.label.eating_disorder_promotion_profile": "Profili yeme bozukluklarını teşvik ediyor",
"report_flow.label.email": "E-posta adresi",
@@ -45,7 +45,6 @@ const CONTENT_I18N_UK_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Реальні або згенеровані ШІ зображення чи відео сексуального насильства над дітьми",
"report_flow.label.discovery_listing": "Запис у Discovery",
"report_flow.label.dislike": "Мені це просто не підходить",
"report_flow.label.dsa": "Вміст, незаконний в ЄС (Акт про цифрові послуги)",
"report_flow.label.eating_disorder_promotion": "Заохочення розладів харчової поведінки",
"report_flow.label.eating_disorder_promotion_profile": "Їхній профіль пропагує розлади харчової поведінки",
"report_flow.label.email": "Адреса електронної пошти",
@@ -45,7 +45,6 @@ const CONTENT_I18N_VI_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "Ảnh hoặc video lạm dụng tình dục trẻ em, dù là thật hay do AI tạo ra",
"report_flow.label.discovery_listing": "Danh sách khám phá",
"report_flow.label.dislike": "Không hợp với tôi",
"report_flow.label.dsa": "Nội dung bất hợp pháp tại EU (Đạo luật Dịch vụ Kỹ thuật số)",
"report_flow.label.eating_disorder_promotion": "Khuyến khích chứng rối loạn ăn uống",
"report_flow.label.eating_disorder_promotion_profile": "Hồ sơ của họ quảng bá chứng rối loạn ăn uống",
"report_flow.label.email": "Địa chỉ email",
@@ -45,7 +45,6 @@ const CONTENT_I18N_ZH_CN_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "真实或 AI 生成的儿童性虐待图像或视频",
"report_flow.label.discovery_listing": "社区发现列表",
"report_flow.label.dislike": "我就是不喜欢",
"report_flow.label.dsa": "在欧盟属违法的内容(《数字服务法》)",
"report_flow.label.eating_disorder_promotion": "鼓励进食障碍行为",
"report_flow.label.eating_disorder_promotion_profile": "其个人资料宣传进食障碍",
"report_flow.label.email": "电子邮件地址",
@@ -45,7 +45,6 @@ const CONTENT_I18N_ZH_TW_MESSAGES = defineContentI18nLocaleMessages({
"report_flow.label.csam": "兒童性虐待的真實或 AI 生成圖片或影片",
"report_flow.label.discovery_listing": "探索列表",
"report_flow.label.dislike": "對我來說不太適合",
"report_flow.label.dsa": "在歐盟屬違法的內容(《數位服務法》)",
"report_flow.label.eating_disorder_promotion": "鼓勵飲食失調",
"report_flow.label.eating_disorder_promotion_profile": "他們的個人檔案宣傳飲食失調",
"report_flow.label.email": "電子郵件地址",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "صور أو مقاطع فيديو حقيقية أو مولدة بالذكاء الاصطناعي تُظهر اعتداءً جنسيًا على الأطفال",
"report_flow.label.discovery_listing": "قائمة الاكتشاف",
"report_flow.label.dislike": "هذا ببساطة لا يناسبني",
"report_flow.label.dsa": "محتوى غير قانوني في الاتحاد الأوروبي (قانون الخدمات الرقمية)",
"report_flow.label.eating_disorder_promotion": "تشجيع سلوكيات الأكل المضطربة",
"report_flow.label.eating_disorder_promotion_profile": "ملفهم الشخصي يروج لاضطرابات الأكل",
"report_flow.label.email": "عنوان البريد الإلكتروني",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Реални или генерирани от изкуствен интелект изображения или видеоклипове на сексуално насилие над деца",
"report_flow.label.discovery_listing": "Списък за откриване",
"report_flow.label.dislike": "Просто не е за мен",
"report_flow.label.dsa": "Съдържание, което е незаконно в ЕС (Акт за цифровите услуги)",
"report_flow.label.eating_disorder_promotion": "Насърчаване на хранителни разстройства",
"report_flow.label.eating_disorder_promotion_profile": "Профилът им насърчава хранителни разстройства",
"report_flow.label.email": "Имейл адрес",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Skutečné nebo pomocí AI vytvořené obrázky či videa sexuálního zneužívání dětí",
"report_flow.label.discovery_listing": "Záznam v seznamu objevování",
"report_flow.label.dislike": "Prostě to není pro mě",
"report_flow.label.dsa": "Obsah, který je v EU nelegální (akt o digitálních službách)",
"report_flow.label.eating_disorder_promotion": "Nabádání k poruchám příjmu potravy",
"report_flow.label.eating_disorder_promotion_profile": "Jejich profil propaguje poruchy příjmu potravy",
"report_flow.label.email": "E-mailová adresa",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Ægte eller AI-genererede billeder eller videoer af seksuelle overgreb mod børn",
"report_flow.label.discovery_listing": "Opslag i Opdag",
"report_flow.label.dislike": "Det er bare ikke noget for mig",
"report_flow.label.dsa": "Indhold, der er ulovligt i EU (retsakten om digitale tjenester, DSA)",
"report_flow.label.eating_disorder_promotion": "Opfordrer til spiseforstyrrelser",
"report_flow.label.eating_disorder_promotion_profile": "Deres profil fremmer spiseforstyrrelser",
"report_flow.label.email": "E-mailadresse",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Reale oder KI-generierte Bilder oder Videos von sexuellem Kindesmissbrauch",
"report_flow.label.discovery_listing": "Eintrag in „Entdecken“",
"report_flow.label.dislike": "Das ist einfach nichts für mich",
"report_flow.label.dsa": "Inhalte, die in der EU illegal sind (Gesetz über digitale Dienste)",
"report_flow.label.eating_disorder_promotion": "Zu gestörtem Essverhalten ermutigen",
"report_flow.label.eating_disorder_promotion_profile": "Das Profil der Person fördert Essstörungen",
"report_flow.label.email": "E-Mail-Adresse",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Πραγματικές ή δημιουργημένες με AI εικόνες ή βίντεο σεξουαλικής κακοποίησης παιδιών",
"report_flow.label.discovery_listing": "Καταχώριση ανακάλυψης",
"report_flow.label.dislike": "Απλά δεν μου ταιριάζει",
"report_flow.label.dsa": "Περιεχόμενο που είναι παράνομο στην ΕΕ (πράξη για τις ψηφιακές υπηρεσίες, DSA)",
"report_flow.label.eating_disorder_promotion": "Ενθάρρυνση διατροφικών διαταραχών",
"report_flow.label.eating_disorder_promotion_profile": "Το προφίλ τους προωθεί διατροφικές διαταραχές",
"report_flow.label.email": "Διεύθυνση email",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
"report_flow.label.discovery_listing": "Discovery listing",
"report_flow.label.dislike": "It's just not for me",
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
"report_flow.label.email": "Email address",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Imágenes o videos reales o generados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Listado en Descubrir",
"report_flow.label.dislike": "Simplemente no es para mí",
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos alimenticios",
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos alimenticios",
"report_flow.label.email": "Dirección de correo electrónico",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Imágenes o vídeos reales o generados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Ficha en Descubrir",
"report_flow.label.dislike": "Simplemente no es para mí",
"report_flow.label.dsa": "Contenido ilegal en la UE (Reglamento de Servicios Digitales)",
"report_flow.label.eating_disorder_promotion": "Fomentar trastornos de la conducta alimentaria",
"report_flow.label.eating_disorder_promotion_profile": "Su perfil promueve trastornos de la conducta alimentaria",
"report_flow.label.email": "Dirección de correo electrónico",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Aitoja tai tekoälyn luomia kuvia tai videoita lapsiin kohdistuvasta seksuaalisesta hyväksikäytöstä",
"report_flow.label.discovery_listing": "Löydä-listaus",
"report_flow.label.dislike": "Se ei vain ole minun juttuni",
"report_flow.label.dsa": "Laiton sisältö EU:ssa (digipalvelusäädös, DSA)",
"report_flow.label.eating_disorder_promotion": "Syömishäiriöihin kannustaminen",
"report_flow.label.eating_disorder_promotion_profile": "Hänen profiilinsa edistää syömishäiriöitä",
"report_flow.label.email": "Sähköpostiosoite",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Images ou vidéos réelles ou générées par IA représentant des abus sexuels sur des enfants",
"report_flow.label.discovery_listing": "Fiche dans Découverte",
"report_flow.label.dislike": "Ce n'est pas pour moi",
"report_flow.label.dsa": "Contenu illégal dans l'UE (règlement sur les services numériques, DSA)",
"report_flow.label.eating_disorder_promotion": "Encourager les troubles du comportement alimentaire",
"report_flow.label.eating_disorder_promotion_profile": "Son profil fait la promotion des troubles du comportement alimentaire",
"report_flow.label.email": "Adresse e-mail",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "תמונות או סרטונים של פגיעה מינית בילדים, אמיתיים או שנוצרו באמצעות AI",
"report_flow.label.discovery_listing": "רישום בגילוי",
"report_flow.label.dislike": "זה פשוט לא בשבילי",
"report_flow.label.dsa": "תוכן בלתי חוקי באיחוד האירופי (חוק השירותים הדיגיטליים, DSA)",
"report_flow.label.eating_disorder_promotion": "עידוד הפרעות אכילה",
"report_flow.label.eating_disorder_promotion_profile": "הפרופיל שלהם מקדם הפרעות אכילה",
"report_flow.label.email": "כתובת אימייל",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "बाल यौन शोषण की असली या AI से बनाई गई तस्वीरें या वीडियो",
"report_flow.label.discovery_listing": "डिस्कवरी लिस्टिंग",
"report_flow.label.dislike": "यह बस मेरे लिए नहीं है",
"report_flow.label.dsa": "यूरोपीय संघ में गैर-कानूनी सामग्री (डिजिटल सेवा अधिनियम)",
"report_flow.label.eating_disorder_promotion": "खाने संबंधी विकार वाली आदतों को बढ़ावा देना",
"report_flow.label.eating_disorder_promotion_profile": "उनकी प्रोफ़ाइल खाने संबंधी विकारों को बढ़ावा देती है",
"report_flow.label.email": "ईमेल पता",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Stvarne ili AI-generirane slike ili videozapisi seksualnog zlostavljanja djece",
"report_flow.label.discovery_listing": "Unos u Otkrivanju",
"report_flow.label.dislike": "Jednostavno mi ne odgovara",
"report_flow.label.dsa": "Sadržaj koji je nezakonit u EU-u (Akt o digitalnim uslugama)",
"report_flow.label.eating_disorder_promotion": "Poticanje poremećaja hranjenja",
"report_flow.label.eating_disorder_promotion_profile": "Njihov profil promiče poremećaje hranjenja",
"report_flow.label.email": "Adresa e-pošte",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Gyermekek szexuális bántalmazását ábrázoló valódi vagy mesterséges intelligencia által generált képek vagy videók",
"report_flow.label.discovery_listing": "Listázás a Felfedezésben",
"report_flow.label.dislike": "Egyszerűen nem nekem való",
"report_flow.label.dsa": "Az EU-ban illegális tartalom (digitális szolgáltatásokról szóló rendelet, DSA)",
"report_flow.label.eating_disorder_promotion": "Kóros étkezési szokásokra buzdítás",
"report_flow.label.eating_disorder_promotion_profile": "A profilja evészavarokat népszerűsít",
"report_flow.label.email": "E-mail-cím",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Gambar atau video kekerasan seksual terhadap anak, baik yang asli maupun buatan AI",
"report_flow.label.discovery_listing": "Listing di Discovery",
"report_flow.label.dislike": "Ini tidak cocok untuk saya",
"report_flow.label.dsa": "Konten yang ilegal di UE (Undang-Undang Layanan Digital/DSA)",
"report_flow.label.eating_disorder_promotion": "Mendorong perilaku makan yang terganggu",
"report_flow.label.eating_disorder_promotion_profile": "Profil mereka mempromosikan gangguan makan",
"report_flow.label.email": "Alamat email",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Immagini o video reali o generati da IA di abusi sessuali su minori",
"report_flow.label.discovery_listing": "Elenco in Scopri",
"report_flow.label.dislike": "Non fa per me",
"report_flow.label.dsa": "Contenuti illegali nell'UE (Regolamento sui servizi digitali, DSA)",
"report_flow.label.eating_disorder_promotion": "Incoraggiare disturbi alimentari",
"report_flow.label.eating_disorder_promotion_profile": "Il suo profilo promuove disturbi alimentari",
"report_flow.label.email": "Indirizzo e-mail",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "児童性的虐待の画像・動画(実在のもの、またはAI生成のもの)",
"report_flow.label.discovery_listing": "「見つける」の掲載内容",
"report_flow.label.dislike": "自分には合わない",
"report_flow.label.dsa": "EUで違法なコンテンツ(デジタルサービス法、DSA)",
"report_flow.label.eating_disorder_promotion": "摂食障害的な食行動を促している",
"report_flow.label.eating_disorder_promotion_profile": "プロフィールが摂食障害を助長している",
"report_flow.label.email": "メールアドレス",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "아동 성착취물(실제 또는 AI로 생성된 이미지 또는 동영상)",
"report_flow.label.discovery_listing": "디스커버리 목록",
"report_flow.label.dislike": "그냥 내 취향이 아닙니다",
"report_flow.label.dsa": "EU에서 불법인 콘텐츠(디지털 서비스법, DSA)",
"report_flow.label.eating_disorder_promotion": "섭식 장애 행동 조장",
"report_flow.label.eating_disorder_promotion_profile": "프로필에서 섭식 장애를 조장합니다",
"report_flow.label.email": "이메일 주소",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Tikri ar dirbtinio intelekto sukurti vaikų seksualinės prievartos vaizdai ar vaizdo įrašai",
"report_flow.label.discovery_listing": "Atradimo sąrašas",
"report_flow.label.dislike": "Tai tiesiog ne man",
"report_flow.label.dsa": "Turinys, kuris neteisėtas ES (Skaitmeninių paslaugų aktas)",
"report_flow.label.eating_disorder_promotion": "Sutrikusių mitybos įpročių skatinimas",
"report_flow.label.eating_disorder_promotion_profile": "Jų profilis propaguoja valgymo sutrikimus",
"report_flow.label.email": "El. pašto adresas",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Echte of door AI gegenereerde beelden of video's van seksueel misbruik van kinderen",
"report_flow.label.discovery_listing": "Vermelding bij Ontdekken",
"report_flow.label.dislike": "Het is gewoon niets voor mij",
"report_flow.label.dsa": "Inhoud die illegaal is in de EU (digitaledienstenverordening)",
"report_flow.label.eating_disorder_promotion": "Gestoord eetgedrag aanmoedigen",
"report_flow.label.eating_disorder_promotion_profile": "Hun profiel promoot eetstoornissen",
"report_flow.label.email": "E-mailadres",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Ekte eller AI-genererte bilder eller videoer av seksuelle overgrep mot barn",
"report_flow.label.discovery_listing": "Oppføring i Oppdag",
"report_flow.label.dislike": "Det er bare ikke noe for meg",
"report_flow.label.dsa": "Innhold som er ulovlig i EU (forordningen om digitale tjenester, DSA)",
"report_flow.label.eating_disorder_promotion": "Oppfordrer til spiseforstyrrelser",
"report_flow.label.eating_disorder_promotion_profile": "Profilen deres fremmer spiseforstyrrelser",
"report_flow.label.email": "E-postadresse",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Prawdziwe lub wygenerowane przez AI obrazy lub filmy przedstawiające wykorzystywanie seksualne dzieci",
"report_flow.label.discovery_listing": "Wpis w sekcji Odkrywanie",
"report_flow.label.dislike": "To po prostu nie jest dla mnie",
"report_flow.label.dsa": "Treści, które są nielegalne w UE (akt o usługach cyfrowych)",
"report_flow.label.eating_disorder_promotion": "Zachęcanie do zaburzeń odżywiania",
"report_flow.label.eating_disorder_promotion_profile": "Ich profil promuje zaburzenia odżywiania",
"report_flow.label.email": "Adres e-mail",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Imagens ou vídeos reais ou gerados por IA de abuso sexual infantil",
"report_flow.label.discovery_listing": "Listagem em Descobrir",
"report_flow.label.dislike": "Não é para mim",
"report_flow.label.dsa": "Conteúdo ilegal na UE (Regulamento dos Serviços Digitais)",
"report_flow.label.eating_disorder_promotion": "Incentivando transtornos alimentares",
"report_flow.label.eating_disorder_promotion_profile": "O perfil dele(a) promove transtornos alimentares",
"report_flow.label.email": "Endereço de e-mail",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Imagini sau videoclipuri reale sau generate de AI cu abuz sexual asupra copiilor",
"report_flow.label.discovery_listing": "Listarea în Discovery",
"report_flow.label.dislike": "Pur și simplu nu este pentru mine",
"report_flow.label.dsa": "Conținut ilegal în UE (Regulamentul privind serviciile digitale)",
"report_flow.label.eating_disorder_promotion": "Încurajarea tulburărilor de alimentație",
"report_flow.label.eating_disorder_promotion_profile": "Profilul său promovează tulburările de alimentație",
"report_flow.label.email": "Adresă de e-mail",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Реальные или созданные ИИ изображения или видео сексуального насилия над детьми",
"report_flow.label.discovery_listing": "Запись в каталоге",
"report_flow.label.dislike": "Мне это не подходит",
"report_flow.label.dsa": "Незаконный в ЕС контент (Закон о цифровых услугах, DSA)",
"report_flow.label.eating_disorder_promotion": "Поощрение расстройств пищевого поведения",
"report_flow.label.eating_disorder_promotion_profile": "Их профиль пропагандирует расстройства пищевого поведения",
"report_flow.label.email": "Адрес электронной почты",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Verkliga eller AI-genererade bilder eller videor av sexuella övergrepp mot barn",
"report_flow.label.discovery_listing": "Listning i Upptäck",
"report_flow.label.dislike": "Det är bara inget för mig",
"report_flow.label.dsa": "Innehåll som är olagligt i EU (förordningen om digitala tjänster)",
"report_flow.label.eating_disorder_promotion": "Uppmuntrar till ätstörningar",
"report_flow.label.eating_disorder_promotion_profile": "Deras profil främjar ätstörningar",
"report_flow.label.email": "E-postadress",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "ภาพหรือวิดีโอการล่วงละเมิดทางเพศเด็ก ไม่ว่าจะเป็นของจริงหรือสร้างโดย AI",
"report_flow.label.discovery_listing": "รายการใน Discovery",
"report_flow.label.dislike": "ไม่เหมาะกับฉัน",
"report_flow.label.dsa": "เนื้อหาที่ผิดกฎหมายในสหภาพยุโรป (กฎหมายบริการดิจิทัล หรือ DSA)",
"report_flow.label.eating_disorder_promotion": "ส่งเสริมพฤติกรรมการกินที่ผิดปกติ",
"report_flow.label.eating_disorder_promotion_profile": "โปรไฟล์ของพวกเขาส่งเสริมความผิดปกติในการกิน",
"report_flow.label.email": "ที่อยู่อีเมล",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Çocuk cinsel istismarına ait gerçek veya yapay zeka ile üretilmiş görseller ya da videolar",
"report_flow.label.discovery_listing": "Keşfet listesi",
"report_flow.label.dislike": "Bana göre değil",
"report_flow.label.dsa": "AB'de yasa dışı içerik (Dijital Hizmetler Yasası)",
"report_flow.label.eating_disorder_promotion": "Düzensiz yeme davranışlarını teşvik etme",
"report_flow.label.eating_disorder_promotion_profile": "Profili yeme bozukluklarını teşvik ediyor",
"report_flow.label.email": "E-posta adresi",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Реальні або згенеровані ШІ зображення чи відео сексуального насильства над дітьми",
"report_flow.label.discovery_listing": "Запис у Discovery",
"report_flow.label.dislike": "Мені це просто не підходить",
"report_flow.label.dsa": "Вміст, незаконний в ЄС (Акт про цифрові послуги)",
"report_flow.label.eating_disorder_promotion": "Заохочення розладів харчової поведінки",
"report_flow.label.eating_disorder_promotion_profile": "Їхній профіль пропагує розлади харчової поведінки",
"report_flow.label.email": "Адреса електронної пошти",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Ảnh hoặc video lạm dụng tình dục trẻ em, dù là thật hay do AI tạo ra",
"report_flow.label.discovery_listing": "Danh sách khám phá",
"report_flow.label.dislike": "Không hợp với tôi",
"report_flow.label.dsa": "Nội dung bất hợp pháp tại EU (Đạo luật Dịch vụ Kỹ thuật số)",
"report_flow.label.eating_disorder_promotion": "Khuyến khích chứng rối loạn ăn uống",
"report_flow.label.eating_disorder_promotion_profile": "Hồ sơ của họ quảng bá chứng rối loạn ăn uống",
"report_flow.label.email": "Địa chỉ email",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "真实或 AI 生成的儿童性虐待图像或视频",
"report_flow.label.discovery_listing": "社区发现列表",
"report_flow.label.dislike": "我就是不喜欢",
"report_flow.label.dsa": "在欧盟属违法的内容(《数字服务法》)",
"report_flow.label.eating_disorder_promotion": "鼓励进食障碍行为",
"report_flow.label.eating_disorder_promotion_profile": "其个人资料宣传进食障碍",
"report_flow.label.email": "电子邮件地址",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "兒童性虐待的真實或 AI 生成圖片或影片",
"report_flow.label.discovery_listing": "探索列表",
"report_flow.label.dislike": "對我來說不太適合",
"report_flow.label.dsa": "在歐盟屬違法的內容(《數位服務法》)",
"report_flow.label.eating_disorder_promotion": "鼓勵飲食失調",
"report_flow.label.eating_disorder_promotion_profile": "他們的個人檔案宣傳飲食失調",
"report_flow.label.email": "電子郵件地址",
@@ -41,7 +41,6 @@
"report_flow.label.csam": "Real or AI-generated images or videos of child sexual abuse",
"report_flow.label.discovery_listing": "Discovery listing",
"report_flow.label.dislike": "It's just not for me",
"report_flow.label.dsa": "Content that's illegal in the EU (Digital Services Act)",
"report_flow.label.eating_disorder_promotion": "Encouraging disordered eating",
"report_flow.label.eating_disorder_promotion_profile": "Their profile promotes eating disorders",
"report_flow.label.email": "Email address",
+6
View File
@@ -32324,6 +32324,10 @@
"nicks": {
"description": "Custom nicknames for users in this group DM",
"anyOf": [{"$ref": "#/components/schemas/ChannelNicknameOverrides"}, {"type": "null"}]
},
"nsfw": {
"description": "Whether the group DM is marked for mature content (owner only)",
"type": ["boolean", "null"]
}
}
},
@@ -39725,6 +39729,7 @@
"MAX_GROUP_DM_RECIPIENTS",
"MAX_GROUP_DMS",
"GROUP_DM_RECIPIENTS_NOT_ADDABLE",
"GROUP_DM_MATURE_CONTENT_INELIGIBLE",
"MAX_GUILD_CHANNELS",
"MAX_GUILD_MEMBERS",
"MAX_GUILD_ROLES",
@@ -40021,6 +40026,7 @@
"You've reached the maximum of {count, plural, one {# group DM recipient} other {# group DM recipients}}.",
"You've reached the maximum of {count, plural, one {# group DM} other {# group DMs}}.",
"One or more selected users can't be added to this group DM.",
"Not everyone in this group is eligible for mature content.",
"You've reached the maximum of {count, plural, one {# community channel} other {# community channels}}.",
"You've reached the maximum of {count, plural, one {# community member} other {# community members}}.",
"You've reached the maximum of {count, plural, one {# community role} other {# community roles}}.",
@@ -121,7 +121,6 @@
"message/private_info/threat_to_share": "item",
"message/root_message/abuse": "screen:abuse",
"message/root_message/dislike": "end",
"message/root_message/dsa": "link:dsa",
"message/root_message/private_info": "screen:private_info",
"message/root_message/something_else": "screen:something_else_message",
"message/root_message/spam": "submit:spam",
@@ -215,7 +214,6 @@
"user/profile_threat/violence_incitement": "submit:violence_incitement",
"user/profile_threat/violent_threat": "submit:violent_threat",
"user/root_user/abuse": "screen:profile_abuse",
"user/root_user/dsa": "link:dsa",
"user/root_user/hate_violence": "screen:profile_hate_violence",
"user/root_user/impersonation": "screen:impersonation",
"user/root_user/something_else": "screen:something_else_user",
@@ -242,6 +240,8 @@
"user/spam_profile/spam_profile": "submit:spam"
},
"retired": {
"message/something_else_message/violence_misinfo": "screen:violence_misinfo"
"message/root_message/dsa": "link:dsa",
"message/something_else_message/violence_misinfo": "screen:violence_misinfo",
"user/root_user/dsa": "link:dsa"
}
}
@@ -106,6 +106,7 @@ const REPORT_FLOW_SURFACES: ReadonlyArray<ReportFlowSurface> = ['in_app', 'dsa']
const REPORT_FLOW_ID_PATTERN = /^[a-z0-9_]{1,48}$/;
const MAX_WALK_SCREENS = 10;
const CRISIS_LINES_URL = 'https://befrienders.org';
const COPYRIGHT_HELP_URL = 'https://fluxer.app/help/copyright';
const HOSTED_REPORT_FLOW_INSTANCE: ReportFlowInstance = {selfHosted: false, guidelinesLinked: true};
const REPORT_FLOW_CHECKED_INSTANCES: ReadonlyArray<ReportFlowInstance> = [
HOSTED_REPORT_FLOW_INSTANCE,
@@ -130,7 +131,6 @@ function isLinkAvailable(link: ReportFlowLinkId, instance: ReportFlowInstance):
return true;
case 'guidelines':
return instance.guidelinesLinked;
case 'dsa':
case 'copyright':
return !instance.selfHosted;
}
@@ -355,10 +355,9 @@ function collectDefinitionErrors(library: ReportFlowLibrary): Array<string> {
checkKey(option.label, optionWhere);
checkOutcome(option.outcome, optionWhere);
const endsWithoutNotice = option.outcome.type === 'end' && option.outcome.noticeId === undefined;
const linksToDsa =
option.outcome.type === 'link' && (option.outcome.link === 'dsa' || option.outcome.link === 'copyright');
if ((endsWithoutNotice || linksToDsa) && option.surface !== 'in_app') {
errors.push(`${optionWhere}: a no-report or DSA link option must be limited to in_app`);
const linksToCopyright = option.outcome.type === 'link' && option.outcome.link === 'copyright';
if ((endsWithoutNotice || linksToCopyright) && option.surface !== 'in_app') {
errors.push(`${optionWhere}: a no-report or copyright link option must be limited to in_app`);
}
}
if (screen.checklist) {
@@ -537,10 +536,8 @@ function getReportFlowInstance(guidelinesUrl: string | null): ReportFlowInstance
function getLinkUrl(link: ReportFlowLinkId, guidelinesUrl: string | null): string | null {
switch (link) {
case 'dsa':
return `${Config.endpoints.webApp}/report`;
case 'copyright':
return `${Config.endpoints.webApp}/report?option=copyright_notice`;
return COPYRIGHT_HELP_URL;
case 'crisis_lines':
return CRISIS_LINES_URL;
case 'guidelines':
@@ -6,7 +6,7 @@ import type {ReportFlowSurface} from '@fluxer/schema/src/domains/report/ReportFl
export type ReportFlowCopyKey = Extract<ContentI18nKey, `report_flow.${string}`>;
export type ReportFlowLinkId = 'dsa' | 'copyright' | 'crisis_lines' | 'guidelines';
export type ReportFlowLinkId = 'copyright' | 'crisis_lines' | 'guidelines';
export type ReportFlowOutcomeDef =
| {type: 'screen'; screenId: string}
@@ -110,7 +110,6 @@ export const REPORT_FLOW_SCREENS: ReadonlyArray<ReportFlowScreenDef> = [
outcome: {type: 'screen', screenId: 'something_else_message'},
},
{id: 'dislike', label: 'report_flow.label.dislike', outcome: {type: 'end'}, surface: 'in_app'},
{id: 'dsa', label: 'report_flow.label.dsa', outcome: {type: 'link', link: 'dsa'}, surface: 'in_app'},
],
},
{
@@ -521,7 +520,6 @@ export const REPORT_FLOW_SCREENS: ReadonlyArray<ReportFlowScreenDef> = [
label: 'report_flow.label.something_else',
outcome: {type: 'screen', screenId: 'something_else_user'},
},
{id: 'dsa', label: 'report_flow.label.dsa', outcome: {type: 'link', link: 'dsa'}, surface: 'in_app'},
],
},
{
@@ -248,7 +248,7 @@ describe('report flow definitions', () => {
expect(() => assertValidReportFlowLibrary(broken)).toThrow(/missing_screen is not defined/);
});
test('a no-report or DSA option that is not limited to the app throws', () => {
test('a no-report option that is not limited to the app throws', () => {
const unlimitedDislike = editScreen(REPORT_FLOW_LIBRARY, 'root_message', (screen) => ({
...screen,
options: screen.options?.map((option) =>
@@ -256,11 +256,6 @@ describe('report flow definitions', () => {
),
}));
expect(() => assertValidReportFlowLibrary(unlimitedDislike)).toThrow(/limited to in_app/);
const unlimitedDsaRow = editScreen(REPORT_FLOW_LIBRARY, 'root_user', (screen) => ({
...screen,
options: screen.options?.map((option) => (option.id === 'dsa' ? {...option, surface: undefined} : option)),
}));
expect(() => assertValidReportFlowLibrary(unlimitedDsaRow)).toThrow(/limited to in_app/);
});
test('other broken definitions throw', () => {
@@ -883,7 +878,6 @@ describe('report flow rendering', () => {
'spam',
'something_else',
'dislike',
'dsa',
]);
expect(root.options[5].outcome).toEqual({type: 'end', screen_id: null, reason: null, notice_id: null, url: null});
expect(root.options[3].outcome).toEqual({
@@ -893,7 +887,6 @@ describe('report flow rendering', () => {
notice_id: null,
url: null,
});
expect(root.options[6].outcome.url).toBe(`${config.endpoints.webApp}/report`);
const copyright = message.screens
.find((screen) => screen.id === 'something_else_message')!
.options.find((option) => option.id === 'copyright');
@@ -902,7 +895,7 @@ describe('report flow rendering', () => {
screen_id: null,
reason: null,
notice_id: null,
url: `${config.endpoints.webApp}/report?option=copyright_notice`,
url: 'https://fluxer.app/help/copyright',
});
expect(root.checklist).toBeNull();
expect(root.next_screen_id).toBeNull();
@@ -920,6 +913,9 @@ describe('report flow rendering', () => {
expect(sexual.options[0].id).toBe('minor_sexual');
const user = getReportFlowResponse('user', 'in_app', 'en-US');
expect(user.guidelines_url).toBe(`${config.endpoints.marketing}/guidelines`);
expect(user.screens.find((screen) => screen.id === 'root_user')?.options.map((option) => option.id)).not.toContain(
'dsa',
);
const intro = user.screens[0];
expect(intro.next_screen_id).toBe('profile_parts');
expect(intro.options_heading).toBe('Learn more');
@@ -984,8 +980,8 @@ describe('report flow rendering', () => {
);
});
test('the in-app copyright rows open the DSA form on the copyright notice', () => {
const url = `${getConfig().endpoints.webApp}/report?option=copyright_notice`;
test('the in-app copyright rows open the copyright help page', () => {
const url = 'https://fluxer.app/help/copyright';
const copyrightUrl = (target: ReportFlowTargetType, screenId: string) =>
getReportFlowResponse(target, 'in_app', 'en-US')
.screens.find((screen) => screen.id === screenId)
@@ -1031,7 +1027,7 @@ describe('report flow rendering', () => {
).not.toMatch(dsaNames);
});
test('self-hosted instances get no guidelines, DSA or copyright link rows', () => {
test('self-hosted instances get no guidelines or copyright link rows', () => {
const hostedUser = getReportFlowResponse('user', 'in_app', 'en-US');
getConfig().instance.selfHosted = true;
const user = getReportFlowResponse('user', 'in_app', 'en-US');
@@ -1042,7 +1038,6 @@ describe('report flow rendering', () => {
expect(intro.options_heading).toBeNull();
expect(intro.next_screen_id).toBe('profile_parts');
const userScreens = new Map(user.screens.map((screen) => [screen.id, screen]));
expect(userScreens.get('root_user')?.options.map((option) => option.id)).not.toContain('dsa');
expect(userScreens.get('something_else_user')?.options.map((option) => option.id)).not.toContain('copyright');
const message = getReportFlowResponse('message', 'in_app', 'en-US');
for (const screen of message.screens) {
@@ -1051,7 +1046,6 @@ describe('report flow rendering', () => {
}
}
const messageScreens = new Map(message.screens.map((screen) => [screen.id, screen]));
expect(messageScreens.get('root_message')?.options.map((option) => option.id)).not.toContain('dsa');
expect(messageScreens.get('something_else_message')?.options.map((option) => option.id)).not.toContain('copyright');
const messageDsa = getReportFlowResponse('message', 'dsa', 'en-US');
expect(
@@ -428,11 +428,12 @@ describe('Report flows', () => {
.get('/reports/flows/message?surface=dsa')
.expect(HTTP_STATUS.OK)
.execute();
const appOnly = ['dislike', 'rude_language', 'dsa', 'copyright', 'worried_self_harm', 'worried_suicide'];
const appOnly = ['dislike', 'rude_language', 'copyright', 'worried_self_harm', 'worried_suicide'];
for (const id of appOnly) {
expect(optionIds(inApp), id).toContain(id);
expect(optionIds(dsa), id).not.toContain(id);
}
expect(optionIds(inApp)).not.toContain('dsa');
expect(optionIds(inApp)).not.toContain('copyright_notice');
expect(optionIds(dsa)).toContain('copyright_notice');
expect(dsa.revision_hash).not.toBe(inApp.revision_hash);
@@ -556,6 +556,14 @@ export class MockStorageService implements IStorageService {
return this.objects.has(this.objectId(bucket, key));
}
setObjectLastModified(bucket: string, key: string, lastModified: Date): void {
const object = this.objects.get(this.objectId(bucket, key));
if (!object) {
throw new Error(`No stored object ${key} in ${bucket}`);
}
object.lastModified = lastModified;
}
reset(): void {
this.objects.clear();
this.multipartUploads.clear();
+39
View File
@@ -37,6 +37,24 @@ const ENTRY_STYLESHEET_LINK_PATTERN = /<link href="([^"]+\.css)" rel="stylesheet
const CSS_URL_PATTERN = /url\(\s*['"]?([^'")]+)/gu;
const DESKTOP_MODULE_ASSET_QUERY = /[?&]m=([a-z][a-z0-9_]{0,63})(?:&|$)/u;
const RESERVED_DESKTOP_MODULE_NAMES = new Set(['assets', 'fluxer_renderer']);
const DESKTOP_MODULE_ASSET_SOURCES = [
{
test: /[\\/]fonts[\\/]files[\\/]FluxerSans(JP|KR|SC|TC)[\\/][^\\/]+\.woff2$/u,
moduleName: (match) => `fluxer_fonts_${match[1].toLowerCase()}`,
},
{
test: /[\\/]@arborium[\\/]([a-z0-9-]+)[\\/]grammar_bg\.wasm$/u,
moduleName: (match) => `fluxer_grammar_${match[1].replace(/-/gu, '_')}`,
},
{
test: /[\\/]deepfilternet3[\\/](df_bg\.wasm|[^\\/]+\.tar\.gz)$/u,
moduleName: () => 'fluxer_deepfilter',
},
{
test: /[\\/]onnxruntime-web[\\/]dist[\\/][^\\/]+\.wasm$|[\\/]camera-effects[\\/]models[\\/][^\\/]+\.onnx$/u,
moduleName: () => 'fluxer_camera_effects',
},
];
class UnusableDesktopModuleQueryError extends Error {
constructor(resource) {
@@ -55,6 +73,26 @@ function desktopModuleAssetName(resource) {
return match[1];
}
function desktopModuleAssetRules(isProduction) {
return DESKTOP_MODULE_ASSET_SOURCES.map((source) => ({
test: source.test,
type: 'asset/resource',
generator: {
filename: (pathData) => {
const match = source.test.exec(pathData.filename ?? '');
if (match == null) {
throw new UnusableDesktopModuleQueryError(pathData.filename ?? '');
}
const moduleName = source.moduleName(match);
const extension = pathData.filename.endsWith('.tar.gz') ? '.tar.gz' : '[ext]';
return isProduction
? `assets/${moduleName}/[contenthash:16]${extension}`
: `assets/${moduleName}/[name].[hash]${extension}`;
},
},
}));
}
function resolveMode() {
const modeIndex = process.argv.indexOf('--mode');
if (modeIndex >= 0) {
@@ -632,6 +670,7 @@ export default () => {
filename: isProduction ? 'assets/[contenthash:16][ext]' : 'assets/[name].[hash][ext]',
},
},
...(isDesktopRenderer ? desktopModuleAssetRules(isProduction) : []),
{
resourceQuery: DESKTOP_MODULE_ASSET_QUERY,
type: 'asset/resource',
+2
View File
@@ -41,6 +41,7 @@ import {useRemScaleTracking} from '@app/features/theme/hooks/useRemScaleTracking
import {useThemeCssVariables} from '@app/features/theme/hooks/useThemeCssVariables';
import Theme from '@app/features/theme/state/Theme';
import ThemeLibrary from '@app/features/theme/state/ThemeLibrary';
import WindowsFont from '@app/features/theme/state/WindowsFont';
import {useThemeStudioBroadcast} from '@app/features/theme_studio/state/ThemeStudioBroadcast';
import ThemeStudioState from '@app/features/theme_studio/state/ThemeStudioState';
import {SVGMasks} from '@app/features/ui/components/SVGMasks';
@@ -174,6 +175,7 @@ export const AppWrapper = observer(({children}: AppWrapperProps) => {
useDocumentClassToggle('reduced-motion', reducedMotion);
useDocumentClassToggle('mobile-layout', MobileLayout.platformMobileDetected || MobileLayout.enabled);
useDocumentClassToggle(UNFOCUSED_FULLY_INTERACTIVE_CLASS, stayInteractiveWhenUnfocused);
useDocumentClassToggle('windows-fluxer-sans', WindowsFont.useFluxerSans);
useDesktopAllowTransparency(isNative);
useWindowEventListeners();
useRemScaleTracking();
+15 -10
View File
@@ -17,56 +17,61 @@
--font-fallback-mono:
'Noto Sans Mono', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', var(--font-fallback-sans);
--font-sans-primary: 'Fluxer Sans';
--font-sans:
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
var(--font-sans-primary), 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Segoe UI', 'Roboto', var(--font-fallback-sans), sans-serif;
--font-mono: 'Fluxer Mono', 'Menlo', 'Monaco', 'Courier New', var(--font-fallback-mono), monospace;
}
:root.platform-windows:not(.windows-fluxer-sans) {
--font-sans-primary: 'Segoe UI Variable Text', 'Segoe UI';
}
:root:lang(ja) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans JP', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans JP', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo',
var(--font-fallback-sans), sans-serif;
}
:root:lang(ko) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans KR', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans KR', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', system-ui, -apple-system,
BlinkMacSystemFont, 'Malgun Gothic', 'Apple SD Gothic Neo', var(--font-fallback-sans), sans-serif;
}
:root:lang(zh-CN),
:root:lang(zh-Hans) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans SC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans SC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans Thai Looped', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', var(--font-fallback-sans), sans-serif;
}
:root:lang(zh-Hant-TW),
:root:lang(zh-TW) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'PingFang TC', 'Microsoft JhengHei', var(--font-fallback-sans), sans-serif;
}
:root:lang(zh-HK),
:root:lang(zh-Hant-HK) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans TC', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans Thai Looped', 'Fluxer Sans SC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'PingFang HK', 'Microsoft JhengHei', var(--font-fallback-sans), sans-serif;
}
:root:lang(ar) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
var(--font-sans-primary), 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Segoe UI', 'Geeza Pro', 'Arabic Typesetting', var(--font-fallback-sans), sans-serif;
}
:root:lang(he) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans Hebrew', 'Fluxer Sans Arabic', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
var(--font-sans-primary), 'Fluxer Sans Hebrew', 'Fluxer Sans Arabic', 'Fluxer Sans Devanagari', 'Fluxer Sans Thai Looped',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Segoe UI', 'Arial Hebrew', var(--font-fallback-sans), sans-serif;
}
@@ -75,13 +80,13 @@
:root:lang(ne),
:root:lang(sa) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans Devanagari', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Thai Looped',
var(--font-sans-primary), 'Fluxer Sans Devanagari', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Thai Looped',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Nirmala UI', 'Noto Sans Devanagari', var(--font-fallback-sans), sans-serif;
}
:root:lang(th) {
--font-sans:
'Fluxer Sans', 'Fluxer Sans Thai Looped', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
var(--font-sans-primary), 'Fluxer Sans Thai Looped', 'Fluxer Sans Arabic', 'Fluxer Sans Hebrew', 'Fluxer Sans Devanagari',
'Fluxer Sans SC', 'Fluxer Sans TC', 'Fluxer Sans JP', 'Fluxer Sans KR', system-ui, -apple-system,
BlinkMacSystemFont, 'Leelawadee UI', 'Thonburi', var(--font-fallback-sans), sans-serif;
}
@@ -54,6 +54,7 @@ import {
getSectionIdsForTab,
getSectionsForTab,
getUserSettingsTabLabel,
PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
tabHasMultipleLinkableSections,
USER_SETTINGS_LABEL_DESCRIPTOR,
} from '@app/features/user/components/settings_utils/SettingsConstants';
@@ -79,6 +80,7 @@ import {buildUserSettingsDeepLink} from '@app/features/user/components/settings_
import {useSettingsContentKey} from '@app/features/user/hooks/useSettingsContentKey';
import {useUnsavedChangesFlash} from '@app/features/user/hooks/useUnsavedChangesFlash';
import {ScrollSpyProvider, useScrollSpyContext} from '@app/features/user/state/ScrollSpyContext';
import UserSettings from '@app/features/user/state/UserSettings';
import Users from '@app/features/user/state/Users';
import {
collapseSettingsTreeTab,
@@ -380,6 +382,7 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
const activeTabPanelId = selectedTab ? `settings-tabpanel-${selectedTab}` : undefined;
const activeTabId = selectedTab ? `settings-tab-${selectedTab}` : undefined;
const shouldShowWhatsNew = hasWhatsNewEntries();
const privacySetupPending = UserSettings.isPrivacySetupPending();
const scrollKey = useMemo(() => {
const subtabKey = contentKey ?? initialSubtab ?? 'root';
if (isSearchActive) {
@@ -585,6 +588,11 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
</div>
}
selected={isSelected}
attentionLabel={
tab.type === 'privacy_safety' && privacySetupPending
? i18n._(PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
: undefined
}
onClick={() => handleTabSelect(tab.type)}
onRequestContentFocus={focusContentPanel}
id={tabId}
@@ -21,6 +21,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands';
import {MentionBadgeAnimated} from '@app/features/ui/components/MentionBadge';
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
import {Spinner} from '@app/features/ui/components/Spinner';
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
@@ -31,6 +32,7 @@ import type {SettingsTab} from '@app/features/user/components/settings_utils/Set
import {
getCategoryLabel,
getUserSettingsTabLabel,
PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
} from '@app/features/user/components/settings_utils/SettingsConstants';
import {isSettingsItemNew} from '@app/features/user/components/settings_utils/SettingsMetadata';
import {
@@ -44,6 +46,7 @@ import {buildUserSettingsDeepLink} from '@app/features/user/components/settings_
import type {MobileNavigationState} from '@app/features/user/hooks/useMobileNavigation';
import {useSettingsContentKey} from '@app/features/user/hooks/useSettingsContentKey';
import {useUnsavedChangesFlash} from '@app/features/user/hooks/useUnsavedChangesFlash';
import UserSettings from '@app/features/user/state/UserSettings';
import Users from '@app/features/user/state/Users';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
@@ -137,9 +140,10 @@ interface PressableSettingsItemProps {
tab: SettingsTab;
onSelect: () => void;
badge?: React.ReactNode;
attention?: string;
}
const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({tab, onSelect, badge}) => {
const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({tab, onSelect, badge, attention}) => {
const {isPressed, pressableProps} = usePressable();
const {handleLongPressCopy, consumeSuppressedClick} = useSettingsPageDeepLinkLongPress(tab.type);
const handleClick = useCallback(
@@ -187,6 +191,9 @@ const PressableSettingsItem: React.FC<PressableSettingsItemProps> = observer(({t
{badge}
</div>
</div>
{attention && (
<NotificationDot label={attention} data-flx="app.mobile-settings-view.pressable-settings-item.attention-dot" />
)}
<ArrowLeftIcon
className={styles.settingsItemArrow}
data-flx="app.mobile-settings-view.pressable-settings-item.settings-item-arrow"
@@ -362,6 +369,7 @@ const MobileSettingsList = observer(
const {i18n} = useLingui();
const currentUser = Users.currentUser;
const shouldShowWhatsNew = hasWhatsNewEntries();
const privacySetupPending = UserSettings.isPrivacySetupPending();
const advancedSettingsHasNew = useMemo(
() =>
getAdvancedSettingItems().some((item) => isSettingsItemNew(item, Date.now(), currentUser?.createdAt ?? null)),
@@ -432,6 +440,11 @@ const MobileSettingsList = observer(
tab={tab}
onSelect={() => onTabSelect(tab.type, tab.label)}
badge={badge}
attention={
tab.type === 'privacy_safety' && privacySetupPending
? i18n._(PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
: undefined
}
data-flx="app.mobile-settings-view.mobile-settings-list.pressable-settings-item.tab-select"
/>
{(!isLastTab || isLastCategory) && (
@@ -406,9 +406,30 @@
border: 0;
}
.sidebarItemTrailing {
position: relative;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
margin-inline-start: 0.25rem;
min-width: 1rem;
min-height: 1rem;
}
.sidebarItemAttentionDot {
position: absolute;
}
.sidebarItem[data-expandable='true'].sidebarItemSelected .sidebarItemAttentionDot,
.sidebarItem[data-expandable='true'][aria-expanded='true'] .sidebarItemAttentionDot,
.sidebarItem[data-expandable='true']:hover .sidebarItemAttentionDot,
.sidebarItem[data-expandable='true'].sidebarItemFocused .sidebarItemAttentionDot {
visibility: hidden;
}
.sidebarItemChevron {
flex-shrink: 0;
margin-inline-start: 0.25rem;
color: var(--text-tertiary);
opacity: 0;
transform: rotate(0deg);
@@ -4,6 +4,7 @@ import Accessibility from '@app/features/accessibility/state/Accessibility';
import styles from '@app/features/app/components/dialogs/shared/SettingsModalLayout.module.css';
import {SETTINGS_SECTIONS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {
@@ -292,6 +293,7 @@ export const SettingsModalSidebarItem: React.FC<SettingsModalSidebarItemProps> =
expandableId,
sectionsGroupId,
toggleOnSelectedClick = true,
attentionLabel,
}) => {
const {tabIndex, buttonRef} = useSettingsModalSidebarItemLogic({selected});
const focusContentAfterActivationRef = useRef(false);
@@ -354,14 +356,28 @@ export const SettingsModalSidebarItem: React.FC<SettingsModalSidebarItemProps> =
>
{label}
</span>
{expandable && (
<CaretRightIcon
className={styles.sidebarItemChevron}
size={remFromPx(16)}
weight="bold"
aria-hidden="true"
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-chevron"
/>
{(expandable || attentionLabel) && (
<span
className={styles.sidebarItemTrailing}
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-trailing"
>
{attentionLabel && (
<NotificationDot
className={styles.sidebarItemAttentionDot}
label={attentionLabel}
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.attention-dot"
/>
)}
{expandable && (
<CaretRightIcon
className={styles.sidebarItemChevron}
size={remFromPx(16)}
weight="bold"
aria-hidden="true"
data-flx="app.settings-modal-layout.settings-modal-sidebar-item.sidebar-item-chevron"
/>
)}
</span>
)}
</>
);
@@ -1283,10 +1283,10 @@ const SKELETON_NAGBAR_ROW_SHAPES: Record<NagbarType, SkeletonNagbarRowShape> = {
[NagbarType.VISIONARY_MFA]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.VOICE_SESSION_RESTORE]: {tone: SkeletonNagbarTone.VOICE, hasActions: true},
[NagbarType.TERMS_ACCEPTANCE]: {tone: SkeletonNagbarTone.LEGAL, hasActions: true},
[NagbarType.PRIVACY_SETUP]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.SOFTWARE_ENCODER]: {tone: SkeletonNagbarTone.ENCODER, hasActions: true},
[NagbarType.STREAMER_MODE]: {tone: SkeletonNagbarTone.STREAMER, hasActions: true},
[NagbarType.DOMAIN_MOVED]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
[NagbarType.WINDOWS_FONT]: {tone: SkeletonNagbarTone.BRAND, hasActions: true},
};
const CONNECTION_SKELETON_NAGBAR_TONES: Record<ConnectionNoticeTone, SkeletonNagbarTone> = {
@@ -253,3 +253,13 @@
height: 1.25rem;
width: 1.25rem;
}
.controlIconWithDot {
mask-image: radial-gradient(circle at 85% 15%, transparent 0.34375rem, #000 calc(0.34375rem + 0.03125rem));
}
.controlDot {
position: absolute;
top: 0.3125rem;
right: 0.3125rem;
}
@@ -1,6 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {UserAreaPopout} from '@app/features/app/components/floating/UserAreaPopout';
import styles from '@app/features/app/components/layout/UserArea.module.css';
import {
@@ -18,9 +17,8 @@ import {getPushToTalkHoldLabel} from '@app/features/input/utils/PushToTalkHint';
import Presence from '@app/features/presence/state/Presence';
import {SettingsContextMenu} from '@app/features/ui/action_menu/SettingsContextMenu';
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {FocusRingWrapper} from '@app/features/ui/components/FocusRingWrapper';
import {NotificationDot} from '@app/features/ui/components/NotificationDot';
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {usePopout} from '@app/features/ui/hooks/usePopout';
@@ -28,8 +26,13 @@ import {TooltipWithKeybind} from '@app/features/ui/keybind_hint/KeybindHint';
import {Popout} from '@app/features/ui/popover/PopoverPopout';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import {USER_SETTINGS_LABEL_DESCRIPTOR} from '@app/features/user/components/settings_utils/SettingsConstants';
import {openUserSettingsModal} from '@app/features/user/commands/UserSettingsModalCommands';
import {
USER_SETTINGS_LABEL_DESCRIPTOR,
USER_SETTINGS_PRIVACY_REVIEW_NEEDED_DESCRIPTOR,
} from '@app/features/user/components/settings_utils/SettingsConstants';
import type {User} from '@app/features/user/models/User';
import UserSettings from '@app/features/user/state/UserSettings';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
import {VoiceConnectionStatus} from '@app/features/voice/components/VoiceConnectionStatus';
import {VoiceAudioSettingsMenu} from '@app/features/voice/components/VoiceSettingsMenus';
@@ -140,14 +143,10 @@ const UserAreaInner = observer(
/>
));
};
const handleSettingsClick = () => {
ModalCommands.push(
modal(
() => <UserSettingsModal data-flx="app.user-area.handle-settings-click.user-settings-modal" />,
'user-settings',
),
);
};
const privacySetupPending = UserSettings.isPrivacySetupPending();
const settingsLabel = privacySetupPending
? i18n._(USER_SETTINGS_PRIVACY_REVIEW_NEEDED_DESCRIPTOR)
: i18n._(USER_SETTINGS_LABEL_DESCRIPTOR);
const storeConnectedChannelId = MediaEngine.channelId;
const forceShowVoiceConnection = DeveloperOptions.forceShowVoiceConnection;
const hasVoiceConnection = !MobileLayout.enabled && (forceShowVoiceConnection || !!storeConnectedChannelId);
@@ -420,7 +419,7 @@ const UserAreaInner = observer(
<Tooltip
text={() => (
<TooltipWithKeybind
label={i18n._(USER_SETTINGS_LABEL_DESCRIPTOR)}
label={settingsLabel}
action="system_toggle_settings"
data-flx="app.user-area.user-area-inner.tooltip-with-keybind--3"
/>
@@ -431,9 +430,9 @@ const UserAreaInner = observer(
<button
ref={settingsButtonRef}
type="button"
aria-label={i18n._(USER_SETTINGS_LABEL_DESCRIPTOR)}
aria-label={settingsLabel}
className={clsx(styles.controlButton, settingsContextMenuOpen && styles.contextMenuHover)}
onClick={handleSettingsClick}
onClick={openUserSettingsModal}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -446,7 +445,16 @@ const UserAreaInner = observer(
}}
data-flx="app.user-area.user-area-inner.control-button.settings-click"
>
<GearIcon className={styles.controlIcon} data-flx="app.user-area.user-area-inner.control-icon--5" />
<GearIcon
className={clsx(styles.controlIcon, privacySetupPending && styles.controlIconWithDot)}
data-flx="app.user-area.user-area-inner.control-icon--5"
/>
{privacySetupPending && (
<NotificationDot
className={styles.controlDot}
data-flx="app.user-area.user-area-inner.privacy-review-dot"
/>
)}
</button>
</FocusRing>
</Tooltip>
@@ -27,12 +27,11 @@ import {
shouldShowPremiumFeatures,
} from '@app/features/premium/utils/PremiumUtils';
import StreamerMode from '@app/features/streamer_mode/state/StreamerMode';
import WindowsFont, {WINDOWS_FONT_CHANGED_AT_MS} from '@app/features/theme/state/WindowsFont';
import Nagbar from '@app/features/ui/state/Nagbar';
import {hasUnavailableElectronNativeContext, isDesktop} from '@app/features/ui/utils/NativeUtils';
import {hasUnavailableElectronNativeContext, isDesktop, isNativeWindows} from '@app/features/ui/utils/NativeUtils';
import {isStandalonePwa} from '@app/features/ui/utils/PwaUtils';
import {PRIVACY_SETUP_VERSION} from '@app/features/user/constants/PrivacySetupConstants';
import StatusPage from '@app/features/user/state/StatusPage';
import UserSettings from '@app/features/user/state/UserSettings';
import Users from '@app/features/user/state/Users';
import MediaEngine, {useVoiceEngineV2Model} from '@app/features/voice/engine/MediaEngineFacade';
import {selectVoiceEngineV2AppConnectionWithFallback} from '@app/features/voice/engine/v2/VoiceEngineV2AppSelectors';
@@ -260,6 +259,12 @@ export const useNagbarConditions = (): NagbarConditions => {
})();
const canShowSoftwareEncoder = SoftwareEncoderWarning.showWarning;
const canShowStreamerMode = StreamerMode.shouldShowNagbar;
const canShowWindowsFont =
isNativeWindows() &&
user != null &&
user.createdAt.getTime() < WINDOWS_FONT_CHANGED_AT_MS &&
!WindowsFont.useFluxerSans &&
!WindowsFont.nagbarDismissed;
const canShowDomainMoved = nagbarState.forceHideDomainMoved
? false
: nagbarState.forceDomainMoved
@@ -287,13 +292,6 @@ export const useNagbarConditions = (): NagbarConditions => {
(!user.privacyAgreedAt || user.privacyAgreedAt.toISOString() < PRIVACY_POLICY_LAST_UPDATED);
return termsOutdated || privacyOutdated;
})();
const needsPrivacySetup = (() => {
if (nagbarState.forceHidePrivacySetup) return false;
if (nagbarState.forcePrivacySetup) return true;
if (!user || !UserSettings.isHydrated()) return false;
const privacySetupVersion = UserSettings.getPrivacySetupVersion();
return privacySetupVersion !== null && privacySetupVersion < PRIVACY_SETUP_VERSION;
})();
return {
canShowBuildEnvironment,
canShowConnection,
@@ -334,10 +332,10 @@ export const useNagbarConditions = (): NagbarConditions => {
canShowVisionaryMfa,
canShowVoiceSessionRestore,
needsTermsAcceptance,
needsPrivacySetup,
canShowSoftwareEncoder,
canShowStreamerMode,
canShowDomainMoved,
canShowWindowsFont,
};
};
export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarState> => {
@@ -367,12 +365,6 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
visible: conditions.needsTermsAcceptance,
dismissible: false,
},
{
type: NagbarType.PRIVACY_SETUP,
priority: -2.75,
visible: conditions.needsPrivacySetup,
dismissible: false,
},
{
type: NagbarType.SCHEDULED_MAINTENANCE,
priority: -1,
@@ -481,6 +473,12 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
visible: conditions.canShowDomainMoved,
dismissible: true,
},
{
type: NagbarType.WINDOWS_FONT,
priority: 4.5,
visible: conditions.canShowWindowsFont,
dismissible: true,
},
];
return selectVisibleNagbars(nagbars);
}, [conditions]);

Some files were not shown because too many files have changed in this diff Show More