543 lines
18 KiB
Markdown
543 lines
18 KiB
Markdown
# 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
|