Skip to content

Commit 1113aea

Browse files
chore(conv-list): Migrate Shimmer to Composable
AI-assistant: Copilot 1.0.6 (Claude Sonnet 4.6) Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent a948668 commit 1113aea

4 files changed

Lines changed: 151 additions & 18 deletions

File tree

app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import com.nextcloud.talk.contacts.ContactsViewModel
9090
import com.nextcloud.talk.contextchat.ContextChatView
9191
import com.nextcloud.talk.contextchat.ContextChatViewModel
9292
import com.nextcloud.talk.conversationlist.ui.ConversationListFab
93+
import com.nextcloud.talk.conversationlist.ui.ConversationListSkeleton
9394
import com.nextcloud.talk.conversationlist.ui.ConversationsEmptyStateView
9495
import com.nextcloud.talk.conversationlist.ui.StatusBannerRow
9596
import com.nextcloud.talk.conversationlist.ui.UnreadMentionBubble
@@ -215,6 +216,7 @@ class ConversationsListActivity :
215216
private val showNoArchivedViewState = MutableStateFlow(false)
216217
private val showUnreadBubbleState = MutableStateFlow(false)
217218
private val isFabVisibleState = MutableStateFlow(true)
219+
private val isShimmerVisibleState = MutableStateFlow(true)
218220
private var roomsQueryDisposable: Disposable? = null
219221
private var openConversationsQueryDisposable: Disposable? = null
220222
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
@@ -279,6 +281,7 @@ class ConversationsListActivity :
279281
setupEmptyStateView()
280282
setupFab()
281283
setupUnreadBubble()
284+
setupShimmer()
282285
initSystemBars()
283286
viewThemeUtils.material.themeSearchCardView(binding.searchToolbarContainer)
284287
viewThemeUtils.material.colorMaterialButtonContent(binding.menuButton, ColorRole.ON_SURFACE_VARIANT)
@@ -311,7 +314,7 @@ class ConversationsListActivity :
311314
adapter = FlexibleAdapter(conversationItems, this, true)
312315
addEmptyItemForEdgeToEdgeIfNecessary()
313316
} else {
314-
binding.loadingContent.visibility = View.GONE
317+
isShimmerVisibleState.value = false
315318
}
316319
adapter?.addListener(this)
317320
prepareViews()
@@ -437,7 +440,7 @@ class ConversationsListActivity :
437440
is ConversationsListViewModel.GetRoomsSuccessState -> {
438441
if (adapterWasNull) {
439442
adapterWasNull = false
440-
binding.loadingContent.visibility = View.GONE
443+
isShimmerVisibleState.value = false
441444
}
442445
initOverallLayout(state.listIsNotEmpty)
443446
binding.swipeRefreshLayoutView.isRefreshing = false
@@ -1228,6 +1231,19 @@ class ConversationsListActivity :
12281231
}
12291232
}
12301233

1234+
private fun setupShimmer() {
1235+
binding.shimmerComposeView.apply {
1236+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
1237+
setContent {
1238+
val colorScheme = remember { viewThemeUtils.getColorScheme(context) }
1239+
val isShimmerVisible by isShimmerVisibleState.collectAsStateWithLifecycle()
1240+
MaterialTheme(colorScheme = colorScheme) {
1241+
ConversationListSkeleton(isVisible = isShimmerVisible)
1242+
}
1243+
}
1244+
}
1245+
}
1246+
12311247
private fun handleUI(show: Boolean) {
12321248
binding.searchText.isEnabled = show
12331249
binding.searchText.isVisible = show
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.conversationlist.ui
9+
10+
import androidx.compose.animation.AnimatedVisibility
11+
import androidx.compose.animation.core.RepeatMode
12+
import androidx.compose.animation.core.animateFloat
13+
import androidx.compose.animation.core.infiniteRepeatable
14+
import androidx.compose.animation.core.rememberInfiniteTransition
15+
import androidx.compose.animation.core.tween
16+
import androidx.compose.animation.fadeIn
17+
import androidx.compose.animation.fadeOut
18+
import androidx.compose.foundation.background
19+
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.Row
22+
import androidx.compose.foundation.layout.Spacer
23+
import androidx.compose.foundation.layout.fillMaxWidth
24+
import androidx.compose.foundation.layout.height
25+
import androidx.compose.foundation.layout.padding
26+
import androidx.compose.foundation.layout.size
27+
import androidx.compose.foundation.layout.width
28+
import androidx.compose.foundation.shape.CircleShape
29+
import androidx.compose.foundation.shape.RoundedCornerShape
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.runtime.Composable
32+
import androidx.compose.runtime.getValue
33+
import androidx.compose.ui.Alignment
34+
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.draw.clip
36+
import androidx.compose.ui.tooling.preview.Preview
37+
import androidx.compose.ui.unit.dp
38+
import android.content.res.Configuration
39+
import androidx.compose.foundation.isSystemInDarkTheme
40+
import androidx.compose.material3.darkColorScheme
41+
import androidx.compose.material3.lightColorScheme
42+
43+
private const val SHIMMER_ITEM_COUNT = 4
44+
private const val SHIMMER_ANIM_DURATION_MS = 800
45+
private const val SHIMMER_ALPHA_MIN = 0.2f
46+
private const val TITLE_WIDTH = 0.6f
47+
private const val SUBLINE_WIDTH = 0.9f
48+
49+
private const val SHIMMER_ALPHA_MAX = TITLE_WIDTH
50+
51+
/**
52+
* Top-level wrapper rendered by the shimmer_compose_view ComposeView.
53+
* Animates in/out via [AnimatedVisibility] and shows skeleton placeholder rows
54+
* while the conversation list is loading for the first time.
55+
*/
56+
@Composable
57+
fun ConversationListSkeleton(isVisible: Boolean, itemCount: Int = SHIMMER_ITEM_COUNT) {
58+
AnimatedVisibility(
59+
visible = isVisible,
60+
enter = fadeIn(),
61+
exit = fadeOut()
62+
) {
63+
val infiniteTransition = rememberInfiniteTransition(label = "shimmer")
64+
val shimmerAlpha by infiniteTransition.animateFloat(
65+
initialValue = SHIMMER_ALPHA_MIN,
66+
targetValue = SHIMMER_ALPHA_MAX,
67+
animationSpec = infiniteRepeatable(
68+
animation = tween(durationMillis = SHIMMER_ANIM_DURATION_MS),
69+
repeatMode = RepeatMode.Reverse
70+
),
71+
label = "shimmerAlpha"
72+
)
73+
val shimmerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = shimmerAlpha)
74+
75+
Column {
76+
repeat(itemCount) {
77+
ShimmerConversationItem(shimmerColor = shimmerColor)
78+
}
79+
}
80+
}
81+
}
82+
83+
@Composable
84+
private fun ShimmerConversationItem(shimmerColor: androidx.compose.ui.graphics.Color) {
85+
Row(
86+
modifier = Modifier
87+
.fillMaxWidth()
88+
.padding(horizontal = 16.dp, vertical = 8.dp),
89+
verticalAlignment = Alignment.CenterVertically
90+
) {
91+
Box(
92+
modifier = Modifier
93+
.size(48.dp)
94+
.clip(CircleShape)
95+
.background(shimmerColor)
96+
)
97+
98+
Spacer(modifier = Modifier.width(16.dp))
99+
100+
Column(modifier = Modifier.weight(1f)) {
101+
Box(
102+
modifier = Modifier
103+
.fillMaxWidth(TITLE_WIDTH)
104+
.height(14.dp)
105+
.clip(RoundedCornerShape(4.dp))
106+
.background(shimmerColor)
107+
)
108+
109+
Spacer(modifier = Modifier.height(6.dp))
110+
111+
Box(
112+
modifier = Modifier
113+
.fillMaxWidth(SUBLINE_WIDTH)
114+
.height(12.dp)
115+
.clip(RoundedCornerShape(4.dp))
116+
.background(shimmerColor)
117+
)
118+
}
119+
}
120+
}
121+
122+
@Preview(showBackground = true, name = "Shimmer – visible")
123+
@Preview(showBackground = true, name = "Shimmer – visible (dark)", uiMode = Configuration.UI_MODE_NIGHT_YES)
124+
@Composable
125+
private fun ShimmerVisibleDarkPreview() {
126+
val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
127+
MaterialTheme(colorScheme = colorScheme) {
128+
ConversationListSkeleton(isVisible = true)
129+
}
130+
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ fun NoArchivedConversationsView() {
124124
}
125125
}
126126

127-
// --- Previews ---
128-
129127
@Preview(showBackground = true)
130128
@Composable
131129
private fun EmptyConversationsWithLogoPreview() {

app/src/main/res/layout/activity_conversations.xml

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,11 @@
170170
android:layout_height="wrap_content"/>
171171
</com.google.android.material.appbar.AppBarLayout>
172172

173-
<LinearLayout
174-
android:id="@+id/loading_content"
173+
<androidx.compose.ui.platform.ComposeView
174+
android:id="@+id/shimmer_compose_view"
175175
android:layout_width="match_parent"
176176
android:layout_height="wrap_content"
177-
android:orientation="vertical"
178-
android:layout_marginTop="50dp">
179-
180-
<include layout="@layout/rv_item_conversation_with_last_message_shimmer" />
181-
182-
<include layout="@layout/rv_item_conversation_with_last_message_shimmer" />
183-
184-
<include layout="@layout/rv_item_conversation_with_last_message_shimmer" />
185-
186-
<include layout="@layout/rv_item_conversation_with_last_message_shimmer" />
187-
188-
</LinearLayout>
177+
android:layout_marginTop="50dp" />
189178

190179
<androidx.compose.ui.platform.ComposeView
191180
android:id="@+id/empty_state_compose_view"

0 commit comments

Comments
 (0)