Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ describe('Activities page', () => {
console.log(response.activity?.parent);
});

it(`restrict replies`, async () => {
await feed.addActivity({
type: 'post',
text: 'apple stock will go up',
restrict_replies: 'people_i_follow', // Options: "everyone", "people_i_follow", "nobody"
});
});

it('restricted visibility', async () => {
const response = await feed.addActivity({
type: 'post',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ describe('Comments page', () => {
});
});

it(`Show/hide comment input`, async () => {
// Lists follow relationships where
// - target feed is owned by current user
// - source feed is owned by activity author
const currentFeed = client.feed(
activity.current_feed?.group_id!,
activity.current_feed?.id!,
);
console.log(currentFeed.currentState.own_followings);

await feed.getOrCreate({
enrichment_options: { enrich_own_followings: true },
});
});

it('Deleting comments', async () => {
await client.deleteComment({
id: comment.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StateStore } from '@stream-io/state-store';
import type { Feed, FeedState } from '../feed';
import { addActivitiesToState, type Feed, type FeedState } from '../feed';
import type { FeedsClient } from '../feeds-client';
import type { ActivityResponse } from '../gen/models';
import {
Expand Down Expand Up @@ -164,8 +164,13 @@ export class ActivityWithStateUpdates {
}) {
this.feed = connectActivityToFeed.call(this.feedsClient, { fid });

const { activities } = addActivitiesToState.bind(this.feed)(
[initialState],
[],
'start',
);
this.feed?.state.partialNext({
activities: [initialState],
activities,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './useAggregatedActivities';
export * from './useIsAggregatedActivityRead';
export * from './useIsAggregatedActivitySeen';
export * from './useActivityComments';
export * from './useOwnFollowings';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useStateStore } from '@stream-io/state-store/react-bindings';

import { useFeedContext } from '../../contexts/StreamFeedContext';
import type { Feed, FeedState } from '../../../../feed';

/**
* A React hook that returns a reactive array of feeds that the feeds's owner is following and is owned by the current user.
*/
export const useOwnFollowings = (feedFromProps?: Feed) => {
const feedFromContext = useFeedContext();
const feed = feedFromProps ?? feedFromContext;

return useStateStore(feed?.state, selector);
};

const selector = ({ own_followings }: FeedState) => ({
own_followings,
});
11 changes: 9 additions & 2 deletions sample-apps/react-sample-app/app/activity/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '@stream-io/feeds-react-sdk';
import {
FeedOwnCapability,
useClientConnectedUser,
useFeedsClient,
useOwnCapabilities,
useStateStore,
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function ActivityPage() {
function ActivityPageContent() {
const params = useParams<{ id: string }>();
const client = useFeedsClient();
const user = useClientConnectedUser();
const { logErrorAndDisplayNotification, logError } = useErrorContext();
const [editedActivityText, setEditedActivityText] = useState('');
const [isEditing, setIsEditing] = useState(false);
Expand All @@ -66,7 +68,7 @@ function ActivityPageContent() {
}, [client, params?.id]);

useEffect(() => {
if (!activityWithStateUpdates) {
if (!activityWithStateUpdates || !user?.id) {
return;
}

Expand All @@ -79,7 +81,11 @@ function ActivityPageContent() {
const [group, id] = fid.split(':');
_feed = client?.feed(group, id);
setFeed(_feed);
if (!_feed?.currentState.watch && !_feed?.currentState.is_loading) {
if (
!(_feed?.id === user.id) &&
!_feed?.currentState.watch &&
!_feed?.currentState.is_loading
) {
shouldStopWatching = true;
return _feed
?.getOrCreate({
Expand All @@ -103,6 +109,7 @@ function ActivityPageContent() {
logError,
activityWithStateUpdates,
client,
user?.id,
]);

const ownCapabilities = useOwnCapabilities(feed);
Expand Down
25 changes: 24 additions & 1 deletion sample-apps/react-sample-app/app/components/NewActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useOwnCapabilities,
} from '@stream-io/feeds-react-sdk';
import { useErrorContext } from '../error-context';
import type { FormEvent} from 'react';
import type { FormEvent } from 'react';
import { useMemo, useState } from 'react';
import { ActivityComposer } from './activity/ActivityComposer';
import { LoadingIndicator } from './LoadingIndicator';
Expand All @@ -18,6 +18,9 @@ export const NewActivity = () => {
const [isSending, setIsSending] = useState<boolean>(false);
const [activityText, setActivityText] = useState('');
const [files, setFiles] = useState<FileList | null>(null);
const [restrictReplies, setRestrictReplies] = useState<
'everyone' | 'people_i_follow' | 'nobody'
>('everyone');

const ownCapabilities = useOwnCapabilities();
const canPost = useMemo(
Expand Down Expand Up @@ -51,6 +54,7 @@ export const NewActivity = () => {
await feed?.addActivity({
type: 'post',
text: activityText,
restrict_replies: restrictReplies,
attachments: fileResponses.map((response, index) => {
const isImage = isImageFile(files![index]);
return {
Expand Down Expand Up @@ -90,6 +94,25 @@ export const NewActivity = () => {
}
}}
/>
<div className="w-full flex items-center gap-2">
<label htmlFor="restrict-replies" className="text-sm text-gray-700">
Who can reply:
</label>
<select
id="restrict-replies"
value={restrictReplies}
onChange={(e) =>
setRestrictReplies(
e.target.value as 'everyone' | 'people_i_follow' | 'nobody',
)
}
className="px-3 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="everyone">Everyone</option>
<option value="people_i_follow">People I follow</option>
<option value="nobody">Nobody</option>
</select>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { ComponentType } from 'react';
import type {
Feed,
FeedState} from '@stream-io/feeds-react-sdk';
import type { Feed, FeedState } from '@stream-io/feeds-react-sdk';
import {
useStateStore,
type ActivityResponse,
Expand All @@ -25,14 +23,16 @@ export type ActivitySearchResultItemProps = {
const selector = (nextValue: FeedState) => ({
own_follows: nextValue.own_follows ?? [],
created_by: nextValue.created_by,
own_followings: nextValue.own_followings ?? [],
});
export const FeedSearchResultItem = ({ item }: FeedSearchResultItemProps) => {
const { ownTimeline } = useOwnFeedsContext();

const { own_follows: ownFollows, created_by: createdBy } = useStateStore(
item.state,
selector,
);
const {
own_follows: ownFollows,
own_followings: ownFollowings,
created_by: createdBy,
} = useStateStore(item.state, selector);

const isFollowing =
ownFollows.some(
Expand All @@ -45,9 +45,14 @@ export const FeedSearchResultItem = ({ item }: FeedSearchResultItemProps) => {
data-testid="search-result-feed"
role="option"
>
<Link className="underline text-blue-500" href={`/users/${item.id}`}>
{createdBy?.name ?? item.id}
</Link>
<div>
<Link className="underline text-blue-500" href={`/users/${item.id}`}>
{createdBy?.name ?? item.id}
</Link>
{ownFollowings.length > 0 && (
<span className="text-sm text-gray-500"> - follows you</span>
)}
</div>
<button
className="text-sm px-2 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { FormEvent} from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type {
Feed,
ActivityWithStateUpdates} from '@stream-io/feeds-react-sdk';
ActivityWithStateUpdates,
} from '@stream-io/feeds-react-sdk';
import {
type ActivityResponse,
type CommentResponse,
useActivityComments,
useClientConnectedUser,
useFeedsClient,
useOwnFollowings,
} from '@stream-io/feeds-react-sdk';
import { PaginatedList } from '../PaginatedList';
import { Comment } from './Comment';
Expand All @@ -24,8 +27,41 @@ export const ActivityCommentSection = ({
activity: ActivityResponse;
}) => {
const client = useFeedsClient();
const currentUser = useClientConnectedUser();
const textareaRef = useRef<HTMLTextAreaElement | null>(null);

const currentFeed = useMemo(() => {
if (!activity.current_feed || !client) {
return undefined;
}
return client.feed(
activity.current_feed.group_id,
activity.current_feed.id,
);
}, [activity.current_feed, client]);

const { own_followings: ownFollowings = [] } =
useOwnFollowings(currentFeed) ?? {};

const canComment = useMemo(() => {
if (currentUser?.id === activity.user.id) {
return true;
}
switch (activity.restrict_replies) {
case 'nobody':
return false;
case 'people_i_follow':
return ownFollowings?.length > 0;
default:
return true;
}
}, [
activity.restrict_replies,
ownFollowings,
currentUser?.id,
activity.user.id,
]);

const {
comments = [],
loadNextPage,
Expand Down Expand Up @@ -103,55 +139,60 @@ export const ActivityCommentSection = ({
</h2>
</div>

{parent && (
<div className="text-black p-2 flex items-center justify-between bg-gray-100 rounded-lg mb-4">
<div className="flex items-center gap-2">
<strong>Replying to:</strong>
{canComment && (
<>
{parent && (
<div className="text-black p-2 flex items-center justify-between bg-gray-100 rounded-lg mb-4">
<div className="flex items-center gap-2">
<strong>Replying to:</strong>
<button
className="text-blue-600 hover:underline"
onClick={() => scrollToComment(parent)}
>
{parent.text}
</button>
</div>
<button
className="ml-2 text-blue-600 hover:underline"
onClick={() => setParent(null)}
>
Cancel reply
</button>
</div>
)}

<form className="mb-6" onSubmit={handleSubmit}>
<div className="py-2 px-4 mb-4 bg-white rounded-lg rounded-t-lg border border-gray-200">
<label htmlFor="comment" className="sr-only">
Your comment
</label>
<textarea
ref={textareaRef}
id="comment"
name="comment-text"
rows={6}
className="px-0 w-full text-sm text-gray-900 border-0 focus:ring-0 focus:outline-none"
placeholder="Write a comment..."
required
/>
</div>
<button
className="text-blue-600 hover:underline"
onClick={() => scrollToComment(parent)}
type="submit"
className=" px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none"
>
{parent.text}
Post comment
</button>
</div>
<button
className="ml-2 text-blue-600 hover:underline"
onClick={() => setParent(null)}
>
Cancel reply
</button>
</div>
</form>
</>
)}

<form className="mb-6" onSubmit={handleSubmit}>
<div className="py-2 px-4 mb-4 bg-white rounded-lg rounded-t-lg border border-gray-200">
<label htmlFor="comment" className="sr-only">
Your comment
</label>
<textarea
ref={textareaRef}
id="comment"
name="comment-text"
rows={6}
className="px-0 w-full text-sm text-gray-900 border-0 focus:ring-0 focus:outline-none"
placeholder="Write a comment..."
required
/>
</div>
<button
type="submit"
className=" px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none"
>
Post comment
</button>
</form>

<PaginatedList
items={comments}
isLoading={isLoadingNextPage}
hasNext={hasNextPage}
renderItem={(c) => (
<Comment
canReply={canComment}
feed={feed}
activityWithStateUpdates={activityWithStateUpdates}
level={0}
Expand Down
Loading
Loading