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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Fix `Message.channelInfo` not populated when parsing `message.new` event. [#5953

### ⬆️ Improved
- Improve `SwipeToReply` component in scroller containers. [#5946](https://github.com/GetStream/stream-chat-android/pull/5946)
- Preserve scroll position on configuration changes in `MessageList`. [#5957](https://github.com/GetStream/stream-chat-android/pull/5957)

### ✅ Added
- Add `ChatTheme.reactionPushEmojiFactory` for customizing the emoji codes for reaction push notifications. [#5935](https://github.com/GetStream/stream-chat-android/pull/5935)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -53,6 +58,8 @@ import io.getstream.chat.android.ui.common.state.messages.list.MessageListItemSt
import io.getstream.chat.android.ui.common.state.messages.list.MessageListState
import io.getstream.chat.android.ui.common.state.messages.list.MyOwn
import io.getstream.chat.android.ui.common.state.messages.list.NewMessageState
import io.getstream.chat.android.ui.common.state.messages.list.Other
import io.getstream.chat.android.ui.common.state.messages.list.Typing
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -299,17 +306,28 @@ internal fun BoxScope.DefaultMessagesHelperContent(
}
}

// Keep track of the last new message state that triggered a scroll to bottom.
// If a configuration change happens, we want to keep the same state
// and not scroll to bottom again if the newMessageState is the same as before the configuration change.
var lastScrollToBottomOnNewMessage by rememberSaveable(saver = MutableStateNewMessageStateSaver) {
mutableStateOf(newMessageState)
}

LaunchedEffect(newMessageState) {
val shouldScrollToBottom = shouldScrollToBottomOnNewMessage(
focusedItemIndex = focusedItemIndex,
firstVisibleItemIndex = firstVisibleItemIndex,
newMessageState = newMessageState,
areNewestMessagesLoaded = areNewestMessagesLoaded,
isScrollInProgress = lazyListState.isScrollInProgress,
)
if (newMessageState != lastScrollToBottomOnNewMessage) {
val shouldScrollToBottom = shouldScrollToBottomOnNewMessage(
focusedItemIndex = focusedItemIndex,
firstVisibleItemIndex = firstVisibleItemIndex,
newMessageState = newMessageState,
areNewestMessagesLoaded = areNewestMessagesLoaded,
isScrollInProgress = lazyListState.isScrollInProgress,
)

if (shouldScrollToBottom) {
lazyListState.animateScrollToItem(0)
if (shouldScrollToBottom) {
lazyListState.animateScrollToItem(0)

lastScrollToBottomOnNewMessage = newMessageState
}
}
}

Expand All @@ -334,6 +352,36 @@ internal fun BoxScope.DefaultMessagesHelperContent(
}
}

/**
* Saves and restores a [MutableState] of [NewMessageState] across recompositions and configuration changes.
*/
private val MutableStateNewMessageStateSaver = Saver<MutableState<NewMessageState?>, String>(
save = { state -> with(NewMessageStateSaver) { save(state.value) } },
restore = { saved -> mutableStateOf(NewMessageStateSaver.restore(saved)) },
)

/**
* Saves and restores the [NewMessageState] across recompositions and configuration changes.
*/
private val NewMessageStateSaver = Saver<NewMessageState?, String>(
save = { value ->
when (value) {
is MyOwn -> "my:${value.ts}"
is Other -> "other:${value.ts}"
is Typing -> "typing"
null -> null
}
},
restore = { saved ->
when {
saved.startsWith("my:") -> MyOwn(saved.removePrefix("my:").toLongOrNull())
saved.startsWith("other:") -> Other(saved.removePrefix("other:").toLongOrNull())
saved == "typing" -> Typing
else -> null
}
},
)

/**
* Determines if the list should scroll to the bottom when a new message arrives, except for certain conditions:
* - If we are focusing on an item we do not wish to take the user off of it.
Expand Down
Loading