Skip to content

Commit ca9cbd2

Browse files
committed
Refactor: Navigate to draft channel instead of creating a new direct channel
When messaging a member from the channel info screen, the app will now navigate to a draft channel instead of immediately creating a new direct channel. This allows the user to compose a message before the channel is actually created. This change also introduces the following new components: - `DraftChannelViewController`: Manages the state and events for the draft channel screen. - `DraftChannelViewState`: Represents the state of the draft channel screen. - `DraftChannelViewAction`: Represents user actions on the draft channel screen. - `DraftChannelViewEvent`: Represents side effects from the draft channel screen. - `DraftChannelScreen`: The composable UI for the draft channel. - `DraftChannelActivity`: The activity that hosts the draft channel screen.
1 parent 9844464 commit ca9cbd2

File tree

20 files changed

+851
-150
lines changed

20 files changed

+851
-150
lines changed

stream-chat-android-compose-sample/src/main/AndroidManifest.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@
8989
android:exported="false"
9090
android:windowSoftInputMode="adjustResize"
9191
/>
92+
<activity
93+
android:name=".feature.channel.draft.DraftChannelActivity"
94+
android:exported="false"
95+
android:windowSoftInputMode="adjustResize"
96+
/>
9297
</application>
93-
94-
</manifest>
98+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.compose.sample.feature.channel.draft
18+
19+
import android.content.Context
20+
import android.content.Intent
21+
import android.os.Bundle
22+
import android.widget.Toast
23+
import androidx.activity.compose.setContent
24+
import androidx.activity.viewModels
25+
import androidx.compose.foundation.layout.systemBarsPadding
26+
import androidx.compose.runtime.LaunchedEffect
27+
import androidx.compose.ui.Modifier
28+
import io.getstream.chat.android.compose.sample.R
29+
import io.getstream.chat.android.compose.sample.ui.BaseConnectedActivity
30+
import io.getstream.chat.android.compose.sample.ui.MessagesActivity
31+
import io.getstream.chat.android.compose.ui.theme.ChatTheme
32+
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewEvent
33+
import kotlinx.coroutines.flow.collectLatest
34+
35+
class DraftChannelActivity : BaseConnectedActivity() {
36+
37+
companion object {
38+
private const val KEY_MEMBER_IDS = "memberIds"
39+
40+
/**
41+
* Creates an [Intent] for starting the [DraftChannelActivity].
42+
*
43+
* @param context The calling [Context], used for building the [Intent].
44+
* @param memberIds The list of member IDs to be used for creating the channel.
45+
*/
46+
fun createIntent(context: Context, memberIds: List<String>) =
47+
Intent(context, DraftChannelActivity::class.java)
48+
.putExtra(KEY_MEMBER_IDS, memberIds.toTypedArray())
49+
}
50+
51+
private val viewModelFactory by lazy {
52+
DraftChannelViewModelFactory(
53+
memberIds = requireNotNull(intent.getStringArrayExtra(KEY_MEMBER_IDS)).toList(),
54+
)
55+
}
56+
private val viewModel by viewModels<DraftChannelViewModel> { viewModelFactory }
57+
58+
override fun onCreate(savedInstanceState: Bundle?) {
59+
super.onCreate(savedInstanceState)
60+
setContent {
61+
ChatTheme {
62+
DraftChannelScreen(
63+
modifier = Modifier.systemBarsPadding(),
64+
viewModel = viewModel,
65+
onNavigationIconClick = ::finish,
66+
)
67+
LaunchedEffect(viewModel) {
68+
viewModel.events.collectLatest { event ->
69+
when (event) {
70+
is DraftChannelViewEvent.NavigateToChannel -> {
71+
startActivity(
72+
MessagesActivity.createIntent(
73+
context = applicationContext,
74+
channelId = event.cid,
75+
),
76+
)
77+
finish()
78+
}
79+
80+
is DraftChannelViewEvent.DraftChannelError ->
81+
Toast.makeText(
82+
applicationContext,
83+
R.string.draft_channel_error,
84+
Toast.LENGTH_SHORT,
85+
).show()
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.compose.sample.feature.channel.draft
18+
19+
import androidx.compose.foundation.layout.fillMaxSize
20+
import androidx.compose.foundation.layout.fillMaxWidth
21+
import androidx.compose.foundation.layout.padding
22+
import androidx.compose.material3.Scaffold
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.platform.LocalContext
27+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
28+
import androidx.lifecycle.viewmodel.compose.viewModel
29+
import io.getstream.chat.android.compose.ui.components.LoadingIndicator
30+
import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer
31+
import io.getstream.chat.android.compose.ui.messages.header.MessageListHeader
32+
import io.getstream.chat.android.compose.ui.theme.ChatTheme
33+
import io.getstream.chat.android.compose.viewmodel.channel.ChannelHeaderViewModel
34+
import io.getstream.chat.android.compose.viewmodel.channel.ChannelHeaderViewModelFactory
35+
import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
36+
import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory
37+
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewAction
38+
import io.getstream.chat.android.ui.common.state.channel.draft.DraftChannelViewState
39+
import io.getstream.chat.android.ui.common.state.messages.list.ChannelHeaderViewState
40+
41+
@Composable
42+
fun DraftChannelScreen(
43+
viewModel: DraftChannelViewModel,
44+
onNavigationIconClick: () -> Unit,
45+
modifier: Modifier = Modifier,
46+
) {
47+
val state by viewModel.state.collectAsStateWithLifecycle()
48+
49+
DraftChannelContent(
50+
modifier = modifier,
51+
state = state,
52+
onNavigationIconClick = onNavigationIconClick,
53+
onViewAction = viewModel::onViewAction,
54+
)
55+
}
56+
57+
@Composable
58+
private fun DraftChannelContent(
59+
state: DraftChannelViewState,
60+
onNavigationIconClick: () -> Unit,
61+
onViewAction: (action: DraftChannelViewAction) -> Unit,
62+
modifier: Modifier = Modifier,
63+
) {
64+
when (state) {
65+
is DraftChannelViewState.Loading -> LoadingIndicator(
66+
modifier = modifier.fillMaxSize(),
67+
)
68+
69+
is DraftChannelViewState.Content -> Scaffold(
70+
modifier = modifier,
71+
topBar = {
72+
DraftChannelTopBar(
73+
cid = state.channel.cid,
74+
onNavigationIconClick = onNavigationIconClick,
75+
)
76+
},
77+
bottomBar = {
78+
DraftChannelBottomBar(
79+
cid = state.channel.cid,
80+
onMessageSent = { onViewAction(DraftChannelViewAction.MessageSent) },
81+
)
82+
},
83+
containerColor = ChatTheme.colors.appBackground,
84+
) { padding ->
85+
ChatTheme.componentFactory.MessageListEmptyContent(
86+
modifier = Modifier
87+
.padding(padding)
88+
.fillMaxSize(),
89+
)
90+
}
91+
}
92+
}
93+
94+
@Composable
95+
private fun DraftChannelTopBar(
96+
cid: String,
97+
onNavigationIconClick: () -> Unit,
98+
) {
99+
val viewModel = viewModel<ChannelHeaderViewModel>(factory = ChannelHeaderViewModelFactory(cid))
100+
val state by viewModel.state.collectAsStateWithLifecycle()
101+
102+
when (val content = state) {
103+
is ChannelHeaderViewState.Loading -> LoadingIndicator(
104+
modifier = Modifier.fillMaxWidth(),
105+
)
106+
107+
is ChannelHeaderViewState.Content -> MessageListHeader(
108+
channel = content.channel,
109+
currentUser = content.currentUser,
110+
connectionState = content.connectionState,
111+
onBackPressed = onNavigationIconClick,
112+
)
113+
}
114+
}
115+
116+
@Composable
117+
private fun DraftChannelBottomBar(
118+
cid: String,
119+
onMessageSent: () -> Unit,
120+
) {
121+
val context = LocalContext.current
122+
val viewModel = viewModel<MessageComposerViewModel>(key = cid, factory = MessagesViewModelFactory(context, cid))
123+
MessageComposer(
124+
viewModel = viewModel,
125+
onSendMessage = { message ->
126+
viewModel.sendMessage(message)
127+
onMessageSent()
128+
},
129+
)
130+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
@file:OptIn(InternalStreamChatApi::class)
18+
19+
package io.getstream.chat.android.compose.sample.feature.channel.draft
20+
21+
import androidx.lifecycle.ViewModel
22+
import androidx.lifecycle.viewModelScope
23+
import io.getstream.chat.android.core.internal.InternalStreamChatApi
24+
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewAction
25+
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewController
26+
import io.getstream.chat.android.ui.common.feature.channel.draft.DraftChannelViewEvent
27+
import io.getstream.chat.android.ui.common.state.channel.draft.DraftChannelViewState
28+
import kotlinx.coroutines.flow.SharedFlow
29+
import kotlinx.coroutines.flow.StateFlow
30+
31+
class DraftChannelViewModel(
32+
private val memberIds: List<String>,
33+
controllerProvider: ViewModel.() -> DraftChannelViewController = {
34+
DraftChannelViewController(
35+
memberIds = memberIds,
36+
scope = viewModelScope,
37+
)
38+
},
39+
) : ViewModel() {
40+
41+
private val controller: DraftChannelViewController by lazy { controllerProvider() }
42+
43+
/**
44+
* @see [DraftChannelViewController.state]
45+
*/
46+
val state: StateFlow<DraftChannelViewState> = controller.state
47+
48+
/**
49+
* @see [DraftChannelViewController.events]
50+
*/
51+
val events: SharedFlow<DraftChannelViewEvent> = controller.events
52+
53+
/**
54+
* @see [DraftChannelViewController.onViewAction]
55+
*/
56+
fun onViewAction(action: DraftChannelViewAction) {
57+
controller.onViewAction(action)
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.compose.sample.feature.channel.draft
18+
19+
import androidx.lifecycle.ViewModel
20+
import androidx.lifecycle.ViewModelProvider
21+
import io.getstream.chat.android.core.internal.InternalStreamChatApi
22+
23+
class DraftChannelViewModelFactory(private val memberIds: List<String>) : ViewModelProvider.Factory {
24+
@OptIn(InternalStreamChatApi::class)
25+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
26+
require(modelClass == DraftChannelViewModel::class.java) {
27+
"DraftChannelViewModelFactory can only create instances of DraftChannelViewModel"
28+
}
29+
@Suppress("UNCHECKED_CAST")
30+
return DraftChannelViewModel(memberIds) as T
31+
}
32+
}

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/channel/DirectChannelInfoActivity.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,11 @@ class DirectChannelInfoActivity : BaseConnectedActivity() {
9595
is ChannelInfoViewEvent.NavigateToPinnedMessages ->
9696
openPinnedMessages()
9797

98-
is ChannelInfoViewEvent.NavigateToChannel ->
99-
// No need to handle this in DirectChannelInfoActivity,
100-
// as it is only applicable for group channels.
101-
Unit
98+
// No need to handle these in DirectChannelInfoActivity,
99+
// as it is only applicable for group channels.
100+
is ChannelInfoViewEvent.NavigateToChannel,
101+
is ChannelInfoViewEvent.NavigateToDraftChannel,
102+
-> Unit
102103
}
103104
}
104105

@@ -137,9 +138,6 @@ class DirectChannelInfoActivity : BaseConnectedActivity() {
137138

138139
ChannelInfoViewEvent.UnbanMemberError,
139140
-> R.string.stream_ui_channel_info_unban_member_error
140-
141-
ChannelInfoViewEvent.NewDirectChannelError,
142-
-> R.string.stream_ui_channel_info_new_direct_channel_error
143141
}
144142
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
145143
}

stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/channel/GroupChannelInfoActivity.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
2626
import androidx.compose.runtime.LaunchedEffect
2727
import androidx.compose.ui.Modifier
2828
import io.getstream.chat.android.compose.sample.R
29+
import io.getstream.chat.android.compose.sample.feature.channel.draft.DraftChannelActivity
2930
import io.getstream.chat.android.compose.sample.ui.BaseConnectedActivity
3031
import io.getstream.chat.android.compose.sample.ui.MessagesActivity
3132
import io.getstream.chat.android.compose.sample.ui.pinned.PinnedMessagesActivity
@@ -95,10 +96,11 @@ class GroupChannelInfoActivity : BaseConnectedActivity() {
9596
is ChannelInfoViewEvent.NavigateToPinnedMessages ->
9697
openPinnedMessages()
9798

98-
is ChannelInfoViewEvent.NavigateToChannel -> {
99-
val intent = MessagesActivity.createIntent(context = this, channelId = event.cid)
100-
startActivity(intent)
101-
}
99+
is ChannelInfoViewEvent.NavigateToChannel ->
100+
startActivity(MessagesActivity.createIntent(context = this, channelId = event.cid))
101+
102+
is ChannelInfoViewEvent.NavigateToDraftChannel ->
103+
startActivity(DraftChannelActivity.createIntent(context = this, memberIds = listOf(event.memberId)))
102104
}
103105
}
104106

@@ -137,9 +139,6 @@ class GroupChannelInfoActivity : BaseConnectedActivity() {
137139

138140
ChannelInfoViewEvent.RemoveMemberError,
139141
-> R.string.stream_ui_channel_info_remove_member_error
140-
141-
ChannelInfoViewEvent.NewDirectChannelError,
142-
-> R.string.stream_ui_channel_info_new_direct_channel_error
143142
}
144143
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
145144
}

0 commit comments

Comments
 (0)