From b429ac07a6bcd9f329bc7008a2e10fba1d7b6302 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 6 Aug 2026 19:35:54 +0200 Subject: [PATCH 1/3] group avatars in conversation header Assisted-by: Claude Code 2.1.223:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/chat/ChatActivity.kt | 26 ++++++++++++++----- .../com/nextcloud/talk/chat/ui/ChatToolbar.kt | 2 ++ .../talk/chat/ui/ChatToolbarState.kt | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 2710b73b47..27fb01d52b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -1894,7 +1894,7 @@ class ChatActivity : chatToolbarState = chatToolbarState.copy( title = buildToolbarTitle(conversation), subtitle = buildToolbarSubtitle(conversation), - avatarUrl = buildAvatarUrl(user, conversation, isOneToOne), + avatarUrl = buildAvatarUrl(user, conversation), credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) }, userStatus = if (isOneToOne) conversation?.status else null, showVoiceCall = isCallsEnabled(capabilitiesReady, conversation), @@ -1924,12 +1924,26 @@ class ChatActivity : else -> "" } - private fun buildAvatarUrl(user: User?, conversation: ConversationModel?, isOneToOne: Boolean): String? = - if (user != null && conversation != null && isOneToOne) { - ApiUtils.getUrlForAvatar(user.baseUrl!!, conversation.name, true, DisplayUtils.isDarkModeOn(this)) - } else { - null + private fun buildAvatarUrl(user: User?, conversation: ConversationModel?): String? { + if (user == null || conversation == null) { + return null } + val isDark = DisplayUtils.isDarkModeOn(this) + return when (conversation.type) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> + ApiUtils.getUrlForAvatar(user.baseUrl!!, conversation.name, true, isDark) + ConversationEnums.ConversationType.ROOM_GROUP_CALL, + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> + ApiUtils.getUrlForConversationAvatarWithVersion( + 1, + user.baseUrl, + conversation.token, + isDark, + conversation.avatarVersion + ) + else -> null + } + } private fun isCallsEnabled(capabilitiesReady: Boolean, conversation: ConversationModel?): Boolean = capabilitiesReady && diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt index 9abcd73532..1cf0c7211b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt @@ -343,6 +343,8 @@ private fun ConversationAvatar( model = request, contentDescription = null, contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.account_circle_96dp), + error = painterResource(R.drawable.account_circle_96dp), modifier = Modifier .fillMaxSize() .clip(CircleShape) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt index 2b7d2dfa71..316c1ea15e 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt @@ -12,7 +12,7 @@ import com.nextcloud.talk.chat.MenuItemData data class ChatToolbarState( val title: String = "", val subtitle: String = "", - /** Non-null only for 1-to-1 conversations; drives avatar display. */ + /** Non-null for 1-to-1 and group/public conversations; drives avatar display. */ val avatarUrl: String? = null, /** HTTP Basic / Bearer credential string for the avatar request. */ val credentials: String? = null, From c7bbd547c3bf5599e2b52d99cf95f06ffd0ce748 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 6 Aug 2026 19:46:04 +0200 Subject: [PATCH 2/3] Note to self / Talk updates avatar in conversation header Assisted-by: Claude Code 2.1.223:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/chat/ChatActivity.kt | 12 +++ .../com/nextcloud/talk/chat/ui/ChatToolbar.kt | 79 ++++++++++++++----- .../talk/chat/ui/ChatToolbarState.kt | 11 ++- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 27fb01d52b..f9c87dc078 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -104,6 +104,7 @@ import com.nextcloud.talk.chat.data.model.FileParameters import com.nextcloud.talk.chat.ui.ChatEmptyState import com.nextcloud.talk.chat.ui.ChatEmptyStateType import com.nextcloud.talk.chat.ui.ChatToolbar +import com.nextcloud.talk.chat.ui.ChatToolbarAvatarType import com.nextcloud.talk.chat.ui.ChatToolbarCallbacks import com.nextcloud.talk.chat.ui.ChatToolbarState import com.nextcloud.talk.chat.ui.MessageActionsBottomSheet @@ -1894,6 +1895,7 @@ class ChatActivity : chatToolbarState = chatToolbarState.copy( title = buildToolbarTitle(conversation), subtitle = buildToolbarSubtitle(conversation), + avatarType = buildAvatarType(conversation), avatarUrl = buildAvatarUrl(user, conversation), credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) }, userStatus = if (isOneToOne) conversation?.status else null, @@ -1924,6 +1926,16 @@ class ChatActivity : else -> "" } + private fun buildAvatarType(conversation: ConversationModel?): ChatToolbarAvatarType = + when (conversation?.type) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL, + ConversationEnums.ConversationType.ROOM_GROUP_CALL, + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> ChatToolbarAvatarType.URL + ConversationEnums.ConversationType.ROOM_SYSTEM -> ChatToolbarAvatarType.SYSTEM + ConversationEnums.ConversationType.NOTE_TO_SELF -> ChatToolbarAvatarType.NOTE_TO_SELF + else -> ChatToolbarAvatarType.NONE + } + private fun buildAvatarUrl(user: User?, conversation: ConversationModel?): String? { if (user == null || conversation == null) { return null diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt index 1cf0c7211b..4653837db0 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt @@ -8,6 +8,7 @@ package com.nextcloud.talk.chat.ui import android.content.res.Configuration +import android.widget.ImageView import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable @@ -57,6 +58,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -69,10 +71,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView import coil.compose.AsyncImage import coil.request.ImageRequest import com.nextcloud.talk.R import com.nextcloud.talk.chat.MenuItemData +import com.nextcloud.talk.extensions.loadNoteToSelfAvatar +import com.nextcloud.talk.extensions.loadSystemAvatar @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -294,8 +299,9 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?) modifier = rowModifier, verticalAlignment = Alignment.CenterVertically ) { - if (state.avatarUrl != null) { + if (state.avatarType != ChatToolbarAvatarType.NONE) { ConversationAvatar( + avatarType = state.avatarType, avatarUrl = state.avatarUrl, credentials = state.credentials, userStatus = state.userStatus, @@ -326,29 +332,66 @@ private fun ConversationHeader(state: ChatToolbarState, onClick: (() -> Unit)?) @Composable private fun ConversationAvatar( - avatarUrl: String, + avatarType: ChatToolbarAvatarType, + avatarUrl: String?, credentials: String?, userStatus: String?, modifier: Modifier = Modifier ) { Box(modifier = modifier) { - val context = LocalContext.current - val request = ImageRequest.Builder(context) - .data(avatarUrl) - .apply { credentials?.let { addHeader("Authorization", it) } } - .crossfade(true) - .build() + when (avatarType) { + ChatToolbarAvatarType.URL -> { + val context = LocalContext.current + val request = ImageRequest.Builder(context) + .data(avatarUrl) + .apply { credentials?.let { addHeader("Authorization", it) } } + .crossfade(true) + .build() - AsyncImage( - model = request, - contentDescription = null, - contentScale = ContentScale.Crop, - placeholder = painterResource(R.drawable.account_circle_96dp), - error = painterResource(R.drawable.account_circle_96dp), - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - ) + AsyncImage( + model = request, + contentDescription = null, + contentScale = ContentScale.Crop, + placeholder = painterResource(R.drawable.account_circle_96dp), + error = painterResource(R.drawable.account_circle_96dp), + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + } + + ChatToolbarAvatarType.SYSTEM -> { + if (LocalInspectionMode.current) { + Icon( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } else { + AndroidView( + factory = { ctx -> ImageView(ctx).apply { loadSystemAvatar() } }, + modifier = Modifier.fillMaxSize() + ) + } + } + + ChatToolbarAvatarType.NOTE_TO_SELF -> { + if (LocalInspectionMode.current) { + Icon( + painter = painterResource(R.drawable.ic_note_to_self), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } else { + AndroidView( + factory = { ctx -> ImageView(ctx).apply { loadNoteToSelfAvatar() } }, + modifier = Modifier.fillMaxSize() + ) + } + } + + ChatToolbarAvatarType.NONE -> Unit + } if (userStatus != null) { UserStatusBadge( diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt index 316c1ea15e..bb2f71ef08 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt @@ -9,10 +9,19 @@ package com.nextcloud.talk.chat.ui import com.nextcloud.talk.chat.MenuItemData +enum class ChatToolbarAvatarType { + NONE, + URL, + SYSTEM, + NOTE_TO_SELF +} + data class ChatToolbarState( val title: String = "", val subtitle: String = "", - /** Non-null for 1-to-1 and group/public conversations; drives avatar display. */ + /** Which kind of avatar (if any) to render in the toolbar. */ + val avatarType: ChatToolbarAvatarType = ChatToolbarAvatarType.NONE, + /** Set only when [avatarType] is [ChatToolbarAvatarType.URL]. */ val avatarUrl: String? = null, /** HTTP Basic / Bearer credential string for the avatar request. */ val credentials: String? = null, From b8c2da9948f653fc9f2e4212b5f013c6b2303a03 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 6 Aug 2026 19:54:04 +0200 Subject: [PATCH 3/3] move search to menu in order to have more space for the other appbar content... Assisted-by: Claude Code 2.1.223:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt | 8 ++++++-- .../main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt | 7 +------ .../com/nextcloud/talk/chat/ui/ChatToolbarCallbacks.kt | 1 - .../java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt | 2 -- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index f9c87dc078..2e6e550580 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -1822,7 +1822,6 @@ class ChatActivity : onSilentVoiceCall = { startACall(true, true) }, onVideoCall = { startACall(false, false) }, onSilentVideoCall = { startACall(false, true) }, - onSearchOpen = { startMessageSearch() }, onSearchClose = { chatViewModel.exitSearchMode() chatToolbarState = chatToolbarState.copy(isSearchMode = false, searchQuery = "") @@ -1901,7 +1900,6 @@ class ChatActivity : userStatus = if (isOneToOne) conversation?.status else null, showVoiceCall = isCallsEnabled(capabilitiesReady, conversation), showVideoCall = isCallsEnabled(capabilitiesReady, conversation), - showSearch = isSearchAvailable(capabilitiesReady, conversation), titleClickable = user?.userId != "?" && !chatToolbarState.isSearchMode, overflowItems = buildOverflowItems(), threadNotificationIcon = buildThreadNotificationIcon(capabilitiesReady), @@ -2008,6 +2006,12 @@ class ChatActivity : val isThread = isChatThread() val capabilitiesReady = ::spreedCapabilities.isInitialized + if (isSearchAvailable(capabilitiesReady, currentConversation)) { + items += MenuItemData( + title = getString(R.string.nc_search), + onClick = { startMessageSearch() } + ) + } if (conversationUser?.userId != "?" && !isThread) { items += MenuItemData( title = getString(R.string.nc_conversation_menu_conversation_info), diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt index 4653837db0..e7bfa2426d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbar.kt @@ -214,11 +214,6 @@ private fun ToolbarActions(state: ChatToolbarState, callbacks: ChatToolbarCallba onLongClick = callbacks.onSilentVideoCall.takeIf { state.supportsSilentCall } ) } - if (state.showSearch) { - IconButton(onClick = callbacks.onSearchOpen) { - Icon(painterResource(R.drawable.ic_search_white_24dp), stringResource(R.string.nc_search)) - } - } if (state.overflowItems.isNotEmpty()) { OverflowMenuButton(items = state.overflowItems) } @@ -475,8 +470,8 @@ private fun NormalModePreview() { userStatus = "online", showVoiceCall = true, showVideoCall = true, - showSearch = true, overflowItems = listOf( + MenuItemData(title = "Search", onClick = {}), MenuItemData(title = "Conversation info", onClick = {}), MenuItemData(title = "Shared items", onClick = {}) ), diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarCallbacks.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarCallbacks.kt index ddbb2f4e71..870cf3f1d1 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarCallbacks.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarCallbacks.kt @@ -14,7 +14,6 @@ data class ChatToolbarCallbacks( val onSilentVoiceCall: () -> Unit = {}, val onVideoCall: () -> Unit = {}, val onSilentVideoCall: () -> Unit = {}, - val onSearchOpen: () -> Unit = {}, val onSearchClose: () -> Unit = {}, val onSearchQueryChange: (String) -> Unit = {}, val onSearchSubmit: () -> Unit = {}, diff --git a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt index bb2f71ef08..0213a99850 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ui/ChatToolbarState.kt @@ -31,8 +31,6 @@ data class ChatToolbarState( val isLoading: Boolean = false, val showVoiceCall: Boolean = false, val showVideoCall: Boolean = false, - /** Whether the server supports message search for this conversation. */ - val showSearch: Boolean = false, val searchQuery: String = "", val overflowItems: List = emptyList(), /** Non-null in thread view; the drawable resource for the current notification level. */