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 @@ -39,6 +39,7 @@ import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationAddedToChannelEvent
import io.getstream.chat.android.client.events.NotificationChannelDeletedEvent
import io.getstream.chat.android.client.events.NotificationChannelMutesUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelTruncatedEvent
import io.getstream.chat.android.client.events.NotificationMarkReadEvent
import io.getstream.chat.android.client.events.NotificationMarkUnreadEvent
Expand Down Expand Up @@ -628,6 +629,22 @@ public fun randomNewMessageEvent(
)
}

public fun randomNotificationChannelPartialUpdatedEvent(
createdAt: Date = randomDate(),
cid: String = randomCID(),
channelType: String = randomString(),
channelId: String = randomString(),
channel: Channel = randomChannel(),
): NotificationChannelPartialUpdatedEvent = NotificationChannelPartialUpdatedEvent(
type = EventType.NOTIFICATION_CHANNEL_PARTIAL_UPDATED,
createdAt = createdAt,
rawCreatedAt = streamFormatter.format(createdAt),
cid = cid,
channelType = channelType,
channelId = channelId,
channel = channel,
)

public fun randomNotificationChannelTruncatedEvent(
createdAt: Date = randomDate(),
cid: String = randomCID(),
Expand Down
23 changes: 23 additions & 0 deletions stream-chat-android-client/api/stream-chat-android-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -2227,6 +2227,29 @@ public final class io/getstream/chat/android/client/events/NotificationChannelMu
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/client/events/NotificationChannelPartialUpdatedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasChannel {
public fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Date;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lio/getstream/chat/android/models/Channel;
public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;)Lio/getstream/chat/android/client/events/NotificationChannelPartialUpdatedEvent;
public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/NotificationChannelPartialUpdatedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/NotificationChannelPartialUpdatedEvent;
public fun equals (Ljava/lang/Object;)Z
public fun getChannel ()Lio/getstream/chat/android/models/Channel;
public fun getChannelId ()Ljava/lang/String;
public fun getChannelType ()Ljava/lang/String;
public fun getCid ()Ljava/lang/String;
public fun getCreatedAt ()Ljava/util/Date;
public fun getRawCreatedAt ()Ljava/lang/String;
public fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/chat/android/client/events/NotificationChannelTruncatedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasChannel, io/getstream/chat/android/client/events/HasGroupedUnreadChannels, io/getstream/chat/android/client/events/HasUnreadCounts {
public fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;IILjava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;IILjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import io.getstream.chat.android.network.models.ChannelConfigWithInfo
import io.getstream.chat.android.network.models.ChannelMemberPartialResponse
import io.getstream.chat.android.network.models.ChannelMemberResponse
import io.getstream.chat.android.network.models.ChannelOwnCapability
import io.getstream.chat.android.network.models.ChannelPartialUpdateResponse
import io.getstream.chat.android.network.models.ChannelPushPreferencesResponse
import io.getstream.chat.android.network.models.ChannelResponse
import io.getstream.chat.android.network.models.ChannelStateResponse
Expand Down Expand Up @@ -309,6 +310,29 @@ internal class DomainMapping(
).syncUnreadCountWithReads(currentUserIdProvider())
.let(channelTransformer::transform)

/**
* Transforms [ChannelPartialUpdateResponse] into a [Channel] holding only the fields it carries. Like for a full
* channel, `name` and `image` come from the custom data, `disabled` is also kept in [Channel.extraData] and the
* channel transformer is applied, so the fields it carries replace local values that were transformed too.
*/
internal fun ChannelPartialUpdateResponse.toDomain(): Channel =
Channel(
id = id,
type = type,
name = custom["name"] as? String ?: "",
image = custom["image"] as? String ?: "",
frozen = frozen,
disabled = disabled,
createdAt = createdAt,
updatedAt = updatedAt,
team = team.orEmpty(),
extraData = custom.withDeclared("disabled" to disabled)
.mapNotNull { (key, value) -> value?.let { key to it } }
.toMap()
.minus(listOf("name", "image"))
.toMutableMap(),
).let(channelTransformer::transform)
Comment thread
gpunto marked this conversation as resolved.

internal fun ChannelResponse.toChannelInfo(): ChannelInfo =
ChannelInfo(
cid = cid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationAddedToChannelEvent
import io.getstream.chat.android.client.events.NotificationChannelDeletedEvent
import io.getstream.chat.android.client.events.NotificationChannelMutesUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelTruncatedEvent
import io.getstream.chat.android.client.events.NotificationInviteAcceptedEvent
import io.getstream.chat.android.client.events.NotificationInviteRejectedEvent
Expand Down Expand Up @@ -125,6 +126,7 @@ import io.getstream.chat.android.network.models.MessageUpdatedEvent as Generated
import io.getstream.chat.android.network.models.NotificationAddedToChannelEvent as GeneratedNotificationAddedToChannelEvent
import io.getstream.chat.android.network.models.NotificationChannelDeletedEvent as GeneratedNotificationChannelDeletedEvent
import io.getstream.chat.android.network.models.NotificationChannelMutesUpdatedEvent as GeneratedNotificationChannelMutesUpdatedEvent
import io.getstream.chat.android.network.models.NotificationChannelPartialUpdatedEvent as GeneratedNotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.network.models.NotificationChannelTruncatedEvent as GeneratedNotificationChannelTruncatedEvent
import io.getstream.chat.android.network.models.NotificationInviteAcceptedEvent as GeneratedNotificationInviteAcceptedEvent
import io.getstream.chat.android.network.models.NotificationInviteRejectedEvent as GeneratedNotificationInviteRejectedEvent
Expand Down Expand Up @@ -469,6 +471,7 @@ internal class EventMapping(
is GeneratedNotificationInviteAcceptedEvent -> toDomain()
is GeneratedNotificationInviteRejectedEvent -> toDomain()
is GeneratedNotificationChannelDeletedEvent -> toDomain()
is GeneratedNotificationChannelPartialUpdatedEvent -> toDomain()
is GeneratedNotificationChannelTruncatedEvent -> toDomain()
is GeneratedNotificationThreadMessageNewEvent -> toDomain()
is GeneratedNotificationMarkUnreadEvent -> toDomain()
Expand Down Expand Up @@ -937,6 +940,25 @@ internal class EventMapping(
)
}

/**
* Transforms the generated [GeneratedNotificationChannelPartialUpdatedEvent] to
* [NotificationChannelPartialUpdatedEvent].
*/
private fun GeneratedNotificationChannelPartialUpdatedEvent.toDomain(): NotificationChannelPartialUpdatedEvent =
with(domainMapping) {
val cid = requireNotNull(cid)
val (channelType, channelId) = cid.cidToTypeAndId()
return NotificationChannelPartialUpdatedEvent(
type = type,
createdAt = createdAt.date,
rawCreatedAt = createdAt.raw,
cid = cid,
channelType = channelType,
channelId = channelId,
channel = channel.toDomain(),
)
}

/**
* Transforms the generated [GeneratedNotificationChannelTruncatedEvent] to [NotificationChannelTruncatedEvent].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationAddedToChannelEvent
import io.getstream.chat.android.client.events.NotificationChannelDeletedEvent
import io.getstream.chat.android.client.events.NotificationChannelMutesUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelTruncatedEvent
import io.getstream.chat.android.client.events.NotificationInviteAcceptedEvent
import io.getstream.chat.android.client.events.NotificationInviteRejectedEvent
Expand Down Expand Up @@ -278,6 +279,7 @@ public class ChannelClient internal constructor(
is NewMessageEvent -> event.cid == cid
is NotificationAddedToChannelEvent -> event.cid == cid
is NotificationChannelDeletedEvent -> event.cid == cid
is NotificationChannelPartialUpdatedEvent -> event.cid == cid
is NotificationChannelTruncatedEvent -> event.cid == cid
is NotificationInviteAcceptedEvent -> event.cid == cid
is NotificationInviteRejectedEvent -> event.cid == cid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,25 @@ public data class NotificationChannelMutesUpdatedEvent(
override val me: User,
) : ChatEvent(), HasOwnUser

/**
* Triggered when a channel is partially updated, for members of the channel who are not watching it. Members who
* are watching it receive [ChannelUpdatedEvent] instead.
*
* The [channel] carries only part of the channel data: [Channel.id], [Channel.type], [Channel.name], [Channel.image],
* [Channel.frozen], [Channel.disabled], [Channel.team], [Channel.createdAt], [Channel.updatedAt] and
* [Channel.extraData] (the complete custom data, plus the `disabled` flag). Other fields, such as members,
* messages, reads, config and [Channel.lastMessageAt], keep their default values.
*/
public data class NotificationChannelPartialUpdatedEvent(
override val type: String,
override val createdAt: Date,
override val rawCreatedAt: String,
override val cid: String,
override val channelType: String,
override val channelId: String,
override val channel: Channel,
) : CidEvent(), HasChannel

/**
* Triggered when a channels' history is truncated
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import io.getstream.chat.android.client.events.CidEvent
import io.getstream.chat.android.client.events.HasChannel
import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationAddedToChannelEvent
import io.getstream.chat.android.client.events.NotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.client.events.NotificationMessageNewEvent
import io.getstream.chat.android.client.internal.state.utils.internal.mergePartialUpdate
import io.getstream.chat.android.client.setup.state.ClientState
import io.getstream.chat.android.client.utils.channel.calculateNewLastMessageAt
import io.getstream.chat.android.models.Channel
Expand Down Expand Up @@ -65,6 +67,8 @@ internal class GroupAwareChatEventHandler(
is ChannelUpdatedByUserEvent,
-> routeByGroup((event as HasChannel).channel, cachedChannel, filter)

is NotificationChannelPartialUpdatedEvent -> routePartialUpdate(event, cachedChannel, filter)

is NotificationAddedToChannelEvent,
is NotificationMessageNewEvent,
-> watchAndAddIfBelongsHere(event.cid, event.channel, filter)
Expand Down Expand Up @@ -120,10 +124,45 @@ internal class GroupAwareChatEventHandler(
private fun watchAndAddIfBelongsHere(cid: String, channel: Channel, filter: FilterObject): EventHandlingResult =
if (channelBelongsHere(channel, filter)) EventHandlingResult.WatchAndAdd(cid) else EventHandlingResult.Skip

/**
* Same checks as [routeByGroup], against [cachedChannel] with the partial update applied. The event's channel
* lacks data such as members and the last message time, so a channel that is not in the list yet is watched to
* get its full data instead of being added from the event.
*
* The cutoff is checked only for a channel in the list, whose last activity is up to date. For any other
* channel only the group is checked: without [cachedChannel] the last activity is unknown, and a stored copy can
* be stale, so the cutoff is left to the list, which checks the watched channel before showing it. The event is
* only sent to members, and a hidden channel is dropped after the watch. The query skips the watch when the
* channel sorts below its loaded pages.
*/
private fun routePartialUpdate(
event: NotificationChannelPartialUpdatedEvent,
cachedChannel: Channel?,
filter: FilterObject,
): EventHandlingResult {
val channel = cachedChannel?.mergePartialUpdate(event.channel) ?: event.channel
val isInList = channels.value?.containsKey(event.cid) == true
val matches = if (isInList) channelBelongsHere(channel, filter) else groupMatches(channel, filter)
val belongsHere = matches &&
(cachedChannel == null || (isCurrentUserMember(cachedChannel) && cachedChannel.hidden != true))
return when {
belongsHere && !isInList -> EventHandlingResult.WatchAndAdd(event.cid)
!belongsHere && isInList -> EventHandlingResult.Remove(event.cid)
else -> EventHandlingResult.Skip
}
}

private fun channelBelongsHere(channel: Channel, filter: FilterObject): Boolean =
resolver.resolve(channel).contains(groupKey) &&
filter.matchesGroupedChannel(group = channel.group, queryTimestamp = channel.queryTimestamp, now = now())

/** [channelBelongsHere] with the cutoff passed, for a channel whose last activity is unknown or may be stale. */
private fun groupMatches(channel: Channel, filter: FilterObject): Boolean {
val checkedAt = now()
return resolver.resolve(channel).contains(groupKey) &&
filter.matchesGroupedChannel(group = channel.group, queryTimestamp = checkedAt, now = checkedAt)
}

/** The channel with [newMessage] counted in its last message time, as the channel state counts it. */
private fun Channel.withNewMessage(newMessage: Message?): Channel = when (newMessage) {
null -> this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.getstream.chat.android.client.events.HasGroupedUnreadChannels
import io.getstream.chat.android.client.events.MarkAllReadEvent
import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationChannelDeletedEvent
import io.getstream.chat.android.client.events.NotificationChannelPartialUpdatedEvent
import io.getstream.chat.android.client.events.NotificationChannelTruncatedEvent
import io.getstream.chat.android.client.events.NotificationMarkReadEvent
import io.getstream.chat.android.client.events.NotificationMarkUnreadEvent
Expand Down Expand Up @@ -94,7 +95,7 @@ internal class GroupedUnreadChannelsUpdater(
current: Map<String, Int>,
batchId: Int,
event: ChannelUpdatedEvent,
): Map<String, Int> = applyDelta(current, batchId, event.cid, event.channel)
): Map<String, Int> = applyDelta(current, batchId, event.cid, event.channel, cachedChannel(event.cid))

/**
* `channel.updated_by_user` delta. Same semantics as the [ChannelUpdatedEvent] overload.
Expand All @@ -103,7 +104,21 @@ internal class GroupedUnreadChannelsUpdater(
current: Map<String, Int>,
batchId: Int,
event: ChannelUpdatedByUserEvent,
): Map<String, Int> = applyDelta(current, batchId, event.cid, event.channel)
): Map<String, Int> = applyDelta(current, batchId, event.cid, event.channel, cachedChannel(event.cid))

/**
* `notification.channel_partial_updated` delta. Same semantics as the [ChannelUpdatedEvent] overload. The
* event's channel carries the custom data, so it holds the new group. The event goes to members who are not
* watching the channel, so it usually has no active state: its old group and unread state then come from
* [storedChannel], which is only read in that case.
*/
suspend fun calculateUpdatedCounts(
current: Map<String, Int>,
batchId: Int,
event: NotificationChannelPartialUpdatedEvent,
storedChannel: suspend () -> Channel?,
): Map<String, Int> =
applyDelta(current, batchId, event.cid, event.channel, cachedChannel(event.cid) ?: storedChannel())

/**
* Records that channel [cid] has been removed within [batchId] — either deleted, or the
Expand Down Expand Up @@ -135,10 +150,11 @@ internal class GroupedUnreadChannelsUpdater(
batchId: Int,
cid: String,
newChannel: Channel,
oldChannel: Channel?,
): Map<String, Int> {
rotateBatchIfNeeded(batchId)
if (cid in removedCids || cid in processedCids) return current
val oldChannel = cachedChannel(cid) ?: return current
if (oldChannel == null) return current
val oldGroup = oldChannel.group
val newGroup = newChannel.group
if (oldGroup == newGroup) return current
Expand Down
Loading
Loading