* ci(e2e): per-job TSIO report groups and commit statuses E2E jobs previously shared a single `e2e/mobile` commit status backed by one TSIO report group, so concurrent platform legs raced to finalize the group and clobbered each other's results. Give each Detox/Maestro job its own report group (`mobile-<flow>-<job>`, total_reports_expected=1) and its own commit status context (`detox-ios`, `detox-android`, `detox-ipad`, `maestro-ios`, `maestro-android`); CMT reports per shard plus an umbrella context. Channel notifications move to a single rollup step so N platform legs no longer spam the webhook, and the five-server topology is dispatched with explicit per-platform inputs that still resolve to the existing SITE_*_URL contract consumed by the test config. Also adds the webhook sidecar launcher, Android emulator CI prep, and TSIO screenshot path linking that keeps folder basenames matching Jest full names. The new detox type-check job surfaced pre-existing broken import paths in maestro/fixtures/calls_seed.ts, which pointed at detox/detox/e2e; corrected so the guard passes on its own first run. Note: the CMT matrix now reads per-platform server URLs (`ios_site_1_url`, `android_site_1_url`, `site_3_url`) instead of a single `url`, which needs the matching Matterwick dispatch change to land alongside this. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): aggregate CMT channel rollup from TSIO and wire notify secrets The CMT umbrella posted a placeholder summary built from CI job outcomes (passed:0, failed:0|1, reports:[]), so the release channel message carried no per-leg detail. It now runs the same tsio-channel-notify-rollup.js path as PR/main: cmt-identity emits a per-shard TSIO config map and the rollup polls every shard group, so counts and per-leg rows come from TSIO. The rollup job also needs id-token: write to mint its OIDC token. Webhook secrets used by the per-job TSIO finalize steps were referenced but never declared in the templates' workflow_call.secrets, so GitHub resolved them to empty strings and channel notify silently no-opped. Declare them in the iOS/Android/Maestro templates and forward them from e2e-detox.yml and the CMT callers. The Maestro PR path worked only because its callers use secrets: inherit. Also from review: - split-tests.js emitted two matrix jobs at parallelism=1 when mm_blocks specs were present; merge into one shard instead of exceeding the requested count. - Move the Android ARTIFACT_RUN_ID emptiness check ahead of download-artifact, which consumes it and otherwise fails with an opaque API error. - resolve-tsio-job-config.sh now reuses webhookBucketForReportName instead of re-deriving the bucket prefix with a looser startsWith check. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): add local preflight for CI PRs The webhook-secret and OIDC-permission gaps in this branch were invisible locally and in a green CI run — the affected steps exit 0 and just skip the notify or commit status. Add a script engineers can run before opening a PR that touches .github/ or detox/utils: cd detox && npm run preflight:ci # :ci-fast skips lint + tsc It runs actionlint (which flags secrets not declared in a callee's workflow_call.secrets), scans for exit-code capture broken by a trailing backslash, asserts every job running a TSIO OIDC script declares id-token: write, then runs the detox/utils tests and npm run check. actionlint and the CodeRabbit CLI are optional and skipped with an install hint when absent. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): use mktemp for preflight logs Fixed /tmp log paths can be pre-created as symlinks, so the preflight script would truncate whatever they point at (CWE-377). Create both logs with mktemp and drop them on exit. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): address review feedback on TSIO statuses and sidecar A cancelled E2E run reached no conclusion, so posting `success` on its commit statuses let a PR merge on a check that never ran. Cancelled runs now post `pending`; `success` is reserved for the one deliberate bypass, the E2E/Override label. Commit-status contexts move into the `e2e-test/*` namespace to match the mattermost monorepo, with the prefix owned by build-tsio-job-config.js and mirrored in the cancel action's context list. Legacy unprefixed and `e2e/*` contexts are dropped rather than carried forward. The `preflight:ci` script and its OIDC-permission checker duplicated what actionlint, the unit tests and `npm run check` already do, so they are removed and detox/CLAUDE.md documents the underlying failure modes and the commands to catch them instead. cloudflared and the TSIO CLI are pinned to known-good releases rather than `latest`, and detox/README.md now documents the webhook sidecar: when a tunnel is needed, the WEBHOOK_PUBLIC_BASE_URL / CLOUDFLARED_TUNNEL_TOKEN precedence, and why quick tunnels report callbacks as unreachable. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): detect upstream cancellation and surface status reset failures generate-report runs with always(), so job.status stays "success" when an upstream job is cancelled and TSIO was published as a real failure. Read the generate-specs/e2e-* needs results so a cancelled run is reported as such. In cancel-e2e-runs, only reset the E2E commit statuses for runs that were actually stopped — a run we failed to cancel keeps publishing its own results. Aggregate createCommitStatus failures across every context and fail the action afterwards instead of silently warning, so a stale check cannot go unnoticed. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
25 KiB
CLAUDE.md
Project Overview
React Native 0.83.9 with New Architecture enabled (RCT_NEW_ARCH_ENABLED=1).
Important Development Notes
- Always run the checks before committing code
- Always test any changes using the mobile mcp before calling something complete or committing code
- Never commit any planning markdown files
Development Workflow
Implementing Multi-Phase Features
When implementing complex features with multiple phases:
- Use subagents for each phase: Launch a subagent (Task tool with
subagent_type: general-purpose) to implement each phase independently - Test after each phase: Use a subagent to test the feature with mobile MCP after implementation
- Commit after each phase: Create a commit for each completed and tested phase
- Never commit planning files: Keep
.mdplanning files (likePLAN.md) out of commits
Testing with Mobile MCP
When testing features that require mobile device interaction:
- Use subagents: Launch a subagent to handle mobile MCP testing to avoid excessive context usage
- The subagent can launch the app, navigate, take screenshots, and verify functionality
- Don't just verify the app launches - actually trigger and interact with the feature being tested (e.g., send messages, click buttons, verify state changes)
Key Commands
iOS Simulator Setup: For detailed iOS simulator setup instructions, troubleshooting CocoaPods issues, and build debugging, see CLAUDE-IOS-SIMULATOR.md.
# Setup
npm run pod-install # iOS CocoaPods
npm run ios-gems # Ruby gems for iOS
# Development
npm start # Start Metro bundler
npm run ios # Run iOS in simulator
npm run android # Run Android in emulator
# TypeScript type checking
npm run tsc
# Fix linting issues
npm run fix
# E2E tests are in separate detox/ package
npm run e2e:ios # cd detox && npm run e2e:ios-test
npm run e2e:android # cd detox && npm run e2e:android-build && npm run e2e:android-test
# Builds delegate to Fastlane via shell scripts
npm run build:ios # ./scripts/build.sh ipa
npm run build:android # ./scripts/build.sh apk
# Utilities
npm run clean # Clean build artifacts
Architecture
Dual Database System (WatermelonDB)
Critical: Two separate SQLite databases via WatermelonDB:
- App Database: One global database for app state, server list (
app/database/models/app/) - Server Databases: One database per connected server for channels, users, posts (
app/database/models/server/)
DatabaseManager singleton (app/database/manager/index.ts) manages all instances. Data operations go through operators with transformers/handlers/comparators.
Sync handlers must handle the full lifecycle: create, update, AND delete. When a handler syncs data from the server (e.g., handleAIBots, handlePlaybookRuns), it must also remove records that no longer exist on the server. Compare the full set of existing DB records against incoming data and call prepareDestroyPermanently() on stale records. Never write a sync handler that only creates/updates.
Important: Some server APIs do not return deleted records in their response. When the API only returns active records, the handler can diff against existing DB records to detect removals (as shown above). However, if the API returns a partial set of records (e.g., paginated without a deletion flag), you may need to request that the API be augmented to include a delete_at field or a separate deletions endpoint so that the handler can correctly identify which records to remove.
Schema documentation: When adding or modifying database tables, update docs/database/server/server.md (or app_database/app-database.md) and bump the schema version number in the header.
Database directory:
- iOS: App Group directory (shared with extensions)
- Android:
${documentDirectory}/databases/
Network Layer
- NetworkManager (
app/managers/network_manager.ts) creates one Client instance per server - Uses
@mattermost/react-native-network-client - Product-specific API calls should live in
client/rest.tswithin the product folder using the ClientMix pattern (seeapp/products/calls/client/rest.tsorapp/products/agents/client/rest.tsfor examples). Don't callclient.doFetch()directly from action files.
Actions Pattern
- Local Actions (
app/actions/local/): Database-only operations - Remote Actions (
app/actions/remote/): Fetch from API → use operators to persist → return{error}on failure - Avoid redundant fetches: Before fetching user profiles, check the DB first. Use
fetchMissingProfilesByIds()from@actions/remote/userinstead of rawclient.getProfilesByIds()— it queries the DB and only fetches missing profiles
Query Layer
Query layer (app/queries/) provides both:
query*: Returns WatermelonDB Queryobserve*: Returns RxJS Observableget*: Returns Promise (async)prepare*: Returns prepared records for batch operations
State Management
Hybrid approach:
- WatermelonDB: Primary data store (persisted)
- Ephemeral Stores (
app/store/): In-memory transient UI state
Navigation
- Uses
expo-routerfor file-based routing - Routes are defined in
app/routes/directory following expo-router conventions:(authenticated)/_layout.tsx: Authenticated routes stack(modals)/: Modal presentations(bottom_sheet)/: Bottom sheet presentations
- Navigation uses
router.push(),router.replace(),router.back()fromexpo-router - Deep linking handled through expo-router's built-in URL routing
Products Architecture
Modular features in app/products/ with their own database models:
agents/: AI agentscalls/: Voice/video callingplaybooks/: Playbooks with dedicated DB models
Agents Streaming Architecture
WebSocket Events:
- Agents use
custom_mattermost-ai_postupdateWebSocket events for streaming (start, message updates, reasoning, tool calls, annotations, end) - NO specific WebSocket event for tool call status changes after approval/rejection
- Updates arrive via standard
POST_EDITEDevent after backend executes tools
Critical Pattern:
- Components must clear local streaming state on
ENDEDevent to switch from ephemeral streaming data to persisted database data - Otherwise stale streaming state prevents POST_EDITED updates from displaying
Custom Native Modules
Located at libraries/@mattermost/:
@mattermost/rnutils- Native utilities (orientation, notifications)@mattermost/rnshare- Share extension integration@mattermost/hardware-keyboard- External keyboard detection@mattermost/secure-pdf-viewer- Secure PDF viewing
Native Bridge patterns (when modifying native modules):
- iOS:
RCT_EXPORT_MODULE(),RCT_EXPORT_METHOD()in Objective-C++ - Android: Extend
ReactContextBaseJavaModule, use@ReactMethod
TypeScript Path Aliases
@actions/* → app/actions/*
@agents/* → app/products/agents/*
@calls/* → app/products/calls/*
@database/* → app/database/*
@queries/* → app/queries/*
@store → app/store/index
@share/* → share_extension/*
@typings/* → types/*
// ... (see tsconfig.json for complete list)
Share Extension
Separate bundle at share_extension/ for Android system share. Shares code with main app but runs independently.
- iOS has its own native implementation for the Share Extension with Swift and SwiftUI
Important: Share extension on Android uses React Navigation (not expo-router like main app).
Testing
Testing guide: See docs/testing_guide.md for how to add and structure unit tests.
Test Organization
- Jest coverage excludes
/components/and/screens/directories - Mock database manager at
app/database/manager/__mocks__/index.ts - E2E tests in separate
detox/package (not in main package.json) - Add mocks to central
setup.tsfile, not individual test files
E2E Status Reporting (TSIO)
E2E results are uploaded to Test System IO (TSIO) and surfaced as GitHub commit statuses. Each platform job owns its own report group (total_reports_expected=1) and commit-status context. Contexts are namespaced e2e-test/*, matching the mattermost monorepo:
| Flow | Context(s) | TSIO group name(s) |
|---|---|---|
| PR E2E | e2e-test/detox-ios, e2e-test/detox-android, e2e-test/detox-ipad, e2e-test/maestro-ios, e2e-test/maestro-android |
mobile-pr-<job> (e.g. mobile-pr-detox-ios) |
Main E2E (run_type=MAIN or Matterwick MASTER) |
same per-job contexts | mobile-main-<job> |
| Compatibility matrix | per shard e2e-test/<tsio-shard-name> + umbrella e2e-test/compatibility-matrix-testing |
mobile-release-<tsio-shard-name> |
Testing Patterns
Database Testing
- Tests use real in-memory databases (LokiJS), not mocks
- Critical: Set
extraLokiOptions: {autosave: false}to prevent memory leaks from lingering timeouts - Always clean up:
await DatabaseManager.destroyServerDatabase(serverUrl)inafterEach - Initialize database in
beforeEach:await DatabaseManager.init([serverUrl])
Ephemeral Store Testing
- Use
jest.resetModules()between tests to clear singleton state - Require fresh imports after reset:
const Store = require('./store').default; - Pattern from
app/store/ephemeral_store.test.ts
WebSocket Testing
- Mock connection objects, not the WebSocket class itself
- Create mock with methods:
onOpen,onClose,onError,onMessage,send,readyState - Use
client.open()in tests to triggeronOpencallback - Pattern from
app/client/websocket/index.test.ts
Timer Testing
- Always use:
jest.useFakeTimers({doNotFake: ['nextTick']})to let promises resolve - Helper:
advanceTimers(ms)fromtest/timer_helpers.tsadvances time AND waits for promises - Never fake
nextTickor async operations will hang
Component Rendering
- Three helpers in
test/intl-test-helper.tsx:renderWithIntl: Basic internationalizationrenderWithIntlAndTheme: + Theme contextrenderWithEverything: + Database + Server URL
- Use
renderWithEverythingwhen components need database access - Wrap async state updates in
act()when testing React components
Remote Action Testing
- Mock
NetworkManager.getClientinbeforeAllto return mock client - Mock client should implement all methods used in tests
- Test pattern: Setup DB state → Call action → Assert no error and data returned
- Pattern from
app/actions/remote/channel.test.ts
DeviceEventEmitter Testing
- Use
DeviceEventEmitter.addListener()to spy on events - Remove listener in cleanup:
listener.remove() - Pattern: Store listener callback in jest.fn(), assert it was called with expected data
Test Utilities
TestHelper singleton at test/test_helper.ts:
generateId(): Deterministic IDs for testssetupServerDatabase(): Complete DB setup with basic entitiesfakeUser(),fakeChannel(), etc.: Entity generators with sensible defaultsfakeUserModel(),fakeChannelModel(): WatermelonDB model creators
Writing Quality Tests
Test names:
- Use
it('should...')format, nottest('happy path') - Check array lengths in addition to individual items to ensure no extra elements
- Test expectations must match actual implementation behavior - if code deletes state immediately, test for
undefined, not for modified state values
Mocking:
- Use
jest.mocked()for full implementations - Use
(thing as jest.Mock)type assertion when mocking partial implementations that don't satisfy the full interface (e.g.,NetworkManager.getClientreturning minimal mock client) - Avoid
anytypes in tests - usetypeof import('./module').defaultfor proper typing of dynamically imported modules
Don't test:
- JavaScript operators (
===,JSON.stringify()) or library behavior (React, RxJS) - "Mocks calling mocks" - thin wrapper functions that just pass data through
- Multiple input variations when behavior is identical (testing 0, 1, 5 items when function doesn't special-case counts)
- Duplicate assertions - if test A verifies X, don't add test B that only verifies X
- AI-generated tests are heavily scrutinized and often rejected if they don't demonstrate real intended behavior
Do test:
- Business logic, error handling, integration points, side effects
- One example per code path is sufficient
Development Practices
TypeScript
- Trust well-formed data - don't add defensive null checks if types guarantee existence
- Use
??(nullish coalescing) instead of||for fallbacks when appropriate - Avoid non-null assertions (
!) - use optional chaining:metadataRes.metadata?.followers?.length ?? 0 - Type function parameters using
ComponentProps<typeof Component>instead ofas constworkarounds - JSDoc comments must match actual parameter names and types
- Prefer const objects over enums for better tree-shaking:
export const Status = { Pending: 0, Done: 1 } as const;with companion typeexport type Status = typeof Status[keyof typeof Status];
React Hooks
react-hooks/exhaustive-depsis enforced as error - incomplete or incorrect dependency arrays will fail CI. Fix deps or use the dedicated hooks below when the omission is intentional.- Always explain why dependencies are omitted from exhaustive-deps with specific comments (and prefer the dedicated hooks below for common cases).
- Run-on-mount effects: Use
useDidMountfrom@hooks/did_mountinstead ofuseEffect(callback, [])with an eslint-disable. The hook encapsulates the "run only on mount" intent and the single exhaustive-deps exception. - Initial value only (no updates): Use
useInitialValuefrom@hooks/initial_valueinstead ofuseMemo(factory, [])with an eslint-disable when the value must be computed once and never recomputed. - Nested property as dependency: When the meaningful dependency is a nested property (e.g.
currentUser?.notifyProps) because the parent object reference may not change when that property changes, depend on that property in the array and add an eslint-disable with a comment explaining why. SeeuseNotificationPropsanduseUserTimezonePropsinapp/hooks/for the pattern. useDidUpdate: The hook takes a dependency list from the caller; the implementation uses an eslint-disable with the comment that dependencies are provided by the caller.- Don't over-optimize with
useMemo- only use for expensive calculations or objects passed as props to children - Use
useCallbackfor render functions passed to components, not for functions called within render - Move module-level constants outside components
- Replace state variables with derived values when possible
- Stable references (refs, dispatch functions) don't need to be in dependency arrays but must have eslint-disable comments
- React-Native-Reanimated shared values (
.value) don't cause re-renders and don't need to be dependencies - Critical: When modifying existing
useEffecthooks, you MUST include ALL dependencies used inside the effect, even if the original code had an incomplete dependency array - ESLint'sreact-hooks/exhaustive-depsrule is enforced strictly - Memoize callbacks for list items: When rendering lists with
.map(), avoid inline arrow functions likeonPress={() => handler(item.id)}. Instead, have the child component accept the ID and call the callback internally, allowing the parent to pass a single memoized callback reference.
React Native & UI
- Don't create hooks inside render functions - extract as local components
- Never use
TouchableOpacity— usePressablewith pressed feedback instead - Prefer
Buttoncomponents overPressablewhen appropriate - Non-memoized inline styles add render stress - define in stylesheet instead
StyleSheet.createis unnecessary when usingmakeStyleSheetFromTheme- just return the plain object- Place
getStyleSheetat file top (after imports, before interfaces/components) not at the bottom - Use
Platform.select()for platform-specific values instead of ternaries - StyleProps support nested lists - no need for custom
concatStyles() - Use
withTiming()consistently for both states in animations - Test components with long strings to ensure proper text handling
- Use constants from
PREFERENCES.THEMESinstead of hardcoded colors - Use
react-native-reanimatedinstead of React Native'sAnimatedAPI for all animations (better performance, runs on UI thread) Pressablemust have pressed feedback: Always add visual feedback via the style callback, e.g.,style={({pressed}) => [pressed && {opacity: 0.72}]}. APressablewithout pressed state feels broken.- Extract static objects used as props into module-level constants: Objects like
hitSlop,contentContainerStyle, or any non-dynamic prop object should be aconstoutside the component to avoid creating new references on every render. - Use
usePreventDoubleTaphook for button press handlers to prevent accidental double submissions - Use
useServerUrl()hook instead of passingserverUrlas a prop - it's available via context - Use existing components: Check for
<Loading>instead of<ActivityIndicator>,safeParseJSON()instead of try/catch JSON.parse - Parent checks before mounting: If a child component would return null for empty data, have the parent conditionally render instead (e.g.,
{items.length > 0 && <ItemList items={items} />}) - Use
@utils/urlutilities:tryOpenURL()instead ofLinking.openURL(),getUrlDomain()withurlParseinstead ofnew URL() - Use
FormattedTextinstead of<Text>{intl.formatMessage(...)}</Text>for static i18n strings. Only fall back toText+intl.formatMessagewhen the text includes dynamic non-translatable content (e.g., a user's display name)
Code Quality & Linting
Import Organization:
- Consolidate all imports from the same module into a single statement
- Use inline
typekeyword for type imports:import {SomeValue, type SomeType} from '@module' - Example:
import {StreamingEvents, type StreamingState} from '@agents/types'
Unused Code:
- Remove unused imports, parameters, interfaces, and variables completely
- ESLint enforces strict no-unused-vars - code with unused declarations will fail CI
- When a parameter is truly unused, remove it from the function signature
Pre-commit:
- Run
npm run fixto auto-fix linting issues before committing - Pre-commit hook runs ESLint + TypeScript checking automatically
Error Handling & Logging
- Use
logError()instead ofconsole.error()orconsole.log() - Use
logDebug()for debug-level information - Don't ignore potential errors silently - handle them or add intentional comments
- Log on early returns in handlers: When a handler returns early (e.g., empty input), add a
logDebugcall so the no-op is traceable - Don't log sensitive information
- Logs are user-exportable and sent to Sentry — never log PII. All
console.*/logError/logWarning/logInfo/logDebugoutput is captured to rolling log files (TurboLoggercaptureConsole) that users can export via "Report a Problem", and the same args are forwarded as Sentry breadcrumbs. Treat both as destinations that may contain PII.- Don't log display names, usernames, emails, message/draft content, push/VoIP tokens, auth tokens/cookies, or raw request/response payloads and event objects.
- Do log stable identifiers instead (user/channel/team IDs, channel
type, counts, booleans likehasFileId). Channel display names are PII — DM/GM names embed usernames. - Wrap errors with
getFullErrorMessage(error)rather than passing the raw error/event object (which can embed URLs, payloads, or PII).
- Add function/class prefix to logs: e.g.,
logError('error on functionName', getFullErrorMessage(error))orlogError('error on ClassName.methodName', error)to make debugging easier
State Management
- Always handle errors from database operations
- Consider race conditions in async functions within effects
- Local state initialized with prop values won't update when props change - sync with
useEffectif needed
Performance
- Create parsers/expensive objects only once, not on every render (use refs or useMemo)
- Memoize AST output to avoid regenerating on every render
- Use named constants instead of magic numbers and check if one already exists
- Never use raw
fontSize,fontWeight, orfontFamilyin styles — always usetypography()from@utils/typography(e.g.,...typography('Body', 200, 'SemiBold')) - Don't set style properties to their default values (e.g.,
marginBottom: 0) — it's dead code
Localization (i18n)
- CRITICAL: Only update
en.json- never modify other language files or Weblate gets corrupted - Adding new strings: Define the message ID and defaultMessage in code using
defineMessages(), then runnpm run i18n-extractto automatically add them toen.json - Default messages in code must match JSON translations exactly, including newlines
- Translation IDs should be descriptive enough for translators to understand context
- Don't reuse translation IDs
- Translate user-facing strings, not debug/error messages
Markdown Component Usage
Required props:
baseTextStyle,value,theme,location- Use
Screens.CHANNELor appropriate constant from@constantsfor thelocationprop textStyles,blockStyles,enableLatex, and similar props are auto-generated via HOC
Important Limitations:
- Cannot embed custom React components inline: The Markdown component renders CommonMark to native components. You cannot insert custom React components (like badges, icons) inline within the markdown text flow
- Link handling: Links in markdown go through
openLink()→ deep link system →tryOpenURL(). Custom URL schemes (e.g.,citation:,action:) will show error alerts unless you implement a full deep link handler inapp/utils/deep_link/ - Post-processing not possible: Unlike web, you cannot post-process the rendered markdown JSX tree to replace markers with React components
Adding New Post Types
When adding custom post types (e.g., for new products):
- Add the type string to
PostTypeunion intypes/api/posts.d.ts - Define constants in your product's constants file (e.g.,
app/products/agents/constants.ts) - Use the globally-available
Posttype (defined intypes/api/posts.d.ts) - no import needed
Platform-Specific
- iOS: Opt out of iOS 18+ features (liquid glass) until UI is properly addressed
- Android: Test on multiple API levels (34, 35, 36) to verify behavior with edge-to-edge changes
- Use
Setinstead ofArrayfor exception lists (faster, ensures uniqueness)
testID Convention
Components use hierarchical testIDs: component.subcomponent.element
- Example:
channel_list.category.CATEGORY.channel_item.ID.display_name - Example:
channel.post_draft.send_action.send.button
Common Mistakes to Avoid
JavaScript Compatibility
- Don't use
Object.hasOwn()- React Native doesn't support ES2022+ features - ❌
Object.hasOwn(obj, 'key') - ✅
'key' in obj
Anti-Patterns
- Conditionals in tests for TypeScript satisfaction
- Creating new arrays/objects on every render when passed as props
- Over-engineering - follow YAGNI principle
- Impossible states with boolean + optional parameters that depend on each other
- Redundant checks - if
array.length >= 3, indices 0,1,2 are guaranteed to exist - Verbose error messages that reveal internal system details
- Mass assignment risks - whitelist fields instead of sending
Partial<Model>objects - Duplicate conditional branches that return identical results - consolidate into a single condition (e.g.,
if (A && B)andif (A && !B)both returning same thing should be justif (A))
Security
- Question whether showing secrets with "eye" toggle is a security concern
- Don't commit files that likely contain secrets (.env, credentials.json)
- Consider pre-populating sensitive fields with
*********instead of actual values
Configuration
- Patches: Applied via
patch-packageon postinstall (patches/directory) - Self-compiled apps: Require your own Mattermost Push Notification Service
- Self-signed certificates: Not supported
Development Notes
Hot Reload & Build Times
- Hot reload: ~3 seconds for JS/TS changes; does NOT work for native code changes
- Native rebuilds: 10-30 minutes (avoid unless necessary)
- Pre-commit hook: Runs ESLint + incremental TypeScript checking
Known Issues
- Many components require
themeprop - check foruseTheme()hook in parent component