Skip to content

Commit ee3b2a3

Browse files
committed
example: react demo new features
1 parent 1563665 commit ee3b2a3

File tree

240 files changed

+3599
-5255
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

240 files changed

+3599
-5255
lines changed

README.md

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ Here are some of the features we support:
4040
- **Search & queries**: Activity search, **query activities**, and **query feeds** endpoints.
4141
- **Modern essentials**: Permissions • OpenAPI spec • GDPR endpoints • realtime WebSocket events • push notifications • “own capabilities” API.
4242

43-
## React sample apps
44-
45-
### React demo app with stories
43+
## React demo app
4644

4745
Deployed version: https://feeds-react-demo.vercel.app
4846

@@ -80,27 +78,80 @@ After the above steps run the following command in `sample-apps/react-demo`:
8078
yarn dev
8179
```
8280

83-
### Advanced React app
81+
## Test Data Generator
8482

85-
Prerequisites:
83+
The `test-data-generator` directory contains scripts to populate your Stream Feeds app with sample data for testing and development purposes.
8684

87-
- Install dependencies: `yarn`
88-
- Build React SDK: `yarn build:client` and `yarn build:react-sdk`
89-
- Create a `.env` file in `sample-apps/react-sample-app` with the following content:
85+
### Setup
86+
87+
1. Create a `.env` file in `test-data-generator/` with your credentials:
9088

9189
```
92-
NEXT_PUBLIC_STREAM_API_KEY=<Your API key>
93-
NEXT_API_SECRET=<Your API secret>
94-
NEXT_PUBLIC_API_URL=<Optionally provide an API URL>
90+
STREAM_API_KEY=<Stream API key>
91+
API_SECRET=<Stream API secret>
92+
API_URL=<Optional, Stream API URL>
9593
```
9694

97-
- Run the `node setup-env.js` script in `sample-apps/react-sample-app`
98-
- If you want to have some pre-made posts in your app, optinally run the `node create-posts.js` script as well
95+
2. Install dependencies: `yarn` (from the repository root)
96+
97+
### Available Scripts
9998

100-
After the above steps run the following command in `sample-apps/react-sample-app`:
99+
Run these commands from the `test-data-generator/` directory:
101100

101+
| Script | Command | Description |
102+
| --------------- | ---------------------- | ------------------------------------------ |
103+
| Create Users | `yarn create-users` | Creates users and their feeds |
104+
| Create Follows | `yarn create-follows` | Sets up follow relationships between users |
105+
| Create Posts | `yarn create-posts` | Generates sample activities/posts |
106+
| Create Stories | `yarn create-stories` | Creates sample stories |
107+
| Download Images | `yarn download-images` | Downloads sample images for posts |
108+
109+
### Create Posts Feature Flags
110+
111+
The `create-posts` script supports a `--features` flag to control which features are included in the generated posts:
112+
113+
```bash
114+
yarn create-posts --features <feature1,feature2,...>
102115
```
103-
yarn dev
116+
117+
**Available features:**
118+
119+
| Feature | Description |
120+
| ------------ | ---------------------------------------- |
121+
| `link` | Adds random URLs to posts |
122+
| `attachment` | Uploads and attaches 1-3 images to posts |
123+
| `mention` | Adds @mentions to other users |
124+
| `poll` | Creates and attaches polls to posts |
125+
| `reaction` | Adds 1-5 reactions from random users |
126+
| `comment` | Adds 1-5 comments from random users |
127+
| `bookmark` | Bookmarks posts by random users |
128+
| `repost` | Creates reposts of existing activities |
129+
130+
**Examples:**
131+
132+
```bash
133+
# Create basic posts without any features
134+
yarn create-posts
135+
136+
# Create posts with polls and reactions
137+
yarn create-posts --features poll,reaction
138+
139+
# Create posts with all content features
140+
yarn create-posts --features link,attachment,mention,poll,reaction,comment,bookmark,repost
141+
```
142+
143+
> 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.
144+
145+
### Usage
146+
147+
Typical order of operations:
148+
149+
```bash
150+
cd test-data-generator
151+
yarn create-users
152+
yarn create-follows
153+
yarn create-posts --features link,attachment,mention,poll,reaction,comment,bookmark,repost
154+
yarn create-stories
104155
```
105156

106157
## Local Setup

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"license": "See license in LICENSE",
88
"workspaces": [
99
"packages/*",
10-
"sample-apps/**"
10+
"sample-apps/**",
11+
"test-data-generator"
1112
],
1213
"scripts": {
1314
"build:all": "yarn workspaces foreach -Avp --topological-dev run build",

sample-apps/react-demo/app/ClientApp.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import {
55
StreamFeeds,
66
FeedsClient,
77
} from '@stream-io/feeds-react-sdk';
8+
import * as Sentry from '@sentry/nextjs';
89
import { AppSkeleton } from './AppSkeleton';
910
import { OwnFeedsContextProvider } from './own-feeds-context';
1011
import { FollowSuggestionsContextProvider } from './follow-suggestions-context';
12+
import { ConnectionAlert } from './components/utility/ConnectionAlert';
1113
import { generateUsername } from 'unique-username-generator';
1214
import { useEffect, useMemo, type PropsWithChildren } from 'react';
1315
import { useSearchParams, useRouter } from 'next/navigation';
@@ -60,6 +62,15 @@ export const ClientApp = ({ children }: PropsWithChildren) => {
6062
},
6163
options: {
6264
base_url: process.env.NEXT_PUBLIC_API_URL,
65+
timeout: 10000,
66+
configure_loggers_options: {
67+
default: {
68+
level: 'error',
69+
sink: (...args: any[]) => {
70+
Sentry.captureException(new Error(args.join(' ')));
71+
},
72+
},
73+
},
6374
},
6475
});
6576

@@ -73,6 +84,7 @@ export const ClientApp = ({ children }: PropsWithChildren) => {
7384

7485
return (
7586
<StreamFeeds client={client}>
87+
<ConnectionAlert />
7688
<OwnFeedsContextProvider>
7789
<FollowSuggestionsContextProvider>
7890
<AppSkeleton>{children}</AppSkeleton>

sample-apps/react-demo/app/activity/[id]/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { ActivityInteractions } from '@/app/components/activity/activity-interactions/ActivityInteractions';
44
import { ActivityContent } from '@/app/components/activity/ActivityContent';
55
import { ActivityHeader } from '@/app/components/activity/ActivityHeader';
6+
import { ActivityParent } from '@/app/components/activity/ActivityParent';
67
import { CommentComposer } from '@/app/components/comments/CommentComposer';
78
import { CommentList } from '@/app/components/comments/CommentList';
9+
import { ErrorCard } from '@/app/components/utility/ErrorCard';
810
import { LoadingIndicator } from '@/app/components/utility/LoadingIndicator';
911
import {
1012
StreamActivityWithStateUpdates,
@@ -26,6 +28,7 @@ export default function ActivityPage() {
2628
const [activityWithStateUpdates, setActivityWithStateUpdates] = useState<
2729
ActivityWithStateUpdates | undefined
2830
>();
31+
const [error, setError] = useState<string | undefined>(undefined);
2932

3033
useEffect(() => {
3134
const _activityWithStateUpdates = client?.activityWithStateUpdates(id);
@@ -36,7 +39,10 @@ export default function ActivityPage() {
3639

3740
useEffect(() => {
3841
if (!activityWithStateUpdates?.currentState.activity) {
39-
activityWithStateUpdates?.get();
42+
activityWithStateUpdates?.get().catch((e) => {
43+
setError(e.message);
44+
throw e;
45+
});
4046
}
4147
}, [activityWithStateUpdates]);
4248

@@ -45,6 +51,10 @@ export default function ActivityPage() {
4551
selector,
4652
) ?? { activity: undefined };
4753

54+
if (error) {
55+
return <ErrorCard message="Failed to load activity" error={`${error}. This can happen if the activity was deleted.`} />;
56+
}
57+
4858
if (!activity || !activityWithStateUpdates) {
4959
return (
5060
<div className="flex items-center justify-center h-full w-full">
@@ -58,6 +68,7 @@ export default function ActivityPage() {
5868
<div className="flex-shrink-0 flex flex-col gap-4">
5969
<ActivityHeader activity={activity} withActions={true} />
6070
<ActivityContent activity={activity} />
71+
<ActivityParent activity={activity} />
6172
<ActivityInteractions activity={activity} />
6273
<div className="text-lg font-semibold">Comments</div>
6374
<CommentComposer activity={activity} />

sample-apps/react-demo/app/bookmarks/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ import {
66
} from '@stream-io/feeds-react-sdk';
77
import { useCallback, useEffect, useState } from 'react';
88
import { ActivityPreview } from '../components/activity/ActivityPreview';
9+
import { ErrorCard } from '../components/utility/ErrorCard';
910
import { LoadingIndicator } from '../components/utility/LoadingIndicator';
1011

1112
export default function Bookmarks() {
1213
const client = useFeedsClient();
1314
const [bookmarks, setBookmarks] = useState<BookmarkResponse[]>([]);
1415
const [next, setNext] = useState<string | undefined>(undefined);
1516
const [isLoading, setIsLoading] = useState(false);
17+
const [error, setError] = useState<string | undefined>(undefined);
1618

1719
const loadBookmarks = useCallback(
1820
(nextCursor?: string) => {
1921
setIsLoading(true);
20-
client
22+
return client
2123
?.queryBookmarks({
2224
limit: 20,
2325
next: nextCursor,
@@ -29,6 +31,10 @@ export default function Bookmarks() {
2931
]);
3032
setNext(response.next);
3133
})
34+
.catch((e) => {
35+
setError(e.message);
36+
throw e;
37+
})
3238
.finally(() => {
3339
setIsLoading(false);
3440
});
@@ -40,6 +46,10 @@ export default function Bookmarks() {
4046
loadBookmarks();
4147
}, [client, loadBookmarks]);
4248

49+
if (error) {
50+
return <ErrorCard message="Failed to load bookmarks" error={error} />;
51+
}
52+
4353
return (
4454
<div className="w-full flex flex-col items-center justify-center gap-4">
4555
<div className="text-lg font-semibold w-full">Bookmarks</div>

sample-apps/react-demo/app/components/ToggleFollowButton.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useFeedsClient, useOwnFollows } from '@stream-io/feeds-react-sdk';
44

55
export const ToggleFollowButton = ({ userId }: { userId: string }) => {
66
const client = useFeedsClient();
7-
const { ownTimeline, ownStoryTimeline } = useOwnFeedsContext();
7+
const { ownTimeline, ownStoryTimeline, reloadTimelines } = useOwnFeedsContext();
88

99
const targetUserFeed = client?.feed('user', userId);
1010

@@ -31,9 +31,8 @@ export const ToggleFollowButton = ({ userId }: { userId: string }) => {
3131
}
3232
});
3333
// Reload timelines to see new activities
34-
await ownTimeline?.getOrCreate({ watch: true, limit: 10 });
35-
await ownStoryTimeline?.getOrCreate({ watch: true });
36-
}, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline]);
34+
await reloadTimelines();
35+
}, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline, reloadTimelines]);
3736

3837
const unfollow = useCallback(async () => {
3938
if (!targetUserFeed || !targetStoryFeed) {
@@ -47,9 +46,8 @@ export const ToggleFollowButton = ({ userId }: { userId: string }) => {
4746
}
4847
});
4948
// Reload timelines to remove activities
50-
await ownTimeline?.getOrCreate({ watch: true });
51-
await ownStoryTimeline?.getOrCreate({ watch: true });
52-
}, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline]);
49+
await reloadTimelines();
50+
}, [targetUserFeed, targetStoryFeed, ownTimeline, ownStoryTimeline, reloadTimelines]);
5351

5452
const toggleFollow = useCallback(() => {
5553
if (isFollowing) {

sample-apps/react-demo/app/components/activity/Activity.tsx

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,27 @@ import { type ActivityResponse } from '@stream-io/feeds-react-sdk';
22
import { ActivityHeader } from './ActivityHeader';
33
import { ActivityInteractions } from './activity-interactions/ActivityInteractions';
44
import { ActivityContent } from './ActivityContent';
5+
import { ActivityParent } from './ActivityParent';
56
import { NavLink } from '../utility/NavLink';
67

7-
const ParentActivityPreview = ({ parent }: { parent: ActivityResponse }) => {
8-
return (
9-
<NavLink href={`/activity/${parent.id}`}>
10-
<div className="border border-base-300 rounded-lg p-3 bg-base-200/50 hover:bg-base-200 transition-colors cursor-pointer mb-2">
11-
<div className="flex items-center gap-2 mb-1">
12-
{parent.user.image && (
13-
<img
14-
src={parent.user.image}
15-
alt={parent.user.name || parent.user.id}
16-
className="w-4 h-4 rounded-full"
17-
/>
18-
)}
19-
<span className="text-xs font-medium text-base-content/80">
20-
{parent.user.name || parent.user.id}
21-
</span>
22-
</div>
23-
{parent.text && (
24-
<p className="text-sm text-base-content/70 line-clamp-2">{parent.text}</p>
25-
)}
26-
{!parent.text && parent.attachments.length > 0 && (
27-
<p className="text-sm text-base-content/50 italic">Media attachment</p>
28-
)}
29-
</div>
30-
</NavLink>
31-
);
32-
};
33-
348
export const Activity = ({
359
activity,
3610
location,
3711
}: {
3812
activity: ActivityResponse;
3913
location: 'timeline' | 'profile' | 'foryou' | 'preview' | 'search';
4014
}) => {
41-
const showParentPreview = activity.parent && location !== 'preview';
4215

4316
return (
4417
<div className="w-full flex flex-col items-start gap-4">
4518
<ActivityHeader
4619
activity={activity}
4720
withFollowButton={location === 'foryou'}
48-
withLink={location === 'timeline' || location === 'profile' || location === 'search'}
21+
withLink={location === 'timeline' || location === 'profile' || location === 'search' || location === 'foryou'}
4922
withActions={location === 'timeline' || location === 'profile'}
5023
/>
51-
<ActivityContent activity={activity} />
52-
{showParentPreview && activity.parent && (
53-
<div className="w-full">
54-
<div className="text-xs text-base-content/60 mb-1 flex items-center gap-1">
55-
<span className="material-symbols-outlined text-[0.75rem]">repeat</span>
56-
<span>Reposted</span>
57-
</div>
58-
<ParentActivityPreview parent={activity.parent} />
59-
</div>
60-
)}
24+
<ActivityContent activity={activity} withoutLinks={location === 'preview'} />
25+
{activity?.parent ? (location === 'preview' ? <ActivityParent activity={activity} /> : <NavLink className="w-full min-w-0 max-w-full" href={`/activity/${activity.parent?.id}`}><ActivityParent activity={activity} /></NavLink>) : null}
6126
{location !== 'preview' && <ActivityInteractions activity={activity} />}
6227
</div>
6328
);

sample-apps/react-demo/app/components/activity/ActivityComposer.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ export const ActivityComposer = ({
5858
[feed, client, activity?.id, parent?.id, onSave],
5959
);
6060

61-
const isReply = !!parent;
62-
6361
return (
6462
<div className="w-full flex flex-col gap-3">
6563
{parent && (
@@ -77,8 +75,6 @@ export const ActivityComposer = ({
7775
initialAttachments={initialAttachments}
7876
initialMentionedUsers={initialMentionedUsers}
7977
onSubmit={handleSubmit}
80-
submitLabel={activity?.id ? 'Save' : isReply ? 'Repost' : 'Post'}
81-
placeholder={isReply ? 'Add a comment...' : undefined}
8278
textareaBorder={textareaBorder}
8379
/>
8480
</div>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { type ActivityResponse } from "@stream-io/feeds-react-sdk";
22
import { Content } from "../common/Content";
33

4-
export const ActivityContent = ({ activity }: { activity: ActivityResponse }) => {
4+
export const ActivityContent = ({ activity, withoutLinks = false }: { activity: ActivityResponse, withoutLinks?: boolean }) => {
55
return (
6-
<Content text={activity.text} attachments={activity.attachments} moderation={activity.moderation} location="activity" mentioned_users={activity.mentioned_users} />
6+
<Content text={activity.text} attachments={activity.attachments} moderation={activity.moderation} location="activity" mentioned_users={activity.mentioned_users} withoutLinks={withoutLinks} />
77
);
88
};

0 commit comments

Comments
 (0)