Skip to content

Commit b5379b0

Browse files
Merge branch 'develop' into rewatch-on-reconnect
2 parents fabc26f + 47ab3a3 commit b5379b0

File tree

7 files changed

+306
-125
lines changed

7 files changed

+306
-125
lines changed

.github/workflows/pr-clean-stale.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: PR cleanup
2+
3+
on:
4+
schedule:
5+
- cron: "0 7 * * 1" # every Monday 07:00 UTC (08:00 CET / 09:00 CEST)
6+
workflow_dispatch:
7+
8+
jobs:
9+
pr-clean-stale:
10+
uses: GetStream/android-ci-actions/.github/workflows/pr-clean-stale.yaml@main
11+
secrets: inherit

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

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
*/
1616
package io.getstream.feeds.android.sample.feed
1717

18-
import android.net.Uri
1918
import androidx.compose.animation.AnimatedVisibility
2019
import androidx.compose.foundation.background
20+
import androidx.compose.foundation.combinedClickable
2121
import androidx.compose.foundation.layout.Arrangement
2222
import androidx.compose.foundation.layout.Box
2323
import androidx.compose.foundation.layout.Column
@@ -49,6 +49,8 @@ import androidx.compose.runtime.remember
4949
import androidx.compose.runtime.setValue
5050
import androidx.compose.ui.Alignment
5151
import androidx.compose.ui.Modifier
52+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
53+
import androidx.compose.ui.platform.LocalHapticFeedback
5254
import androidx.compose.ui.res.painterResource
5355
import androidx.compose.ui.text.font.FontWeight
5456
import androidx.compose.ui.unit.dp
@@ -63,7 +65,9 @@ import io.getstream.feeds.android.client.api.model.FeedId
6365
import io.getstream.feeds.android.client.api.model.ThreadedCommentData
6466
import io.getstream.feeds.android.sample.R
6567
import io.getstream.feeds.android.sample.components.LoadingScreen
68+
import io.getstream.feeds.android.sample.feed.CommentsSheetViewModel.Event
6669
import io.getstream.feeds.android.sample.ui.util.ScrolledToBottomEffect
70+
import io.getstream.feeds.android.sample.ui.util.conditional
6771
import io.getstream.feeds.android.sample.ui.util.rippleClickable
6872
import io.getstream.feeds.android.sample.util.AsyncResource
6973

@@ -90,13 +94,13 @@ fun ColumnScope.CommentsBottomSheet(navigator: DestinationsNavigator) {
9094
}
9195

9296
is AsyncResource.Content -> {
93-
val comments by state.data.comments.collectAsStateWithLifecycle()
97+
val comments by state.data.second.comments.collectAsStateWithLifecycle()
98+
val currentUserId = state.data.first.id
9499

95100
CommentsBottomSheetContent(
96101
comments = comments,
97-
onLoadMore = viewModel::onLoadMore,
98-
onLikeClick = viewModel::onLikeClick,
99-
onPostComment = viewModel::onPostComment,
102+
currentUserId = currentUserId,
103+
onEvent = viewModel::onEvent,
100104
)
101105
}
102106
}
@@ -105,9 +109,8 @@ fun ColumnScope.CommentsBottomSheet(navigator: DestinationsNavigator) {
105109
@Composable
106110
private fun ColumnScope.CommentsBottomSheetContent(
107111
comments: List<ThreadedCommentData>,
108-
onLoadMore: () -> Unit,
109-
onLikeClick: (ThreadedCommentData) -> Unit,
110-
onPostComment: (text: String, parentCommentId: String?, attachmentUris: List<Uri>) -> Unit,
112+
currentUserId: String,
113+
onEvent: (Event) -> Unit,
111114
) {
112115
var createCommentData: CreateCommentData? by remember { mutableStateOf(null) }
113116
var expandedCommentId: String? by remember { mutableStateOf(null) }
@@ -122,20 +125,21 @@ private fun ColumnScope.CommentsBottomSheetContent(
122125
Box(Modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp)) {
123126
val lazyListState = rememberLazyListState()
124127

125-
ScrolledToBottomEffect(lazyListState, action = onLoadMore)
128+
ScrolledToBottomEffect(lazyListState, action = { onEvent(Event.OnScrollToBottom) })
126129

127130
LazyColumn(state = lazyListState) {
128131
items(comments) { comment ->
129132
Comment(
130133
data = comment,
134+
currentUserId = currentUserId,
131135
isExpanded = comment.id == expandedCommentId,
132136
onExpandClick = {
133137
expandedCommentId = comment.id.takeUnless { it == expandedCommentId }
134138
},
135139
onReplyClick = { commentId ->
136140
createCommentData = CreateCommentData(commentId)
137141
},
138-
onLikeClick = onLikeClick,
142+
onEvent = onEvent,
139143
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
140144
)
141145
}
@@ -163,7 +167,7 @@ private fun ColumnScope.CommentsBottomSheetContent(
163167
onDismiss = { createCommentData = null },
164168
requireText = true,
165169
onPost = { text, attachments ->
166-
onPostComment(text, createCommentData?.replyParentId, attachments)
170+
onEvent(Event.OnPost(text, createCommentData?.replyParentId, attachments))
167171
createCommentData = null
168172
},
169173
)
@@ -173,20 +177,36 @@ private fun ColumnScope.CommentsBottomSheetContent(
173177
@Composable
174178
private fun Comment(
175179
data: ThreadedCommentData,
180+
currentUserId: String,
176181
isExpanded: Boolean,
177182
onExpandClick: () -> Unit,
178183
onReplyClick: (commentId: String) -> Unit,
179-
onLikeClick: (ThreadedCommentData) -> Unit,
184+
onEvent: (Event) -> Unit,
180185
modifier: Modifier = Modifier,
181186
) {
182187
var expandedCommentId: String? by remember { mutableStateOf(null) }
188+
var showContextMenu by remember { mutableStateOf(false) }
189+
var showEditDialog by remember { mutableStateOf(false) }
190+
191+
val hapticFeedback = LocalHapticFeedback.current
183192

184193
Column(modifier.fillMaxWidth()) {
185194
Column(
186195
Modifier.background(
187196
MaterialTheme.colorScheme.secondaryContainer,
188197
shape = RoundedCornerShape(16.dp),
189198
)
199+
.conditional(data.user.id == currentUserId) {
200+
combinedClickable(
201+
indication = null,
202+
interactionSource = null,
203+
onClick = {},
204+
onLongClick = {
205+
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
206+
showContextMenu = true
207+
},
208+
)
209+
}
190210
.padding(16.dp)
191211
.fillMaxWidth(),
192212
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -205,7 +225,7 @@ private fun Comment(
205225
Text(
206226
text = if (hasOwnHeart) "Unlike" else "Like",
207227
modifier =
208-
Modifier.rippleClickable { onLikeClick(data) }
228+
Modifier.rippleClickable { onEvent(Event.OnLike(data)) }
209229
.padding(horizontal = 8.dp, vertical = 4.dp),
210230
)
211231
Text(
@@ -243,17 +263,47 @@ private fun Comment(
243263
data.replies?.forEach { reply ->
244264
Comment(
245265
data = reply,
266+
currentUserId = currentUserId,
246267
isExpanded = reply.id == expandedCommentId,
247268
onExpandClick = {
248269
expandedCommentId = reply.id.takeUnless { it == expandedCommentId }
249270
},
250271
onReplyClick = onReplyClick,
251-
onLikeClick = onLikeClick,
272+
onEvent = onEvent,
252273
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp),
253274
)
254275
}
255276
}
256277
}
278+
279+
// Context menu dialog for long press
280+
if (showContextMenu) {
281+
ContentContextMenuDialog(
282+
title = "Comment Options",
283+
showEdit = true,
284+
onDismiss = { showContextMenu = false },
285+
onEdit = {
286+
showEditDialog = true
287+
showContextMenu = false
288+
},
289+
onDelete = {
290+
onEvent(Event.OnDelete(data.id))
291+
showContextMenu = false
292+
},
293+
)
294+
}
295+
296+
// Edit comment dialog
297+
if (showEditDialog) {
298+
EditContentDialog(
299+
data.text.orEmpty(),
300+
onDismiss = { showEditDialog = false },
301+
onSave = { newText ->
302+
onEvent(Event.OnEdit(commentId = data.id, text = newText))
303+
showEditDialog = false
304+
},
305+
)
306+
}
257307
}
258308
}
259309

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

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
2727
import io.getstream.feeds.android.client.api.file.FeedUploadPayload
2828
import io.getstream.feeds.android.client.api.file.FileType
2929
import io.getstream.feeds.android.client.api.model.ThreadedCommentData
30+
import io.getstream.feeds.android.client.api.model.User
3031
import io.getstream.feeds.android.client.api.model.request.ActivityAddCommentRequest
3132
import io.getstream.feeds.android.client.api.state.Activity
3233
import io.getstream.feeds.android.network.models.AddCommentReactionRequest
34+
import io.getstream.feeds.android.network.models.UpdateCommentRequest
3335
import io.getstream.feeds.android.sample.login.LoginManager
3436
import io.getstream.feeds.android.sample.util.AsyncResource
3537
import io.getstream.feeds.android.sample.util.copyToCache
@@ -56,20 +58,25 @@ constructor(
5658
private val args = CommentsBottomSheetDestination.argsFrom(savedStateHandle)
5759
private var canLoadMoreComments = true
5860

59-
private val activity =
61+
private val internalState =
6062
flow {
61-
val activity =
62-
loginManager
63-
.currentState()
64-
?.client
65-
?.activity(activityId = args.activityId, fid = args.fid)
66-
emit(AsyncResource.notNull(activity))
63+
val pair =
64+
loginManager.currentState()?.let { (user, client) ->
65+
user to client.activity(activityId = args.activityId, fid = args.fid)
66+
}
67+
68+
emit(AsyncResource.notNull(pair))
6769
}
6870
.stateIn(viewModelScope, SharingStarted.Eagerly, AsyncResource.Loading)
6971

72+
val activity =
73+
internalState
74+
.map { resource -> resource.map(Pair<User, Activity>::second) }
75+
.stateIn(viewModelScope, SharingStarted.Eagerly, AsyncResource.Loading)
76+
7077
val state =
71-
activity
72-
.map { loadingState -> loadingState.map(Activity::state) }
78+
internalState
79+
.map { loadingState -> loadingState.map { it.first to it.second.state } }
7380
.stateIn(viewModelScope, SharingStarted.Eagerly, AsyncResource.Loading)
7481

7582
init {
@@ -78,7 +85,17 @@ constructor(
7885
}
7986
}
8087

81-
fun onLoadMore() {
88+
fun onEvent(event: Event) {
89+
when (event) {
90+
Event.OnScrollToBottom -> loadMore()
91+
is Event.OnEdit -> edit(id = event.commentId, text = event.text)
92+
is Event.OnPost -> post(event.text, event.replyParentId, event.attachments)
93+
is Event.OnLike -> toggleLike(event.comment)
94+
is Event.OnDelete -> delete(event.commentId)
95+
}
96+
}
97+
98+
private fun loadMore() {
8299
if (!canLoadMoreComments) return
83100
activity.withFirstContent(viewModelScope) {
84101
queryMoreComments()
@@ -87,7 +104,7 @@ constructor(
87104
}
88105
}
89106

90-
fun onLikeClick(comment: ThreadedCommentData) {
107+
private fun toggleLike(comment: ThreadedCommentData) {
91108
activity.withFirstContent(viewModelScope) {
92109
if (comment.ownReactions.any { it.type == "heart" }) {
93110
deleteCommentReaction(comment.id, "heart")
@@ -100,7 +117,20 @@ constructor(
100117
}
101118
}
102119

103-
fun onPostComment(text: String, replyParentId: String?, attachments: List<Uri>) {
120+
private fun delete(commentId: String) {
121+
activity.withFirstContent(viewModelScope) {
122+
deleteComment(commentId).logResult(TAG, "Deleting comment: $commentId")
123+
}
124+
}
125+
126+
private fun edit(id: String, text: String) {
127+
activity.withFirstContent(viewModelScope) {
128+
updateComment(id, UpdateCommentRequest(text))
129+
.logResult(TAG, "Editing comment $id with text: $text")
130+
}
131+
}
132+
133+
private fun post(text: String, replyParentId: String?, attachments: List<Uri>) {
104134
activity.withFirstContent(viewModelScope) {
105135
val attachmentFiles =
106136
context.copyToCache(attachments).getOrElse { error ->
@@ -129,6 +159,22 @@ constructor(
129159
}
130160
}
131161

162+
sealed interface Event {
163+
data object OnScrollToBottom : Event
164+
165+
data class OnLike(val comment: ThreadedCommentData) : Event
166+
167+
data class OnDelete(val commentId: String) : Event
168+
169+
data class OnEdit(val commentId: String, val text: String) : Event
170+
171+
data class OnPost(
172+
val text: String,
173+
val replyParentId: String?,
174+
val attachments: List<Uri>,
175+
) : Event
176+
}
177+
132178
companion object {
133179
private const val TAG = "CommentsSheetViewModel"
134180
}

0 commit comments

Comments
 (0)