Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,40 @@ jobs:

- name: 🧪 Validate CommonJS bundle with Node ${{ env.NODE_VERSION }}
run: yarn validate-cjs

deploy-vite-example:
runs-on: ubuntu-latest
needs:
- tsc
- test
name: Deploy Vite Example to Vercel
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: prj_2Rq8kuNd0BmqKd2NwvkDjX9Htnx5
VITE_STREAM_API_KEY: ${{ vars.VITE_STREAM_API_KEY }}
NODE_ENV: production
steps:
- uses: actions/checkout@v4

- uses: ./.github/actions/setup-node
env:
NODE_ENV: ''

- name: Build SDK
run: yarn build

- name: Vercel Pull/Build/Deploy (Preview)
working-directory: examples/vite
if: ${{ github.ref_name != 'master' }}
run: >
npx vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} &&
npx vercel build &&
npx vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}

- name: Vercel Pull/Build/Deploy (Production)
working-directory: examples/vite
if: ${{ github.ref_name == 'master' }}
run: >
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} &&
npx vercel build --prod &&
npx vercel deploy --prod --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
27 changes: 2 additions & 25 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,9 @@
{
"branches": [
{
"name": "rc",
"channel": "rc",
"prerelease": "rc"
},
{
"name": "master",
"channel": "latest"
},
{
"name": "release-v12",
"channel": "latest"
},
{
"name": "release-v11",
"channel": "v11",
"range": "11.x"
},
{
"name": "release-v10",
"channel": "v10",
"range": "10.x"
},
{
"name": "release-v9",
"channel": "v9",
"range": "9.x"
"channel": "canary",
"prerelease": "canary"
}
],
"plugins": [
Expand Down
3 changes: 2 additions & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"human-id": "^4.1.3",
"react": "link:../../node_modules/react",
"react-dom": "link:../../node_modules/react-dom",
"stream-chat-react": "link:../../"
Expand All @@ -26,7 +27,7 @@
"eslint-plugin-react-refresh": "^0.4.6",
"sass": "^1.75.0",
"typescript": "^5.4.5",
"vite": "^7.2.2",
"vite": "^7.3.0",
"vite-plugin-babel": "^1.3.2"
}
}
87 changes: 46 additions & 41 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import {
ChannelFilters,
ChannelOptions,
Expand All @@ -24,43 +24,68 @@ import {
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';
import { humanId } from 'human-id';

init({ data });

const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, property) => searchParams.get(property as string),
}) as unknown as Record<string, string | null>;
const apiKey = import.meta.env.VITE_STREAM_API_KEY;

const parseUserIdFromToken = (token: string) => {
const [, payload] = token.split('.');
if (!apiKey) {
throw new Error('VITE_STREAM_API_KEY is not defined');
}

if (!payload) throw new Error('Token is missing');

return JSON.parse(atob(payload))?.user_id;
};

const apiKey = params.key ?? (import.meta.env.VITE_STREAM_KEY as string);
const userToken = params.ut ?? (import.meta.env.VITE_USER_TOKEN as string);
const userId = parseUserIdFromToken(userToken);

const filters: ChannelFilters = {
members: { $in: [userId] },
type: 'messaging',
archived: false,
};
const options: ChannelOptions = { limit: 5, presence: true, state: true };
const sort: ChannelSort = { pinned_at: 1, last_message_at: -1, updated_at: -1 };

// @ts-ignore
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;

const useUser = () => {
const userId = useMemo(() => {
return (
new URLSearchParams(window.location.search).get('user_id') ||
localStorage.getItem('user_id') ||
humanId({ separator: '_', capitalize: false })
);
}, []);

useEffect(() => {
const storedUserId = localStorage.getItem('user_id');

if (userId && storedUserId === userId) return;

localStorage.setItem('user_id', userId);
}, [userId]);

const tokenProvider = useCallback(() => {
return fetch(
`https://pronto.getstream.io/api/auth/create-token?environment=shared-chat-redesign&user_id=${userId}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to have this publicly hard-coded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, Zita does the same for her feeds tutorial. The secret is not visible, the app behind it is for testing purposes only, tokens are short lived and can be revoked if necessary - the secret can be rotated too preventing Pronto from generating new ones.

)
.then((response) => response.json())
.then((data) => data.token as string);
}, [userId]);

return { userId: userId, tokenProvider };
};

const App = () => {
const { userId, tokenProvider } = useUser();

const chatClient = useCreateChatClient({
apiKey,
tokenOrProvider: userToken,
tokenOrProvider: tokenProvider,
userData: { id: userId },
});

const filters: ChannelFilters = useMemo(
() => ({
members: { $in: [userId] },
type: 'messaging',
archived: false,
}),
[userId],
);

useEffect(() => {
if (!chatClient) return;

Expand All @@ -77,26 +102,6 @@ const App = () => {

if (!chatClient) return <>Loading...</>;

chatClient.axiosInstance.interceptors.response.use(
(response) => {
// Simulate a 500 for specific routes
if (response.config.url?.includes('/delivered')) {
// throw a fake error like a real server would
const error = {
response: {
status: 500,
statusText: 'Internal Server Error',
data: { error: 'Simulated server error' },
config: response.config,
},
};
return Promise.reject(error);
}

return response;
},
(error) => Promise.reject(error),
);
return (
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>
<ChatView>
Expand Down
142 changes: 39 additions & 103 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
@@ -1,130 +1,66 @@
body {
margin: 0;
@layer stream, stream-overrides;

@import url('stream-chat-react/dist/css/v2/index.css') layer(stream);

:root {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

html,
body,
#root {
margin: unset;
padding: unset;
height: 100%;
body {
margin: 0;
}

@layer stream, emoji-replacement;

@import url('stream-chat-react/css/v2/index.css') layer(stream);
// use in combination with useImageFlagEmojisOnWindows prop on Chat component
// @import url('stream-chat-react/css/v2/emoji-replacement.css') layer(emoji-replacement);

#root {
display: flex;
height: 100%;
height: 100vh;
}

& > div.str-chat {
height: 100%;
width: 100%;
display: flex;
@layer stream-overrides {
.str-chat {
--max-content-width: 800px;
}

.str-chat__channel-list {
position: fixed;
z-index: 1;
height: 100%;
width: 0;
.str-chat__channel-list, .str-chat__thread-list-container {
flex-basis: 350px;
flex-shrink: 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
max-width: 1000px;

&--open {
width: 30%;
position: fixed;
}
transition: width 0.3s ease-out;
}

.str-chat__channel {
flex: 1;
min-width: 0;
width: 100%;
}

.str-chat__main-panel {
min-width: 0;
flex: 1;

&--thread-open {
display: none;
}
.str-chat__main-panel, .str-chat__thread-container {
align-items: center;
}

.str-chat__thread {
flex: 1;
height: 100%;
position: absolute;
z-index: 1;
.str-chat__main-panel-inner {
width: 100%;
}

.str-chat__channel-header .str-chat__header-hamburger {
width: 30px;
height: 38px;
padding: var(--xxs-p);
margin-right: var(--xs-m);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
background: transparent;

&:hover {
svg path {
fill: var(--primary-color);
}
}
.str-chat__message-list-scroll {
max-width: var(--max-content-width);
width: 100%;
}

@media screen and (min-width: 768px) {
.str-chat__channel-list {
width: 30%;
position: initial;
z-index: 0;
}

.str-chat__chat-view__channels {
.str-chat__thread {
position: initial;
z-index: 0;
}
}

.str-chat__channel-header .str-chat__header-hamburger {
display: none;
}

.str-chat__channel-header, .str-chat__thread-header {
max-width: var(--max-content-width);
width: 100%;
}

@media screen and (min-width: 1024px) {
.str-chat__main-panel {
min-width: 0;

&--thread-open {
max-width: 55%;
display: flex;
}
}

.str-chat__chat-view__channels {
.str-chat__thread {
max-width: 45%;
}
}

.str-chat__channel-header .str-chat__header-hamburger {
display: none;
}
.str-chat__message-input {
max-width: var(--max-content-width);
// scrollbar-gutter: stable;
// scrollbar-width: thin;
// overflow-y: hidden;
// flex-shrink: 0;
}

.str-chat__thread-list-container {
max-width: 350px;
.str-chat__list, .str-chat__virtual-list {
display: flex;
justify-content: center;
scrollbar-gutter: stable;
scrollbar-width: thin;
}
}
8 changes: 7 additions & 1 deletion examples/vite/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_STREAM_API_KEY?: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
2 changes: 1 addition & 1 deletion examples/vite/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig, loadEnv, type PluginOption } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import babel from 'vite-plugin-babel';
import react from '@vitejs/plugin-react';

Expand Down
Loading