Skip to content

Commit bc7298d

Browse files
committed
Refactor group channel editing logic to move image selection and caching state from the UI to GroupChannelEditViewModel.
1 parent e4bb42e commit bc7298d

9 files changed

Lines changed: 394 additions & 112 deletions

File tree

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import androidx.compose.runtime.LaunchedEffect
4444
import androidx.compose.runtime.getValue
4545
import androidx.compose.runtime.mutableStateOf
4646
import androidx.compose.runtime.remember
47-
import androidx.compose.runtime.rememberCoroutineScope
4847
import androidx.compose.runtime.saveable.rememberSaveable
4948
import androidx.compose.runtime.setValue
5049
import androidx.compose.ui.Alignment
@@ -74,12 +73,9 @@ import io.getstream.chat.android.compose.ui.theme.StreamTokens
7473
import io.getstream.chat.android.compose.ui.util.bottomBorder
7574
import io.getstream.chat.android.compose.viewmodel.channel.GroupChannelEditViewEvent
7675
import io.getstream.chat.android.compose.viewmodel.channel.GroupChannelEditViewModel
77-
import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
7876
import io.getstream.chat.android.models.Channel
7977
import io.getstream.chat.android.previewdata.PreviewChannelData
8078
import io.getstream.chat.android.ui.common.contract.internal.CaptureMediaContract
81-
import kotlinx.coroutines.launch
82-
import kotlinx.coroutines.withContext
8379
import java.io.File
8480

8581
@OptIn(ExperimentalMaterial3Api::class)
@@ -95,9 +91,6 @@ internal fun GroupChannelEditScreen(
9591
var channelName by rememberSaveable(stateSaver = TextFieldValue.Saver) {
9692
mutableStateOf(TextFieldValue(text = channel.name, selection = TextRange(channel.name.length)))
9793
}
98-
var pendingImagePath by rememberSaveable { mutableStateOf<String?>(null) }
99-
val pendingImageFile = pendingImagePath?.let(::File)
100-
var removeImage by rememberSaveable { mutableStateOf(false) }
10194
var showImagePicker by rememberSaveable { mutableStateOf(false) }
10295

10396
LaunchedEffect(viewModel) {
@@ -115,6 +108,8 @@ internal fun GroupChannelEditScreen(
115108
}
116109
}
117110

111+
val pendingImageFile = state.pendingImageFile
112+
val removeImage = state.removeImage
118113
val displayChannel = remember(channel, pendingImageFile, removeImage) {
119114
when {
120115
pendingImageFile != null -> channel.copy(image = Uri.fromFile(pendingImageFile).toString())
@@ -126,25 +121,20 @@ internal fun GroupChannelEditScreen(
126121
GroupChannelEditContent(
127122
channel = displayChannel,
128123
channelName = channelName,
129-
isSaving = state.isSaving,
124+
isBusy = state.isBusy,
130125
onChannelNameChange = { channelName = it },
131126
onNavigationIconClick = onDismiss,
132-
onSaveActionClick = { viewModel.save(channelName.text, pendingImageFile, removeImage) },
127+
onSaveActionClick = { viewModel.save(channelName.text) },
133128
onUploadPictureClick = { showImagePicker = true },
134129
)
135130

136131
ImagePickerSheet(
137132
visible = showImagePicker,
138133
showRemoveOption = (channel.image.isNotBlank() || pendingImageFile != null) && !removeImage,
139134
onDismiss = { showImagePicker = false },
140-
onImageSelected = { file ->
141-
pendingImagePath = file.absolutePath
142-
removeImage = false
143-
},
144-
onImageRemoved = {
145-
pendingImagePath = null
146-
removeImage = true
147-
},
135+
onGalleryUriPicked = viewModel::importGalleryImage,
136+
onImageSelected = viewModel::setPendingImage,
137+
onImageRemoved = viewModel::removeImage,
148138
)
149139
}
150140

@@ -154,21 +144,14 @@ private fun ImagePickerSheet(
154144
visible: Boolean,
155145
showRemoveOption: Boolean,
156146
onDismiss: () -> Unit = {},
147+
onGalleryUriPicked: (Uri) -> Unit = {},
157148
onImageSelected: (File) -> Unit = {},
158149
onImageRemoved: () -> Unit = {},
159150
) {
160-
val context = LocalContext.current
161-
val scope = rememberCoroutineScope()
162-
163151
val pickMediaLauncher = rememberLauncherForActivityResult(
164152
contract = ActivityResultContracts.PickVisualMedia(),
165153
) { uri ->
166-
uri ?: return@rememberLauncherForActivityResult
167-
scope.launch(DispatcherProvider.IO) {
168-
uri.toCacheFile(context)?.let { file ->
169-
withContext(DispatcherProvider.Main) { onImageSelected(file) }
170-
}
171-
}
154+
uri?.let(onGalleryUriPicked)
172155
}
173156

174157
val capturePhotoLauncher = rememberCaptureMediaLauncher(
@@ -211,7 +194,7 @@ private fun ImagePickerSheet(
211194
private fun GroupChannelEditContent(
212195
channel: Channel,
213196
channelName: TextFieldValue,
214-
isSaving: Boolean,
197+
isBusy: Boolean = false,
215198
onChannelNameChange: (TextFieldValue) -> Unit = {},
216199
onNavigationIconClick: () -> Unit = {},
217200
onSaveActionClick: () -> Unit = {},
@@ -220,7 +203,7 @@ private fun GroupChannelEditContent(
220203
Scaffold(
221204
topBar = {
222205
GroupChannelEditTopBar(
223-
isSaving = isSaving,
206+
isBusy = isBusy,
224207
onNavigationIconClick = onNavigationIconClick,
225208
onSaveActionClick = onSaveActionClick,
226209
)
@@ -245,12 +228,12 @@ private fun GroupChannelEditContent(
245228
onClick = onUploadPictureClick,
246229
text = stringResource(R.string.stream_ui_channel_info_edit_upload_picture),
247230
style = StreamButtonStyleDefaults.primaryGhost,
248-
enabled = !isSaving,
231+
enabled = !isBusy,
249232
)
250233
Spacer(modifier = Modifier.size(StreamTokens.spacing2xl))
251234
ChannelNameField(
252235
value = channelName,
253-
enabled = !isSaving,
236+
enabled = !isBusy,
254237
onValueChange = onChannelNameChange,
255238
)
256239
}
@@ -260,7 +243,7 @@ private fun GroupChannelEditContent(
260243
@Composable
261244
@OptIn(ExperimentalMaterial3Api::class)
262245
private fun GroupChannelEditTopBar(
263-
isSaving: Boolean,
246+
isBusy: Boolean,
264247
onNavigationIconClick: () -> Unit,
265248
onSaveActionClick: () -> Unit,
266249
) {
@@ -276,7 +259,7 @@ private fun GroupChannelEditTopBar(
276259
},
277260
navigationIcon = { ChannelInfoNavigationIcon(onClick = onNavigationIconClick) },
278261
actions = {
279-
if (isSaving) {
262+
if (isBusy) {
280263
LoadingIndicator(
281264
modifier = Modifier
282265
.padding(end = StreamTokens.spacingSm)
@@ -426,7 +409,6 @@ internal fun GroupChannelEditPlaceholder() {
426409
GroupChannelEditContent(
427410
channel = PreviewChannelData.channelWithImage,
428411
channelName = TextFieldValue(text = ""),
429-
isSaving = false,
430412
)
431413
}
432414

@@ -443,24 +425,23 @@ internal fun GroupChannelEditFilled() {
443425
GroupChannelEditContent(
444426
channel = PreviewChannelData.channelWithImage.copy(name = "Channel Name"),
445427
channelName = TextFieldValue(text = "Channel Name"),
446-
isSaving = false,
447428
)
448429
}
449430

450431
@Preview
451432
@Composable
452-
private fun GroupChannelEditSavingPreview() {
433+
private fun GroupChannelEditBusyPreview() {
453434
ChatTheme {
454-
GroupChannelEditSaving()
435+
GroupChannelEditBusy()
455436
}
456437
}
457438

458439
@Composable
459-
internal fun GroupChannelEditSaving() {
440+
internal fun GroupChannelEditBusy() {
460441
GroupChannelEditContent(
461442
channel = PreviewChannelData.channelWithImage.copy(name = "Channel Name"),
462443
channelName = TextFieldValue(text = "Channel Name"),
463-
isSaving = true,
444+
isBusy = true,
464445
)
465446
}
466447

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditUtils.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public fun GroupChannelInfoScreen(
113113
currentUser: User? = ChatClient.instance().getCurrentUser(),
114114
onNavigationIconClick: () -> Unit = {},
115115
) {
116+
val context = LocalContext.current
116117
val headerViewModel = viewModel<ChannelHeaderViewModel>(factory = viewModelFactory)
117118
val infoViewModel = viewModel<ChannelInfoViewModel>(factory = viewModelFactory)
118119
val headerState by headerViewModel.state.collectAsStateWithLifecycle()
@@ -136,7 +137,10 @@ public fun GroupChannelInfoScreen(
136137
FullscreenDialog(onDismissRequest = { showEditChannel = false }) {
137138
ViewModelStore {
138139
val editViewModel = viewModel<GroupChannelEditViewModel>(
139-
factory = GroupChannelEditViewModelFactory(cid = channel.cid),
140+
factory = GroupChannelEditViewModelFactory(
141+
context = context,
142+
cid = channel.cid,
143+
),
140144
)
141145
GroupChannelEditScreen(
142146
viewModel = editViewModel,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2014-2026 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.viewmodel.channel
18+
19+
import android.content.Context
20+
import android.net.Uri
21+
import io.getstream.chat.android.client.internal.file.StreamFileManager
22+
import java.io.File
23+
import java.util.UUID
24+
25+
/**
26+
* Copies a picked gallery or document [Uri] into app cache as a local image [File].
27+
*/
28+
internal fun interface GalleryImageCopier {
29+
/**
30+
* @param uri The content or file [Uri] to read.
31+
* @return A file in app cache, or `null` if the [Uri] cannot be read or written.
32+
*/
33+
fun copyToCache(uri: Uri): File?
34+
}
35+
36+
/**
37+
* [GalleryImageCopier] that reads the [Uri] via [android.content.ContentResolver] and writes a
38+
* JPEG into timestamped cache using [StreamFileManager].
39+
*
40+
* @param context Used for [android.content.ContentResolver] and cache directory access.
41+
* @param fileManager Writes the stream into cache.
42+
*/
43+
internal class ContentResolverImageCopier(
44+
private val context: Context,
45+
private val fileManager: StreamFileManager = StreamFileManager(),
46+
) : GalleryImageCopier {
47+
48+
override fun copyToCache(uri: Uri): File? {
49+
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
50+
return fileManager.writeFileInTimestampedCache(
51+
context = context,
52+
fileName = "image_${UUID.randomUUID()}.jpg",
53+
source = inputStream,
54+
).getOrNull()
55+
}
56+
}

0 commit comments

Comments
 (0)