Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
lint:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read
pull-requests: read

jobs:
pr-title:
name: Validate PR Title
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/scheduled_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
# Monday at 9:00 UTC
- cron: '0 9 * * 1'

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
build:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/type.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
test:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
test:
runs-on: ubuntu-latest
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## [9.50.2](https://github.com/GetStream/stream-chat-js/compare/v9.50.1...v9.50.2) (2026-07-16)

### Bug Fixes

* **AttachmentManager:** add permission bypass for custom upload functions ([#1800](https://github.com/GetStream/stream-chat-js/issues/1800)) ([30e6bc4](https://github.com/GetStream/stream-chat-js/commit/30e6bc41f9d779da50078cf844883dc2713c346c))

## [9.50.1](https://github.com/GetStream/stream-chat-js/compare/v9.50.0...v9.50.1) (2026-07-09)

### Bug Fixes

* prevent reload if ThreadManager has never been activated ([#1798](https://github.com/GetStream/stream-chat-js/issues/1798)) ([affbb9c](https://github.com/GetStream/stream-chat-js/commit/affbb9cac73eea798084879523b9bad709ad4dc1))

## [9.50.0](https://github.com/GetStream/stream-chat-js/compare/v9.49.0...v9.50.0) (2026-07-03)

### Features
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ $ yarn run test-unit

We use [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. We enforce it during the build process. If your IDE has integration with these tools, it's recommended to set them up.

## JSDoc

See [`JSDOC.md`](./JSDOC.md) for the canonical JSDoc format used in this repository. The format is enforced by `eslint-plugin-jsdoc`.

## Commit message convention

Since we're autogenerating our [CHANGELOG](./CHANGELOG.md), we need to follow a specific commit message convention.
Expand Down
66 changes: 66 additions & 0 deletions JSDOC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# JSDoc style guide

This repository uses TSDoc-flavored JSDoc. The TypeScript signature is the source of truth for parameter and return **types** and for **optionality** — JSDoc only carries human-readable descriptions and the few semantic tags listed below.

The format is enforced by `eslint-plugin-jsdoc`. Run `yarn lint` to validate locally.

## Canonical block

````ts
/**
* One-sentence summary in sentence case, ending with a period.
*
* Optional longer prose paragraph. Wrap at ~100 columns.
*
* @param name - Description in sentence case, ending with a period.
* @param options - Send options.
* @param options.skip_enrich_url - Skip URL enrichment for this message.
* @returns Description of the return value.
* @throws When the channel is frozen.
* @deprecated Use {@link newName} instead.
* @example
* ```ts
* client.connectUser({ id: 'foo' }, token);
* ```
*/
````

## Rules

- **No `{Type}` annotations on `@param` / `@returns`.** TypeScript already provides them. Enforced by `jsdoc/no-types`.
- **No bracketed-optional syntax** (`@param [name]`). TypeScript marks optionality via `?` or default values. Enforced by `jsdoc/check-param-names`.
- **Use `@returns`, not `@return`.** Enforced by `jsdoc/check-tag-names`.
- **Drop legacy tags**: `@method`, `@memberof`, `@class`, `@type` — TypeScript provides these.
- **Allowed tags**: `@param`, `@returns`, `@throws`, `@example`, `@default`, `@deprecated`, `@see`, `@internal`, `@private`, `@experimental`, `@remarks`, `@template`, and the inline `{@link}`.
- **Hyphen before description**: `@param name - description`. Enforced by `jsdoc/require-hyphen-before-param-description`.
- **Destructured object params** use dot notation: `@param options.foo - ...`.
- **`@deprecated`** must point at the replacement: `@deprecated Use {@link newName} instead.`
- **Short single-line form** `/** Foo. */` is allowed only when there are no tags and the description fits on one line.
- **Field-level JSDoc** on interfaces/types may use the single-line form (mirrors `src/gen/models/index.ts`).

## Casing in prose

Apply consistently in JSDoc and `//` comments. Do **not** rewrite identifiers, string literals, or `@example` code blocks.

| Wrong | Right |
| ------------ | ------------ |
| `websocket` | `WebSocket` |
| `sdk` | `SDK` |
| `api` | `API` |
| `url` | `URL` |
| `json` | `JSON` |
| `http(s)` | `HTTP(S)` |
| `jwt` | `JWT` |
| `id` (prose) | `ID` |
| `javascript` | `JavaScript` |
| `typescript` | `TypeScript` |

## Reusing field descriptions

Hand-written types in `src/types.ts` and elsewhere often share field names with the OpenAPI-generated types in `src/gen/models/index.ts` (`cid`, `created_at`, `channel_id`, `team`, `duration`, etc.). When documenting such a field, reuse the wording from the generated model for consistency.

## When in doubt

- Cross-check that `@param` names match the actual parameter names.
- Add `@returns` iff the function returns something other than `void` / `Promise<void>`.
- Keep existing wording verbatim except to fix grammar, typos, casing, or factual mismatches with the signature.
39 changes: 37 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import jsdoc from 'eslint-plugin-jsdoc';

import importPlugin from 'eslint-plugin-import';

Expand All @@ -18,6 +20,7 @@ export default tseslint.config(
},
plugins: {
import: importPlugin,
'unused-imports': unusedImports,
},
settings: {
react: {
Expand Down Expand Up @@ -71,9 +74,18 @@ export default tseslint.config(
},
],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{ ignoreRestSiblings: false, caughtErrors: 'none' },
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: false,
caughtErrors: 'none',
},
],
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
Expand All @@ -83,6 +95,29 @@ export default tseslint.config(
'@typescript-eslint/no-require-imports': 'off', // TODO: remove this rule once all files are .mjs (and require is not used)
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
ignores: ['src/gen/**'],
files: ['src/**/*.{js,ts}'],
plugins: {
jsdoc,
},
rules: {
'jsdoc/no-types': 'error',
'jsdoc/check-param-names': ['error', { checkDestructured: false }],
'jsdoc/check-tag-names': [
'error',
{ definedTags: ['internal', 'experimental', 'remarks'] },
],
'jsdoc/require-param-description': 'warn',
'jsdoc/require-returns-description': 'warn',
'jsdoc/require-hyphen-before-param-description': ['warn', 'always'],
'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }],
'jsdoc/no-multi-asterisks': 'error',
'jsdoc/empty-tags': 'error',
'jsdoc/no-bad-blocks': 'error',
},
},
);
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"/src"
],
"dependencies": {
"@stream-io/logger": "^2.0.0",
"@types/jsonwebtoken": "^9.0.8",
"@types/ws": "^8.18.1",
"axios": "^1.16.1",
Expand All @@ -75,6 +76,8 @@
"esbuild": "^0.28.0",
"eslint": "^9.39.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^63.0.7",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.6.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.5",
Expand All @@ -90,7 +93,7 @@
"start": "concurrently 'tsc --watch' './scripts/bundle.mjs --watch'",
"types": "tsc --noEmit",
"lint": "yarn run prettier && yarn run eslint",
"lint-fix": "yarn run prettier-fix && yarn run eslint-fix",
"lint-fix": "yarn run eslint-fix; yarn run prettier-fix",
"prettier": "prettier '**/*.{json,js,mjs,ts,yml,md}' --check",
"prettier-fix": "yarn run prettier --write",
"eslint": "eslint --max-warnings 0",
Expand All @@ -104,7 +107,8 @@
"fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1",
"semantic-release": "semantic-release",
"postinstall": "node -e \"require('fs').existsSync('scripts/install-husky.mjs') && import('./scripts/install-husky.mjs')\"",
"prepare": "yarn run build"
"prepare": "yarn run build",
"generate-client": "./scripts/generate-client.sh"
},
"engines": {
"node": ">=18"
Expand Down
11 changes: 11 additions & 0 deletions scripts/generate-client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail

OUTPUT_DIR="../stream-chat-js/src/gen"
CHAT_DIR="../chat"

rm -rf $OUTPUT_DIR

( cd $CHAT_DIR ; make openapi ; make -C projects/chat-manager build; build/chat-manager openapi generate-client --language ts --spec releases/v2/chat-clientside-api.yaml --output $OUTPUT_DIR )

yarn lint-fix
120 changes: 120 additions & 0 deletions scripts/generate-filter-types.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { parseArgs, type ParseArgsOptionsConfig } from 'node:util';
import { parse } from 'yaml';

const options = {
spec: {
type: 'string',
short: 's',
},
out: {
type: 'string',
short: 'o',
},
} satisfies ParseArgsOptionsConfig;

type OpenAPISpecification = {
components: {
schemas: {
[key: string]: {
properties?: Partial<
Record<
'filter_conditions' | string,
Partial<
Record<
'x-stream-filter-fields' | string,
Record<
string,
{
operators: string[];
type: string;
}
>
>
>
>
>;
};
};
};
};

const { values } = parseArgs({
args: process.argv,
options,
allowPositionals: true,
tokens: false,
});

const specPath = values.spec;
const outputPath = values.out;

if (!specPath || !outputPath) {
console.error(
'Usage: node generate-filter-types.mts -s <path-to-openapi-specification> -o <output-file>',
);
process.exit(1);
}

const spec = parse(readFileSync(specPath, 'utf8')) as OpenAPISpecification;
const schemas = spec.components?.schemas;

if (!schemas) {
console.error('No components.schemas found in the specification');
process.exit(1);
}

const lines = [];

const typeMapping = {
string: 'string',
number: 'number',
boolean: 'boolean',
date: 'Date',
};

const snakeToCamelCase = (snakeCaseString: string) =>
snakeCaseString
.split('_')
.map((wordSegment) => wordSegment.slice(0, 1).toUpperCase() + wordSegment.slice(1))
.join('');

for (const [schemaName, schema] of Object.entries(schemas)) {
if (!schema.properties) {
console.log(schemaName, 'missing');
continue;
}

for (const [propertyName, propertyDef] of Object.entries(schema.properties)) {
if (!propertyDef?.['x-stream-filter-fields']) continue;

const filterFields = propertyDef['x-stream-filter-fields'];

let typeName = `${schemaName}${snakeToCamelCase(propertyName)}`;

const fieldEntries = Object.entries(filterFields).map(
([fieldName, fieldDefinition]) => {
// TODO: add support for such properties later on (custom/nested filters)
if (fieldDefinition.type === 'object' || fieldName.startsWith('_')) {
return '';
}

const operators = fieldDefinition.operators.length
? fieldDefinition.operators.map((operator) => `"${operator}"`).join(' | ')
: 'never';
return ` "${fieldName}": { type: ${typeMapping[fieldDefinition.type as keyof typeof typeMapping] ?? `"${fieldDefinition.type}"`}; operators: ${operators} };`;
},
);

lines.push(`export type ${typeName} = {`);
lines.push(...fieldEntries);
lines.push(`};\n`);
}
}

if (lines.length > 0) {
writeFileSync(outputPath, '\n' + lines.join('\n') + '\n');
console.log(`Appended ${lines.length} lines to ${outputPath}`);
} else {
console.log('No filter types found');
}
Loading