diff --git a/examples/tutorial/.env.example b/examples/tutorial/.env.example index 46895730ee..470fb5bd6c 100644 --- a/examples/tutorial/.env.example +++ b/examples/tutorial/.env.example @@ -1,5 +1,7 @@ -# Required: your Stream app's public key. -VITE_API_KEY=REPLACE_WITH_API_KEY +# Required: your Stream app's public key. This is the variable name that +# `getstream env --target vite` writes, so you can generate it instead of +# pasting it by hand. (VITE_API_KEY is still read as a fallback.) +VITE_STREAM_API_KEY=REPLACE_WITH_API_KEY # Optional. If unset, the app defaults to user_id "react-tutorial" and # derives user_name from it. You can also override either value per-run diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index 90854e0949..15af1b34ca 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -1,4 +1,96 @@ -This folder contains the source code for [Chat React tutorial](https://github.com/GetStream/getstream.io-tutorials/blob/main/chat/tutorials/react-tutorial.mdx). It contains multiple versions of apps representing the tutorial steps. +This folder contains the source code for the [Chat React tutorial](https://getstream.io/chat/sdk/react/tutorial/). It contains multiple versions of apps representing the tutorial steps. + +The tutorial source lives in the website repo at [`content/pages/chat_sdk_react_tutorial.mdx`](https://github.com/GetStream/getstream.io/blob/main/content/pages/chat_sdk_react_tutorial.mdx). (It used to live in `GetStream/getstream.io-tutorials`, which is now archived.) + +## Step folders + +Folder names match the tutorial's step numbers, so `4-channel-list` is the tutorial's "Step 4 - Add a channel list". The tutorial's Step 0 (environment) and Step 1 (project + credentials) have no runnable counterpart, so the folders start at 2. The two `optional-*` folders are the tutorial's optional recipes, which sit after the numbered path. + +| Folder | Tutorial section | +| --------------------------------- | ------------------------------------------------- | +| `2-client-setup` | Step 2 - Connect the client | +| `3-core-component-setup` | Step 3 - Get a working chat UI | +| `4-channel-list` | Step 4 - Add a channel list | +| `5-theming` | Step 5 - Theme it | +| `6-custom-ui-components` | Step 6 - Replace an SDK component | +| `7-emoji-picker` | Step 7 - Enable the emoji picker and autocomplete | +| `optional-custom-attachment-type` | Optional - add a custom attachment type | +| `optional-livestream` | Optional - a livestream-style chat app | + +If you change a step's code here, update the matching code block in the tutorial too, and vice versa. + +### `layout.css` is duplicated on purpose + +The tutorial has the reader create a single `src/layout.css` in Step 3 and +rewrite it in Step 5. Each step folder here carries its own copy so the folder is +a self-contained snapshot of the app at that step, which means there are only two +distinct versions of the file: + +| Version | In | +| -------- | -------------------------------------------------------------------------- | +| Step 3's | `3-core-component-setup`, `4-channel-list` | +| Step 5's | `5-theming`, `6-custom-ui-components`, `7-emoji-picker`, both `optional-*` | + +Every file in a group is byte-identical, so any drift shows up in a diff. If you +edit one, edit the whole group. + +Parts of each copy are inert inside the step browser. That is expected, and none +of it should be "cleaned up" here, because the file has to stay a faithful copy of +what the tutorial tells the reader to write: + +- The `custom-theme` tokens do nothing in `7-emoji-picker` and + `optional-livestream`, which don't pass `theme="custom-theme"` to ``. The + reader's single `layout.css` holds the tokens and leaves them unused for those + same two examples. +- The `.str-chat__channel-list` / `__channel` / `__thread` widths are overridden + by `.tutorial-browser__step-shell .str-chat__*` in `tutorial-main.css`, which + wins on specificity (0,2,0 against 0,1,0). The tutorial's widths assume the app + owns the whole page; here it is sized to fit a preview card. +- The `html` / `body` / `#root` rules are real, but `tutorial-main.css` declares + them too, so the chrome does not depend on a step's stylesheet. + +None of this costs bundle size: Vite collapses the identical copies, so the built +CSS contains one `width: 30%` and one `@layer stream`. + +### One deliberate deviation: unlayered theme tokens + +The tutorial puts the custom theme tokens in a CSS layer: + +```css +@layer stream, stream-overrides; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +@layer stream-overrides { + .custom-theme { + /* tokens */ + } +} +``` + +The themed steps here declare them unlayered instead: + +```css +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +.str-chat.custom-theme { + /* tokens */ +} +``` + +Why: the step browser renders every step in a single document, so all seven +stylesheets are live at once. Steps 3 and 4 import the SDK stylesheet +_unlayered_ (as the tutorial has them, since Step 5 is where you're taught to +move it into a layer), and unlayered CSS outranks every `@layer` regardless of +specificity. A layered override would silently do nothing. + +`.str-chat.custom-theme` (specificity 0,2,0) also beats the SDK's own +`.str-chat` (0,1,0) regardless of source order, and it only matches the steps +that actually pass `theme="custom-theme"`, so the themed steps can't leak into +the unthemed ones. + +**This deviation exists only to make the step browser work. In your own app, +follow the tutorial and keep the tokens in the layer.** The tutorial app is a Yarn workspace (`@stream-io/stream-chat-react-tutorial`) under the repo's monorepo, so it consumes the local `stream-chat-react` SDK through `workspace:^` and shares its dependencies with the root install. diff --git a/examples/tutorial/src/1-client-setup/index.html b/examples/tutorial/src/1-client-setup/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/1-client-setup/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/1-client-setup/main.tsx b/examples/tutorial/src/1-client-setup/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/1-client-setup/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/1-client-setup/App.tsx b/examples/tutorial/src/2-client-setup/App.tsx similarity index 100% rename from examples/tutorial/src/1-client-setup/App.tsx rename to examples/tutorial/src/2-client-setup/App.tsx diff --git a/examples/tutorial/src/1-client-setup/credentials.ts b/examples/tutorial/src/2-client-setup/credentials.ts similarity index 85% rename from examples/tutorial/src/1-client-setup/credentials.ts rename to examples/tutorial/src/2-client-setup/credentials.ts index 0236916296..77277926e1 100644 --- a/examples/tutorial/src/1-client-setup/credentials.ts +++ b/examples/tutorial/src/2-client-setup/credentials.ts @@ -8,7 +8,9 @@ // ?user_id=alice&user_name=Alice // + display name override // // Notes: -// - apiKey is the one thing you still need to set (via VITE_API_KEY). +// - apiKey is the one thing you still need to set. `getstream env --target vite` +// writes VITE_STREAM_API_KEY, which is what the tutorial tells you to run; +// VITE_API_KEY is still accepted for older local setups. // - The token endpoint and environment default to the values shared with // the other example apps in this repo; override with VITE_TOKEN_ENDPOINT // and VITE_TOKEN_ENVIRONMENT if you're pointing at a different Stream @@ -16,7 +18,7 @@ const searchParams = new URLSearchParams(window.location.search); -export const apiKey = import.meta.env.VITE_API_KEY; +export const apiKey = import.meta.env.VITE_STREAM_API_KEY || import.meta.env.VITE_API_KEY; export const userId = searchParams.get('user_id') || import.meta.env.VITE_USER_ID || 'react-tutorial'; diff --git a/examples/tutorial/src/2-core-component-setup/index.html b/examples/tutorial/src/2-core-component-setup/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/2-core-component-setup/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/2-core-component-setup/main.tsx b/examples/tutorial/src/2-core-component-setup/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/2-core-component-setup/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/3-channel-list/index.html b/examples/tutorial/src/3-channel-list/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/3-channel-list/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/3-channel-list/layout.css b/examples/tutorial/src/3-channel-list/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/3-channel-list/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/3-channel-list/main.tsx b/examples/tutorial/src/3-channel-list/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/3-channel-list/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/2-core-component-setup/App.tsx b/examples/tutorial/src/3-core-component-setup/App.tsx similarity index 95% rename from examples/tutorial/src/2-core-component-setup/App.tsx rename to examples/tutorial/src/3-core-component-setup/App.tsx index d6067dfb13..d7681f9890 100644 --- a/examples/tutorial/src/2-core-component-setup/App.tsx +++ b/examples/tutorial/src/3-core-component-setup/App.tsx @@ -14,7 +14,7 @@ import { import 'stream-chat-react/dist/css/index.css'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/2-core-component-setup/layout.css b/examples/tutorial/src/3-core-component-setup/layout.css similarity index 52% rename from examples/tutorial/src/2-core-component-setup/layout.css rename to examples/tutorial/src/3-core-component-setup/layout.css index c3cf99687a..5fa14209f5 100644 --- a/examples/tutorial/src/2-core-component-setup/layout.css +++ b/examples/tutorial/src/3-core-component-setup/layout.css @@ -1,21 +1,21 @@ html, body, #root { - height: 100%; + height: 100%; } body { - margin: 0; + margin: 0; } #root { - display: flex; + display: flex; } .str-chat__channel-list { - width: 30%; + width: 30%; } .str-chat__channel { - width: 100%; + width: 100%; } .str-chat__thread { - width: 45%; -} \ No newline at end of file + width: 45%; +} diff --git a/examples/tutorial/src/2-core-component-setup/stream-chat.d.ts b/examples/tutorial/src/3-core-component-setup/stream-chat.d.ts similarity index 100% rename from examples/tutorial/src/2-core-component-setup/stream-chat.d.ts rename to examples/tutorial/src/3-core-component-setup/stream-chat.d.ts diff --git a/examples/tutorial/src/4-channel-list/App.tsx b/examples/tutorial/src/4-channel-list/App.tsx new file mode 100644 index 0000000000..f86d16843f --- /dev/null +++ b/examples/tutorial/src/4-channel-list/App.tsx @@ -0,0 +1,57 @@ +import type { ChannelFilters, ChannelOptions, ChannelSort, User } from 'stream-chat'; +import { + Channel, + ChannelHeader, + ChannelList, + Chat, + MessageComposer, + MessageList, + Thread, + useCreateChatClient, + Window, +} from 'stream-chat-react'; + +import 'stream-chat-react/dist/css/index.css'; +import './layout.css'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; + +const user: User = { + id: userId, + name: userName, + image: `https://getstream.io/random_png/?name=${userName}`, +}; + +const sort: ChannelSort = { last_message_at: -1 }; +const filters: ChannelFilters = { + type: 'messaging', + members: { $in: [userId] }, +}; +const options: ChannelOptions = { + limit: 10, +}; + +const App = () => { + const client = useCreateChatClient({ + apiKey, + tokenOrProvider: tokenProvider, + userData: user, + }); + + if (!client) return
Setting up client & connection...
; + + return ( + + + + + + + + + + + + ); +}; + +export default App; diff --git a/examples/tutorial/src/4-channel-list/layout.css b/examples/tutorial/src/4-channel-list/layout.css new file mode 100644 index 0000000000..5fa14209f5 --- /dev/null +++ b/examples/tutorial/src/4-channel-list/layout.css @@ -0,0 +1,21 @@ +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/4-custom-ui-components/index.html b/examples/tutorial/src/4-custom-ui-components/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/4-custom-ui-components/layout.css b/examples/tutorial/src/4-custom-ui-components/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/4-custom-ui-components/main.tsx b/examples/tutorial/src/4-custom-ui-components/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/4-custom-ui-components/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/5-custom-attachment-type/index.html b/examples/tutorial/src/5-custom-attachment-type/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/5-custom-attachment-type/layout.css b/examples/tutorial/src/5-custom-attachment-type/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/5-custom-attachment-type/main.tsx b/examples/tutorial/src/5-custom-attachment-type/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/5-custom-attachment-type/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/3-channel-list/App.tsx b/examples/tutorial/src/5-theming/App.tsx similarity index 94% rename from examples/tutorial/src/3-channel-list/App.tsx rename to examples/tutorial/src/5-theming/App.tsx index 5d37369fdf..68fc6190b1 100644 --- a/examples/tutorial/src/3-channel-list/App.tsx +++ b/examples/tutorial/src/5-theming/App.tsx @@ -12,7 +12,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/5-theming/layout.css b/examples/tutorial/src/5-theming/layout.css new file mode 100644 index 0000000000..cacf7577dc --- /dev/null +++ b/examples/tutorial/src/5-theming/layout.css @@ -0,0 +1,60 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. + In your own app, follow the tutorial and keep these in the layer. */ +.str-chat.custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/4-custom-ui-components/App.tsx b/examples/tutorial/src/6-custom-ui-components/App.tsx similarity index 97% rename from examples/tutorial/src/4-custom-ui-components/App.tsx rename to examples/tutorial/src/6-custom-ui-components/App.tsx index 60566f2c27..064857d61b 100644 --- a/examples/tutorial/src/4-custom-ui-components/App.tsx +++ b/examples/tutorial/src/6-custom-ui-components/App.tsx @@ -17,7 +17,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, @@ -148,7 +148,7 @@ const App = () => { diff --git a/examples/tutorial/src/6-custom-ui-components/layout.css b/examples/tutorial/src/6-custom-ui-components/layout.css new file mode 100644 index 0000000000..cacf7577dc --- /dev/null +++ b/examples/tutorial/src/6-custom-ui-components/layout.css @@ -0,0 +1,60 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. + In your own app, follow the tutorial and keep these in the layer. */ +.str-chat.custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/6-emoji-picker/index.html b/examples/tutorial/src/6-emoji-picker/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/6-emoji-picker/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/6-emoji-picker/layout.css b/examples/tutorial/src/6-emoji-picker/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/6-emoji-picker/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/6-emoji-picker/main.tsx b/examples/tutorial/src/6-emoji-picker/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/6-emoji-picker/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/6-emoji-picker/App.tsx b/examples/tutorial/src/7-emoji-picker/App.tsx similarity index 96% rename from examples/tutorial/src/6-emoji-picker/App.tsx rename to examples/tutorial/src/7-emoji-picker/App.tsx index 5f339501be..aeb7c27003 100644 --- a/examples/tutorial/src/6-emoji-picker/App.tsx +++ b/examples/tutorial/src/7-emoji-picker/App.tsx @@ -18,7 +18,7 @@ import { init, SearchIndex } from 'emoji-mart'; import data from '@emoji-mart/data'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/7-emoji-picker/layout.css b/examples/tutorial/src/7-emoji-picker/layout.css new file mode 100644 index 0000000000..cacf7577dc --- /dev/null +++ b/examples/tutorial/src/7-emoji-picker/layout.css @@ -0,0 +1,60 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. + In your own app, follow the tutorial and keep these in the layer. */ +.str-chat.custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/7-livestream/index.html b/examples/tutorial/src/7-livestream/index.html deleted file mode 100644 index 7877092389..0000000000 --- a/examples/tutorial/src/7-livestream/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/tutorial/src/7-livestream/layout.css b/examples/tutorial/src/7-livestream/layout.css deleted file mode 100644 index 2fd790e68c..0000000000 --- a/examples/tutorial/src/7-livestream/layout.css +++ /dev/null @@ -1,53 +0,0 @@ -@layer stream, stream-overrides; -@import 'stream-chat-react/dist/css/index.css' layer(stream); - -@layer stream-overrides { - .custom-theme { - /* Accent */ - --str-chat__accent-primary: #0d47a1; - - /* Message bubble colors */ - --str-chat__chat-bg-outgoing: #1e3a8a; - --str-chat__chat-bg-attachment-outgoing: #0d47a1; - --str-chat__chat-bg-incoming: #dbeafe; - --str-chat__chat-text-outgoing: #ffffff; - --str-chat__chat-reply-indicator-outgoing: #93c5fd; - - /* Links */ - --str-chat__text-link: #1e40af; - --str-chat__chat-text-link: #93c5fd; - - /* Panel backgrounds */ - --str-chat__background-core-elevation-1: #dbeafe; /* channel list, surrounding panels */ - --str-chat__background-core-app: #c7dafc; /* message list background */ - - /* Focus ring */ - --str-chat__border-utility-focused: #1e40af; - - /* Radii */ - --str-chat__radius-max: 8px; - --str-chat__button-radius-full: 6px; - } -} - -html, -body, -#root { - height: 100%; -} -body { - margin: 0; -} -#root { - display: flex; -} - -.str-chat__channel-list { - width: 30%; -} -.str-chat__channel { - width: 100%; -} -.str-chat__thread { - width: 45%; -} diff --git a/examples/tutorial/src/7-livestream/main.tsx b/examples/tutorial/src/7-livestream/main.tsx deleted file mode 100644 index e17d50b103..0000000000 --- a/examples/tutorial/src/7-livestream/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/examples/tutorial/src/App.tsx b/examples/tutorial/src/App.tsx index 35a6b0092f..267f15f350 100644 --- a/examples/tutorial/src/App.tsx +++ b/examples/tutorial/src/App.tsx @@ -1,13 +1,14 @@ import { useEffect, useState } from 'react'; import type { ComponentType } from 'react'; -import ClientSetupStep from './1-client-setup/App'; -import CoreComponentSetupStep from './2-core-component-setup/App'; -import ChannelListStep from './3-channel-list/App'; -import CustomUiComponentsStep from './4-custom-ui-components/App'; -import CustomAttachmentTypeStep from './5-custom-attachment-type/App'; -import EmojiPickerStep from './6-emoji-picker/App'; -import LivestreamStep from './7-livestream/App'; +import ClientSetupStep from './2-client-setup/App'; +import CoreComponentSetupStep from './3-core-component-setup/App'; +import ChannelListStep from './4-channel-list/App'; +import ThemingStep from './5-theming/App'; +import CustomUiComponentsStep from './6-custom-ui-components/App'; +import EmojiPickerStep from './7-emoji-picker/App'; +import CustomAttachmentTypeStep from './optional-custom-attachment-type/App'; +import LivestreamStep from './optional-livestream/App'; import './tutorial-main.css'; type TutorialStep = { @@ -17,52 +18,64 @@ type TutorialStep = { Component: ComponentType; }; +// Titles and order mirror the published tutorial, so a step here maps 1:1 to a +// heading there: https://getstream.io/chat/sdk/react/tutorial/ +// +// The tutorial's Step 0 (environment) and Step 1 (project + credentials) have no +// runnable counterpart, so this browser starts at Step 2. const steps: TutorialStep[] = [ { id: 'client-setup', - title: '1. Client Setup', + title: 'Step 2. Connect the client', description: 'Connect the SDK to your Stream app and verify the chat client is ready.', Component: ClientSetupStep, }, { id: 'core-component-setup', - title: '2. Core Components', + title: 'Step 3. Get a working chat UI', description: 'Render the first complete chat UI with Channel, MessageList, MessageComposer, and Thread.', Component: CoreComponentSetupStep, }, { id: 'channel-list', - title: '3. Channel List', + title: 'Step 4. Add a channel list', description: 'Add channel navigation so the tutorial app feels like a real messaging experience.', Component: ChannelListStep, }, + { + id: 'theming', + title: 'Step 5. Theme it', + description: + 'Brand the default theme by overriding the SDK design tokens. Everything from here on carries the custom theme.', + Component: ThemingStep, + }, { id: 'custom-ui-components', - title: '4. Custom UI Components', + title: 'Step 6. Replace an SDK component', description: 'Use WithComponents to replace SDK-owned UI surfaces without rebuilding the whole app.', Component: CustomUiComponentsStep, }, { - id: 'custom-attachment-type', - title: '5. Custom Attachment Type', + id: 'emoji-picker', + title: 'Step 7. Emoji picker and autocomplete', description: - 'Render a branded product attachment while keeping the default attachment fallbacks.', - Component: CustomAttachmentTypeStep, + 'Wire the SDK EmojiPicker into MessageComposer with emoji-mart search support.', + Component: EmojiPickerStep, }, { - id: 'emoji-picker', - title: '6. Emoji Picker', + id: 'custom-attachment-type', + title: 'Optional. Custom attachment type', description: - 'Wire a custom EmojiPicker into MessageComposer with emoji-mart search support.', - Component: EmojiPickerStep, + 'Render a branded product attachment while keeping the default attachment fallbacks.', + Component: CustomAttachmentTypeStep, }, { id: 'livestream', - title: '7. Livestream', + title: 'Optional. Livestream-style chat', description: 'Switch the layout to a livestream-style experience with VirtualizedMessageList.', Component: LivestreamStep, @@ -137,7 +150,12 @@ const App = () => {
-
+ {/* The `step-` class lets tutorial-main.css target an individual + step's chrome. Only `step-client-setup` needs it today. */} +
diff --git a/examples/tutorial/src/5-custom-attachment-type/App.tsx b/examples/tutorial/src/optional-custom-attachment-type/App.tsx similarity index 94% rename from examples/tutorial/src/5-custom-attachment-type/App.tsx rename to examples/tutorial/src/optional-custom-attachment-type/App.tsx index 3cd1483ee8..4229326028 100644 --- a/examples/tutorial/src/5-custom-attachment-type/App.tsx +++ b/examples/tutorial/src/optional-custom-attachment-type/App.tsx @@ -19,7 +19,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, @@ -93,7 +93,9 @@ const App = () => { await channel.watch(); const hasProductMessage = channel.state.messages.some((message) => - message.attachments?.some(isProductAttachment), + message.attachments?.some( + (attachment) => 'type' in attachment && attachment.type === 'product', + ), ); if (!hasProductMessage) { diff --git a/examples/tutorial/src/optional-custom-attachment-type/layout.css b/examples/tutorial/src/optional-custom-attachment-type/layout.css new file mode 100644 index 0000000000..cacf7577dc --- /dev/null +++ b/examples/tutorial/src/optional-custom-attachment-type/layout.css @@ -0,0 +1,60 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. + In your own app, follow the tutorial and keep these in the layer. */ +.str-chat.custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/5-custom-attachment-type/stream-chat.d.ts b/examples/tutorial/src/optional-custom-attachment-type/stream-chat.d.ts similarity index 100% rename from examples/tutorial/src/5-custom-attachment-type/stream-chat.d.ts rename to examples/tutorial/src/optional-custom-attachment-type/stream-chat.d.ts diff --git a/examples/tutorial/src/7-livestream/App.tsx b/examples/tutorial/src/optional-livestream/App.tsx similarity index 95% rename from examples/tutorial/src/7-livestream/App.tsx rename to examples/tutorial/src/optional-livestream/App.tsx index 5ab17f6cbb..afa2633026 100644 --- a/examples/tutorial/src/7-livestream/App.tsx +++ b/examples/tutorial/src/optional-livestream/App.tsx @@ -11,7 +11,7 @@ import { } from 'stream-chat-react'; import './layout.css'; -import { apiKey, tokenProvider, userId, userName } from '../1-client-setup/credentials'; +import { apiKey, tokenProvider, userId, userName } from '../2-client-setup/credentials'; const user: User = { id: userId, diff --git a/examples/tutorial/src/optional-livestream/layout.css b/examples/tutorial/src/optional-livestream/layout.css new file mode 100644 index 0000000000..cacf7577dc --- /dev/null +++ b/examples/tutorial/src/optional-livestream/layout.css @@ -0,0 +1,60 @@ +@layer stream; +@import 'stream-chat-react/dist/css/index.css' layer(stream); + +/* One deliberate deviation from the tutorial, which wraps these tokens in + `@layer stream-overrides { .custom-theme { ... } }`. The step browser + (src/App.tsx) renders every step in one document, and the steps before this + one import the SDK stylesheet *unlayered*. Unlayered CSS outranks every + @layer, so the tutorial's layered override would silently do nothing here. + Declaring it unlayered on `.str-chat.custom-theme` (0,2,0) beats the SDK's + own `.str-chat` (0,1,0) regardless of source order, and only matches the + steps that actually pass `theme="custom-theme"`. + In your own app, follow the tutorial and keep these in the layer. */ +.str-chat.custom-theme { + /* Accent color - used by mentions, read receipts, attachment actions, focus states, etc. */ + --str-chat__accent-primary: #0d47a1; + + /* Message bubble colors */ + --str-chat__chat-bg-outgoing: #1e3a8a; + --str-chat__chat-bg-attachment-outgoing: #0d47a1; + --str-chat__chat-bg-incoming: #dbeafe; + --str-chat__chat-text-outgoing: #ffffff; + --str-chat__chat-reply-indicator-outgoing: #93c5fd; + + /* Link colors (inside bubbles and elsewhere) */ + --str-chat__text-link: #1e40af; + --str-chat__chat-text-link: #93c5fd; + + /* Panel backgrounds */ + --str-chat__background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ + --str-chat__background-core-app: #c7dafc; /* message list background */ + + /* Focus ring */ + --str-chat__border-utility-focused: #1e40af; + + /* Radii - the SDK uses --radius-max / --button-radius-full for pill shapes */ + --str-chat__radius-max: 8px; + --str-chat__button-radius-full: 6px; +} + +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +.str-chat__channel-list { + width: 30%; +} +.str-chat__channel { + width: 100%; +} +.str-chat__thread { + width: 45%; +} diff --git a/examples/tutorial/src/tutorial-main.css b/examples/tutorial/src/tutorial-main.css index c6076e0fc9..7b0397a277 100644 --- a/examples/tutorial/src/tutorial-main.css +++ b/examples/tutorial/src/tutorial-main.css @@ -1,7 +1,37 @@ +/* Host document rules. Every step's layout.css sets these too, because the + tutorial has the reader write them - but the chrome must not depend on a + step's stylesheet for its own layout. Declared here so the browser stands on + its own if steps are ever loaded lazily or in isolation. */ +html, +body, +#root { + height: 100%; +} +body { + margin: 0; +} +#root { + display: flex; +} + +/* Chrome only - deliberately NOT `.tutorial-browser *`, so the SDK's own + box-sizing is left alone. These panes are sized in viewport units *and* + padded, so with the default content-box the padding is added on top of 100vh + and pushes the chat UI (and its composer) below the fold. */ +.tutorial-browser, +.tutorial-browser__sidebar, +.tutorial-browser__main, +.tutorial-browser__header, +.tutorial-browser__preview-card, +.tutorial-browser__step-button { + box-sizing: border-box; +} + .tutorial-browser { - min-height: 100vh; + height: 100vh; width: 100%; display: flex; + overflow: hidden; background: linear-gradient(180deg, #eff5ff 0%, #f7fafc 32%, #eef7f6 100%); } @@ -11,10 +41,7 @@ background: rgba(255, 255, 255, 0.82); backdrop-filter: blur(16px); padding: 24px 20px; - position: sticky; - top: 0; - align-self: start; - height: 100vh; + height: 100%; overflow-y: auto; } @@ -84,7 +111,11 @@ flex-direction: column; padding: 20px; gap: 16px; - min-height: 100vh; + /* Exactly the viewport, not "at least" - the preview card below flexes into + whatever is left after the header, instead of overflowing the window. */ + height: 100%; + min-height: 0; + overflow: hidden; } .tutorial-browser__header { @@ -137,6 +168,14 @@ overflow: hidden; } +/* Step 2 renders bare text (`Chat with client is ready!`) with no + chat chrome, so it lands in the card's 28px corner arc and the first glyph + gets clipped. The other steps fill the corners with the channel header and + composer bars, which round cleanly, so they stay flush. */ +.tutorial-browser__step-shell.step-client-setup { + padding: 24px 28px; +} + .tutorial-browser__step-shell > * { flex: 1 1 auto; min-width: 0; diff --git a/examples/tutorial/vite.config.ts b/examples/tutorial/vite.config.ts index 0466183af6..d9e728778b 100644 --- a/examples/tutorial/vite.config.ts +++ b/examples/tutorial/vite.config.ts @@ -3,4 +3,11 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], + // `stream-chat-react` is consumed as a workspace dependency, so Vite serves + // its built output from outside this app's root and resolves that copy's + // `react` import separately from the app's. Without deduping, the SDK and the + // app end up on two React instances and every hook call throws. + resolve: { + dedupe: ['react', 'react-dom'], + }, });