diff --git a/AGENTS.md b/AGENTS.md index f1c7368f..5d1ff894 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,20 +21,21 @@ Agents should prioritize backwards compatibility, API stability, and high test c ### Project layout (monorepo) packages/ -feeds-client/ # Core Feeds API client -src/ -common/ # Common utilities (API client, state management, real-time) -feed/ # Feed management and event handlers -feeds-client/ # Main FeedsClient class -gen/ # Generated API clients and models -utils/ # Utility functions -types.ts # Type definitions -@react-bindings/ # React hooks and contexts -react-sdk/ # React SDK wrapper -react-native-sdk/ # React Native SDK wrapper + feeds-client/ # Core Feeds API client + src/ + activity-with-state-updates/ # Activity state management + bindings/ # Framework bindings + common/ # Common utilities (API client, state management, real-time) + feed/ # Feed management and event handlers + feeds-client/ # Main FeedsClient class + gen/ # Generated API clients and models + utils/ # Utility functions + react-sdk/ # React SDK wrapper with hooks and contexts + react-native-sdk/ # React Native SDK wrapper sample-apps/ -react-sample-app/ # Next.js sample application for React -react-native/ # React Native sample application + react-demo/ # Next.js demo application (stream-feeds-react-demo) + react-sample-app/ # Next.js sample application (facebook-clone) + react-native/ # React Native sample application (ExpoTikTokApp) Use the closest folder's patterns and conventions when editing. @@ -148,11 +149,11 @@ Commit / PR conventions Testing policy • Add/extend tests in each package's test directories with .test.ts suffix. • Cover: -• Core FeedsClient and Feed classes -• Event handlers and state management - see ai-docs/ai-state-management for details -• React hooks and contexts (@react-bindings) -• Utility functions (token creation, rate limiting, search) -• Generated API clients and their interactions + • Core FeedsClient and Feed classes + • Event handlers and state management - see ai-docs/ai-state-management for details + • React hooks and contexts (react-sdk, react-native-sdk) + • Utility functions (token creation, rate limiting, search) + • Generated API clients and their interactions • Integration tests are in `__integration-tests__/` directories Security & credentials @@ -184,3 +185,32 @@ Quick agent checklist (per commit) • Test affected packages individually if needed End of machine guidance. Edit this file to refine agent behavior over time; keep human-facing details in README.md and docs. + +## React Demo app + +### Purpose + +This is a React demo application showcasing the Stream Feeds SDK. Both source code quality and visual design should be excellent—this app serves as a reference implementation. + +### UI Framework + +This project uses **Tailwind CSS** with **daisyUI** for styling. + +#### daisyUI Setup for Cursor + +To get accurate daisyUI code generation, use one of these methods: + +**Quick use in chat:** +``` +@web https://daisyui.com/llms.txt +``` + +### Stream Feeds SDK Documentation + +If something is not clear, ask for a documentation link + +### Quality Standards + +- **Source code**: Clean, well-structured, following React best practices +- **Design**: Modern, polished UI using daisyUI components effectively +- **Both must be excellent**—this is a showcase application diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1f6c0163 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Install dependencies (run from repository root) +yarn + +# Build all packages +yarn build:all + +# Build specific packages +yarn build:client # Build feeds-client only +yarn build:react-sdk # Build React SDK only +yarn build:react-native-sdk # Build React Native SDK only + +# Development mode with watch (in packages/feeds-client) +yarn start +``` + +## Testing + +```bash +# Run all tests +yarn test:ci:all + +# Run tests for library packages only (no sample apps) +yarn test:ci:libs + +# Run tests for feeds-client (in packages/feeds-client) +yarn test # Run all tests with vitest +yarn test:unit # Run unit tests only (excludes integration tests) +yarn test # Run specific test file, e.g., yarn test feed.test + +# Run a single test file +yarn vitest run path/to/test.test.ts + +# Integration tests require environment variables: +# VITE_STREAM_API_KEY and VITE_STREAM_API_SECRET (see __integration-tests__/utils.ts) +``` + +## Linting + +```bash +yarn lint:all # Lint all TypeScript files +yarn lint:all:fix # Lint and auto-fix +``` + +## Verification After Changes + +**IMPORTANT**: After making code changes, always verify the build and lint status: + +```bash +yarn build:all # Ensure all packages build successfully +yarn lint:all # Check for linting errors +``` + +These commands should be run from the repository root before considering changes complete. + +## Code Generation + +```bash +yarn generate # Regenerate OpenAPI types (runs generate-openapi.sh) +yarn lint:gen # Lint and format generated code +``` + +## Architecture + +This is a Yarn 4 monorepo with three main packages and sample applications. + +### Package Hierarchy + +``` +@stream-io/feeds-client (packages/feeds-client) + └── @stream-io/feeds-react-sdk (packages/react-sdk) - re-exports feeds-client + └── @stream-io/feeds-react-native-sdk (packages/react-native-sdk) - re-exports feeds-client +``` + +### feeds-client Structure + +The core SDK with two entry points: +- Main entry (`index.ts`): `FeedsClient`, `Feed`, state management, types +- React bindings entry (`/react-bindings`): hooks, contexts, wrapper components + +Key classes: +- **FeedsClient** (`src/feeds-client/feeds-client.ts`): Main client for API communication, WebSocket connections, and state management. Extends auto-generated `FeedsApi`. +- **Feed** (`src/feed/feed.ts`): Represents a single feed with its state. Extends auto-generated `FeedApi`. +- **StateStore**: From `@stream-io/state-store`, manages reactive state for both client and feeds. + +### Generated Code + +`src/gen/` contains OpenAPI-generated code: +- `models/`: API request/response types +- `feeds/`: `FeedsApi` and `FeedApi` base classes +- `model-decoders/`: WebSocket event decoders + +### React Bindings + +Located in `src/bindings/react/`: +- **Contexts**: `StreamFeedsContext`, `StreamFeedContext`, `StreamActivityWithStateUpdatesContext`, `StreamSearchContext` +- **Hooks**: `useCreateFeedsClient`, client/feed/search state hooks +- **Wrappers**: `StreamFeeds`, `StreamFeed`, `StreamActivityWithStateUpdates`, `StreamSearch` + +### Event Handling + +WebSocket events are processed through handlers in `src/feed/event-handlers/`. Each event type (activity, comment, follow, bookmark, etc.) has dedicated handler files that update the appropriate state stores. + +### Sample Apps + +- `sample-apps/react-demo`: Next.js demo app with stories feature with DaisyUI and Tailwind +- `sample-apps/react-sample-app`: Advanced Next.js example +- `sample-apps/react-native/ExpoTikTokApp`: Expo React Native example + +## Key Patterns + +- State management uses `@stream-io/state-store` with React bindings via `useSyncExternalStore` +- API types are generated from OpenAPI spec - don't manually edit files in `src/gen/` +- Integration tests in `__integration-tests__/` require Stream API credentials +- Tests use vitest; configuration is in `vite.config.ts` diff --git a/README.md b/README.md index 3cef82e0..0e0fdc42 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,7 @@ Here are some of the features we support: - **Search & queries**: Activity search, **query activities**, and **query feeds** endpoints. - **Modern essentials**: Permissions • OpenAPI spec • GDPR endpoints • realtime WebSocket events • push notifications • “own capabilities” API. -## React sample apps - -### React demo app with stories +## React demo app Deployed version: https://feeds-react-demo.vercel.app @@ -80,27 +78,80 @@ After the above steps run the following command in `sample-apps/react-demo`: yarn dev ``` -### Advanced React app +## Test Data Generator -Prerequisites: +The `test-data-generator` directory contains scripts to populate your Stream Feeds app with sample data for testing and development purposes. -- Install dependencies: `yarn` -- Build React SDK: `yarn build:client` and `yarn build:react-sdk` -- Create a `.env` file in `sample-apps/react-sample-app` with the following content: +### Setup + +1. Create a `.env` file in `test-data-generator/` with your credentials: ``` -NEXT_PUBLIC_STREAM_API_KEY= -NEXT_API_SECRET= -NEXT_PUBLIC_API_URL= +STREAM_API_KEY= +API_SECRET= +API_URL= ``` -- Run the `node setup-env.js` script in `sample-apps/react-sample-app` -- If you want to have some pre-made posts in your app, optinally run the `node create-posts.js` script as well +2. Install dependencies: `yarn` (from the repository root) + +### Available Scripts -After the above steps run the following command in `sample-apps/react-sample-app`: +Run these commands from the `test-data-generator/` directory: +| Script | Command | Description | +| --------------- | ---------------------- | ------------------------------------------ | +| Create Users | `yarn create-users` | Creates users and their feeds | +| Create Follows | `yarn create-follows` | Sets up follow relationships between users | +| Create Posts | `yarn create-posts` | Generates sample activities/posts | +| Create Stories | `yarn create-stories` | Creates sample stories | +| Download Images | `yarn download-images` | Downloads sample images for posts | + +### Create Posts Feature Flags + +The `create-posts` script supports a `--features` flag to control which features are included in the generated posts: + +```bash +yarn create-posts --features ``` -yarn dev + +**Available features:** + +| Feature | Description | +| ------------ | ---------------------------------------- | +| `link` | Adds random URLs to posts | +| `attachment` | Uploads and attaches 1-3 images to posts | +| `mention` | Adds @mentions to other users | +| `poll` | Creates and attaches polls to posts | +| `reaction` | Adds 1-5 reactions from random users | +| `comment` | Adds 1-5 comments from random users | +| `bookmark` | Bookmarks posts by random users | +| `repost` | Creates reposts of existing activities | + +**Examples:** + +```bash +# Create basic posts without any features +yarn create-posts + +# Create posts with polls and reactions +yarn create-posts --features poll,reaction + +# Create posts with all content features +yarn create-posts --features link,attachment,mention,poll,reaction,comment,bookmark,repost +``` + +> Note: Each feature has a probability of being included (not every post will have every enabled feature). Link and attachment are mutually exclusive per post. + +### Usage + +Typical order of operations: + +```bash +cd test-data-generator +yarn create-users +yarn create-follows +yarn create-posts --features link,attachment,mention,poll,reaction,comment,bookmark,repost +yarn create-stories ``` ## Local Setup diff --git a/package.json b/package.json index 4e88128e..bde4d7ef 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "license": "See license in LICENSE", "workspaces": [ "packages/*", - "sample-apps/**" + "sample-apps/**", + "test-data-generator" ], "scripts": { "build:all": "yarn workspaces foreach -Avp --topological-dev run build", diff --git a/packages/feeds-client/__integration-tests__/activity-websocket-events.test.ts b/packages/feeds-client/__integration-tests__/activity-websocket-events.test.ts index 147c87d6..837c9ab8 100644 --- a/packages/feeds-client/__integration-tests__/activity-websocket-events.test.ts +++ b/packages/feeds-client/__integration-tests__/activity-websocket-events.test.ts @@ -164,7 +164,7 @@ describe('Activity state updates via WebSocket events', () => { text: 'Test activity', }); - await waitForEvent(feed, 'feeds.activity.added', { timeoutMs: 1000 }); + await waitForEvent(feed, 'feeds.activity.added', { timeoutMs: 10000 }); expect( feed.state diff --git a/packages/feeds-client/__integration-tests__/utils.ts b/packages/feeds-client/__integration-tests__/utils.ts index 93d4962b..4dcafba3 100644 --- a/packages/feeds-client/__integration-tests__/utils.ts +++ b/packages/feeds-client/__integration-tests__/utils.ts @@ -64,7 +64,7 @@ export const waitForEvent = ( client: FeedsClient | Feed, type: FeedsEvent['type'] | WSEvent['type'], { - timeoutMs = 3000, + timeoutMs = 10000, shouldReject = false, }: { timeoutMs?: number; diff --git a/packages/feeds-client/src/bindings/react/contexts/StreamActivityWithStateUpdatesContext.tsx b/packages/feeds-client/src/bindings/react/contexts/StreamActivityWithStateUpdatesContext.tsx new file mode 100644 index 00000000..5b94052f --- /dev/null +++ b/packages/feeds-client/src/bindings/react/contexts/StreamActivityWithStateUpdatesContext.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; + +import type { ActivityWithStateUpdates } from '../../../activity-with-state-updates/activity-with-state-updates'; + +export const StreamActivityWithStateUpdatesContext = createContext(undefined); + +/** + * The props for the StreamActivityWithStateUpdatesProvider component. + */ +export type StreamActivityWithStateUpdatesContextProps = { + activityWithStateUpdates: ActivityWithStateUpdates; +}; + +/** + * Hook to access the nearest ActivityWithStateUpdates instance. + */ +export const useActivityWithStateUpdatesContext = () => { + return useContext(StreamActivityWithStateUpdatesContext); +}; diff --git a/packages/feeds-client/src/bindings/react/hooks/feed-state-hooks/useActivityComments.ts b/packages/feeds-client/src/bindings/react/hooks/feed-state-hooks/useActivityComments.ts index b1cd841c..9859b3f6 100644 --- a/packages/feeds-client/src/bindings/react/hooks/feed-state-hooks/useActivityComments.ts +++ b/packages/feeds-client/src/bindings/react/hooks/feed-state-hooks/useActivityComments.ts @@ -7,6 +7,7 @@ import { checkHasAnotherPage } from '../../../../utils'; import type { Feed, FeedState } from '../../../../feed'; import type { ActivityState, ActivityWithStateUpdates } from '../../../../activity-with-state-updates/activity-with-state-updates'; import type { ActivityResponse, CommentResponse } from '../../../../gen/models'; +import { useActivityWithStateUpdatesContext } from '../../contexts/StreamActivityWithStateUpdatesContext'; const canLoadComments = ( feedOrActivity: Feed | ActivityResponse | ActivityWithStateUpdates, @@ -36,15 +37,17 @@ type UseCommentsReturnType = { export function useActivityComments({ feed: feedFromProps, parentComment, - activity, + activity: activityFromProps, }: { feed?: Feed; parentComment?: CommentResponse; activity?: ActivityResponse | ActivityWithStateUpdates; -}) { +} = {}) { const feedFromContext = useFeedContext(); const feed = feedFromProps ?? feedFromContext; - const feedOrActivity = feed ?? activity; + const activityFromContext = useActivityWithStateUpdatesContext(); + const activity = activityFromProps ?? activityFromContext; + const feedOrActivity = (activity && canLoadComments(activity)) ? activity : feed; if (!feedOrActivity) { throw new Error('Feed or activity is required'); diff --git a/packages/feeds-client/src/bindings/react/hooks/useCreateFeedsClient.ts b/packages/feeds-client/src/bindings/react/hooks/useCreateFeedsClient.ts index c71b23a7..6601d7e4 100644 --- a/packages/feeds-client/src/bindings/react/hooks/useCreateFeedsClient.ts +++ b/packages/feeds-client/src/bindings/react/hooks/useCreateFeedsClient.ts @@ -47,7 +47,7 @@ export const useCreateFeedsClient = ({ const connectionPromise = cachedUserData ? _client - .connectUser(cachedUserData, tokenOrProvider) + .connectUser(cachedUserData, tokenOrProvider!) .then(() => { setError(null); }) diff --git a/packages/feeds-client/src/bindings/react/index.ts b/packages/feeds-client/src/bindings/react/index.ts index 959567ac..d2ac8f44 100644 --- a/packages/feeds-client/src/bindings/react/index.ts +++ b/packages/feeds-client/src/bindings/react/index.ts @@ -11,6 +11,7 @@ export * from './hooks/search-state-hooks'; export * from './contexts/StreamFeedsContext'; export * from './contexts/StreamFeedContext'; +export * from './contexts/StreamActivityWithStateUpdatesContext'; export * from './contexts/StreamSearchContext'; export * from './contexts/StreamSearchResultsContext'; @@ -18,5 +19,6 @@ export * from './contexts/StreamSearchResultsContext'; export * from './wrappers/StreamFeeds'; export * from './wrappers/StreamFeed'; +export * from './wrappers/StreamActivityWithStateUpdates'; export * from './wrappers/StreamSearch'; export * from './wrappers/StreamSearchResults'; diff --git a/packages/feeds-client/src/bindings/react/wrappers/StreamActivityWithStateUpdates.tsx b/packages/feeds-client/src/bindings/react/wrappers/StreamActivityWithStateUpdates.tsx new file mode 100644 index 00000000..6649b8ce --- /dev/null +++ b/packages/feeds-client/src/bindings/react/wrappers/StreamActivityWithStateUpdates.tsx @@ -0,0 +1,24 @@ +import type { PropsWithChildren } from 'react'; + +import { StreamActivityWithStateUpdatesContext } from '../contexts/StreamActivityWithStateUpdatesContext'; +import type { ActivityWithStateUpdates } from '../../../activity-with-state-updates/activity-with-state-updates'; + +/** + * The props for the StreamActivityWithStateUpdates component. It accepts an `ActivityWithStateUpdates` instance. + */ +export type StreamActivityWithStateUpdatesProps = { + activityWithStateUpdates: ActivityWithStateUpdates; +}; + +export const StreamActivityWithStateUpdates = ({ + activityWithStateUpdates, + children, +}: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +StreamActivityWithStateUpdates.displayName = 'StreamActivityWithStateUpdates'; diff --git a/packages/feeds-client/src/feed/feed.ts b/packages/feeds-client/src/feed/feed.ts index 0916a96f..403be9a3 100644 --- a/packages/feeds-client/src/feed/feed.ts +++ b/packages/feeds-client/src/feed/feed.ts @@ -872,12 +872,13 @@ export class Feed extends FeedApi { return response; } - async unfollow(feedOrFid: Feed | string) { + async unfollow(feedOrFid: Feed | string, options?: Omit[0], 'source' | 'target'>) { const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.feed; const response = await this.client.unfollow({ source: this.feed, target: fid, + ...options, }); return response; diff --git a/packages/feeds-client/src/feeds-client/feeds-client.ts b/packages/feeds-client/src/feeds-client/feeds-client.ts index 26a742b7..ed44db0e 100644 --- a/packages/feeds-client/src/feeds-client/feeds-client.ts +++ b/packages/feeds-client/src/feeds-client/feeds-client.ts @@ -380,10 +380,7 @@ export class FeedsClient extends FeedsApi { return Promise.resolve(); }; - connectUser = async ( - user: UserRequest | { id: '!anon' }, - tokenProvider?: TokenOrProvider, - ) => { + connectUser = async (user: UserRequest, tokenProvider: TokenOrProvider) => { this.checkIfUserIsConnected(); this.tokenManager.setTokenOrProvider(tokenProvider); @@ -500,11 +497,10 @@ export class FeedsClient extends FeedsApi { return response; }; - deleteComment = async (request: { - id: string; - hard_delete?: boolean; - }): Promise> => { - const response = await super.deleteComment(request); + deleteComment = async ( + ...args: Parameters + ): Promise> => { + const response = await super.deleteComment(...args); const { activity, comment } = response; for (const feed of this.allActiveFeeds) { handleCommentDeleted.bind(feed)({ comment }, false); @@ -545,11 +541,10 @@ export class FeedsClient extends FeedsApi { return this.addActivityReaction(request); }; - deleteActivityReaction = async (request: { - activity_id: string; - type: string; - }): Promise> => { - const response = await super.deleteActivityReaction(request); + deleteActivityReaction = async ( + ...args: Parameters + ): Promise> => { + const response = await super.deleteActivityReaction(...args); for (const feed of this.allActiveFeeds) { handleActivityReactionDeleted.bind(feed)(response, false); } @@ -571,11 +566,10 @@ export class FeedsClient extends FeedsApi { return response; }; - deleteCommentReaction = async (request: { - id: string; - type: string; - }): Promise> => { - const response = await super.deleteCommentReaction(request); + deleteCommentReaction = async ( + ...args: Parameters + ): Promise> => { + const response = await super.deleteCommentReaction(...args); for (const feed of this.allActiveFeeds) { handleCommentReactionDeleted.bind(feed)(response, false); } diff --git a/sample-apps/react-demo/.gitignore b/sample-apps/react-demo/.gitignore index f3a635a8..8a2a7585 100644 --- a/sample-apps/react-demo/.gitignore +++ b/sample-apps/react-demo/.gitignore @@ -38,3 +38,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +certificates \ No newline at end of file diff --git a/sample-apps/react-demo/app/AppSkeleton.tsx b/sample-apps/react-demo/app/AppSkeleton.tsx index 2df2d1bd..eb81db35 100644 --- a/sample-apps/react-demo/app/AppSkeleton.tsx +++ b/sample-apps/react-demo/app/AppSkeleton.tsx @@ -1,11 +1,11 @@ 'use client'; -import { useNotificationStatus } from '@stream-io/feeds-react-sdk'; +import { useClientConnectedUser, useNotificationStatus } from '@stream-io/feeds-react-sdk'; import { type PropsWithChildren } from 'react'; import { FollowSuggestions } from './components/FollowSuggestions'; import { useOwnFeedsContext } from './own-feeds-context'; import { SearchInput } from './components/utility/SearchInput'; -import { NavLink } from './components/utility/NavLink'; +import { MenuNavLink } from './components/utility/NavLink'; export const AppSkeleton = ({ children }: PropsWithChildren) => { const { ownNotifications } = useOwnFeedsContext(); @@ -101,7 +101,7 @@ const Dock = ({ @@ -114,33 +114,35 @@ const Dock = ({ }; const HomeLink = () => { - return ; + return ; }; const PopularLink = () => { - return ; + return ; }; const ExploreLink = () => { - return ; + return ; }; const NotificationsLink = ({ children }: { children?: React.ReactNode }) => { return ( - + {children} - + ); }; const ProfileLink = () => { - return ; + const currentUser = useClientConnectedUser(); + + return ; }; const AddLink = () => { - return ; + return ; }; const BookmarksLink = () => { - return ; + return ; }; diff --git a/sample-apps/react-demo/app/ClientApp.tsx b/sample-apps/react-demo/app/ClientApp.tsx index bcf95c64..a9bb10f4 100644 --- a/sample-apps/react-demo/app/ClientApp.tsx +++ b/sample-apps/react-demo/app/ClientApp.tsx @@ -5,13 +5,16 @@ import { StreamFeeds, FeedsClient, } from '@stream-io/feeds-react-sdk'; +import * as Sentry from '@sentry/nextjs'; import { AppSkeleton } from './AppSkeleton'; import { OwnFeedsContextProvider } from './own-feeds-context'; import { FollowSuggestionsContextProvider } from './follow-suggestions-context'; +import { ConnectionAlert } from './components/utility/ConnectionAlert'; import { generateUsername } from 'unique-username-generator'; import { useEffect, useMemo, type PropsWithChildren } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { LoadingIndicator } from './components/utility/LoadingIndicator'; +import { userIdToUserName } from './utility/user-id-to-name'; export const ClientApp = ({ children }: PropsWithChildren) => { const searchParams = useSearchParams(); @@ -37,14 +40,14 @@ export const ClientApp = ({ children }: PropsWithChildren) => { const CURRENT_USER = useMemo( () => ({ id: USER_ID, - name: process.env.NEXT_PUBLIC_USER_NAME ?? USER_ID, + name: process.env.NEXT_PUBLIC_USER_NAME ?? userIdToUserName(USER_ID), token: process.env.NEXT_PUBLIC_USER_TOKEN ? process.env.NEXT_PUBLIC_USER_TOKEN : process.env.NEXT_PUBLIC_TOKEN_URL ? () => - fetch( - `${process.env.NEXT_PUBLIC_TOKEN_URL}&user_id=${USER_ID}`, - ).then((res) => res.json().then((data) => data.token)) + fetch( + `${process.env.NEXT_PUBLIC_TOKEN_URL}&user_id=${USER_ID}`, + ).then((res) => res.json().then((data) => data.token)) : new FeedsClient(API_KEY!).devToken(USER_ID), }), [USER_ID, API_KEY], @@ -57,6 +60,18 @@ export const ClientApp = ({ children }: PropsWithChildren) => { id: CURRENT_USER.id, name: CURRENT_USER.name, }, + options: { + base_url: process.env.NEXT_PUBLIC_API_URL, + timeout: 10000, + configure_loggers_options: { + default: { + level: 'error', + sink: (...args: any[]) => { + Sentry.captureException(new Error(args.join(' '))); + }, + }, + }, + }, }); if (!client) { @@ -69,6 +84,7 @@ export const ClientApp = ({ children }: PropsWithChildren) => { return ( + {children} diff --git a/sample-apps/react-demo/app/activity-compose/page.tsx b/sample-apps/react-demo/app/activity-compose/page.tsx index e7af77ea..76bf929d 100644 --- a/sample-apps/react-demo/app/activity-compose/page.tsx +++ b/sample-apps/react-demo/app/activity-compose/page.tsx @@ -3,17 +3,20 @@ import { StreamFeed } from '@stream-io/feeds-react-sdk'; import { ActivityComposer } from '../components/activity/ActivityComposer'; import { useOwnFeedsContext } from '../own-feeds-context'; +import { useNavigation } from '../utility/navigation'; export default function ActivityComposePage() { const { ownFeed } = useOwnFeedsContext(); + const navigateToHome = useNavigation('/'); + if (!ownFeed) { return null; } return ( - + ); } diff --git a/sample-apps/react-demo/app/activity/[id]/page.tsx b/sample-apps/react-demo/app/activity/[id]/page.tsx index 3329122c..3426c059 100644 --- a/sample-apps/react-demo/app/activity/[id]/page.tsx +++ b/sample-apps/react-demo/app/activity/[id]/page.tsx @@ -1,12 +1,15 @@ 'use client'; -import { ActivityActions } from '@/app/components/activity/activity-actions/ActivityActions'; +import { ActivityInteractions } from '@/app/components/activity/activity-interactions/ActivityInteractions'; import { ActivityContent } from '@/app/components/activity/ActivityContent'; import { ActivityHeader } from '@/app/components/activity/ActivityHeader'; +import { ActivityParent } from '@/app/components/activity/ActivityParent'; import { CommentComposer } from '@/app/components/comments/CommentComposer'; import { CommentList } from '@/app/components/comments/CommentList'; +import { ErrorCard } from '@/app/components/utility/ErrorCard'; import { LoadingIndicator } from '@/app/components/utility/LoadingIndicator'; import { + StreamActivityWithStateUpdates, useFeedsClient, useStateStore, type ActivityState, @@ -25,6 +28,7 @@ export default function ActivityPage() { const [activityWithStateUpdates, setActivityWithStateUpdates] = useState< ActivityWithStateUpdates | undefined >(); + const [error, setError] = useState(undefined); useEffect(() => { const _activityWithStateUpdates = client?.activityWithStateUpdates(id); @@ -35,7 +39,10 @@ export default function ActivityPage() { useEffect(() => { if (!activityWithStateUpdates?.currentState.activity) { - activityWithStateUpdates?.get(); + activityWithStateUpdates?.get().catch((e) => { + setError(e.message); + throw e; + }); } }, [activityWithStateUpdates]); @@ -44,6 +51,10 @@ export default function ActivityPage() { selector, ) ?? { activity: undefined }; + if (error) { + return ; + } + if (!activity || !activityWithStateUpdates) { return (
@@ -53,12 +64,18 @@ export default function ActivityPage() { } return ( -
- - - - - +
+
+ + + + +
Comments
+ +
+ + +
); } diff --git a/sample-apps/react-demo/app/bookmarks/page.tsx b/sample-apps/react-demo/app/bookmarks/page.tsx index 4082bd7e..66f0bd31 100644 --- a/sample-apps/react-demo/app/bookmarks/page.tsx +++ b/sample-apps/react-demo/app/bookmarks/page.tsx @@ -6,6 +6,7 @@ import { } from '@stream-io/feeds-react-sdk'; import { useCallback, useEffect, useState } from 'react'; import { ActivityPreview } from '../components/activity/ActivityPreview'; +import { ErrorCard } from '../components/utility/ErrorCard'; import { LoadingIndicator } from '../components/utility/LoadingIndicator'; export default function Bookmarks() { @@ -13,11 +14,12 @@ export default function Bookmarks() { const [bookmarks, setBookmarks] = useState([]); const [next, setNext] = useState(undefined); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); const loadBookmarks = useCallback( (nextCursor?: string) => { setIsLoading(true); - client + return client ?.queryBookmarks({ limit: 20, next: nextCursor, @@ -29,6 +31,10 @@ export default function Bookmarks() { ]); setNext(response.next); }) + .catch((e) => { + setError(e.message); + throw e; + }) .finally(() => { setIsLoading(false); }); @@ -40,6 +46,10 @@ export default function Bookmarks() { loadBookmarks(); }, [client, loadBookmarks]); + if (error) { + return ; + } + return (
Bookmarks
@@ -53,13 +63,17 @@ export default function Bookmarks() {
) : ( -
- {bookmarks.map((bookmark) => ( - - ))} + <> +
    + {bookmarks.map((bookmark) => ( +
  • + +
  • + ))} + +
{next && (
+ )}
); diff --git a/sample-apps/react-demo/app/components/FeedSearchResult.tsx b/sample-apps/react-demo/app/components/FeedSearchResult.tsx index ac9bd1c0..216e756d 100644 --- a/sample-apps/react-demo/app/components/FeedSearchResult.tsx +++ b/sample-apps/react-demo/app/components/FeedSearchResult.tsx @@ -5,6 +5,7 @@ import { } from '@stream-io/feeds-react-sdk'; import { ToggleFollowButton } from './ToggleFollowButton'; import { Avatar } from './utility/Avatar'; +import { NavLink } from './utility/NavLink'; const selector = (state: FeedState) => ({ createdBy: state.created_by }); @@ -12,13 +13,13 @@ export const FeedSearchResult = ({ feed }: { feed: Feed }) => { const { createdBy } = useStateStore(feed.state, selector); return ( -
-
- -
-
- {createdBy?.name} -
+
+ + +
+ {createdBy?.name} +
+
); diff --git a/sample-apps/react-demo/app/components/FollowSuggestions.tsx b/sample-apps/react-demo/app/components/FollowSuggestions.tsx index dce877d2..005a4551 100644 --- a/sample-apps/react-demo/app/components/FollowSuggestions.tsx +++ b/sample-apps/react-demo/app/components/FollowSuggestions.tsx @@ -16,7 +16,7 @@ export const FollowSuggestions = () => {

Who to follow?

-
+
{suggestedFeeds.length === 0 ? (

No suggestions

) : ( diff --git a/sample-apps/react-demo/app/components/ToggleFollowButton.tsx b/sample-apps/react-demo/app/components/ToggleFollowButton.tsx index 25ffab1d..a7474677 100644 --- a/sample-apps/react-demo/app/components/ToggleFollowButton.tsx +++ b/sample-apps/react-demo/app/components/ToggleFollowButton.tsx @@ -4,7 +4,7 @@ import { useFeedsClient, useOwnFollows } from '@stream-io/feeds-react-sdk'; export const ToggleFollowButton = ({ userId }: { userId: string }) => { const client = useFeedsClient(); - const { ownTimeline, ownStoryTimeline } = useOwnFeedsContext(); + const { ownTimeline, ownStoryTimeline, reloadTimelines } = useOwnFeedsContext(); const targetUserFeed = client?.feed('user', userId); @@ -31,25 +31,23 @@ export const ToggleFollowButton = ({ userId }: { userId: string }) => { } }); // Reload timelines to see new activities - await ownTimeline?.getOrCreate({ watch: true, limit: 10 }); - await ownStoryTimeline?.getOrCreate({ watch: true }); - }, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline]); + await reloadTimelines(); + }, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline, reloadTimelines]); const unfollow = useCallback(async () => { if (!targetUserFeed || !targetStoryFeed) { return; } - await ownTimeline?.unfollow(targetUserFeed); + await ownTimeline?.unfollow(targetUserFeed, { delete_notification_activity: true }); await ownStoryTimeline?.unfollow(targetStoryFeed).catch((e) => { if (e instanceof Error && !e.message.includes(`story:`)) { throw e; } }); // Reload timelines to remove activities - await ownTimeline?.getOrCreate({ watch: true }); - await ownStoryTimeline?.getOrCreate({ watch: true }); - }, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline]); + await reloadTimelines(); + }, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline, reloadTimelines]); const toggleFollow = useCallback(() => { if (isFollowing) { @@ -61,9 +59,8 @@ export const ToggleFollowButton = ({ userId }: { userId: string }) => { return (