montana/Russian/Site/messenger/CLAUDE.md
2026-05-18 18:05:32 +03:00

543 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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