Skip to content

Commit 5763c13

Browse files
feat(conv-list): Migrate conversation list screen
AI-assistant: Copilot 1.0.10 (Claude Sonnet 4.6) Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent 9ae89e8 commit 5763c13

19 files changed

Lines changed: 882 additions & 968 deletions

app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public void login() throws InterruptedException {
6767

6868
try {
6969
// Delete account if exists
70-
onView(withId(R.id.switch_account_button)).perform(click());
70+
onView(withContentDescription(R.string.nc_settings)).perform(click());
7171
onView(withId(R.id.settings_remove_account)).perform(click());
7272
onView(withText(R.string.nc_settings_remove)).perform(click());
7373
// The remove button must be clicked two times
@@ -120,7 +120,7 @@ public void login() throws InterruptedException {
120120

121121
Thread.sleep(5 * 1000);
122122

123-
onView(withId(R.id.switch_account_button)).perform(click());
123+
onView(withContentDescription(R.string.nc_settings)).perform(click());
124124
onView(withId(R.id.user_name)).check(matches(withText("User One")));
125125

126126
activityScenario.onActivity(activity -> {

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

Lines changed: 138 additions & 562 deletions
Large diffs are not rendered by default.

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.nextcloud.talk.conversationlist.ui
99
import androidx.compose.foundation.clickable
1010
import androidx.compose.foundation.layout.Box
1111
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.PaddingValues
1213
import androidx.compose.foundation.layout.Row
1314
import androidx.compose.foundation.layout.Spacer
1415
import androidx.compose.foundation.layout.fillMaxSize
@@ -46,6 +47,7 @@ import androidx.compose.ui.text.buildAnnotatedString
4647
import androidx.compose.ui.text.font.FontWeight
4748
import androidx.compose.ui.text.style.TextOverflow
4849
import androidx.compose.ui.text.withStyle
50+
import androidx.compose.ui.unit.Dp
4951
import androidx.compose.ui.unit.dp
5052
import coil.compose.AsyncImage
5153
import coil.request.ImageRequest
@@ -82,7 +84,9 @@ fun ConversationList(
8284
onScrollChanged: (scrolledDown: Boolean) -> Unit = {},
8385
/** Called when the list stops scrolling; delivers the last-visible item index. */
8486
onScrollStopped: (lastVisibleIndex: Int) -> Unit = {},
85-
listState: LazyListState = rememberLazyListState()
87+
listState: LazyListState = rememberLazyListState(),
88+
/** Extra bottom padding added as LazyColumn contentPadding so the last item is reachable above the nav bar. */
89+
contentBottomPadding: Dp = 0.dp
8690
) {
8791
var prevIndex by remember { mutableIntStateOf(listState.firstVisibleItemIndex) }
8892
var prevOffset by remember { mutableIntStateOf(listState.firstVisibleItemScrollOffset) }
@@ -116,7 +120,8 @@ fun ConversationList(
116120
) {
117121
LazyColumn(
118122
state = listState,
119-
modifier = Modifier.fillMaxSize()
123+
modifier = Modifier.fillMaxSize(),
124+
contentPadding = PaddingValues(bottom = contentBottomPadding)
120125
) {
121126
items(
122127
items = entries,
@@ -194,7 +199,7 @@ private fun MessageResultListItem(result: SearchMessageEntry, credentials: Strin
194199
modifier = Modifier
195200
.fillMaxWidth()
196201
.clickable { onClick() }
197-
.padding(horizontal = 16.dp, vertical = 8.dp),
202+
.padding(horizontal = 16.dp, vertical = 16.dp),
198203
verticalAlignment = Alignment.CenterVertically
199204
) {
200205
AsyncImage(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> U
4949
FloatingActionButton(
5050
onClick = { if (isEnabled) onClick() },
5151
modifier = Modifier
52-
.padding(8.dp)
5352
.alpha(if (isEnabled) 1f else DISABLED_ALPHA)
5453
) {
5554
Icon(
@@ -61,9 +60,10 @@ fun ConversationListFab(isVisible: Boolean, isEnabled: Boolean, onClick: () -> U
6160
}
6261

6362
@Composable
64-
fun UnreadMentionBubble(visible: Boolean, onClick: () -> Unit) {
63+
fun UnreadMentionBubble(visible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
6564
AnimatedVisibility(
6665
visible = visible,
66+
modifier = modifier,
6767
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
6868
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
6969
) {

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -644,12 +644,25 @@ private fun LastMessageContent(
644644

645645
// Deleted comment
646646
if (chatMessage.isDeletedCommentMessage) {
647+
val parsedText = ChatUtils.getParsedMessage(chatMessage.message, chatMessage.messageParameters) ?: ""
648+
val youPrefix = stringResource(R.string.nc_formatted_message_you, parsedText)
649+
val groupFormat = stringResource(R.string.nc_formatted_message)
650+
val guestLabel = stringResource(R.string.nc_guest)
651+
val displayText = when {
652+
chatMessage.actorId == currentUser.userId -> youPrefix
653+
model.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> parsedText
654+
else -> {
655+
val actorName = chatMessage.actorDisplayName?.takeIf { it.isNotBlank() }
656+
?: if (chatMessage.actorType == "guests" || chatMessage.actorType == "emails") {
657+
guestLabel
658+
} else {
659+
""
660+
}
661+
if (actorName.isBlank()) parsedText else String.format(groupFormat, actorName, parsedText)
662+
}
663+
}
647664
Text(
648-
text = buildHighlightedText(
649-
ChatUtils.getParsedMessage(chatMessage.message, chatMessage.messageParameters) ?: "",
650-
searchQuery,
651-
primaryColor
652-
),
665+
text = buildHighlightedText(displayText, searchQuery, primaryColor),
653666
modifier = modifier,
654667
maxLines = 1,
655668
overflow = TextOverflow.Ellipsis,
@@ -696,7 +709,11 @@ private fun LastMessageContent(
696709

697710
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
698711
var name = chatMessage.message ?: ""
699-
if (name == "{file}") name = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
712+
name = if (name == "{file}") {
713+
chatMessage.messageParameters?.get("file")?.get("name") ?: ""
714+
} else {
715+
ChatUtils.getParsedMessage(name, chatMessage.messageParameters) ?: name
716+
}
700717
val mime = chatMessage.messageParameters?.get("file")?.get("mimetype")
701718
val icon = attachmentIconRes(mime)
702719
val prefix = authorPrefix(chatMessage, currentUser)
@@ -833,7 +850,7 @@ private fun LastMessageContent(
833850
} else {
834851
""
835852
}
836-
String.format(groupFormat, actorName, parsedText)
853+
if (actorName.isBlank()) parsedText else String.format(groupFormat, actorName, parsedText)
837854
}
838855
}
839856
Text(
@@ -1602,7 +1619,7 @@ private fun PreviewLastMessageDeleted() =
16021619
model = previewModel(
16031620
displayName = "Alice",
16041621
lastMessage = previewMsg(
1605-
message = "Message deleted",
1622+
message = "You: Message deleted",
16061623
messageType = "comment_deleted"
16071624
)
16081625
),

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

Lines changed: 101 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.height
2525
import androidx.compose.foundation.layout.offset
2626
import androidx.compose.foundation.layout.padding
2727
import androidx.compose.foundation.layout.size
28+
import androidx.compose.foundation.layout.statusBarsPadding
2829
import androidx.compose.foundation.layout.width
2930
import androidx.compose.foundation.shape.CircleShape
3031
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -91,14 +92,14 @@ data class ConversationListTopBarState(
9192

9293
@Suppress("LongParameterList")
9394
data class ConversationListTopBarActions(
94-
val onSearchQueryChange: (String) -> Unit,
95-
val onSearchActivate: () -> Unit,
96-
val onSearchClose: () -> Unit,
97-
val onFilterClick: () -> Unit,
98-
val onThreadsClick: () -> Unit,
99-
val onAvatarClick: () -> Unit,
100-
val onNavigateBack: () -> Unit,
101-
val onAccountChooserClick: () -> Unit
95+
val onSearchQueryChange: (String) -> Unit = {},
96+
val onSearchActivate: () -> Unit = {},
97+
val onSearchClose: () -> Unit = {},
98+
val onFilterClick: () -> Unit = {},
99+
val onThreadsClick: () -> Unit = {},
100+
val onAvatarClick: () -> Unit = {},
101+
val onNavigateBack: () -> Unit = {},
102+
val onAccountChooserClick: () -> Unit = {}
102103
)
103104

104105
@OptIn(ExperimentalMaterial3Api::class)
@@ -117,18 +118,11 @@ fun ConversationListTopBar(
117118
}
118119
}
119120

120-
Column(modifier = modifier.fillMaxWidth()) {
121+
Column(modifier = modifier.fillMaxWidth().statusBarsPadding()) {
121122
when (val mode = state.mode) {
122123
is TopBarMode.SearchBarIdle -> TopBarIdleContent(
123-
onSearchActivate = actions.onSearchActivate,
124-
onFilterClick = actions.onFilterClick,
125-
onThreadsClick = actions.onThreadsClick,
126-
onAvatarClick = actions.onAvatarClick,
127-
showAvatarBadge = state.showAvatarBadge,
128-
avatarUrl = state.avatarUrl,
129-
credentials = state.credentials,
130-
showFilterActive = state.showFilterActive,
131-
showThreadsButton = state.showThreadsButton
124+
state = state,
125+
actions = actions
132126
)
133127
is TopBarMode.SearchActive -> TopBarSearchActiveContent(
134128
query = mode.query,
@@ -148,91 +142,106 @@ fun ConversationListTopBar(
148142
}
149143
}
150144

151-
@Suppress("LongParameterList")
152145
@Composable
153-
private fun TopBarIdleContent(
154-
onSearchActivate: () -> Unit,
155-
onFilterClick: () -> Unit,
156-
onThreadsClick: () -> Unit,
157-
onAvatarClick: () -> Unit,
158-
showAvatarBadge: Boolean,
159-
avatarUrl: String?,
160-
credentials: String,
161-
showFilterActive: Boolean,
162-
showThreadsButton: Boolean
163-
) {
146+
private fun TopBarIdleContent(state: ConversationListTopBarState, actions: ConversationListTopBarActions) {
164147
Row(
165148
modifier = Modifier
166149
.fillMaxWidth()
167150
.padding(start = 16.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
168151
verticalAlignment = Alignment.CenterVertically
169152
) {
170-
Card(
153+
IdleSearchBarCard(
154+
state = state,
155+
actions = actions,
171156
modifier = Modifier
172157
.weight(1f)
173-
.height(50.dp),
174-
shape = RoundedCornerShape(25.dp),
175-
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
158+
.height(50.dp)
159+
)
160+
Spacer(modifier = Modifier.width(8.dp))
161+
AvatarButton(
162+
avatarUrl = state.avatarUrl,
163+
credentials = state.credentials,
164+
showBadge = state.showAvatarBadge,
165+
onClick = actions.onAvatarClick
166+
)
167+
}
168+
}
169+
170+
@Composable
171+
private fun IdleSearchBarCard(
172+
state: ConversationListTopBarState,
173+
actions: ConversationListTopBarActions,
174+
modifier: Modifier = Modifier
175+
) {
176+
Card(
177+
modifier = modifier,
178+
shape = RoundedCornerShape(25.dp),
179+
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
180+
) {
181+
Box(modifier = Modifier.fillMaxSize()) {
182+
Text(
183+
text = stringResource(R.string.appbar_search_in, stringResource(R.string.nc_app_product_name)),
184+
modifier = Modifier
185+
.fillMaxWidth()
186+
.align(Alignment.CenterStart)
187+
.padding(start = 16.dp, end = if (state.showThreadsButton) 80.dp else 48.dp)
188+
.clickable { actions.onSearchActivate() },
189+
style = MaterialTheme.typography.bodyLarge,
190+
color = MaterialTheme.colorScheme.onSurfaceVariant,
191+
maxLines = 1,
192+
overflow = TextOverflow.Ellipsis
193+
)
194+
IdleSearchBarActions(
195+
state = state,
196+
actions = actions,
197+
modifier = Modifier.align(Alignment.CenterEnd)
198+
)
199+
}
200+
}
201+
}
202+
203+
@Composable
204+
private fun IdleSearchBarActions(
205+
state: ConversationListTopBarState,
206+
actions: ConversationListTopBarActions,
207+
modifier: Modifier = Modifier
208+
) {
209+
Row(
210+
modifier = modifier,
211+
horizontalArrangement = Arrangement.End,
212+
verticalAlignment = Alignment.CenterVertically
213+
) {
214+
IconButton(
215+
onClick = actions.onFilterClick,
216+
modifier = Modifier.size(48.dp)
176217
) {
177-
Box(modifier = Modifier.fillMaxSize()) {
178-
Text(
179-
text = stringResource(R.string.appbar_search_in, stringResource(R.string.nc_app_product_name)),
180-
modifier = Modifier
181-
.fillMaxWidth()
182-
.align(Alignment.CenterStart)
183-
.padding(start = 16.dp, end = if (showThreadsButton) 80.dp else 48.dp)
184-
.clickable { onSearchActivate() },
185-
style = MaterialTheme.typography.bodyLarge,
186-
color = MaterialTheme.colorScheme.onSurfaceVariant,
187-
maxLines = 1,
188-
overflow = TextOverflow.Ellipsis
189-
)
190-
Row(
191-
modifier = Modifier.align(Alignment.CenterEnd),
192-
horizontalArrangement = Arrangement.End,
193-
verticalAlignment = Alignment.CenterVertically
194-
) {
195-
IconButton(
196-
onClick = onFilterClick,
197-
modifier = Modifier.size(48.dp)
198-
) {
199-
Box(
200-
Modifier.fillMaxSize(),
201-
contentAlignment = if (showThreadsButton) Alignment.CenterEnd else Alignment.Center
202-
) {
203-
Icon(
204-
painter = painterResource(R.drawable.ic_baseline_filter_list_24),
205-
contentDescription = stringResource(R.string.nc_filter),
206-
tint = if (showFilterActive) {
207-
MaterialTheme.colorScheme.primary
208-
} else {
209-
MaterialTheme.colorScheme.onSurfaceVariant
210-
}
211-
)
212-
}
213-
}
214-
if (showThreadsButton) {
215-
IconButton(
216-
onClick = onThreadsClick,
217-
modifier = Modifier.size(48.dp)
218-
) {
219-
Icon(
220-
painter = painterResource(R.drawable.outline_forum_24),
221-
contentDescription = stringResource(R.string.threads),
222-
tint = MaterialTheme.colorScheme.onSurfaceVariant
223-
)
224-
}
218+
Box(
219+
Modifier.fillMaxSize(),
220+
contentAlignment = if (state.showThreadsButton) Alignment.CenterEnd else Alignment.Center
221+
) {
222+
Icon(
223+
painter = painterResource(R.drawable.ic_baseline_filter_list_24),
224+
contentDescription = stringResource(R.string.nc_filter),
225+
tint = if (state.showFilterActive) {
226+
MaterialTheme.colorScheme.primary
227+
} else {
228+
MaterialTheme.colorScheme.onSurfaceVariant
225229
}
226-
}
230+
)
231+
}
232+
}
233+
if (state.showThreadsButton) {
234+
IconButton(
235+
onClick = actions.onThreadsClick,
236+
modifier = Modifier.size(48.dp)
237+
) {
238+
Icon(
239+
painter = painterResource(R.drawable.outline_forum_24),
240+
contentDescription = stringResource(R.string.threads),
241+
tint = MaterialTheme.colorScheme.onSurfaceVariant
242+
)
227243
}
228244
}
229-
Spacer(modifier = Modifier.width(8.dp))
230-
AvatarButton(
231-
avatarUrl = avatarUrl,
232-
credentials = credentials,
233-
showBadge = showAvatarBadge,
234-
onClick = onAvatarClick
235-
)
236245
}
237246
}
238247

@@ -366,7 +375,8 @@ private fun TopBarTitleContent(
366375
},
367376
colors = TopAppBarDefaults.topAppBarColors(
368377
containerColor = MaterialTheme.colorScheme.surface
369-
)
378+
),
379+
windowInsets = WindowInsets(0)
370380
)
371381
}
372382

0 commit comments

Comments
 (0)