diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt index 765ca7c49a..f87eb65a48 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt @@ -2,84 +2,25 @@ * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ package com.nextcloud.talk.contacts import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.Intent import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.compose.setContent -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.List -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import autodagger.AutoInjector -import coil.compose.AsyncImage -import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.conversationcreation.ConversationCreationActivity +import com.nextcloud.talk.contacts.components.SetStatusBarColor import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser -import com.nextcloud.talk.openconversations.ListOpenConversationsActivity -import com.nextcloud.talk.utils.bundle.BundleKeys import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -108,7 +49,7 @@ class ContactsActivityCompose : BaseActivity() { contactsViewModel.getContactsFromSearchParams() } val colorScheme = viewThemeUtils.getColorScheme(this) - val uiState = contactsViewModel.contactsViewState.collectAsState() + val uiState = contactsViewModel.contactsViewState.collectAsStateWithLifecycle() val selectedParticipants = remember { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableArrayListExtra("selectedParticipants", AutocompleteUser::class.java) @@ -123,319 +64,15 @@ class ContactsActivityCompose : BaseActivity() { MaterialTheme( colorScheme = colorScheme ) { - val context = LocalContext.current - Scaffold( - topBar = { - AppBar( - title = stringResource(R.string.nc_app_product_name), - context = context, - contactsViewModel = contactsViewModel - ) - }, - content = { - Column( - Modifier.padding(it) - .background(colorResource(id = R.color.bg_default)) - ) { - ConversationCreationOptions(context = context, contactsViewModel = contactsViewModel) - ContactsList( - contactsUiState = uiState.value, - contactsViewModel = contactsViewModel, - context = context - ) - } - } + ContactsScreen( + contactsViewModel = contactsViewModel, + uiState = uiState.value ) } SetStatusBarColor() } } - - @Composable - private fun SetStatusBarColor() { - val view = LocalView.current - val isDarkMod = isSystemInDarkTheme() - - DisposableEffect(isDarkMod) { - val activity = view.context as Activity - activity.window.statusBarColor = resources.getColor(R.color.bg_default) - - WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply { - isAppearanceLightStatusBars = !isDarkMod - } - - onDispose { } - } - } -} - -@Composable -fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewModel, context: Context) { - var isSelected by remember { mutableStateOf(contactsViewModel.selectedParticipantsList.value.contains(contact)) } - val roomUiState by contactsViewModel.roomViewState.collectAsState() - val isAddParticipants = contactsViewModel.isAddParticipantsView.collectAsState() - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = { - if (!isAddParticipants.value) { - contactsViewModel.createRoom( - CompanionClass.ROOM_TYPE_ONE_ONE, - contact.source!!, - contact.id!!, - null - ) - } else { - isSelected = !isSelected - if (isSelected) { - contactsViewModel.selectContact(contact) - } else { - contactsViewModel.deselectContact(contact) - } - } - } - ), - verticalAlignment = Alignment.CenterVertically - ) { - val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) } - val errorPlaceholderImage: Int = R.drawable.account_circle_96dp - val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) - AsyncImage( - model = loadedImage, - contentDescription = stringResource(R.string.user_avatar), - modifier = Modifier.size(width = 45.dp, height = 45.dp) - ) - Text(modifier = Modifier.padding(16.dp), text = contact.label!!) - if (isAddParticipants.value) { - if (isSelected) { - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle), - contentDescription = "Selected", - tint = Color.Blue, - modifier = Modifier.padding(end = 8.dp) - ) - } - } - } - when (roomUiState) { - is RoomUiState.Success -> { - val conversation = (roomUiState as RoomUiState.Success).conversation - val bundle = Bundle() - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token) - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - context.startActivity(chatIntent) - } - is RoomUiState.Error -> { - val errorMessage = (roomUiState as RoomUiState.Error).message - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Error: $errorMessage", color = Color.Red) - } - } - is RoomUiState.None -> {} - } -} - -@SuppressLint("UnrememberedMutableState") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppBar(title: String, context: Context, contactsViewModel: ContactsViewModel) { - val searchQuery by contactsViewModel.searchQuery.collectAsState() - val searchState = contactsViewModel.searchState.collectAsState() - val isAddParticipants = contactsViewModel.isAddParticipantsView.collectAsState() - - TopAppBar( - title = { Text(text = title) }, - navigationIcon = { - IconButton(onClick = { - (context as? Activity)?.finish() - }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) - } - }, - actions = { - IconButton(onClick = { - contactsViewModel.updateSearchState(true) - }) { - Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon)) - } - if (isAddParticipants.value) { - Text( - text = stringResource(id = R.string.nc_contacts_done), - modifier = Modifier.clickable { - val resultIntent = Intent().apply { - putParcelableArrayListExtra( - "selectedParticipants", - ArrayList( - contactsViewModel - .selectedParticipantsList.value - ) - ) - } - (context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent) - (context as? Activity)?.finish() - } - ) - } - } - ) - if (searchState.value) { - Row { - DisplaySearch( - text = searchQuery, - onTextChange = { searchQuery -> - contactsViewModel.updateSearchQuery(query = searchQuery) - contactsViewModel.getContactsFromSearchParams() - }, - contactsViewModel = contactsViewModel - ) - } - } -} - -@Composable -fun ConversationCreationOptions(context: Context, contactsViewModel: ContactsViewModel) { - val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsState() - if (!isAddParticipants) { - Column { - Row( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) - .clickable { - val intent = Intent(context, ConversationCreationActivity::class.java) - context.startActivity(intent) - }, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24), - modifier = Modifier - .width(40.dp) - .height(40.dp) - .padding(8.dp), - contentDescription = null - ) - Text( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - text = stringResource(R.string.nc_create_new_conversation), - maxLines = 1, - fontSize = 16.sp - ) - } - Row( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) - .clickable { - val intent = Intent(context, ListOpenConversationsActivity::class.java) - context.startActivity(intent) - }, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.AutoMirrored.Filled.List, - modifier = Modifier - .width(40.dp) - .height(40.dp) - .padding(8.dp), - contentDescription = null - ) - Text( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - text = stringResource(R.string.nc_join_open_conversations), - fontSize = 16.sp - ) - } - } - } -} - -@Composable -fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel, context: Context) { - when (contactsUiState) { - is ContactsUiState.None -> { - } - is ContactsUiState.Loading -> { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - is ContactsUiState.Success -> { - val contacts = contactsUiState.contacts - Log.d(CompanionClass.TAG, "Contacts:$contacts") - if (contacts != null) { - ContactsItem(contacts, contactsViewModel, context) - } - } - is ContactsUiState.Error -> { - val errorMessage = contactsUiState.message - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Error: $errorMessage", color = Color.Red) - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ContactsItem(contacts: List, contactsViewModel: ContactsViewModel, context: Context) { - val groupedContacts: Map> = contacts.groupBy { contact -> - ( - if (contact.source == "users") { - contact.label?.first()?.uppercase() - } else { - contact.source?.replaceFirstChar { actorType -> - actorType.uppercase() - } - } - ).toString() - } - LazyColumn( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - contentPadding = PaddingValues(all = 10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - groupedContacts.forEach { (initial, contactsForInitial) -> - stickyHeader { - Column { - Surface(Modifier.fillParentMaxWidth()) { - Header(initial) - } - HorizontalDivider(thickness = 0.1.dp, color = Color.Black) - } - } - items(contactsForInitial) { contact -> - ContactItemRow( - contact = contact, - contactsViewModel = contactsViewModel, - context = context - ) - Log.d(CompanionClass.TAG, "Contacts:$contact") - } - } - } -} - -@Composable -fun Header(header: String) { - Text( - text = header, - modifier = Modifier - .fillMaxSize() - .background(colorResource(id = R.color.bg_default)) - .padding(start = 60.dp), - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold - ) } class CompanionClass { diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt new file mode 100644 index 0000000000..7aefffdb68 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nextcloud.talk.R +import com.nextcloud.talk.contacts.components.AppBar +import com.nextcloud.talk.contacts.components.ContactsList +import com.nextcloud.talk.contacts.components.ConversationCreationOptions + +@Composable +fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiState) { + val context = LocalContext.current + + val searchQuery by contactsViewModel.searchQuery.collectAsStateWithLifecycle() + val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle() + val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle() + val autocompleteUsers by contactsViewModel.selectedParticipantsList.collectAsStateWithLifecycle() + + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.nc_app_product_name), + searchQuery = searchQuery, + isSearchActive = isSearchActive, + isAddParticipants = isAddParticipants, + autocompleteUsers = autocompleteUsers, + onEnableSearch = { + contactsViewModel.setSearchActive(true) + }, + onDisableSearch = { + contactsViewModel.setSearchActive(false) + }, + onUpdateSearchQuery = { + contactsViewModel.updateSearchQuery(query = it) + }, + onUpdateAutocompleteUsers = { + contactsViewModel.getContactsFromSearchParams() + } + ) + }, + content = { + Column( + Modifier.padding(it) + .background(colorResource(id = R.color.bg_default)) + ) { + ConversationCreationOptions( + context = context, + contactsViewModel = contactsViewModel + ) + ContactsList( + contactsUiState = uiState, + contactsViewModel = contactsViewModel, + context = context + ) + } + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt index 8212428835..18c4ea2b38 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt @@ -29,8 +29,8 @@ class ContactsViewModel @Inject constructor( val searchQuery: StateFlow = _searchQuery private val shareTypes: MutableList = mutableListOf(ShareType.User.shareType) val shareTypeList: List = shareTypes - private val _searchState = MutableStateFlow(false) - val searchState: StateFlow = _searchState + private val _isSearchActive = MutableStateFlow(false) + val isSearchActive: StateFlow = _isSearchActive private val selectedParticipants = MutableStateFlow>(emptyList()) val selectedParticipantsList: StateFlow> = selectedParticipants.asStateFlow() private val _isAddParticipantsView = MutableStateFlow(false) @@ -57,8 +57,8 @@ class ContactsViewModel @Inject constructor( fun updateSelectedParticipants(participants: List) { selectedParticipants.value = participants } - fun updateSearchState(searchState: Boolean) { - _searchState.value = searchState + fun setSearchActive(searchState: Boolean) { + _isSearchActive.value = searchState } fun updateShareTypes(value: List) { diff --git a/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt b/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt deleted file mode 100644 index 5b990aeb78..0000000000 --- a/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2024 Sowjanya Kota - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.contacts - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.nextcloud.talk.R - -@Composable -fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewModel: ContactsViewModel) { - val keyboardController = LocalSoftwareKeyboardController.current - TextField( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxWidth() - .height(60.dp), - colors = TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - - value = text, - onValueChange = { onTextChange(it) }, - placeholder = { - Text( - text = stringResource(R.string.nc_search) - ) - }, - textStyle = TextStyle( - fontSize = 16.sp - ), - singleLine = true, - leadingIcon = { - IconButton( - onClick = { - onTextChange("") - contactsViewModel.updateSearchState(false) - } - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = stringResource(R.string.back_button) - ) - } - }, - - trailingIcon = { - if (text.isNotEmpty()) { - IconButton( - onClick = { - onTextChange("") - } - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.close_icon) - ) - } - } - }, - - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Search - ), - - keyboardActions = KeyboardActions( - onSearch = { - if (text.trim().isNotEmpty()) { - keyboardController?.hide() - } else { - return@KeyboardActions - } - } - ), - maxLines = 1 - ) -} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/AppBar.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/AppBar.kt new file mode 100644 index 0000000000..aa161023dd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/AppBar.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.nextcloud.talk.R +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser + +@SuppressLint("UnrememberedMutableState") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppBar( + title: String, + searchQuery: String, + isSearchActive: Boolean, + isAddParticipants: Boolean, + autocompleteUsers: List, + onEnableSearch: () -> Unit, + onDisableSearch: () -> Unit, + onUpdateSearchQuery: (String) -> Unit, + onUpdateAutocompleteUsers: () -> Unit +) { + val context = LocalContext.current + + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + IconButton(onClick = { + (context as? Activity)?.finish() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) + } + }, + actions = { + IconButton(onClick = onEnableSearch) { + Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon)) + } + if (isAddParticipants) { + Text( + text = stringResource(id = R.string.nc_contacts_done), + modifier = Modifier.clickable { + val resultIntent = Intent().apply { + putParcelableArrayListExtra( + "selectedParticipants", + ArrayList(autocompleteUsers) + ) + } + (context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent) + (context as? Activity)?.finish() + } + ) + } + } + ) + if (isSearchActive) { + Row { + SearchComponent( + text = searchQuery, + onTextChange = { searchQuery -> + onUpdateSearchQuery(searchQuery) + onUpdateAutocompleteUsers() + }, + onDisableSearch = onDisableSearch + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt new file mode 100644 index 0000000000..e45afd2913 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt @@ -0,0 +1,118 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.contacts.RoomUiState +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.utils.bundle.BundleKeys + +@Composable +fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewModel, context: Context) { + var isSelected by remember { mutableStateOf(contactsViewModel.selectedParticipantsList.value.contains(contact)) } + val roomUiState by contactsViewModel.roomViewState.collectAsState() + val isAddParticipants = contactsViewModel.isAddParticipantsView.collectAsState() + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + if (!isAddParticipants.value) { + contactsViewModel.createRoom( + CompanionClass.ROOM_TYPE_ONE_ONE, + contact.source!!, + contact.id!!, + null + ) + } else { + isSelected = !isSelected + if (isSelected) { + contactsViewModel.selectContact(contact) + } else { + contactsViewModel.deselectContact(contact) + } + } + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) } + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier.size(width = 45.dp, height = 45.dp) + ) + Text(modifier = Modifier.padding(16.dp), text = contact.label!!) + if (isAddParticipants.value) { + if (isSelected) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle), + contentDescription = "Selected", + tint = Color.Blue, + modifier = Modifier.padding(end = 8.dp) + ) + } + } + } + when (roomUiState) { + is RoomUiState.Success -> { + val conversation = (roomUiState as RoomUiState.Success).conversation + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + is RoomUiState.Error -> { + val errorMessage = (roomUiState as RoomUiState.Error).message + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + is RoomUiState.None -> {} + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt new file mode 100644 index 0000000000..d87b5dc410 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ContactsItem(contacts: List, contactsViewModel: ContactsViewModel, context: Context) { + val groupedContacts: Map> = contacts.groupBy { contact -> + ( + if (contact.source == "users") { + contact.label?.first()?.uppercase() + } else { + contact.source?.replaceFirstChar { actorType -> + actorType.uppercase() + } + } + ).toString() + } + LazyColumn( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + contentPadding = PaddingValues(all = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + groupedContacts.forEach { (initial, contactsForInitial) -> + stickyHeader { + Column { + Surface(Modifier.fillParentMaxWidth()) { + Header(initial) + } + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) + } + } + items(contactsForInitial) { contact -> + ContactItemRow( + contact = contact, + contactsViewModel = contactsViewModel, + context = context + ) + Log.d(CompanionClass.TAG, "Contacts:$contact") + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt new file mode 100644 index 0000000000..df856c9c21 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsUiState +import com.nextcloud.talk.contacts.ContactsViewModel + +@Composable +fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel, context: Context) { + when (contactsUiState) { + is ContactsUiState.None -> { + } + is ContactsUiState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is ContactsUiState.Success -> { + val contacts = contactsUiState.contacts + Log.d(CompanionClass.TAG, "Contacts:$contacts") + if (contacts != null) { + ContactsItem(contacts, contactsViewModel, context) + } + } + is ContactsUiState.Error -> { + val errorMessage = contactsUiState.message + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt new file mode 100644 index 0000000000..6793aa0b6f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt @@ -0,0 +1,97 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.conversationcreation.ConversationCreationActivity +import com.nextcloud.talk.openconversations.ListOpenConversationsActivity + +@Composable +fun ConversationCreationOptions(context: Context, contactsViewModel: ContactsViewModel) { + val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsState() + if (!isAddParticipants) { + Column { + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + .clickable { + val intent = Intent(context, ConversationCreationActivity::class.java) + context.startActivity(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24), + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + contentDescription = null + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_create_new_conversation), + maxLines = 1, + fontSize = 16.sp + ) + } + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) + .clickable { + val intent = Intent(context, ListOpenConversationsActivity::class.java) + context.startActivity(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.AutoMirrored.Filled.List, + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + contentDescription = null + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_join_open_conversations), + fontSize = 16.sp + ) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt new file mode 100644 index 0000000000..a1428143a7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R + +@Composable +fun Header(header: String) { + Text( + text = header, + modifier = Modifier + .fillMaxSize() + .background(colorResource(id = R.color.bg_default)) + .padding(start = 60.dp), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/SearchComponent.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/SearchComponent.kt new file mode 100644 index 0000000000..bf1c10594a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/SearchComponent.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R + +@Composable +fun SearchComponent(text: String, onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) { + val keyboardController = LocalSoftwareKeyboardController.current + + TextField( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .height(60.dp), + value = text, + onValueChange = { onTextChange(it) }, + placeholder = { Text(text = stringResource(R.string.nc_search)) }, + textStyle = TextStyle(fontSize = 16.sp), + singleLine = true, + leadingIcon = { LeadingIcon(onTextChange, onDisableSearch) }, + trailingIcon = { TrailingIcon(text, onTextChange) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = searchKeyboardActions(text, keyboardController), + colors = searchTextFieldColors(), + maxLines = 1 + ) +} + +@Composable +fun searchTextFieldColors() = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + +@Composable +fun LeadingIcon(onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) { + IconButton( + onClick = { + onTextChange("") + onDisableSearch() + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.back_button) + ) + } +} + +@Composable +fun TrailingIcon(text: String, onTextChange: (String) -> Unit) { + if (text.isNotEmpty()) { + IconButton( + onClick = { onTextChange("") } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.close_icon) + ) + } + } +} + +fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) = + KeyboardActions( + onSearch = { + if (text.trim().isNotEmpty()) { + keyboardController?.hide() + } + } + ) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/SetStatusBarColor.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/SetStatusBarColor.kt new file mode 100644 index 0000000000..e341b0914b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/SetStatusBarColor.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.toArgb + +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.colorResource +import androidx.core.view.WindowCompat +import com.nextcloud.talk.R + +@Composable +fun SetStatusBarColor() { + val view = LocalView.current + val isDarkMod = isSystemInDarkTheme() + val statusBarColor = colorResource(R.color.bg_default).toArgb() + + DisposableEffect(isDarkMod) { + val activity = view.context as Activity + activity.window.statusBarColor = statusBarColor + + WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply { + isAppearanceLightStatusBars = !isDarkMod + } + onDispose { } + } +}