Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/actions/websocket/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import DatabaseManager from '@database/manager';
import {getPostById} from '@queries/servers/post';
import {deletePreferences, differsFromLocalNameFormat, getHasCRTChanged} from '@queries/servers/preference';
import EphemeralStore from '@store/ephemeral_store';
import {logDebug} from '@utils/log';

export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
if (EphemeralStore.isEnablingCRT()) {
Expand All @@ -18,6 +19,7 @@ export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSo
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const preference: PreferenceType = JSON.parse(msg.data.preference);
logDebug('[WS] PREFERENCE_CHANGED', preference.category, preference.name);
handleSavePostAdded(serverUrl, [preference]);
Comment on lines 20 to 23
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These logDebug statements log preference values received over WebSocket. Preference values can be large (e.g., theme JSON) and may contain sensitive/user-specific data; logging them can bloat logs and impact performance in production. Consider logging only category/name (and maybe value length), or gating value logging behind a debug/dev flag.

Copilot uses AI. Check for mistakes.

const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, [preference]);
Expand Down Expand Up @@ -48,6 +50,7 @@ export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebS
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
logDebug('[WS] PREFERENCES_CHANGED', preferences.map((p) => `${p.category}/${p.name}`).join(', '));
handleSavePostAdded(serverUrl, preferences);

const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, preferences);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,28 @@ describe('AttachmentQuickAction', () => {
});
});

it('should pass showAttachLogs to openAttachmentOptions', async () => {
const {getByTestId} = renderWithIntlAndTheme(
<AttachmentQuickAction
{...baseProps}
showAttachLogs={true}
/>,
);

const button = getByTestId('test-attachment');
fireEvent.press(button);

await waitFor(() => {
expect(mockOpenAttachmentOptions).toHaveBeenCalledWith(
expect.any(Object), // intl
expect.any(Object), // theme
expect.objectContaining({
showAttachLogs: true,
}),
);
});
});

it('should handle default fileCount when not provided', async () => {
const propsWithoutFileCount = {
...baseProps,
Expand Down
12 changes: 4 additions & 8 deletions app/components/post_draft/quick_actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {map} from 'rxjs/operators';
import {Preferences} from '@constants';
import {withServerUrl} from '@context/server';
import {observeIsBoREnabled, observeIsPostPriorityEnabled} from '@queries/servers/post';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observePreferenceAsBool} from '@queries/servers/preference';
import {observeCanUploadFiles} from '@queries/servers/security';
import {observeConfigBooleanValue, observeMaxFileCount} from '@queries/servers/system';

Expand All @@ -26,20 +26,16 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhancedProps) => {
const canUploadFiles = observeCanUploadFiles(database);
const maxFileCount = observeMaxFileCount(database);
const allowDownloadLogs = observeConfigBooleanValue(database, 'AllowDownloadLogs', true);
const attachLogsPref = queryPreferencesByCategoryAndName(
database,
Preferences.CATEGORIES.ADVANCED_SETTINGS,
Preferences.ATTACH_APP_LOGS,
).observe();
const attachLogsEnabled = observePreferenceAsBool(database, Preferences.CATEGORIES.ADVANCED_SETTINGS, Preferences.ATTACH_APP_LOGS);

Comment on lines 26 to 30
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attachLogsPref now holds an Observable<boolean> (via observePreferenceAsBool), not a preference model/array. Renaming it to something like attachLogsEnabled (or similar) would avoid confusion when reading the combineLatest logic.

Copilot uses AI. Check for mistakes.
return {
canUploadFiles,
isAgentsEnabled: observeIsAgentsEnabled(serverUrl),
isPostPriorityEnabled: observeIsPostPriorityEnabled(database),
isBoREnabled: observeIsBoREnabled(database),
maxFileCount,
showAttachLogs: combineLatest([allowDownloadLogs, attachLogsPref]).pipe(
map(([allowed, prefs]) => allowed && prefs?.[0]?.value === 'true'),
showAttachLogs: combineLatest([allowDownloadLogs, attachLogsEnabled]).pipe(
map(([allowed, enabled]) => allowed && enabled),
),
};
});
Expand Down
11 changes: 11 additions & 0 deletions app/queries/servers/preference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See LICENSE.txt for license information.

import {Database, Model, Q} from '@nozbe/watermelondb';
import {of as of$, type Observable} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

import {Preferences} from '@constants';
import {MM_TABLES} from '@constants/database';
Expand Down Expand Up @@ -118,3 +120,12 @@ export const queryEmojiPreferences = (database: Database, name: string) => {
export const queryAdvanceSettingsPreferences = (database: Database, name?: string, value?: string) => {
return queryPreferencesByCategoryAndName(database, ADVANCED_SETTINGS, name, value);
};

export const observePreferenceAsBool = (database: Database, category: string, name: string, defaultValue = false): Observable<boolean> => {
return queryPreferencesByCategoryAndName(database, category, name).
observe().
pipe(
switchMap((prefs) => (prefs.length ? prefs[0].observe() : of$(undefined))),
map((pref) => (pref ? pref.value === 'true' : defaultValue)),
);
Comment on lines +124 to +130
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

observePreferenceAsBool currently switches from query.observe() to prefs[0].observe(), which will emit on any record field change (not just value) and adds extra complexity. Since WatermelonDB supports observeWithColumns, consider using queryPreferencesByCategoryAndName(...).observeWithColumns(['value']) and mapping to the boolean, plus distinctUntilChanged(), to (a) still react to value changes and (b) avoid unnecessary emissions/re-renders when unrelated fields update.

Copilot uses AI. Check for mistakes.
};
35 changes: 35 additions & 0 deletions app/screens/report_a_problem/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE.txt for license information.

import {Database} from '@nozbe/watermelondb';
import {act} from '@testing-library/react-native';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wait for observable propagation before asserting the updated value.

The assertion on Line 207 can race async emission from the DB observable chain and intermittently flake.

💡 Suggested fix
-import {act} from '@testing-library/react-native';
+import {act, waitFor} from '@testing-library/react-native';
@@
-        // * Should react to the value change
-        expect(getByTestId('attachLogsEnabled')).toHaveTextContent('false');
+        // * Should react to the value change
+        await waitFor(() => {
+            expect(getByTestId('attachLogsEnabled')).toHaveTextContent('false');
+        });

Also applies to: 206-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/screens/report_a_problem/index.test.tsx` at line 5, The test is asserting
a value too soon and can race the DB observable; replace the immediate expect
with an async wait: use await act(async () => { /* trigger the change */ }) if
you need to flush effects, or better import and use waitFor from
`@testing-library/react-native` and change the assertion to await waitFor(() =>
expect(<selector-or-getBy...>()).toEqual/ToBe(...)); target the test that
triggers the DB observable update and update the assertion in index.test.tsx so
it waits for the propagated value rather than asserting synchronously.

import React, {type ComponentProps} from 'react';
import {View, Text} from 'react-native';

Expand Down Expand Up @@ -171,4 +172,38 @@ describe('screens/report_a_problem/index', () => {
expect(getByTestId('attachLogsEnabled')).toHaveTextContent('false');
expect(getByTestId('currentUserId')).toHaveTextContent('user2');
});

it('should react to attachLogsEnabled preference value changes', async () => {
await operator.handlePreferences({
preferences: [{
user_id: 'user1',
category: 'advanced_settings',
name: 'attach_app_logs',
value: 'true',
}],
prepareRecordsOnly: false,
});

const Component = enhanced;
const {getByTestId} = renderWithEverything(<Component componentId={'ReportProblem'}/>, {database});

// * Initially true
expect(getByTestId('attachLogsEnabled')).toHaveTextContent('true');

// # Update the existing preference value from 'true' to 'false'
await act(async () => {
await operator.handlePreferences({
preferences: [{
user_id: 'user1',
category: 'advanced_settings',
name: 'attach_app_logs',
value: 'false',
}],
prepareRecordsOnly: false,
});
});

// * Should react to the value change
expect(getByTestId('attachLogsEnabled')).toHaveTextContent('false');
});
});
9 changes: 3 additions & 6 deletions app/screens/report_a_problem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import {withObservables} from '@nozbe/watermelondb/react';
import {switchMap} from '@nozbe/watermelondb/utils/rx';
import {of as of$} from 'rxjs';
import {map} from 'rxjs/operators';
import {switchMap} from 'rxjs/operators';

import {Preferences} from '@constants';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observePreferenceAsBool} from '@queries/servers/preference';
import {observeConfigBooleanValue, observeConfigValue, observeCurrentUserId, observeLicense, observeReportAProblemMetadata} from '@queries/servers/system';

import ReportProblem from './report_problem';
Expand All @@ -23,9 +22,7 @@ const enhanced = withObservables([], ({database}) => {
isLicensed: observeLicense(database).pipe(switchMap((license) => (license ? of$(license.IsLicensed) : of$(false)))),
metadata: observeReportAProblemMetadata(database),
currentUserId: observeCurrentUserId(database),
attachLogsEnabled: queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.ADVANCED_SETTINGS, Preferences.ATTACH_APP_LOGS).
observe().
pipe(map((prefs) => prefs?.[0]?.value === 'true')),
attachLogsEnabled: observePreferenceAsBool(database, Preferences.CATEGORIES.ADVANCED_SETTINGS, Preferences.ATTACH_APP_LOGS),
};
});

Expand Down
6 changes: 4 additions & 2 deletions app/screens/report_a_problem/report_problem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {getCommonStyleSheet} from './styles';

import type {AvailableScreens} from '@typings/screens/navigation';
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
export const REPORT_PROBLEM_CLOSE_BUTTON_ID = 'close-report-problem';

type Props = {
componentId: AvailableScreens;
Expand Down Expand Up @@ -150,7 +149,10 @@ const ReportProblem = ({
});

return (
<View style={styles.container}>
<View
style={styles.container}
testID='report_problem.screen'
>
<View style={styles.body}>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.detailsSection}>
Expand Down
2 changes: 2 additions & 0 deletions detox/e2e/support/ui/screen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import PostOptionsScreen from './post_options';
import PushNotificationSettingsScreen from './push_notification_settings';
import ReactionsScreen from './reactions';
import RecentMentionsScreen from './recent_mentions';
import ReportProblemScreen from './report_problem';
import SavedMessagesScreen from './saved_messages';
import ScheduleMessageScreen from './scheduled_message_screen';
import SearchMessagesScreen from './search_messages';
Expand Down Expand Up @@ -92,6 +93,7 @@ export {
PushNotificationSettingsScreen,
ReactionsScreen,
RecentMentionsScreen,
ReportProblemScreen,
SavedMessagesScreen,
SearchMessagesScreen,
SelectTimezoneScreen,
Expand Down
51 changes: 51 additions & 0 deletions detox/e2e/support/ui/screen/report_problem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {SettingsScreen} from '@support/ui/screen';
import {timeouts} from '@support/utils';

class ReportProblemScreen {
testID = {
reportProblemScreen: 'report_problem.screen',
backButton: 'screen.back.button',
enableLogAttachmentsToggleOff: 'report_problem.enable_log_attachments.toggled.false.button',
enableLogAttachmentsToggleOn: 'report_problem.enable_log_attachments.toggled.true.button',
};

reportProblemScreen = element(by.id(this.testID.reportProblemScreen));
backButton = element(by.id(this.testID.backButton));
enableLogAttachmentsToggleOff = element(by.id(this.testID.enableLogAttachmentsToggleOff));
enableLogAttachmentsToggleOn = element(by.id(this.testID.enableLogAttachmentsToggleOn));

toBeVisible = async () => {
await waitFor(this.reportProblemScreen).toExist().withTimeout(timeouts.TEN_SEC);

Comment on lines +20 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a visibility matcher in toBeVisible() for stronger E2E reliability.

toExist() may pass before the screen is actually visible to users; toBeVisible() is safer and matches the method intent.

💡 Suggested fix
     toBeVisible = async () => {
-        await waitFor(this.reportProblemScreen).toExist().withTimeout(timeouts.TEN_SEC);
+        await waitFor(this.reportProblemScreen).toBeVisible().withTimeout(timeouts.TEN_SEC);
 
         return this.reportProblemScreen;
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toBeVisible = async () => {
await waitFor(this.reportProblemScreen).toExist().withTimeout(timeouts.TEN_SEC);
toBeVisible = async () => {
await waitFor(this.reportProblemScreen).toBeVisible().withTimeout(timeouts.TEN_SEC);
return this.reportProblemScreen;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@detox/e2e/support/ui/screen/report_problem.ts` around lines 20 - 22, The
toBeVisible method currently waits for this.reportProblemScreen toExist(), which
can pass before the UI is actually visible; change the matcher to
waitFor(this.reportProblemScreen).toBeVisible().withTimeout(timeouts.TEN_SEC)
inside the toBeVisible function so the test asserts actual visibility (refer to
toBeVisible, this.reportProblemScreen, and timeouts.TEN_SEC).

return this.reportProblemScreen;
};

open = async () => {
await SettingsScreen.reportProblemOption.tap();

return this.toBeVisible();
};

back = async () => {
await this.backButton.tap();
await waitFor(this.reportProblemScreen).not.toBeVisible().withTimeout(timeouts.TEN_SEC);
};

enableAttachLogs = async () => {
await waitFor(this.enableLogAttachmentsToggleOff).toExist().withTimeout(timeouts.FOUR_SEC);
await this.enableLogAttachmentsToggleOff.tap();
await waitFor(this.enableLogAttachmentsToggleOn).toExist().withTimeout(timeouts.FOUR_SEC);
};

disableAttachLogs = async () => {
await waitFor(this.enableLogAttachmentsToggleOn).toExist().withTimeout(timeouts.FOUR_SEC);
await this.enableLogAttachmentsToggleOn.tap();
await waitFor(this.enableLogAttachmentsToggleOff).toExist().withTimeout(timeouts.FOUR_SEC);
};
}

const reportProblemScreen = new ReportProblemScreen();
export default reportProblemScreen;
Loading
Loading