# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. # Instructions You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep experience in our project's simplified React-like API. You are working on a modern web app for Telegram. - **Be concise.** Only change code directly related to the current task; leave unrelated parts untouched. - **Reuse** existing types, functions and components. Search before creating a new one. - **No new libraries.** Use existing dependencies only. If a task truly can't be done without a new library, stop and explain why. - **Do not** write tests. - **SCSS modules:** - Name classes in camelCase. - Import as `styles` in your component: ```scss /* Component.module.scss */ .myWrapper { /*…*/ } ``` ```tsx /* Component.tsx */ import styles from "./Component.module.scss";
``` - Use [buildClassName.ts](mdc:src/util/buildClassName.ts) to merge multiple class names. - **Always extract styles to files** - avoid inline styles unless absolutely necessary. - **If file already imports styles**, check where they come from and add new styles there - don't create new style files. - Prefer rem units for all measurements. Exceptions are possible, but usually rare. - No complex or broad selectors. Prefer basic classes. - **Code Style:** - Early returns. - Prefix boolean variables with primary or modal auxiliaries (e.g. `isOpen`, `willUpdate`, `shouldRender`). - Functions should start with a verb (e.g. `openModal`, `closeDialog`, `handleClick`). - Prefer checking required parameter before calling a function, avoid making it optional and checking at the beginning of the function. - Only leave comments for complex logic. - Avoid using default values for props that can be intentionally undefined/false. - No unnecessary `as` casts. Prefer `satisfies` where possible. - Do not use `null`. There's linter rule to enforce it. - **IMPORTANT: Avoid conditional spread operators** - TypeScript doesn't check if spread fields match the target type. ```typescript // ❌ BAD - No type checking { ...condition && { field: value } } // ✅ GOOD - Full type checking { field: condition ? value : undefined } ``` - **IMPORTANT: Use string templates for inline styles** - Always use template literals for style prop. Teact does not support object: ```typescript // ✅ CORRECT style={`transform: translateX(${value}%)`} // ❌ WRONG style={{ transform: `translateX(${value}%)` }} style={{ '--custom-prop': value } as React.CSSProperties} ``` - **IMPORTANT: Font weights in CSS** - Always use existing CSS variables for font-weight. Never use numeric values or custom values. ```scss // ✅ CORRECT font-weight: var(--font-weight-medium); font-weight: var(--font-weight-semibold); // ❌ WRONG font-weight: 600; font-weight: bold; ``` - **Localization & Text Rules:** - **ALWAYS** use `lang()` for all text content - never hardcode strings. - `lang()` can accept parameters: `lang('Key', { param: value })`. - Add new translations to `src/assets/localization/fallback.strings`. - **After your solution:** 1. Think like on a code review and identify any shortcomings. 2. Fix those issues. Repeat review-fix cycle until you are sure about code quality. 3. Present the improved result. - **When deeper debugging is needed:** 1. Outline clear, step-by-step debugging instructions in your output. 2. Remove any temporary debug code once the issue is resolved. - **Linter commands** After finishing your changes, run `npm run check:ts` if you touched TypeScript files and/or `npm run check:css` for SCSS. If linter reports incorrect import order, try fixing it using command. If it fails, make ONE try to fix it manually and leave it as is. - **Lint errors you can't fix manually:** Suggest running `npx eslint --fix `. # Telegram Web API Guide ## 1. API Definition - The master file is `src/lib/gramjs/tl/static/api.tl` (TL syntax). - **Don't edit** this autogenerated file. TypeScript types live in `api.d.ts`. - We use GramJS inside a web worker; UI code uses plain objects (`Api*` types) in `src/api/types`. ## 2. Generating Code 1. Make sure to include the method name in `api.json`. 2. Run: ```bash npm run gramjs:tl ``` to regenerate `api.d.ts`. 3. In `src/api/gramjs/methods/`, pick a file for your method, then: * Name fetchers `fetch*` if the TL method starts with `get`. * Use a destructured parameter object. * Call the API via: ```ts const result = await invokeRequest( new GramJs.namespace.MethodName({ /* params */ }) ); ``` * If `result` is `undefined`, return `undefined` to signal an error. * Convert any returned GramJS classes into plain `Api*` objects. Convesion from and to Api* objects is done by `apiBuilders` (function name starts with `buildApi*`) and `gramjsBuilders` (function name `buildInput*`). ## 3. Using the API * In your actions, call: ```ts const result = await callApi('methodName', { /* params */ }); ``` * Always check for `undefined` before proceeding. * **IMPORTANT: Do not pass `accessHash` directly to API methods.** Methods that accept separate `id` and `accessHash` parameters are outdated. Instead, pass the full `ApiPeer`, `ApiChat`, or `ApiUser` object. The `buildInput*` functions in `gramjsBuilders` will extract the necessary fields. ## 4. Example ```ts // src/api/gramjs/methods/users.ts export async function fetchUsers({ users }: { users: ApiUser[] }) { const result = await invokeRequest(new GramJs.users.GetUsers({ id: users.map(({ id, accessHash }) => buildInputUser(id, accessHash)), })); if (!result || !result.length) { return undefined; } const apiUsers = result.map(buildApiUser).filter(Boolean); const userStatusesById = buildApiUserStatuses(result); return { users: apiUsers, userStatusesById, }; } // src/global/actions/api/users.ts addActionHandler('loadUser', async (global, actions, { userId }) => { const user = selectUser(global, userId); if (!user) return; const res = await callApi('fetchUsers', { users: [user] }); if (!res) return; // update global state... }); ``` ## 5. Handling Updates * Updates come in via `mtpUpdateHandler.ts`. * They're routed through `src/global/actions/apiUpdaters` to merge into global state. * Types are defined in `src/api/types/updates.ts`. ## Component Style Guide ### 1. Basics & Imports * All components use JSX and render with Teact. * Do not import "react". React types are available globally in React namespace (e.g. React.MouseEvent). * Built-in hooks live in Teact library. Import them from there. ### 2. Props & Types * Split your props into two types: * **OwnProps**: data passed in by the parent * **StateProps**: data injected by `withGlobal` HOC * Merge them as `OwnProps & StateProps` when defining your component. * You can skip one or both if they are not used. * **Order rule**: list any handlers or functions *last* in your props definitions. * Do not pass unmemoized objects as props into memo() components. ### 3. Hooks * **useLastCallback** is your go-to for callbacks, since it won't trigger re-renders and always uses the latest scope. * Only use **useCallback** when you really need to memoize a render function. * Prefer **useFlag()** over `useState()` for simple boolean toggles. `useState` is preferred when component just calls `setState(someVariable)`. * Check the `hooks/` folders for additional utilities. * Avoid adding new `useEffect` where possible. ### 4. Component Signature > **Migrate** any old `FC` syntax to the new form. ```ts // Before const OldComp: FC = ({ … }) => { … } // After const NewComp = (props: OwnProps & StateProps) => { … } ``` ### 5. Memoization * Wrap most components with `memo()` to avoid unnecessary updates. Consider skipping memo for simple wrapper components whose children change on almost every render. * Don't pass freshly created objects or arrays as props to memoized components. * **Exceptions** (no memo): `ListItem`, `Button`, `MenuItem`, etc. ### 6. Localization * Call `const lang = useLang()` at the top of your component. * Look up the localization guide for how to add new language keys. ### 7. Icons * Use `` component for icons. Available icons are listed in `src/types/icons/index.ts` --- ### Example ```ts import { memo, useState, useRef } from '../../lib/teact/teact'; import { withGlobal, getActions } from '../../global'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import styles from './Component.module.scss'; type OwnProps = { id: string; className?: string; onClick?: NoneToVoidFunction; }; type StateProps = { stateValue?: string; }; // Constants first const MAX_ITEMS = 10 const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => { const { someAction } = getActions(); // Should always be first, if actions are used const ref = useRef(); const [color, setColor] = useState('#FF00FF'); const [isOpen, open, close] = useFlag(); const lang = useLang(); // Somewhere near the top, after state definition const handleClick = useLastCallback(() => { if (!ref.current) return; const el = ref.current; setColor(el.dataset.value); close(); onClick?.(); someAction(el.dataset.value); }); return (

{stateValue}

); } export default memo(withGlobal((global, { id }): Complete => { const stateValue = selectValue(global, id); return { stateValue, }; })(Component) ) ``` ## Global State Overview Global State is our single, app-wide store, similar to Redux or Zustand. All its code lives under `src/global/`, with subfolders grouping related functionality (for example, `selectors/users.ts` holds all user-related selectors). ### 1. Folder Structure * **`actions/`**: Actions that are used to update global from any point in the app * **`selectors/`**: Pure functions that read data (e.g. `selectors/users.ts`). * **`reducers/`**: Functions that update global state. * **`types/`**: All TypeScript types live in `src/global/types`. * **`cache.ts`**: Manages saving a slimmed-down copy of global to IndexedDB. ### 2. Actions 1. **Preffered** way to update global. When inside action, use `setGlobal`, or simple `return` if sync. 2. **Sync actions** return type should be `ActionReturnType`. 3. **Async actions** return type should be `Promise`. 4. If you add or remove an action, update `actions.ts` accordingly. 5. Actions in `ui` folder should be only sync. ### 3. Multi-Tab Support * Actions and selectors can accept a `tabId` parameter, so we don't lose tab context when working with multiple tabs. * **`tabId` is required** if calling an action or selector that can accept it. * **Exception**: UI components may call without `tabId` (they receive it automatically). ### 4. Selectors & Reducers * If logic takes more than one line, create a new selector or reducer in the appropriate folder and file. * **Selectors must be pure**: only use their inputs and global. Don't allocate new objects or arrays, as that breaks memoization. ### 5. Data Constraints * Global may only store serializable primitives (strings, numbers, booleans). * When you change a type that's cached in `cache.ts`, add a migration to avoid errors from new selectors. --- ## Component Guidelines ### 1. Accessing Global in Components * Prefer existing `withGlobal` (a `mapStateToProps` helper) to pull in state. * There is an experimental `useSelector` API available. If your value can be retrieved using simple selector and `withGlobal` is not present, use it. * **Use** `getGlobal` **only** inside callbacks for one-off reads (it's non-reactive). ### 2. Performance * Wrap `withGlobal` in `memo` so the component re-renders only on real data changes. * **Don't** return new arrays or objects inside `withGlobal`; that defeats memoization. * If you need to filter or map a list, use `useShallowSelector` to retrieve reactive array and perform computation in `useMemo`. * Force `Complete` return type for `withGlobal` parameter, as it ensures that all defined properties are passed. ### 3. Example Component ```ts type OwnProps = { id: string }; type StateProps = { someValue?: string; otherValue?: number; thirdValue: boolean; }; const Component = ({ id, someValue, otherValue, thirdValue, }: OwnProps & StateProps) => { // component logic... }; export default memo( withGlobal((global, { id }) => { const { otherValue } = selectTabState(global); const someValue = selectSomeValue(global, id); const thirdValue = Boolean(global.rawValue); return { someValue, otherValue, thirdValue, }; })(Component); ); ``` # Localization Guide **1. Setup & Fallback** * Translations live on [Translation Platform](https://translations.telegram.org/). * Fallback file: `src/assets/localization/fallback.strings`. **2. Getting Strings** ```ts const lang = useLang(); // Simple lang('SimpleKey'); // Plurals lang('PluralKey', undefined, { pluralValue: 3 }); lang('PluralKey', { count: 3 }, { pluralValue: 3 }); // if key has variables // String replacements lang('ReplKey', { name: 'Amy' }); // JSX nodes (e.g. links) lang('LinkKey', { link: }, { withNodes: true }); // Markdown lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true }); ``` **3. Adding a New Key** 0. Make sure key does not exist already. 1. Search Translation Platform for similar strings to get the correct wording. 2. Add it to `fallback.strings`. 3. If it's plural, include `_one` and `_other`. 4. Run `npm run lang:ts`. **4. Naming Rules** * **PascalCase** (no dots). * Use short, clear prefixes for context (e.g. `Acc` for accessibility). * Keep names under ~30 chars, shorten consistently if needed. **5. API & Options** * **Basic**: `lang(key, vars?, options?) → string` * **Advanced** (`withNodes`): returns `TeactNode[]` so you can inject JSX. * **Other options**: * `withMarkdown` (for simple markdown + emojis) * `renderTextFilters` (custom filters) * `specialReplacement` (for replacing substrings, e.g. icons) * **Object syntax**: Simple form that returns string can be used in some actions. ```ts actions.showNotification({ key: 'LangKey' }); lang.with({ key: 'hello', vars: { name }, options: { withNodes: true } }); ``` **6. Handy Extensions** * `lang.region(code)` → country name * `lang.conjunction(['a','b','c'])` → "a, b, and c" * `lang.disjunction(['x','y'])` → "x or y" * `lang.number(1234)` → locale-formatted number * Flags: `lang.isRtl`, `lang.code`, `lang.rawCode` **7. Beyond React** Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax. # ⚠️ IMPORTANT: Fasterdom & Rendering Phases ## Rendering Cycle ``` --- frame start --- 1. effects 2. requested measures (DOM reads) 3. render JSX → DOM 4. layout effects 5. requested mutations (DOM writes) 6. forced reflow measure (avoid!) 7. forced reflow mutate (avoid!) --- frame end --- ``` ## Phase Rules | Hook/Context | Can READ (measure) | Can WRITE (mutate) | |--------------|-------------------|-------------------| | `useLayoutEffect` | ❌ NO | ✅ YES | | `useLayout` (deprecated) | ✅ YES | ❌ NO | | Event handlers (default) | ✅ YES | ❌ NO (use `requestMutation`) | | `requestMeasure` callback | ✅ YES | ❌ NO | | `requestMutation` callback | ❌ NO | ✅ YES | ## Usage Patterns ```typescript // ✅ CORRECT: Read in measure phase, write in mutation phase requestMeasure(() => { const width = element.offsetWidth; // READ requestMutation(() => { element.style.width = `${width * 2}px`; // WRITE }); }); // ❌ WRONG: Alternating reads/writes causes layout thrashing const width = element.offsetWidth; // READ → reflow element.style.width = `${width * 2}px`; // WRITE → reflow const height = element.offsetHeight; // READ → reflow again! ``` ## Signals: State Without Re-renders Signals deliver updates **without causing component renders**. Use for frequently-updated values. ```typescript // Create signal const [getValue, setValue] = createSignal(initialValue); // Get value getValue(); // Set value (notifies subscribers, NO re-render) setValue(newValue); // Subscribe to changes getValue.subscribe(() => { /* react to change */ }); ``` **Signal Hooks:** - `useSignal()` – Create signal tied to component - `useDerivedSignal()` – Derive new signal from other signals/variables - `useDerivedState()` – Convert signal to render variable (triggers re-render) - `useStateRef()` – Access current value without it being a dependency **When to use signals:** - Typing text, caret position - Animation state tracking - Values that change frequently but don't need re-render - Cross-component communication without prop drilling ## Key Optimization Hooks | Hook | Purpose | |------|---------| | `useLastCallback` | Stable callback reference, always latest scope | | `useStateRef` | Access state without triggering effects | | `useLayoutEffectWithPrevDeps` | Synchronous effect with previous values | | `useSyncEffect` | Effect that runs during render (not RAF) | | `useResizeObserver` | Efficient element size observation | | `useIntersectionObserver` | Viewport visibility tracking | ## Heavy Animation Handling ```typescript // Mark animation start (pauses non-critical updates) const endAnimation = beginHeavyAnimation(duration); // Run code only when fully idle (no animations + browser idle) onFullyIdle(() => { // Safe for heavy computations }); ``` ## Performance Checklist 1. **Animations first** – Evaluate if code negatively impacts animations 2. **Simplify algorithms** – Move complex ones to `onFullyIdle` 3. **No loops in selectors** – Avoid iterations in `withGlobal` selectors 4. **Minimize re-renders** – Especially in `Message`, `Chat`, `Sticker`, etc. 5. **Understand effect timing** – `useEffect` vs `useLayoutEffect` 6. **Prefer signals** – When you need effects only, not renders 7. **Use `requestForcedReflow`** – Only as last resort for sync measure+mutate