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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import java.util.Date
* @property updatedAt The date and time when this aggregation was last updated.
* @property userCount The number of unique users involved in these activities.
* @property userCountTruncated Indicates if the user count is truncated.
* @property isRead Whether this group has been read. Relevant for notification feeds.
* @property isSeen Whether this group has been seen. Relevant for notification feeds.
* @property isWatched Whether this group was watched. Relevant for stories.
*/
public data class AggregatedActivityData(
public val activities: List<ActivityData>,
Expand All @@ -42,6 +45,9 @@ public data class AggregatedActivityData(
public val updatedAt: Date,
public val userCount: Int,
public val userCountTruncated: Boolean,
public val isRead: Boolean?,
public val isSeen: Boolean?,
public val isWatched: Boolean?,
) {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ internal fun AggregatedActivityResponse.toModel(): AggregatedActivityData {
updatedAt = updatedAt,
userCount = userCount,
userCountTruncated = userCountTruncated,
isRead = isRead,
isSeen = isSeen,
isWatched = isWatched,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import io.getstream.feeds.android.client.internal.utils.updateIf
import io.getstream.feeds.android.client.internal.utils.upsert
import io.getstream.feeds.android.client.internal.utils.upsertAll
import io.getstream.feeds.android.network.models.NotificationStatusResponse
import java.util.Date
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -293,9 +294,11 @@ internal class FeedStateImpl(
it.isFollowRequest && it.targetFeed.fid == fid -> {
newRequests += it
}

it.isFollowing(fid) -> {
newFollowing += it
}

it.isFollowerOf(fid) -> {
newFollowers += it
}
Expand Down Expand Up @@ -366,10 +369,44 @@ internal class FeedStateImpl(
aggregatedActivities: List<AggregatedActivityData>,
notificationStatus: NotificationStatusResponse?,
) {
notificationStatus?.let(::updateReadSeenStatus)
updateAggregatedActivities(aggregatedActivities, prependNew = true)
_notificationStatus.update { notificationStatus }
}

private fun updateReadSeenStatus(notificationStatus: NotificationStatusResponse) {
val lastReadAt = notificationStatus.lastReadAt
val lastSeenAt = notificationStatus.lastSeenAt
val readIds = notificationStatus.readActivities.orEmpty()
val seenIds = notificationStatus.seenActivities.orEmpty()

updateActivitiesWhere({ true }) { activity ->
val isRead = activity.id.isMarked(activity.updatedAt, lastReadAt, readIds)
val isSeen = activity.id.isMarked(activity.updatedAt, lastSeenAt, seenIds)

if (activity.isRead != isRead || activity.isSeen != isSeen) {
activity.copy(isRead = isRead, isSeen = isSeen)
} else {
activity
}
}
_aggregatedActivities.update { current ->
current.map { group ->
val isRead = group.group.isMarked(group.updatedAt, lastReadAt, readIds)
val isSeen = group.group.isMarked(group.updatedAt, lastSeenAt, seenIds)

if (group.isRead != isRead || group.isSeen != isSeen) {
group.copy(isRead = isRead, isSeen = isSeen)
} else {
group
}
}
}
}

private fun String.isMarked(updated: Date, lastMarked: Date?, markedIds: List<String>) =
(lastMarked != null && updated.before(lastMarked)) || this in markedIds

override fun onStoriesFeedUpdated(
activities: List<ActivityData>,
aggregatedActivities: List<AggregatedActivityData>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import io.getstream.feeds.android.client.internal.test.TestData.pollVoteData
import io.getstream.feeds.android.network.models.FeedOwnCapability
import io.getstream.feeds.android.network.models.NotificationStatusResponse
import io.mockk.mockk
import java.util.Date
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
Expand Down Expand Up @@ -605,6 +606,8 @@ internal class FeedStateImplTest {
activityCount = it,
group = "group-$it",
userCount = it,
isRead = false,
isSeen = false,
)
}

Expand Down Expand Up @@ -635,6 +638,77 @@ internal class FeedStateImplTest {
assertEquals(notificationStatus, feedState.notificationStatus.value)
}

@Test
fun `onNotificationFeedUpdated, update isRead and isSeen from notification status`() = runTest {
val old = activityData("old", updatedAt = 1000)
val recent = activityData("recent", updatedAt = 3000)
val markedById = activityData("marked-by-id", updatedAt = 5000)
setupInitialState(activities = listOf(old, recent, markedById))

val notificationStatus =
NotificationStatusResponse(
unread = 0,
unseen = 1,
lastReadAt = Date(2000),
lastSeenAt = Date(4000),
readActivities = listOf("marked-by-id"),
seenActivities = listOf("marked-by-id"),
)

feedState.onNotificationFeedUpdated(emptyList(), notificationStatus)

val expected =
listOf(
activityData("old", updatedAt = 1000, isRead = true, isSeen = true),
activityData("recent", updatedAt = 3000, isRead = false, isSeen = true),
activityData("marked-by-id", updatedAt = 5000, isRead = true, isSeen = true),
)
assertEquals(expected, feedState.activities.value)
}

@Test
fun `onNotificationFeedUpdated, update isRead and isSeen on aggregated activities`() = runTest {
val oldGroup = aggregatedActivityData(group = "group-old", updatedAt = Date(1000))
val recentGroup = aggregatedActivityData(group = "group-recent", updatedAt = Date(3000))
val markedByGroup = aggregatedActivityData(group = "group-marked", updatedAt = Date(5000))
setupInitialState(aggregatedActivities = listOf(oldGroup, recentGroup, markedByGroup))

val notificationStatus =
NotificationStatusResponse(
unread = 0,
unseen = 1,
lastReadAt = Date(2000),
lastSeenAt = Date(4000),
readActivities = listOf("group-marked"),
seenActivities = listOf("group-marked"),
)

feedState.onNotificationFeedUpdated(emptyList(), notificationStatus)

val expected =
listOf(
aggregatedActivityData(
group = "group-old",
updatedAt = Date(1000),
isRead = true,
isSeen = true,
),
aggregatedActivityData(
group = "group-recent",
updatedAt = Date(3000),
isRead = false,
isSeen = true,
),
aggregatedActivityData(
group = "group-marked",
updatedAt = Date(5000),
isRead = true,
isSeen = true,
),
)
assertEquals(expected, feedState.aggregatedActivities.value)
}

@Test
fun `on onStoriesFeedUpdated, update matching activities and upsert groups`() = runTest {
val initialActivities = List(3) { activityData("story-$it") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ internal class FeedEventHandlerTest(
updatedAt = Date(),
userCount = 1,
userCountTruncated = false,
isRead = null,
isSeen = null,
isWatched = null,
)
)
private val notificationStatus = NotificationStatusResponse(unread = 0, unseen = 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ internal object TestData {
hidden: Boolean = false,
user: UserData = userData("user-1"),
currentFeed: FeedData? = null,
updatedAt: Long = 1000,
isRead: Boolean? = null,
isSeen: Boolean? = null,
isWatched: Boolean? = null,
): ActivityData =
ActivityData(
attachments = emptyList(),
Expand All @@ -245,9 +249,9 @@ internal object TestData {
hidden = hidden,
id = id,
interestTags = emptyList(),
isRead = null,
isSeen = null,
isWatched = null,
isRead = isRead,
isSeen = isSeen,
isWatched = isWatched,
latestReactions = emptyList(),
location = null,
mentionedUsers = emptyList(),
Expand All @@ -271,7 +275,7 @@ internal object TestData {
shareCount = 0,
text = text,
type = type,
updatedAt = Date(1000),
updatedAt = Date(updatedAt),
user = user,
visibility = ActivityDataVisibility.Public,
visibilityTag = null,
Expand All @@ -286,6 +290,9 @@ internal object TestData {
updatedAt: Date = Date(1000),
userCount: Int = 1,
userCountTruncated: Boolean = false,
isRead: Boolean? = null,
isSeen: Boolean? = null,
isWatched: Boolean? = null,
): AggregatedActivityData =
AggregatedActivityData(
activities = activities,
Expand All @@ -296,6 +303,9 @@ internal object TestData {
updatedAt = updatedAt,
userCount = userCount,
userCountTruncated = userCountTruncated,
isRead = isRead,
isSeen = isSeen,
isWatched = isWatched,
)

fun appData(name: String = "Test App"): AppData =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ private fun NotificationsScreen(
val activities by state.aggregatedActivities.collectAsStateWithLifecycle()
LazyColumn {
items(activities) {
val isActivityRead = notificationStatus?.readActivities?.contains(it.group) == true
NotificationItem(
data = it,
isActivityRead = isActivityRead,
isActivityRead = it.isRead == true,
onMarkRead = { onMarkAggregatedActivityRead(it) },
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,7 @@ class NotificationsViewModel @Inject constructor(loginManager: LoginManager) : V
fun onMarkAggregatedActivityRead(activity: AggregatedActivityData) {
feed.withFirstContent(viewModelScope) {
// Check that the activity is not already read
val notificationStatus = state.notificationStatus.value
if (notificationStatus?.readActivities?.contains(activity.group) == true) {
return@withFirstContent
}
if (activity.isRead == true) return@withFirstContent
// Mark the aggregated activity as read
val request = MarkActivityRequest(markRead = listOf(activity.group))
markActivity(request)
Expand Down
Loading