Skip to content

Latest commit

 

History

History
541 lines (415 loc) · 18.2 KB

File metadata and controls

541 lines (415 loc) · 18.2 KB

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:
      /* Component.module.scss */
      .myWrapper { /**/ }
      /* Component.tsx */
      import styles from "./Component.module.scss";
      <div className={buildClassName(styles.myWrapper, "legacy-class")} />
    • Use 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.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.
      // ❌ 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:
      // ✅ 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.
      // ✅ 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:
   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:
    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:

    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

// 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 function types 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.

// 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

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

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.
  • Fallback file: src/assets/localization/fallback.strings.

2. Getting Strings

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

  1. Make sure key does not exist already.
  2. Search Translation Platform for similar strings to get the correct wording.
  3. Add it to fallback.strings.
  4. If it's plural, include _one and _other.
  5. 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.

    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

// ✅ 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.

// 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

// 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 timinguseEffect vs useLayoutEffect
  6. Prefer signals – When you need effects only, not renders
  7. Use requestForcedReflow – Only as last resort for sync measure+mutate