Skip to content

Commit 64e447c

Browse files
style(detekt): reduce cyclomatic complexity
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent 4a00f0e commit 64e447c

2 files changed

Lines changed: 115 additions & 64 deletions

File tree

app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationList.kt

Lines changed: 113 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
package com.nextcloud.talk.conversationlist.ui
88

99
import androidx.compose.animation.core.Animatable
10+
import androidx.compose.animation.core.AnimationVector1D
1011
import androidx.compose.animation.core.Spring
12+
import androidx.compose.animation.core.SpringSpec
1113
import androidx.compose.animation.core.spring
1214
import androidx.compose.foundation.background
1315
import androidx.compose.foundation.clickable
@@ -40,19 +42,23 @@ import androidx.compose.runtime.LaunchedEffect
4042
import androidx.compose.runtime.derivedStateOf
4143
import androidx.compose.runtime.getValue
4244
import androidx.compose.runtime.mutableIntStateOf
43-
import androidx.compose.runtime.mutableStateOf
4445
import androidx.compose.runtime.remember
4546
import androidx.compose.runtime.rememberCoroutineScope
4647
import androidx.compose.runtime.rememberUpdatedState
4748
import androidx.compose.runtime.setValue
4849
import androidx.compose.runtime.snapshotFlow
50+
import kotlinx.coroutines.CoroutineScope
4951
import kotlinx.coroutines.flow.first
5052
import kotlinx.coroutines.launch
5153
import androidx.compose.ui.Alignment
5254
import androidx.compose.ui.Modifier
5355
import androidx.compose.ui.draw.clip
5456
import androidx.compose.ui.graphics.Color
57+
import androidx.compose.ui.hapticfeedback.HapticFeedback
5558
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
59+
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
60+
import androidx.compose.ui.input.pointer.PointerId
61+
import androidx.compose.ui.input.pointer.PointerInputScope
5662
import androidx.compose.ui.input.pointer.pointerInput
5763
import androidx.compose.ui.layout.onSizeChanged
5864
import androidx.compose.ui.platform.LocalContext
@@ -109,7 +115,8 @@ fun ConversationList(
109115
listState: LazyListState = rememberLazyListState(),
110116
/** Extra bottom padding added as LazyColumn contentPadding so the last item is reachable above the nav bar. */
111117
contentBottomPadding: Dp = 0.dp,
112-
onSwipeConversation: (ConversationOpsAction, ConversationModel) -> Unit = { _, _ -> }
118+
onSwipeConversation: (ConversationOpsAction, ConversationModel) -> Unit = { _, _ -> },
119+
isOnline: Boolean = true
113120
) {
114121
var prevIndex by remember { mutableIntStateOf(listState.firstVisibleItemIndex) }
115122
var prevOffset by remember { mutableIntStateOf(listState.firstVisibleItemScrollOffset) }
@@ -185,7 +192,8 @@ fun ConversationList(
185192
is ConversationListEntry.ConversationEntry ->
186193
SwipeableConversationItem(
187194
model = entry.model,
188-
onSwipe = { action -> onSwipeConversation(action, entry.model) }
195+
onSwipe = { action -> onSwipeConversation(action, entry.model) },
196+
enabled = isOnline
189197
) {
190198
ConversationListItem(
191199
model = entry.model,
@@ -233,11 +241,11 @@ private const val NON_DESTRUCTIVE_SWIPE_THRESHOLD = 0.20f
233241

234242
private const val NON_DESTRUCTIVE_SWIPE_END_LIMIT = 0.3f
235243

236-
@Suppress("LongMethod")
237244
@Composable
238245
private fun SwipeableConversationItem(
239246
model: ConversationModel,
240247
onSwipe: (ConversationOpsAction) -> Unit,
248+
enabled: Boolean = true,
241249
content: @Composable () -> Unit
242250
) {
243251
val currentModel by rememberUpdatedState(model)
@@ -251,8 +259,6 @@ private fun SwipeableConversationItem(
251259
spring<Float>(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium)
252260
}
253261
var itemWidth by remember { mutableIntStateOf(0) }
254-
var hapticFiredRight by remember { mutableStateOf(false) }
255-
var hapticFiredLeft by remember { mutableStateOf(false) }
256262

257263
val swipeDirection by remember {
258264
derivedStateOf {
@@ -280,63 +286,18 @@ private fun SwipeableConversationItem(
280286
modifier = Modifier
281287
.fillMaxWidth()
282288
.onSizeChanged { size -> itemWidth = size.width }
283-
.pointerInput(Unit) {
284-
awaitEachGesture {
285-
val startToEndThreshold = itemWidth * NON_DESTRUCTIVE_SWIPE_THRESHOLD
286-
val startToEndLimit = itemWidth * NON_DESTRUCTIVE_SWIPE_END_LIMIT
287-
val endToStartThreshold = -itemWidth * DESTRUCTIVE_SWIPE_THRESHOLD
288-
val endToStartLimit = -itemWidth.toFloat()
289-
290-
val down = awaitFirstDown(requireUnconsumed = false)
291-
var horizontalStarted = false
292-
val dragStart = awaitHorizontalTouchSlopOrCancellation(down.id) { change, _ ->
293-
val dx = abs(change.position.x - change.previousPosition.x)
294-
val dy = abs(change.position.y - change.previousPosition.y)
295-
if (dx > dy) {
296-
change.consume()
297-
horizontalStarted = true
298-
}
299-
}
300-
if (dragStart != null && horizontalStarted) {
301-
hapticFiredRight = false
302-
hapticFiredLeft = false
303-
coroutineScope.launch { popScale.snapTo(1f) }
304-
horizontalDrag(dragStart.id) { change ->
305-
val delta = change.position.x - change.previousPosition.x
306-
val newOffset = (offsetX.value + delta).coerceIn(endToStartLimit, startToEndLimit)
307-
coroutineScope.launch { offsetX.snapTo(newOffset) }
308-
if (!hapticFiredRight && startToEndThreshold > 0 && newOffset >= startToEndThreshold) {
309-
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
310-
hapticFiredRight = true
311-
coroutineScope.launch {
312-
popScale.snapTo(POP_SCALE_PEAK)
313-
popScale.animateTo(1f, popAnimationSpec)
314-
}
315-
}
316-
if (!hapticFiredLeft && endToStartThreshold < 0 && newOffset <= endToStartThreshold) {
317-
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
318-
hapticFiredLeft = true
319-
coroutineScope.launch {
320-
popScale.snapTo(POP_SCALE_PEAK)
321-
popScale.animateTo(1f, popAnimationSpec)
322-
}
323-
}
324-
change.consume()
325-
}
326-
val finalOffset = offsetX.value
327-
if (hapticFiredRight && finalOffset >= startToEndThreshold) {
328-
val action = if (currentModel.unreadMessages > 0) {
329-
ConversationOpsAction.MarkAsRead
330-
} else {
331-
ConversationOpsAction.MarkAsUnread
332-
}
333-
currentOnSwipe(action)
334-
} else if (hapticFiredLeft && finalOffset <= endToStartThreshold) {
335-
currentOnSwipe(ConversationOpsAction.Leave)
336-
}
337-
coroutineScope.launch { offsetX.animateTo(0f, spring()) }
338-
}
339-
}
289+
.pointerInput(enabled) {
290+
if (!enabled) return@pointerInput
291+
awaitSwipeGesture(
292+
getItemWidth = { itemWidth },
293+
coroutineScope = coroutineScope,
294+
haptic = haptic,
295+
offsetX = offsetX,
296+
popScale = popScale,
297+
popAnimationSpec = popAnimationSpec,
298+
getModel = { currentModel },
299+
onSwipe = { currentOnSwipe(it) }
300+
)
340301
}
341302
) {
342303
SwipeBackground(
@@ -357,6 +318,95 @@ private fun SwipeableConversationItem(
357318
}
358319
}
359320

321+
private suspend fun PointerInputScope.awaitSwipeGesture(
322+
getItemWidth: () -> Int,
323+
coroutineScope: CoroutineScope,
324+
haptic: HapticFeedback,
325+
offsetX: Animatable<Float, AnimationVector1D>,
326+
popScale: Animatable<Float, AnimationVector1D>,
327+
popAnimationSpec: SpringSpec<Float>,
328+
getModel: () -> ConversationModel,
329+
onSwipe: (ConversationOpsAction) -> Unit
330+
) {
331+
awaitEachGesture {
332+
val width = getItemWidth()
333+
val startToEndThreshold = width * NON_DESTRUCTIVE_SWIPE_THRESHOLD
334+
val startToEndLimit = width * NON_DESTRUCTIVE_SWIPE_END_LIMIT
335+
val endToStartThreshold = -width * DESTRUCTIVE_SWIPE_THRESHOLD
336+
val endToStartLimit = -width.toFloat()
337+
338+
val down = awaitFirstDown(requireUnconsumed = false)
339+
var horizontalStarted = false
340+
val dragStart = awaitHorizontalTouchSlopOrCancellation(down.id) { change, _ ->
341+
val dx = abs(change.position.x - change.previousPosition.x)
342+
val dy = abs(change.position.y - change.previousPosition.y)
343+
if (dx > dy) {
344+
change.consume()
345+
horizontalStarted = true
346+
}
347+
}
348+
if (dragStart == null || !horizontalStarted) return@awaitEachGesture
349+
handleConfirmedSwipeDrag(
350+
dragId = dragStart.id,
351+
coroutineScope = coroutineScope,
352+
haptic = haptic,
353+
offsetX = offsetX,
354+
popScale = popScale,
355+
popAnimationSpec = popAnimationSpec,
356+
startToEndThreshold = startToEndThreshold,
357+
startToEndLimit = startToEndLimit,
358+
endToStartThreshold = endToStartThreshold,
359+
endToStartLimit = endToStartLimit,
360+
getModel = getModel,
361+
onSwipe = onSwipe
362+
)
363+
}
364+
}
365+
366+
private suspend fun AwaitPointerEventScope.handleConfirmedSwipeDrag(
367+
dragId: PointerId,
368+
coroutineScope: CoroutineScope,
369+
haptic: HapticFeedback,
370+
offsetX: Animatable<Float, AnimationVector1D>,
371+
popScale: Animatable<Float, AnimationVector1D>,
372+
popAnimationSpec: SpringSpec<Float>,
373+
startToEndThreshold: Float,
374+
startToEndLimit: Float,
375+
endToStartThreshold: Float,
376+
endToStartLimit: Float,
377+
getModel: () -> ConversationModel,
378+
onSwipe: (ConversationOpsAction) -> Unit
379+
) {
380+
var hapticFiredRight = false
381+
var hapticFiredLeft = false
382+
coroutineScope.launch { popScale.snapTo(1f) }
383+
horizontalDrag(dragId) { change ->
384+
val delta = change.position.x - change.previousPosition.x
385+
val newOffset = (offsetX.value + delta).coerceIn(endToStartLimit, startToEndLimit)
386+
coroutineScope.launch { offsetX.snapTo(newOffset) }
387+
if (!hapticFiredRight && startToEndThreshold > 0 && newOffset >= startToEndThreshold) {
388+
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
389+
hapticFiredRight = true
390+
coroutineScope.launch { popScale.snapTo(POP_SCALE_PEAK); popScale.animateTo(1f, popAnimationSpec) }
391+
}
392+
if (!hapticFiredLeft && endToStartThreshold < 0 && newOffset <= endToStartThreshold) {
393+
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
394+
hapticFiredLeft = true
395+
coroutineScope.launch { popScale.snapTo(POP_SCALE_PEAK); popScale.animateTo(1f, popAnimationSpec) }
396+
}
397+
change.consume()
398+
}
399+
val finalOffset = offsetX.value
400+
when {
401+
hapticFiredRight && finalOffset >= startToEndThreshold -> onSwipe(resolveReadUnreadAction(getModel()))
402+
hapticFiredLeft && finalOffset <= endToStartThreshold -> onSwipe(ConversationOpsAction.Leave)
403+
}
404+
coroutineScope.launch { offsetX.animateTo(0f, spring()) }
405+
}
406+
407+
private fun resolveReadUnreadAction(model: ConversationModel): ConversationOpsAction =
408+
if (model.unreadMessages > 0) ConversationOpsAction.MarkAsRead else ConversationOpsAction.MarkAsUnread
409+
360410
@Composable
361411
private fun SwipeBackground(
362412
modifier: Modifier = Modifier,

app/src/main/java/com/nextcloud/talk/conversationlist/ui/ConversationsListScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,8 @@ fun ConversationsListScreen(
310310
onScrollStopped = callbacks.onScrollStopped,
311311
listState = lazyListState,
312312
contentBottomPadding = paddingValues.calculateBottomPadding(),
313-
onSwipeConversation = callbacks.onConversationOpsAction
313+
onSwipeConversation = callbacks.onConversationOpsAction,
314+
isOnline = isOnline
314315
)
315316
}
316317
// Empty-state overlay (centered; handles its own visibility)

0 commit comments

Comments
 (0)