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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@

### ✅ Added
- Add `FileAttachmentPreviewContent`, `FileAttachmentContent`,`FileAttachmentItem`, `FileUploadContent` and `FileUploadItem` to `ChatComponentFactory`. [#5791](https://github.com/GetStream/stream-chat-android/pull/5791)
- Introduce internal `DraftChannelViewController` and experimental classes `DraftChannelViewState`, `DraftChannelViewAction`, and `DraftChannelViewEvent`. [#5797](https://github.com/GetStream/stream-chat-android/pull/5797)
- Message member from the member bottom sheet of the channel info screen. [#5797](https://github.com/GetStream/stream-chat-android/pull/5797)

### ⚠️ Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
android:exported="false"
android:windowSoftInputMode="adjustResize"
/>
<activity
android:name=".feature.channel.draft.DraftChannelActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize"
/>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.sample.feature.channel.draft

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import io.getstream.chat.android.compose.sample.R
import io.getstream.chat.android.compose.sample.ui.BaseConnectedActivity
import io.getstream.chat.android.compose.sample.ui.MessagesActivity
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewEvent
import kotlinx.coroutines.flow.collectLatest

class DraftChannelActivity : BaseConnectedActivity() {

companion object {
private const val KEY_MEMBER_IDS = "memberIds"

/**
* Creates an [Intent] for starting the [DraftChannelActivity].
*
* @param context The calling [Context], used for building the [Intent].
* @param memberIds The list of member IDs to be used for creating the channel.
*/
fun createIntent(context: Context, memberIds: List<String>) =
Intent(context, DraftChannelActivity::class.java)
.putExtra(KEY_MEMBER_IDS, memberIds.toTypedArray())
}

private val viewModelFactory by lazy {
DraftChannelViewModelFactory(
memberIds = requireNotNull(intent.getStringArrayExtra(KEY_MEMBER_IDS)).toList(),
)
}
private val viewModel by viewModels<DraftChannelViewModel> { viewModelFactory }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ChatTheme {
DraftChannelScreen(
modifier = Modifier.systemBarsPadding(),
viewModel = viewModel,
onNavigationIconClick = ::finish,
)
LaunchedEffect(viewModel) {
viewModel.events.collectLatest { event ->
when (event) {
is DraftChannelViewEvent.NavigateToChannel -> {
startActivity(
MessagesActivity.createIntent(
context = applicationContext,
channelId = event.cid,
),
)
finish()
}

is DraftChannelViewEvent.DraftChannelError ->
Toast.makeText(
applicationContext,
R.string.draft_channel_error,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.sample.feature.channel.draft

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import io.getstream.chat.android.compose.ui.components.LoadingIndicator
import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer
import io.getstream.chat.android.compose.ui.messages.header.MessageListHeader
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.viewmodel.channel.ChannelHeaderViewModel
import io.getstream.chat.android.compose.viewmodel.channel.ChannelHeaderViewModelFactory
import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewAction
import io.getstream.chat.android.ui.common.state.channel.draft.DraftChannelViewState
import io.getstream.chat.android.ui.common.state.messages.list.ChannelHeaderViewState

@Composable
fun DraftChannelScreen(
viewModel: DraftChannelViewModel,
onNavigationIconClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val state by viewModel.state.collectAsStateWithLifecycle()

DraftChannelContent(
modifier = modifier,
state = state,
onNavigationIconClick = onNavigationIconClick,
onViewAction = viewModel::onViewAction,
)
}

@Composable
private fun DraftChannelContent(
state: DraftChannelViewState,
onNavigationIconClick: () -> Unit,
onViewAction: (action: DraftChannelViewAction) -> Unit,
modifier: Modifier = Modifier,
) {
when (state) {
is DraftChannelViewState.Loading -> LoadingIndicator(
modifier = modifier.fillMaxSize(),
)

is DraftChannelViewState.Content -> Scaffold(
modifier = modifier,
topBar = {
DraftChannelTopBar(
cid = state.channel.cid,
onNavigationIconClick = onNavigationIconClick,
)
},
bottomBar = {
DraftChannelBottomBar(
cid = state.channel.cid,
onMessageSent = { onViewAction(DraftChannelViewAction.MessageSent) },
)
},
containerColor = ChatTheme.colors.appBackground,
) { padding ->
ChatTheme.componentFactory.MessageListEmptyContent(
modifier = Modifier
.padding(padding)
.fillMaxSize(),
)
}
}
}

@Composable
private fun DraftChannelTopBar(
cid: String,
onNavigationIconClick: () -> Unit,
) {
val viewModel = viewModel<ChannelHeaderViewModel>(factory = ChannelHeaderViewModelFactory(cid))
val state by viewModel.state.collectAsStateWithLifecycle()

when (val content = state) {
is ChannelHeaderViewState.Loading -> LoadingIndicator(
modifier = Modifier.fillMaxWidth(),
)

is ChannelHeaderViewState.Content -> MessageListHeader(
channel = content.channel,
currentUser = content.currentUser,
connectionState = content.connectionState,
onBackPressed = onNavigationIconClick,
)
}
}

@Composable
private fun DraftChannelBottomBar(
cid: String,
onMessageSent: () -> Unit,
) {
val context = LocalContext.current
val viewModel = viewModel<MessageComposerViewModel>(key = cid, factory = MessagesViewModelFactory(context, cid))
MessageComposer(
viewModel = viewModel,
onSendMessage = { message ->
viewModel.sendMessage(message)
onMessageSent()
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@file:OptIn(InternalStreamChatApi::class)

package io.getstream.chat.android.compose.sample.feature.channel.draft

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.getstream.chat.android.core.internal.InternalStreamChatApi
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewAction
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewController
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewEvent
import io.getstream.chat.android.ui.common.state.channel.draft.DraftChannelViewState
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

class DraftChannelViewModel(
private val memberIds: List<String>,
controllerProvider: ViewModel.() -> DraftChannelViewController = {
DraftChannelViewController(
memberIds = memberIds,
scope = viewModelScope,
)
},
) : ViewModel() {

private val controller: DraftChannelViewController by lazy { controllerProvider() }

/**
* @see [DraftChannelViewController.state]
*/
val state: StateFlow<DraftChannelViewState> = controller.state

/**
* @see [DraftChannelViewController.events]
*/
val events: SharedFlow<DraftChannelViewEvent> = controller.events

/**
* @see [DraftChannelViewController.onViewAction]
*/
fun onViewAction(action: DraftChannelViewAction) {
controller.onViewAction(action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.sample.feature.channel.draft

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.getstream.chat.android.core.internal.InternalStreamChatApi

class DraftChannelViewModelFactory(private val memberIds: List<String>) : ViewModelProvider.Factory {
@OptIn(InternalStreamChatApi::class)
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass == DraftChannelViewModel::class.java) {
"DraftChannelViewModelFactory can only create instances of DraftChannelViewModel"
}
@Suppress("UNCHECKED_CAST")
return DraftChannelViewModel(memberIds) as T
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,33 @@ class DirectChannelInfoActivity : BaseConnectedActivity() {
LaunchedEffect(viewModel) {
viewModel.events.collectLatest { event ->
when (event) {
is ChannelInfoViewEvent.Error ->
showError(event)

is ChannelInfoViewEvent.NavigateUp -> {
setResult(RESULT_OK)
finish()
}

is ChannelInfoViewEvent.NavigateToPinnedMessages ->
openPinnedMessages()

else -> Unit
is ChannelInfoViewEvent.Error -> showError(event)
is ChannelInfoViewEvent.Navigation -> onNavigationEvent(event)
is ChannelInfoViewEvent.Modal -> Unit
}
}
}
}
}

private fun onNavigationEvent(event: ChannelInfoViewEvent.Navigation) {
when (event) {
is ChannelInfoViewEvent.NavigateUp -> {
setResult(RESULT_OK)
finish()
}

is ChannelInfoViewEvent.NavigateToPinnedMessages ->
openPinnedMessages()

// No need to handle these in DirectChannelInfoActivity,
// as it is only applicable for group channels.
is ChannelInfoViewEvent.NavigateToChannel,
is ChannelInfoViewEvent.NavigateToDraftChannel,
-> Unit
}
}

private fun openPinnedMessages() {
val intent = PinnedMessagesActivity.createIntent(
context = this,
Expand Down
Loading
Loading