420 lines
13 KiB
Markdown
420 lines
13 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.
|
|
|
|
- **Code Style:**
|
|
- Early returns.
|
|
- Prefix boolean variables with primary or modal auxiliaries (e.q. `isOpen`, `willUpdate`, `shouldRender`).
|
|
- Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`).
|
|
- Prefer checking required parameter before calling a function, avoid making it optinal and checking at the beginning of the function.
|
|
- Only leave comments for complex logic.
|
|
- 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-bold);
|
|
|
|
// ❌ 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. Critique it—identify any shortcomings.
|
|
2. Fix those issues, do more planning.
|
|
3. Present the improved result.
|
|
|
|
- **When deeper debugging is needed:**
|
|
1. Outline clear, step-by-step debugging instructions for the operator.
|
|
2. Remove any temporary debug code once the issue is resolved.
|
|
|
|
- **Lint errors you can't fix manually:**
|
|
Suggest running `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.
|
|
|
|
## 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.
|
|
* Only import from `'react'` when you need React **types** that are not provided in Teact.
|
|
* 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 function types *last* in your props definitions.
|
|
|
|
### 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.
|
|
* Check the `hooks/` folders for additional utilities.
|
|
|
|
### 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.
|
|
* 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.
|
|
|
|
---
|
|
|
|
### Example
|
|
|
|
```ts
|
|
import React, { useFlag } from '../../lib/teact/teact';
|
|
|
|
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.value);
|
|
close();
|
|
onClick?.();
|
|
someAction(el.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 }): 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
|
|
|
|
* **Use** `withGlobal` (a `mapStateToProps` helper) to pull in state.
|
|
* **Avoid** the experimental `useSelector` API.
|
|
* **Use** `getGlobal` **only** inside hooks 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, **pass IDs as props** and do the heavy work in a `useMemo` hook.
|
|
|
|
### 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 });
|
|
|
|
// 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**
|
|
|
|
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.
|