Conversation List Screen: Java/XML to Kotlin/Compose migration#162
Conversation List Screen: Java/XML to Kotlin/Compose migration#162m4pl wants to merge 38 commits into
Conversation
bad5319 to
d8c26b7
Compare
d8c26b7 to
b9681c6
Compare
a6b46ac to
000a06c
Compare
| import com.android.messaging.data.conversationlist.model.ConversationListMessageStatus | ||
|
|
||
| @Immutable | ||
| internal data class ConversationListItemUiModel( |
| val description = when { | ||
| isArchive -> stringResource(R.string.action_archive) | ||
| else -> stringResource(R.string.mark_as_unread) | ||
| } |
There was a problem hiding this comment.
This seems like it'll use R.string.mark_as_unread for content description even if the swipe action is mark as read
| private int upgradeToVersion3(final SQLiteDatabase db) { | ||
| db.execSQL("ALTER TABLE " + DatabaseHelper.CONVERSATIONS_TABLE + " ADD COLUMN " + | ||
| DatabaseHelper.ConversationColumns.PINNED + " INT DEFAULT(0)"); | ||
| LogUtil.i(TAG, "Upgraded database to version 3"); | ||
| return 3; | ||
| } |
There was a problem hiding this comment.
Seems to be missing the index creation
There was a problem hiding this comment.
As a side note, we should probably eventually add tests to ensure a migrated database is the same as newly created database
| enableEdgeToEdge() | ||
|
|
||
| setContent { | ||
| AppTheme { |
There was a problem hiding this comment.
The theme for ConversationListActivity in the manifest is still @style/LaunchTheme, maybe we should use Theme.Compose instead
| var pendingAddContactDestination by rememberSaveable { mutableStateOf<String?>(null) } | ||
| var pendingDelete by rememberSaveable { mutableStateOf(false) } | ||
| var pendingBlockConversationId by rememberSaveable { mutableStateOf<String?>(null) } | ||
| var pendingBlockDestination by rememberSaveable { mutableStateOf<String?>(null) } | ||
| var pendingSnooze by rememberSaveable { mutableStateOf(false) } |
There was a problem hiding this comment.
These use rememberSaveable but they seem like they can be out of sync with the actual state in the ViewModel, e.g
- Enable Android developer option "Don't keep activities", or otherwise force the app process/activity to be destroyed while in the background
- Open Messaging, long-press a conversation, and tap the delete action so the confirmation dialog is visible.
- Background the app, then return to Messaging from Recents.
- Observe that the delete dialog can be restored even though the new view-model state no longer has the original selection. When only one conversation was selected for deletion, the dialog title can change from "Delete this conversation" to "Delete these conversations?" after the process death, and the delete button is a no-op.
- Repeat with snooze, the dialog will be a no-op
It seems that for block or add contact from a row action, the restored dialog can still reference the old destination after the underlying list state has been recreated
It might be better to just make these remember
|
|
||
| @Composable | ||
| internal fun ConversationListDialogs( | ||
| selectedCount: Int, |
There was a problem hiding this comment.
Maybe we should sanity check this against a selectedCount of 0. Dialogs are still shown when selectedCount is 0 it seems (e.g. the Don't keep activities enabled with Delete and Snooze dialogs)
| if (isSnoozeVisible) { | ||
| SnoozeChatDialog( | ||
| onDismiss = onDismissSnooze, | ||
| onConfirm = { option -> | ||
| onAction(Action.SnoozeOptionSelected(option)) | ||
| onDismissSnooze() | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
The snooze chat dialog should use plural strings when multiple chats are selected. The dialog currently says "Snooze chat" and "You won't get notifications from this chat for the selected period"
| TwoLineListItem( | ||
| onClick = onClick, | ||
| leadingContent = { | ||
| ConversationListItemAvatar( | ||
| item = item, | ||
| isSelectionMode = isSelectionMode, | ||
| onToggleSelection = onClick, | ||
| onMessageClick = onAvatarMessageClick, | ||
| onCallClick = onAvatarCallClick, | ||
| onContactClick = onAvatarContactClick, | ||
| onInfoClick = onAvatarInfoClick, | ||
| ) | ||
| }, | ||
| titleContent = { | ||
| ConversationListItemHeader(item) | ||
| }, | ||
| modifier = modifier.testTag( | ||
| tag = conversationListItemTestTag(item.conversationId), | ||
| ), | ||
| onLongClick = onLongClick, | ||
| color = itemContainerColor(item), | ||
| subtitleContent = { | ||
| ConversationListItemBody(item) | ||
| }, | ||
| trailingContent = { | ||
| ConversationListItemTrailing(item) | ||
| }, | ||
| ) |
There was a problem hiding this comment.
This appears to regress a lot of the accessibility content descriptions compared to the old Messages code, e.g.
https://github.com/GrapheneOS/Messaging/blob/12/res/values/strings.xml#L372-L389
When I use TalkBack with the old messages app, for successful messages, it will say stuff like
Message from
<contact>,<message body>, time<time>
This is from
<string name="one_on_one_incoming_successful_message_prefix">Message from <xliff:g id="sender">%1$s</xliff:g>: <xliff:g id="message">%2$s</xliff:g>. Time: <xliff:g id="time">%3$s</xliff:g>.</string>e.g.
Message from Bob, "Hello there". Time: 33 minutes
and it will not select the avatar. The only other things TalkBack will scroll over are attachments, e.g. for an audio attachment, it will say "Play audio attachment"
When I use TalkBack with this PR, it will just say,
Bob, 33 minutes, "Hello there"
and for the top conversation it might say "in list, 57 items"
Swiping on TalkBack to try to go to the next may select the avatar (when the contact doesn't have a custom avatar in contacts) and it'll just say "capital F" for a contact with first name F, or "unlabeled" for just a number or a group avatar
| @Composable | ||
| private fun ConversationListItemBadgeIcon(icon: ImageVector) { | ||
| Icon( | ||
| imageVector = icon, | ||
| contentDescription = null, | ||
| modifier = Modifier.size(ItemBadgeIconSize), | ||
| tint = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| ) | ||
| } |
There was a problem hiding this comment.
We should avoid null contentDescriptions and describe what the badge is indicating, or the accessibility hint on the conversation list item itself should mention the status instead of the badge icon being traversable
| ) { contentPadding -> | ||
| Box( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .padding(top = contentPadding.calculateTopPadding()) | ||
| .onGloballyPositioned { coordinates -> | ||
| pinAnimationController?.updateContentTop(coordinates.boundsInRoot().top) | ||
| } | ||
| .background(backdropColor) | ||
| .clip(ContentCornerShape) | ||
| .background(MaterialTheme.colorScheme.background), | ||
| ) { |
| @Composable | ||
| private fun ConversationListItemTrailing(item: ConversationListItemUiModel) { | ||
| Row( | ||
| modifier = Modifier.padding(end = ItemTrailingEndPadding), | ||
| horizontalArrangement = Arrangement.spacedBy(ItemHeaderSpacing), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| ConversationListItemPreviewThumbnail(item.snippet.preview) | ||
| } | ||
| } |
There was a problem hiding this comment.
I think the thumbnail placement makes it look unaligned or not uniform, because it offsets the timestamp compared to other conversations that don't have the thumbnail



Migrated the Conversation List screen from Java + XML to Kotlin + Compose. Please verify the list animations (archive, undo, pin/unpin, scroll-to-top) manually on a device, as I had some issues after writing the code. I also tried to move some components to ui/common/components.
Screenshots
Video
Screen_recording_20260625_015257.mp4