# Stories

Stories are activities with an expiration date, usually used to share information that is available for a limited amount of time. Most often there is a dedicated feed for users to post expiring activities, and there is a separate timeline for reading stories.

## Built-in story groups

There are two built-in story groups:

- `story` for users to post their stories to
- `stories` for users to follow others' stories

It's possible to update the built-in story groups or create custom ones.

## Creating story groups

If you prefer to create your own story feed groups, you can do so with custom feed groups.

<admonition type="info">

Creating or updating feed groups is only possible server-side.

</admonition>

The following example defines two feed groups:

- `my-story` for users to post their stories to
- `my-stories` for users to follow others' stories

There are two settings specific to story feed groups:

- You can enable tracking watched activities: this makes it possible to track which stories a user has seen or not (please note that this is different from notification configurations, typically you don't need that for stories)
- You can decide if watched stories should be removed from the feed. It's `false` for the built-in story groups

<Tabs>

```js label="Node"
await serverClient.feeds.createFeedGroup({
  id: `my-story`,
  stories: {
    skip_watched: false,
    track_watched: true,
  },
});

await serverClient.feeds.createFeedGroup({
  id: `my-stories`,
  stories: {
    skip_watched: false,
    track_watched: true,
  },
  aggregation: {
    // Typically stories are aggregated by user id
    format: "{{ user_id }}",
  },
  activity_selectors: [{ type: "following" }],
});
```

```php label="php"
$feedsClient->createFeedGroup(
    new GeneratedModels\CreateFeedGroupRequest(
        id: 'my-story',
        stories: new GeneratedModels\StoriesConfig(
            skipWatched: false,
            trackWatched: true
        )
    )
);

$feedsClient->createFeedGroup(
    new GeneratedModels\CreateFeedGroupRequest(
        id: 'my-stories',
        stories: new GeneratedModels\StoriesConfig(
            skipWatched: false,
            trackWatched: true
        ),
        aggregation: new GeneratedModels\AggregationConfig(
            format: '{{ user_id }}'
        ),
        activitySelectors: [
            ['type' => 'following']
        ]
    )
);
```

```go label="Go"
ctx := context.Background()

// Create my-story feed group
_, err := client.Feeds().CreateFeedGroup(ctx, &getstream.CreateFeedGroupRequest{
    ID: "my-story",
    Stories: &getstream.StoriesConfig{
        SkipWatched: getstream.PtrTo(false),
        TrackWatched: getstream.PtrTo(true),
    },
})
if err != nil {
    log.Fatal("Error creating feed group:", err)
}

// Create my-stories feed group
_, err = client.Feeds().CreateFeedGroup(ctx, &getstream.CreateFeedGroupRequest{
    ID: "my-stories",
    Stories: &getstream.StoriesConfig{
        SkipWatched: getstream.PtrTo(false),
        TrackWatched: getstream.PtrTo(true),
    },
    Aggregation: &getstream.AggregationConfig{
        Format: getstream.PtrTo("{{ user_id }}"),
    },
    ActivitySelectors: []getstream.ActivitySelectorConfig{
        {Type: getstream.PtrTo("following")},
    },
})
if err != nil {
    log.Fatal("Error creating feed group:", err)
}
```

```java label="Java"
import io.getstream.services.FeedsImpl;
import io.getstream.models.*;

FeedsImpl feedsClient = new FeedsImpl(new StreamHTTPClient("<API key>", "<API secret>"));

// Create my-story feed group
CreateFeedGroupRequest storyRequest = CreateFeedGroupRequest.builder()
    .id("my-story")
    .stories(StoriesConfig.builder()
        .skipWatched(false)
        .trackWatched(true)
        .build())
    .build();
feedsClient.createFeedGroup(storyRequest).execute();

// Create my-stories feed group
CreateFeedGroupRequest storiesRequest = CreateFeedGroupRequest.builder()
    .id("my-stories")
    .stories(StoriesConfig.builder()
        .skipWatched(false)
        .trackWatched(true)
        .build())
    .aggregation(AggregationConfig.builder()
        .format("{{ user_id }}")
        .build())
    .activitySelectors(List.of(
        ActivitySelectorConfig.builder().type("following").build()
    ))
    .build();
feedsClient.createFeedGroup(storiesRequest).execute();
```

```csharp label="C#"
// Create my-story feed group
var storyRequest = new CreateFeedGroupRequest
{
    ID = "my-story",
    Stories = new StoriesConfig
    {
        SkipWatched = false,
        TrackWatched = true
    }
};
await _feedsV3Client.CreateFeedGroupAsync(storyRequest);

// Create my-stories feed group
var storiesRequest = new CreateFeedGroupRequest
{
    ID = "my-stories",
    Stories = new StoriesConfig
    {
        SkipWatched = false,
        TrackWatched = true
    },
    Aggregation = new AggregationConfig
    {
        Format = "{{ user_id }}"
    },
    ActivitySelectors = new List<ActivitySelectorConfig>
    {
        new() { Type = "following" }
    }
};
await _feedsV3Client.CreateFeedGroupAsync(storiesRequest);
```

```python label="Python"
# Create my-story feed group
story_request = {
    "id": "my-story",
    "stories": {
        "skip_watched": False,
        "track_watched": True
    }
}
self.client.feeds.create_feed_group(**story_request)

# Create my-stories feed group
stories_request = {
    "id": "my-stories",
    "stories": {
        "skip_watched": False,
        "track_watched": True
    },
    "aggregation": {
        "format": "{{ user_id }}"
    },
    "activity_selectors": [
        {"type": "following"}
    ]
}
self.client.feeds.create_feed_group(**stories_request)
```

```ruby label="Ruby"
require 'getstream_ruby'

client = GetStreamRuby.manual(
  api_key: 'api_key',
  api_secret: 'api_secret'
)

# Create my-story feed group
story_request = GetStream::Generated::Models::CreateFeedGroupRequest.new(
  id: 'my-story',
  stories: GetStream::Generated::Models::StoriesConfig.new(
    skip_watched: false,
    track_watched: true
  )
)
client.feeds.create_feed_group(story_request)

# Create my-stories feed group
stories_request = GetStream::Generated::Models::CreateFeedGroupRequest.new(
  id: 'my-stories',
  stories: GetStream::Generated::Models::StoriesConfig.new(
    skip_watched: false,
    track_watched: true
  ),
  aggregation: GetStream::Generated::Models::AggregationConfig.new(
    format: '{{ user_id }}'
  ),
  activity_selectors: [
    GetStream::Generated::Models::ActivitySelectorConfig.new(type: 'following')
  ]
)
client.feeds.create_feed_group(stories_request)
```

</Tabs>

## Posting stories

Usually stories are activities containing a single image or video. But you can implement them any way you want.

<Tabs>

```kotlin label="Kotlin"
val johnStoryFeed = client.feed(group = "story", id = "john")
johnStoryFeed.getOrCreate()

val tomorrow = Calendar.getInstance().apply {
    add(Calendar.DAY_OF_YEAR, 1)
}

johnStoryFeed.addActivity(
    request = FeedAddActivityRequest(
        type = "post",
        attachments = listOf(
            Attachment(
                imageUrl = "https://example.com/image.jpg",
            )
        ),
        expiresAt = tomorrow.toInstant().toString()
    )
)
```

```js label="JavaScript"
const johnStoryFeed = client.feed("story", "john");
await johnStoryFeed.getOrCreate();

let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

await johnStoryFeed.addActivity({
  type: "post",
  attachments: [
    {
      image_url: "https://example.com/image.jpg",
      custom: {},
    },
  ],
  expires_at: tomorrow.toISOString(),
});
```

```js label="React"
const johnStoryFeed = client.feed("story", "john");
await johnStoryFeed.getOrCreate();

let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

await johnStoryFeed.addActivity({
  type: "post",
  attachments: [
    {
      image_url: "https://example.com/image.jpg",
      custom: {},
    },
  ],
  expires_at: tomorrow.toISOString(),
});
```

```js label="React Native"
const johnStoryFeed = client.feed("story", "john");
await johnStoryFeed.getOrCreate();

let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

await johnStoryFeed.addActivity({
  type: "post",
  attachments: [
    {
      image_url: "https://example.com/image.jpg",
      custom: {},
    },
  ],
  expires_at: tomorrow.toISOString(),
});
```

```js label="Node"
const johnStoryFeed = client.feeds.feed("story", "john");
await johnStoryFeed.getOrCreate({
  user_id: "john",
});

let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

await client.feeds.addActivity({
  type: "post",
  feeds: ["story:john"],
  attachments: [
    {
      image_url: "https://example.com/image.jpg",
      custom: {},
    },
  ],
  expires_at: tomorrow.toISOString(),
  user_id: "john",
});
```

```php label="php"
$johnStoryFeed = $feedsClient->feed('story', 'john');
$johnStoryFeed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(userID: 'john')
);

$tomorrow = new DateTime();
$tomorrow->modify('+1 day');

$feedsClient->addActivity(
    new GeneratedModels\AddActivityRequest(
        type: 'post',
        feeds: ['story:john'],
        attachments: [
            [
                'image_url' => 'https://example.com/image.jpg',
                'custom' => (object)[]
            ]
        ],
        expiresAt: $tomorrow->format('c'),
        userID: 'john'
    )
);
```

</Tabs>

Activities on story feeds are ordered cronologically.

## Following story feeds

Following someone's story feed is the same as following any other feed:

<Tabs>

```kotlin label="Kotlin"
val saraStoryTimeline = client.feed(
    query = FeedQuery(group = "stories", id = "sara", watch = true)
)
saraStoryTimeline.getOrCreate()

saraStoryTimeline.follow(johnStoryFeed.fid)
```

```js label="JavaScript"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

await saraStoryTimeline.follow(johnStoryFeed);
```

```js label="React"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

await saraStoryTimeline.follow(johnStoryFeed);
```

```js label="React Native"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

await saraStoryTimeline.follow(johnStoryFeed);
```

```js label="Node"
await client.feeds.follow({ source: "stories:sara", target: "story:john" });
```

```php label="php"
$saraStoryTimeline = $feedsClient->feed('stories', 'sara');
$saraStoryTimeline->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(userID: 'sara')
);

$feedsClient->follow(
    new GeneratedModels\FollowRequest(
        source: 'stories:sara',
        target: 'story:john'
    )
);
```

</Tabs>

## Reading story timeline and marking activities as watched

<Tabs>

```kotlin label="Kotlin"
val saraStoryTimeline = client.feed(
    query = FeedQuery(group = "stories", id = "sara", watch = true)
)
saraStoryTimeline.getOrCreate()

// Since story timeline is aggregated by user id, we read aggregated activities
val johnStories = saraStoryTimeline.state.aggregatedActivities.value[0]

// Display all of John's active stories
johnStories.activities.forEach { activity ->
    // True if we watched a given story
    println(activity.isWatched)
}

// Mark a story as watched
saraStoryTimeline.markActivity(
    MarkActivityRequest(
        markWatched = listOf(johnStories.activities[0].id)
    )
)
```

```js label="JavaScript"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

// Since story timeline is aggregated by user id, we read aggregated activities
const johnStories =
  saraStoryTimeline.state.getLatestValue().aggregated_activities[0];
// True if we watched all active stories of a user
console.log(johnStories.is_watched);

// Display all of John's active stories
johnStories.activities.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as read
await saraStoryTimeline.markActivity({
  mark_watched: [johnStories.activities[0].id],
});
```

```js label="React"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

const { aggregated_activities } =
  useAggregatedActivities(saraStoryTimeline) ?? {};

// Since story timeline is aggregated by user id, we read aggregated activities
const johnStories = aggregated_activities[0];
// True if we watched all active stories of a user
console.log(johnStories.is_watched);

// Display all of John's active stories
johnStories.activities.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as read
await saraStoryTimeline.markActivity({
  mark_watched: [johnStories.activities[0].id],
});
```

```js label="React Native"
const saraStoryTimeline = client.feed("stories", "sara");
await saraStoryTimeline.getOrCreate({ watch: true });

const { aggregated_activities } =
  useAggregatedActivities(saraStoryTimeline) ?? {};

// Since story timeline is aggregated by user id, we read aggregated activities
const johnStories = aggregated_activities[0];
// True if we watched all active stories of a user
console.log(johnStories.is_watched);

// Display all of John's active stories
johnStories.activities.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as read
await saraStoryTimeline.markActivity({
  mark_watched: [johnStories.activities[0].id],
});
```

```js label="Node"
const saraStoryTimeline = client.feeds.feed("stories", "sara");
const response = await saraStoryTimeline.getOrCreate({ user_id: "sara" });

// Since story timeline is aggregated by user id, we read aggregated activities
const johnStories = response.aggregated_activities[0];
// True if we watched all active stories of a user
console.log(johnStories.is_watched);

// Display all of John's active stories
johnStories.activities.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as read
await saraStoryTimeline.markActivity({
  mark_watched: [johnStories.activities[0].id],
  user_id: "sara",
});
```

```php label="php"
$saraStoryTimeline = $feedsClient->feed('stories', 'sara');
$response = $saraStoryTimeline->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(userID: 'sara')
);

// Since story timeline is aggregated by user id, we read aggregated activities
$johnStories = $response->getData()->aggregatedActivities[0];
// True if we watched all active stories of a user
echo $johnStories->isWatched ? 'true' : 'false';

// Display all of John's active stories
foreach ($johnStories->activities as $activity) {
    // True if we watched a given story
    echo $activity->isWatched ? 'true' : 'false';
}

// Mark a story as read
$saraStoryTimeline->markActivity(
    new GeneratedModels\MarkActivityRequest(
        markWatched: [$johnStories->activities[0]->id],
        userID: 'sara'
    )
);
```

</Tabs>

Story groups in story timelines are sorted by unread count. Watched story groups are returned last (unless the feed group is configured not to return watched stories).

When marking an activity as watched, `feeds.stories_feed.updated` WebSocket event is dispatched to clients of the user who marked the activity (Sara in this example). There are no WebSocket events sent when a user adds to their story, you need to reread the feed to get updates.

While there is no limit of how many active stories a user can have, `aggregated_activities` return the latest 100 activities for each group.

Pagination for story groups work the same way as for any other feeds. This is how you can load the next page of story groups:

<Tabs>

```swift label="Swift"
let feed = client.feed(
    for: .init(
        group: "user",
        id: "john",
        activityLimit: 10
    )
)
// Page 1
try await feed.getOrCreate()
let activities = feed.state.activities // First 10 activities

// Page 2
let page2Activities = try await feed.queryMoreActivities(limit: 10)

let page1And2Activities = feed.state.activities
```

```kotlin label="Kotlin"
val feed = client.feed(
    query = FeedQuery(
        group = "user",
        id = "john",
        activityLimit = 10
    )
)
// Page 1
feed.getOrCreate()
val activities = feed.state.activities // The flow emits the first 10 activities

// Page 2
val page2Activities: Result<List<ActivityData>> = feed.queryMoreActivities(limit = 10)

val page1And2Activities = feed.state.activities
```

```js label="JavaScript"
const feed = client.feed("user", "jack");

// First page
await feed.getOrCreate({
  limit: 10,
});

// Second page
await feed.getNextPage();

console.log(feed.state.getLatestValue().is_loading_activities);
// Truthy if feed has next page
console.log(feed.state.getLatestValue().next);
console.log(feed.state.getLatestValue().activities);
// Only if feed group has aggregation turned on
console.log(feed.state.getLatestValue().aggregated_activities);
```

```js label="React"
const feed = client.feed("user", "jack");

// First page
await feed.getOrCreate({
  limit: 10,
});

const { activities, loadNextPage, is_loading, has_next_page } =
  useFeedActivities(feed) ?? {};
// Only if feed group has aggregation turned on
const { aggregated_activities, is_loading, has_next_page } =
  useAggregatedActivities(feed) ?? {};
```

```js label="React Native"
const feed = client.feed("user", "jack");

// First page
await feed.getOrCreate({
  limit: 10,
});

const { activities, loadNextPage, is_loading, has_next_page } =
  useFeedActivities(feed) ?? {};
// Only if feed group has aggregation turned on
const { aggregated_activities, is_loading, has_next_page } =
  useAggregatedActivities(feed) ?? {};
```

```dart label="Dart"
final feed = client.feedFromQuery(
  const FeedQuery(
    fid: FeedId(group: 'user', id: 'john'),
    activityLimit: 10,
  ),
);

// Page 1
await feed.getOrCreate();
final activities = feed.state.activities; // First 10 activities

// Page 2
final page2Activities = await feed.queryMoreActivities(limit: 10);

final page1And2Activities = feed.state.activities;
```

```js label="Node"
const feed = client.feeds.feed("user", "jack");
const firstPage = await feed.getOrCreate({
  limit: 10,
  user_id: "user_id",
});

const nextPage = await feed.getOrCreate({
  next: firstPage.next,
  limit: 10,
  user_id: "user_id",
});
```

```go label="Go"
feed := client.Feeds().Feed("user", "john")

// First page
firstPage, err := feed.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{
  Limit:  getstream.PtrTo(10),
  UserID: getstream.PtrTo("john"),
})
if err != nil {
  log.Fatal("Error getting first page:", err)
}

// Second page request using next cursor
nextPage, err := feed.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{
  Next:   firstPage.Data.Next,
  Limit:  getstream.PtrTo(10),
  UserID: getstream.PtrTo("john"),
})
if err != nil {
  log.Fatal("Error getting next page:", err)
}

log.Printf("First page activities count: %d", len(firstPage.Data.Activities))
log.Printf("Next page activities count: %d", len(nextPage.Data.Activities))
```

```java label="Java"
testFeed = new Feed("user", testUserId, feeds);
testFeed2 = new Feed("user", testUserId2, feeds);

GetOrCreateFeedRequest feedRequest1 =
    GetOrCreateFeedRequest.builder().userID(testUserId).build();
GetOrCreateFeedRequest feedRequest2 =
    GetOrCreateFeedRequest.builder().userID(testUserId2).build();

GetOrCreateFeedResponse feedResponse1 = testFeed.getOrCreate(feedRequest1).getData();
GetOrCreateFeedResponse feedResponse2 = testFeed2.getOrCreate(feedRequest2).getData();
testFeedId = feedResponse1.getFeed().getFeed();
testFeedId2 = feedResponse2.getFeed().getFeed();
```

```php label="php"
$feed = $feedsClient->feed('user', 'jack');

$feedResponse1 = $feed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(userID: "jack", limit: 10)
);

$feedResponse2 = $feed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(userID: "jack", limit: 10, next: $feedResponse1->getData()->next)
);
```

```csharp label="C#"
var feedResponse1 = await _feedsV3Client.GetOrCreateFeedAsync(
    FeedGroupID: "user",
    FeedID: _testFeedId,
    request: new GetOrCreateFeedRequest { UserID = _testUserId }
);
var feedResponse2 = await _feedsV3Client.GetOrCreateFeedAsync(
    FeedGroupID: "user",
    FeedID: _testFeedId2,
    request: new GetOrCreateFeedRequest { UserID = _testUserId2 }
);
```

```python label="Python"
feed_response_1 = self.test_feed.get_or_create(user_id=self.test_user_id)
feed_response_2 = self.test_feed_2.get_or_create(
    user_id=self.test_user_id_2
)
```

```ruby label="Ruby"
feed = client.feed('user', 'jack')

# First page
first_page = feed.get_or_create_feed(
  GetStream::Generated::Models::GetOrCreateFeedRequest.new(
    user_id: 'jack',
    limit: 10
  )
)

# Second page using next cursor from first page
if first_page.next
  second_page = feed.get_or_create_feed(
    GetStream::Generated::Models::GetOrCreateFeedRequest.new(
      user_id: 'jack',
      limit: 10,
      next: first_page.next
    )
  )
end
```

</Tabs>


## Reading story feed

Following someone's story feed is one way to read stories. However, it's also possible to read someone's story feed directly:

<Tabs>

```kotlin label="Kotlin"
// Sara reads John's story feed
// By default new activities are added to the start of the list, but this is not what we want for stories
val johnStoryFeed = saraClient.feed(
    query = FeedQuery(group = "story", id = "john", activityLimit = 100, watch = true),
    onNewActivity = { _, _, _ -> InsertionAction.AddToEnd }
)
johnStoryFeed.getOrCreate()

val johnStories = johnStoryFeed.state.activities.value

// Display all of John's active stories
johnStories.forEach { activity ->
    // True if we watched a given story
    println(activity.isWatched)
}

// Mark a story as watched
johnStoryFeed.markActivity(
    MarkActivityRequest(
        markWatched = listOf(johnStories[0].id)
    )
)
```

```js label="JavaScript"
// Sara reads John's story feed
const johnStoryFeed = saraClient.feed("story", "john", {
  // By default new activities are added to the start of the list, but this is not what we want for stories
  onNewActivity: () => "add-to-end",
});

// Alternatively set after feed is created
johnStoryFeed.onNewActivity = () => "add-to-end";

await johnStoryFeed.getOrCreate({
  watch: true,
  limit: 100,
});

const johnStories = johnStoryFeed.state.getLatestValue().activities;

// Display all of John's active stories
johnStories.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as watched
await johnStoryFeed.markActivity({
  mark_watched: [johnStories[0].id],
});
```

```js label="React"
// Sara reads John's story feed
const johnStoryFeed = saraClient.feed("story", "john", {
  // By default new activities are added to the start of the list, but this is not what we want for stories
  onNewActivity: () => "add-to-end",
});

// Alternatively set after feed is created
johnStoryFeed.onNewActivity = () => "add-to-end";

await johnStoryFeed.getOrCreate({
  watch: true,
  limit: 100,
});

const {
  activities: johnStories,
  is_loading,
  has_next_page,
  loadNextPage,
} = useFeedActivities(feed) ?? {};

// Display all of John's active stories
johnStories.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as watched
await johnStoryFeed.markActivity({
  mark_watched: [johnStories[0].id],
});
```

```js label="React Native"
// Sara reads John's story feed
const johnStoryFeed = saraClient.feed("story", "john", {
  // By default new activities are added to the start of the list, but this is not what we want for stories
  onNewActivity: () => "add-to-end",
});

// Alternatively set after feed is created
johnStoryFeed.onNewActivity = () => "add-to-end";

await johnStoryFeed.getOrCreate({
  watch: true,
  limit: 100,
});

const {
  activities: johnStories,
  is_loading,
  has_next_page,
  loadNextPage,
} = useFeedActivities(feed) ?? {};

// Display all of John's active stories
johnStories.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as watched
await johnStoryFeed.markActivity({
  mark_watched: [johnStories[0].id],
});
```

```js label="Node"
const johnStoryFeed = client.feeds.feed("story", "john");

const response = await johnStoryFeed.getOrCreate({
  limit: 100,
  user_id: "john",
});

const johnStories = response.activities;

johnStories.forEach((activity) => {
  // True if we watched a given story
  console.log(activity.is_watched);
});

// Mark a story as watched
await johnStoryFeed.markActivity({
  mark_watched: [johnStories[0].id],
  user_id: "sara",
});
```

```php label="php"
// Sara reads John's story feed
$johnStoryFeed = $feedsClient->feed('story', 'john');

$response = $johnStoryFeed->getOrCreateFeed(
    new GeneratedModels\GetOrCreateFeedRequest(
        userID: 'john',
        limit: 100
    )
);

$johnStories = $response->getData()->activities;

// Display all of John's active stories
foreach ($johnStories as $activity) {
    // True if we watched a given story
    echo $activity->isWatched ? 'true' : 'false';
}

// Mark a story as watched
$johnStoryFeed->markActivity(
    new GeneratedModels\MarkActivityRequest(
        markWatched: [$johnStories[0]->id],
        userID: 'sara'
    )
);
```

</Tabs>

When marking an activity as watched, `feeds.stories_feed.updated` WebSocket event is dispatched to clients of the user who marked the activity (Sara in this example). If a new story is added to the feed, a `feeds.activity.added` event is dispatched.

## Reading expired stories

Users can list the expired stories that they created, but it's not possible to read other users' expired stories:

<Tabs>

```kotlin label="Kotlin"
val now = Date()
val query = ActivitiesQuery(
    filter = Filters.and(
        ActivitiesFilterField.expiresAt.lessOrEqual(now.toInstant().toString()),
        ActivitiesFilterField.userId.equal(john.id)
    ),
    sort = listOf(ActivitiesSort(ActivitiesSortField.CreatedAt, SortDirection.REVERSE))
)
val activityList = client.activityList(query)
val result = activityList.get()
```

```js label="JavaScript"
const now = new Date();
const result = await client.queryActivities({
  filter: {
    expires_at: { $lte: now.toISOString() },
    user_id: john.id,
  },
  sort: [{ field: "created_at", direction: -1 }],
});
```

```js label="React"
const now = new Date();
const result = await client.queryActivities({
  filter: {
    expires_at: { $lte: now.toISOString() },
    user_id: john.id,
  },
  sort: [{ field: "created_at", direction: -1 }],
});
```

```js label="React Native"
const now = new Date();
const result = await client.queryActivities({
  filter: {
    expires_at: { $lte: now.toISOString() },
    user_id: john.id,
  },
  sort: [{ field: "created_at", direction: -1 }],
});
```

```js label="Node"
const now = new Date();
const result = await client.feeds.queryActivities({
  filter: {
    expires_at: { $lte: now.toISOString() },
    user_id: john.id,
  },
  sort: [{ field: "created_at", direction: -1 }],
});
```

```php label="php"
$now = new DateTime();
$result = $feedsClient->queryActivities(
    new GeneratedModels\QueryActivitiesRequest(
        filter: (object)[
            'expires_at' => ['$lte' => $now->format('c')],
            'user_id' => $john->id
        ],
        sort: [
            ['field' => 'created_at', 'direction' => -1]
        ]
    )
);
```

</Tabs>


---

This page was last updated at 2026-05-08T12:56:50.179Z.

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/dotnet-csharp/stories/](https://getstream.io/activity-feeds/docs/dotnet-csharp/stories/).