A CLAUDE.md is just a markdown file at the root of your repo. Copy the content below into your own project's CLAUDE.md to give your agent the same context.
npx versuz@latest install ajaxy-telegram-tt --kind=claude-mdcurl -o CLAUDE.md https://raw.githubusercontent.com/Ajaxy/telegram-tt/HEAD/CLAUDE.md# 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";
<div className={buildClassName(styles.myWrapper, "legacy-class")} />
```
- 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 <filename>`.
# 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<boolean>()` 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<OwnProps & StateProps> = ({ … }) => { … }
// 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 `<Icon>` 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<HTMLDivElement>();
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 (
<div ref={ref} className={styles.root + (className ? ` ${className}` : '')}>
<button onClick={handleClick}>{lang('ButtonKey')}</button>
<p>{stateValue}</p>
</div>
);
}
export default memo(withGlobal<OwnProps>((global, { id }): Complete<StateProps> => {
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<void>`.
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<StateProps>` 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<OwnProps>((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: <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