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..2e6e550580 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 @@ -1821,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 = "") @@ -1894,12 +1894,12 @@ class ChatActivity : chatToolbarState = chatToolbarState.copy( title = buildToolbarTitle(conversation), subtitle = buildToolbarSubtitle(conversation), - avatarUrl = buildAvatarUrl(user, conversation, isOneToOne), + avatarType = buildAvatarType(conversation), + avatarUrl = buildAvatarUrl(user, conversation), credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) }, 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), @@ -1924,13 +1924,37 @@ 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 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 + } + 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 && CapabilitiesUtil.isAbleToCall(spreedCapabilities) && @@ -1982,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 9abcd73532..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 @@ -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 @@ -209,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) } @@ -294,8 +294,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,27 +327,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, - 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( @@ -430,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 2b7d2dfa71..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 @@ -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 only for 1-to-1 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, @@ -22,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. */