Skip to content

Commit 6e41f61

Browse files
authored
Show progress indicator while posting content (#70)
1 parent ae31b27 commit 6e41f61

File tree

6 files changed

+155
-85
lines changed

6 files changed

+155
-85
lines changed

stream-feeds-android-sample/src/main/java/io/getstream/feeds/android/sample/feed/CommentsBottomSheet.kt

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@ fun CommentsBottomSheet(navigator: DestinationsNavigator) {
9898
is AsyncResource.Content -> {
9999
val comments by state.data.second.comments.collectAsStateWithLifecycle()
100100
val currentUserId = state.data.first.id
101+
val createContentState by viewModel.createContentState.collectAsStateWithLifecycle()
101102

102103
Column {
103104
CommentsBottomSheetContent(
104105
comments = comments,
105106
currentUserId = currentUserId,
107+
createContentState = createContentState,
106108
onEvent = viewModel::onEvent,
107109
)
108110
}
@@ -115,9 +117,9 @@ fun CommentsBottomSheet(navigator: DestinationsNavigator) {
115117
private fun ColumnScope.CommentsBottomSheetContent(
116118
comments: List<ThreadedCommentData>,
117119
currentUserId: String,
120+
createContentState: CreateContentState,
118121
onEvent: (Event) -> Unit,
119122
) {
120-
var createCommentData: CreateCommentData? by remember { mutableStateOf(null) }
121123
var expandedCommentId: String? by remember { mutableStateOf(null) }
122124

123125
Text(
@@ -141,9 +143,7 @@ private fun ColumnScope.CommentsBottomSheetContent(
141143
onExpandClick = {
142144
expandedCommentId = comment.id.takeUnless { it == expandedCommentId }
143145
},
144-
onReplyClick = { commentId ->
145-
createCommentData = CreateCommentData(commentId)
146-
},
146+
onReplyClick = { commentId -> onEvent(Event.OnReply(commentId)) },
147147
onEvent = onEvent,
148148
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
149149
)
@@ -154,7 +154,7 @@ private fun ColumnScope.CommentsBottomSheetContent(
154154
}
155155

156156
FloatingActionButton(
157-
onClick = { createCommentData = CreateCommentData() },
157+
onClick = { onEvent(Event.OnAddContent) },
158158
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
159159
shape = CircleShape,
160160
) {
@@ -166,17 +166,13 @@ private fun ColumnScope.CommentsBottomSheetContent(
166166
}
167167
}
168168

169-
if (createCommentData != null) {
170-
CreateContentBottomSheet(
171-
title = "Add comment",
172-
onDismiss = { createCommentData = null },
173-
requireText = true,
174-
onPost = { text, attachments ->
175-
onEvent(Event.OnPost(text, createCommentData?.replyParentId, attachments))
176-
createCommentData = null
177-
},
178-
)
179-
}
169+
CreateContentBottomSheet(
170+
state = createContentState,
171+
title = "Add comment",
172+
onDismiss = { onEvent(Event.OnContentCreateDismiss) },
173+
onPost = { text, attachments -> onEvent(Event.OnPost(text, attachments)) },
174+
requireText = true,
175+
)
180176
}
181177

182178
@Composable
@@ -311,5 +307,3 @@ private fun Comment(
311307
}
312308
}
313309
}
314-
315-
@JvmInline private value class CreateCommentData(val replyParentId: String? = null)

stream-feeds-android-sample/src/main/java/io/getstream/feeds/android/sample/feed/CommentsSheetViewModel.kt

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import io.getstream.feeds.android.sample.util.notNull
4141
import io.getstream.feeds.android.sample.util.withFirstContent
4242
import io.getstream.feeds.android.sample.utils.logResult
4343
import javax.inject.Inject
44+
import kotlinx.coroutines.flow.MutableStateFlow
4445
import kotlinx.coroutines.flow.SharingStarted
46+
import kotlinx.coroutines.flow.StateFlow
47+
import kotlinx.coroutines.flow.asStateFlow
4548
import kotlinx.coroutines.flow.flow
4649
import kotlinx.coroutines.flow.map
4750
import kotlinx.coroutines.flow.stateIn
@@ -79,6 +82,10 @@ constructor(
7982
.map { loadingState -> loadingState.map { it.first to it.second.state } }
8083
.stateIn(viewModelScope, SharingStarted.Eagerly, AsyncResource.Loading)
8184

85+
private var replyParentId: String? = null
86+
private val _createContentState = MutableStateFlow(CreateContentState.Hidden)
87+
val createContentState: StateFlow<CreateContentState> = _createContentState.asStateFlow()
88+
8289
init {
8390
activity.withFirstContent(viewModelScope) {
8491
get().logResult(TAG, "Loading activity: $activityId")
@@ -89,9 +96,12 @@ constructor(
8996
when (event) {
9097
Event.OnScrollToBottom -> loadMore()
9198
is Event.OnEdit -> edit(id = event.commentId, text = event.text)
92-
is Event.OnPost -> post(event.text, event.replyParentId, event.attachments)
99+
is Event.OnPost -> post(event.text, replyParentId, event.attachments)
93100
is Event.OnLike -> toggleLike(event.comment)
94101
is Event.OnDelete -> delete(event.commentId)
102+
Event.OnAddContent -> showAddContent(true)
103+
Event.OnContentCreateDismiss -> showAddContent(false)
104+
is Event.OnReply -> showAddContent(true, event.parentId)
95105
}
96106
}
97107

@@ -130,49 +140,67 @@ constructor(
130140
}
131141
}
132142

143+
private fun showAddContent(show: Boolean, replyParentId: String? = null) {
144+
_createContentState.value =
145+
if (show) CreateContentState.Composing else CreateContentState.Hidden
146+
this.replyParentId = replyParentId
147+
}
148+
133149
private fun post(text: String, replyParentId: String?, attachments: List<Uri>) {
150+
_createContentState.value = CreateContentState.Posting
151+
134152
activity.withFirstContent(viewModelScope) {
135153
val attachmentFiles =
136154
context.copyToCache(attachments).getOrElse { error ->
137155
Log.e(TAG, "Failed to copy attachments", error)
156+
_createContentState.value = CreateContentState.Composing
138157
return@withFirstContent
139158
}
140159

141-
addComment(
142-
ActivityAddCommentRequest(
143-
comment = text,
144-
activityId = activityId,
145-
parentId = replyParentId,
146-
createNotificationActivity = true,
147-
attachmentUploads =
148-
attachmentFiles.map {
149-
FeedUploadPayload(file = it, type = FileType.Image("jpeg"))
150-
},
151-
),
152-
attachmentUploadProgress = { file, progress ->
153-
Log.d(TAG, "Uploading attachment: ${file.type}, progress: $progress")
154-
},
155-
)
156-
.logResult(TAG, "Adding comment to activity: $activityId")
160+
val result =
161+
addComment(
162+
ActivityAddCommentRequest(
163+
comment = text,
164+
activityId = activityId,
165+
parentId = replyParentId,
166+
createNotificationActivity = true,
167+
attachmentUploads =
168+
attachmentFiles.map {
169+
FeedUploadPayload(file = it, type = FileType.Image("jpeg"))
170+
},
171+
),
172+
attachmentUploadProgress = { file, progress ->
173+
Log.d(TAG, "Uploading attachment: ${file.type}, progress: $progress")
174+
},
175+
)
176+
.logResult(TAG, "Adding comment to activity: $activityId")
157177

158178
deleteFiles(attachmentFiles)
179+
180+
_createContentState.value =
181+
result.fold(
182+
onSuccess = { CreateContentState.Hidden },
183+
onFailure = { CreateContentState.Composing },
184+
)
159185
}
160186
}
161187

162188
sealed interface Event {
163189
data object OnScrollToBottom : Event
164190

191+
data object OnAddContent : Event
192+
165193
data class OnLike(val comment: ThreadedCommentData) : Event
166194

167195
data class OnDelete(val commentId: String) : Event
168196

169197
data class OnEdit(val commentId: String, val text: String) : Event
170198

171-
data class OnPost(
172-
val text: String,
173-
val replyParentId: String?,
174-
val attachments: List<Uri>,
175-
) : Event
199+
data class OnPost(val text: String, val attachments: List<Uri>) : Event
200+
201+
data object OnContentCreateDismiss : Event
202+
203+
data class OnReply(val parentId: String) : Event
176204
}
177205

178206
companion object {

stream-feeds-android-sample/src/main/java/io/getstream/feeds/android/sample/feed/CreateContentBottomSheet.kt

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
2828
import androidx.compose.foundation.layout.padding
2929
import androidx.compose.foundation.layout.size
3030
import androidx.compose.foundation.text.KeyboardOptions
31+
import androidx.compose.material3.CircularProgressIndicator
3132
import androidx.compose.material3.ExperimentalMaterial3Api
3233
import androidx.compose.material3.Icon
3334
import androidx.compose.material3.IconButton
3435
import androidx.compose.material3.MaterialTheme
3536
import androidx.compose.material3.ModalBottomSheet
3637
import androidx.compose.material3.OutlinedTextField
38+
import androidx.compose.material3.SheetValue
3739
import androidx.compose.material3.Text
3840
import androidx.compose.material3.TextButton
3941
import androidx.compose.material3.rememberModalBottomSheetState
4042
import androidx.compose.runtime.Composable
4143
import androidx.compose.runtime.getValue
4244
import androidx.compose.runtime.mutableStateOf
4345
import androidx.compose.runtime.remember
46+
import androidx.compose.runtime.rememberUpdatedState
4447
import androidx.compose.runtime.setValue
4548
import androidx.compose.ui.Alignment
4649
import androidx.compose.ui.Modifier
@@ -51,22 +54,39 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
5154
import androidx.compose.ui.unit.dp
5255
import androidx.compose.ui.unit.sp
5356

57+
enum class CreateContentState {
58+
Hidden,
59+
Composing,
60+
Posting,
61+
}
62+
5463
@OptIn(ExperimentalMaterial3Api::class)
5564
@Composable
5665
fun CreateContentBottomSheet(
66+
state: CreateContentState,
5767
title: String,
5868
onDismiss: () -> Unit,
5969
onPost: (text: String, attachments: List<Uri>) -> Unit,
6070
requireText: Boolean,
61-
extraActions: @Composable RowScope.() -> Unit = {},
71+
extraActions: @Composable RowScope.(inputEnabled: Boolean) -> Unit = {},
6272
) {
73+
if (state == CreateContentState.Hidden) return
74+
75+
val state by rememberUpdatedState(state)
76+
val sheetState =
77+
rememberModalBottomSheetState(
78+
skipPartiallyExpanded = true,
79+
// Do not allow hiding while posting
80+
confirmValueChange = { it != SheetValue.Hidden || state != CreateContentState.Posting },
81+
)
82+
val inputEnabled = state == CreateContentState.Composing
83+
6384
var postText by remember { mutableStateOf("") }
64-
var attachments by remember { mutableStateOf<List<Uri>>(emptyList()) }
65-
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
85+
var attachments by remember { mutableStateOf(emptyList<Uri>()) }
6686

6787
ModalBottomSheet(
6888
onDismissRequest = onDismiss,
69-
sheetState = bottomSheetState,
89+
sheetState = sheetState,
7090
modifier = Modifier.fillMaxWidth(),
7191
) {
7292
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
@@ -78,16 +98,21 @@ fun CreateContentBottomSheet(
7898
) {
7999
Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Companion.Bold)
80100

81-
val canPost = attachments.isNotEmpty() && !requireText || postText.isNotBlank()
82-
TextButton(onClick = { onPost(postText, attachments) }, enabled = canPost) {
83-
Text(text = "Submit", fontWeight = FontWeight.Companion.Medium)
101+
if (state == CreateContentState.Posting) {
102+
CircularProgressIndicator()
103+
} else {
104+
val canPost = attachments.isNotEmpty() && !requireText || postText.isNotBlank()
105+
TextButton(onClick = { onPost(postText, attachments) }, enabled = canPost) {
106+
Text(text = "Submit", fontWeight = FontWeight.Companion.Medium)
107+
}
84108
}
85109
}
86110

87111
// Text Input
88112
OutlinedTextField(
89113
value = postText,
90114
onValueChange = { postText = it },
115+
enabled = inputEnabled,
91116
placeholder = { Text("What's on your mind?") },
92117
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
93118
minLines = 3,
@@ -106,6 +131,7 @@ fun CreateContentBottomSheet(
106131
AttachmentButton(
107132
hasAttachment = hasAttachments,
108133
onAttachmentsSelected = { uris -> attachments = uris },
134+
enabled = inputEnabled,
109135
)
110136

111137
if (hasAttachments) {
@@ -117,21 +143,26 @@ fun CreateContentBottomSheet(
117143
}
118144

119145
// Display any extra actions passed to the bottom sheet
120-
extraActions()
146+
extraActions(inputEnabled)
121147
}
122148
}
123149
}
124150
}
125151

126152
@Composable
127-
private fun AttachmentButton(hasAttachment: Boolean, onAttachmentsSelected: (List<Uri>) -> Unit) {
153+
private fun AttachmentButton(
154+
hasAttachment: Boolean,
155+
onAttachmentsSelected: (List<Uri>) -> Unit,
156+
enabled: Boolean,
157+
) {
128158
val activityLauncher =
129159
rememberLauncherForActivityResult(PickMultipleVisualMedia(), onAttachmentsSelected)
130160

131161
IconButton(
132162
onClick = {
133163
activityLauncher.launch(PickVisualMediaRequest(mediaType = PickVisualMedia.ImageOnly))
134-
}
164+
},
165+
enabled = enabled,
135166
) {
136167
Icon(
137168
painter = painterResource(android.R.drawable.ic_menu_gallery),

stream-feeds-android-sample/src/main/java/io/getstream/feeds/android/sample/feed/CreatePollButton.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ import io.getstream.feeds.android.sample.R
6060

6161
@OptIn(ExperimentalMaterial3Api::class)
6262
@Composable
63-
fun CreatePollButton(onCreatePoll: (PollFormData) -> Unit) {
63+
fun CreatePollButton(onCreatePoll: (PollFormData) -> Unit, enabled: Boolean) {
6464
var showPollBottomSheet by remember { mutableStateOf(false) }
6565

66-
IconButton(onClick = { showPollBottomSheet = true }) {
66+
IconButton(onClick = { showPollBottomSheet = true }, enabled = enabled) {
6767
Icon(
6868
painter = painterResource(R.drawable.poll),
6969
contentDescription = "Create Poll",

0 commit comments

Comments
 (0)