diff --git a/app/build.gradle b/app/build.gradle index 993794069..469f4a39b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -309,7 +309,7 @@ dependencies { //compose implementation(platform("androidx.compose:compose-bom:2024.09.00")) implementation("androidx.compose.ui:ui") - implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material3:material3:1.2.1' implementation("androidx.compose.ui:ui-tooling-preview") implementation 'androidx.activity:activity-compose:1.9.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.5' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 410dd82da..256e2b2b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -129,6 +129,9 @@ + + diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index c330c33cb..d08d2b317 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -9,9 +9,15 @@ package com.nextcloud.talk.api import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import retrofit2.http.DELETE +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Query import retrofit2.http.QueryMap import retrofit2.http.Url @@ -39,4 +45,55 @@ interface NcApiCoroutines { @Url url: String?, @QueryMap options: Map? ): RoomOverall + + /* + QueryMap items are as follows: + - "roomName" : "newName" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken + */ + @FormUrlEncoded + @PUT + suspend fun renameRoom( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("roomName") roomName: String? + ): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun openConversation( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("scope") scope: Int + ): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun setConversationDescription( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("description") description: String? + ): GenericOverall + + @POST + suspend fun addParticipant( + @Header("Authorization") authorization: String?, + @Url url: String?, + @QueryMap options: Map? + ): AddParticipantOverall + + @POST + suspend fun makeRoomPublic(@Header("Authorization") authorization: String?, @Url url: String): GenericOverall + + @DELETE + suspend fun makeRoomPrivate(@Header("Authorization") authorization: String?, @Url url: String): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun setPassword( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Field("password") password: String? + ): GenericOverall } 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 fe828a4b7..5fdbc3c73 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityCompose.kt @@ -11,17 +11,20 @@ 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 @@ -46,17 +49,25 @@ 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.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 autodagger.AutoInjector import coil.compose.AsyncImage @@ -64,6 +75,7 @@ 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.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.openconversations.ListOpenConversationsActivity import com.nextcloud.talk.utils.bundle.BundleKeys @@ -82,8 +94,31 @@ class ContactsActivityCompose : BaseActivity() { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java] setContent { + val isAddParticipants = intent.getBooleanExtra("isAddParticipants", false) + contactsViewModel.updateIsAddParticipants(isAddParticipants) + if (isAddParticipants) { + contactsViewModel.updateShareTypes( + listOf( + ShareType.Group.shareType, + ShareType.Email.shareType, + ShareType.Circle.shareType + ) + ) + contactsViewModel.getContactsFromSearchParams() + } val colorScheme = viewThemeUtils.getColorScheme(this) val uiState = contactsViewModel.contactsViewState.collectAsState() + val selectedParticipants = remember { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra("selectedParticipants", AutocompleteUser::class.java) + ?: emptyList() + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra("selectedParticipants") ?: emptyList() + } + } + val participants = selectedParticipants.toSet().toMutableList() + contactsViewModel.updateSelectedParticipants(participants) MaterialTheme( colorScheme = colorScheme ) { @@ -98,23 +133,242 @@ class ContactsActivityCompose : BaseActivity() { }, content = { Column(Modifier.padding(it)) { - ConversationCreationOptions(context = context) + ConversationCreationOptions(context = context, contactsViewModel = contactsViewModel) ContactsList( contactsUiState = uiState.value, contactsViewModel = contactsViewModel, - context = context + context = context, + selectedParticipants = selectedParticipants.toMutableList() ) } } ) } + + 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 { } } - setupSystemColors() } } @Composable -fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel, context: Context) { +fun ContactItemRow( + contact: AutocompleteUser, + contactsViewModel: ContactsViewModel, + context: Context, + selectedContacts: MutableList +) { + var isSelected by remember { mutableStateOf(selectedContacts.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 + selectedContacts.apply { + if (isSelected) { + add(contact) + } else { + remove(contact) + } + } + contactsViewModel.updateSelectedParticipants(selectedContacts) + } + } + ), + 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, + selectedParticipants: MutableList +) { when (contactsUiState) { is ContactsUiState.None -> { } @@ -127,13 +381,13 @@ fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsVi val contacts = contactsUiState.contacts Log.d(CompanionClass.TAG, "Contacts:$contacts") if (contacts != null) { - ContactsItem(contacts, contactsViewModel, context) + ContactsItem(contacts, contactsViewModel, context, selectedParticipants) } } is ContactsUiState.Error -> { val errorMessage = contactsUiState.message Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Error: $errorMessage", color = MaterialTheme.colorScheme.error) + Text(text = "Error: $errorMessage", color = Color.Red) } } } @@ -141,7 +395,12 @@ fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsVi @OptIn(ExperimentalFoundationApi::class) @Composable -fun ContactsItem(contacts: List, contactsViewModel: ContactsViewModel, context: Context) { +fun ContactsItem( + contacts: List, + contactsViewModel: ContactsViewModel, + context: Context, + selectedParticipants: MutableList +) { val groupedContacts: Map> = contacts.groupBy { contact -> ( if (contact.source == "users") { @@ -166,11 +425,16 @@ fun ContactsItem(contacts: List, contactsViewModel: ContactsVi Surface(Modifier.fillParentMaxWidth()) { Header(initial) } - HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant) + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) } } items(contactsForInitial) { contact -> - ContactItemRow(contact = contact, contactsViewModel = contactsViewModel, context = context) + ContactItemRow( + contact = contact, + contactsViewModel = contactsViewModel, + context = context, + selectedContacts = selectedParticipants + ) Log.d(CompanionClass.TAG, "Contacts:$contact") } } @@ -185,147 +449,11 @@ fun Header(header: String) { .fillMaxSize() .background(Color.Transparent) .padding(start = 60.dp), - color = MaterialTheme.colorScheme.primary, + color = Color.Blue, fontWeight = FontWeight.Bold ) } -@Composable -fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewModel, context: Context) { - val roomUiState by contactsViewModel.roomViewState.collectAsState() - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - contactsViewModel.createRoom( - CompanionClass.ROOM_TYPE_ONE_ONE, - contact.source!!, - contact.id!!, - null - ) - }, - 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!!) - } - when (roomUiState) { - is RoomUiState.Success -> { - val conversation = (roomUiState as RoomUiState.Success).conversation - val bundle = Bundle() - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token) - // bundle.putString(BundleKeys.KEY_ROOM_ID, conversation?.roomId) - 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 = MaterialTheme.colorScheme.error) - } - } - 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() - - 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 (searchState.value) { - DisplaySearch( - text = searchQuery, - onTextChange = { searchQuery -> - contactsViewModel.updateSearchQuery(query = searchQuery) - contactsViewModel.getContactsFromSearchParams() - }, - contactsViewModel = contactsViewModel - ) - } -} - -@Composable -fun ConversationCreationOptions(context: Context) { - Column { - Row( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), - 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 - ) - } - } -} - class CompanionClass { companion object { internal val TAG = ContactsActivityCompose::class.simpleName 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 7b0a2e450..7dda67591 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt @@ -30,6 +30,10 @@ class ContactsViewModel @Inject constructor( val shareTypeList: List = shareTypes private val _searchState = MutableStateFlow(false) val searchState: StateFlow = _searchState + private val selectedParticipants = MutableStateFlow>(emptyList()) + val selectedParticipantsList: StateFlow> = selectedParticipants + private val _isAddParticipantsView = MutableStateFlow(false) + val isAddParticipantsView: StateFlow = _isAddParticipantsView init { getContactsFromSearchParams() @@ -39,12 +43,19 @@ class ContactsViewModel @Inject constructor( _searchQuery.value = query } + fun updateSelectedParticipants(participants: List) { + selectedParticipants.value = participants + } fun updateSearchState(searchState: Boolean) { _searchState.value = searchState } - fun updateShareTypes(value: String) { - shareTypes.add(value) + fun updateShareTypes(value: List) { + shareTypes.addAll(value) + } + + fun updateIsAddParticipants(value: Boolean) { + _isAddParticipantsView.value = value } fun getContactsFromSearchParams() { @@ -62,7 +73,6 @@ class ContactsViewModel @Inject constructor( } } } - fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?) { viewModelScope.launch { try { diff --git a/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt b/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt index f7ba7d61b..5a793d40e 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/SearchComponent.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -30,6 +31,7 @@ import com.nextcloud.talk.R @Composable fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewModel: ContactsViewModel) { + val isAddParticipants = contactsViewModel.isAddParticipantsView.collectAsState() val keyboardController = LocalSoftwareKeyboardController.current TextField( modifier = Modifier @@ -42,7 +44,6 @@ fun DisplaySearch(text: String, onTextChange: (String) -> Unit, contactsViewMode text = stringResource(R.string.nc_search) ) }, - textStyle = TextStyle( fontSize = 16.sp ), diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt index fb8f2dc5b..39fad325c 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt @@ -10,7 +10,7 @@ package com.nextcloud.talk.contacts enum class ShareType(val shareType: String) { User("0"), Group("1"), - Email(""), - Circle(""), - Federated("") + Email("4"), + Remote("5"), + Circle("7") } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt new file mode 100644 index 000000000..51297510a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -0,0 +1,535 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +@file:Suppress("DEPRECATION") + +package com.nextcloud.talk.conversationcreation + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +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.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import androidx.lifecycle.ViewModelProvider +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.contacts.ContactsActivityCompose +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationCreationActivity : BaseActivity() { + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val conversationCreationViewModel = ViewModelProvider( + this, + viewModelFactory + )[ConversationCreationViewModel::class.java] + setContent { + val colorScheme = viewThemeUtils.getColorScheme(this) + val context = LocalContext.current + MaterialTheme( + colorScheme = colorScheme + ) { + ConversationCreationScreen(conversationCreationViewModel, context) + } + SetStatusBarColor() + } + } +} + +@Composable +private fun SetStatusBarColor() { + val view = LocalView.current + val isDarkMod = isSystemInDarkTheme() + + DisposableEffect(isDarkMod) { + val activity = view.context as Activity + activity.window.statusBarColor = activity.getColor(R.color.bg_default) + + WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply { + isAppearanceLightStatusBars = !isDarkMod + } + + onDispose { } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConversationCreationScreen(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + + onResult = { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val selectedParticipants = data?.getParcelableArrayListExtra("selectedParticipants") + ?: emptyList() + val participants = selectedParticipants.toMutableList() + conversationCreationViewModel.updateSelectedParticipants(participants) + } + } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.nc_new_conversation)) }, + navigationIcon = { + IconButton(onClick = { + (context as? Activity)?.finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back_button) + ) + } + } + ) + }, + content = { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + DefaultUserAvatar() + UploadAvatar() + ConversationNameAndDescription(conversationCreationViewModel) + AddParticipants(launcher, context, conversationCreationViewModel) + RoomCreationOptions(conversationCreationViewModel) + CreateConversation(conversationCreationViewModel, context) + } + } + ) +} + +@Composable +fun DefaultUserAvatar() { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + AsyncImage( + model = R.drawable.ic_circular_group, + contentDescription = stringResource(id = R.string.user_avatar), + modifier = Modifier + .size(width = 84.dp, height = 84.dp) + .padding(top = 8.dp) + ) + } +} + +@Composable +fun UploadAvatar() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center + ) { + IconButton(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_photo_camera_24), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + IconButton(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ic_folder_multiple_image), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + IconButton(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.baseline_tag_faces_24), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + IconButton(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ic_delete_grey600_24dp), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } +} + +@Composable +fun ConversationNameAndDescription(conversationCreationViewModel: ConversationCreationViewModel) { + val conversationRoomName = conversationCreationViewModel.roomName.collectAsState() + val conversationDescription = conversationCreationViewModel.conversationDescription.collectAsState() + OutlinedTextField( + value = conversationRoomName.value, + onValueChange = { + conversationCreationViewModel.updateRoomName(it) + }, + label = { Text(text = stringResource(id = R.string.nc_call_name)) }, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp) + .fillMaxWidth() + ) + OutlinedTextField( + value = conversationDescription.value, + onValueChange = { + conversationCreationViewModel.updateConversationDescription(it) + }, + label = { Text(text = stringResource(id = R.string.nc_conversation_description)) }, + modifier = Modifier + .padding(top = 8.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth() + ) +} + +@SuppressLint("SuspiciousIndentation") +@Composable +fun AddParticipants( + launcher: ManagedActivityResultLauncher, + context: Context, + conversationCreationViewModel: ConversationCreationViewModel +) { + val participants = conversationCreationViewModel.selectedParticipants.collectAsState().value + + Column( + modifier = Modifier + .fillMaxHeight() + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + ) { + Row { + Text( + text = stringResource(id = R.string.nc_participants).uppercase(), + fontSize = 14.sp, + modifier = Modifier.padding(start = 0.dp, bottom = 16.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + if (participants.isNotEmpty()) { + Text( + text = stringResource(id = R.string.nc_edit), + fontSize = 12.sp, + modifier = Modifier + .padding(start = 16.dp, bottom = 16.dp) + .clickable { + val intent = Intent(context, ContactsActivityCompose::class.java) + intent.putParcelableArrayListExtra( + "selectedParticipants", + participants as ArrayList + ) + intent.putExtra("isAddParticipants", true) + intent.putExtra("isAddParticipantsEdit", true) + launcher.launch(intent) + }, + textAlign = TextAlign.Right + ) + } + } + participants.toSet().forEach { participant -> + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + val imageUri = participant.id?.let { conversationCreationViewModel.getImageUri(it, true) } + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(id = R.string.user_avatar), + modifier = Modifier.size(width = 32.dp, height = 32.dp) + ) + participant.label?.let { + Text( + text = it, + modifier = Modifier.padding(all = 16.dp), + fontSize = 15.sp + ) + } + } + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(context, ContactsActivityCompose::class.java) + intent.putExtra("isAddParticipants", true) + launcher.launch(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + if (participants.isEmpty()) { + Icon( + painter = painterResource(id = R.drawable.ic_account_plus), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Text( + text = stringResource(id = R.string.nc_add_participants), + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } +} + +@Composable +fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewModel) { + val isGuestsAllowed = conversationCreationViewModel.isGuestsAllowed.value + val isConversationAvailableForRegisteredUsers = conversationCreationViewModel + .isConversationAvailableForRegisteredUsers.value + val isOpenForGuestAppUsers = conversationCreationViewModel.openForGuestAppUsers.value + + Text( + text = stringResource(id = R.string.nc_new_conversation_visibility).uppercase(), + fontSize = 14.sp, + modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp) + ) + ConversationOptions( + icon = R.drawable.ic_avatar_link, + text = R.string.nc_guest_access_allow_title, + switch = { + Switch( + checked = isGuestsAllowed, + onCheckedChange = { + conversationCreationViewModel.isGuestsAllowed.value = it + } + ) + }, + showDialog = false, + conversationCreationViewModel = conversationCreationViewModel + ) + + if (isGuestsAllowed) { + ConversationOptions( + icon = R.drawable.ic_lock_grey600_24px, + text = R.string.nc_set_password, + showDialog = true, + conversationCreationViewModel = conversationCreationViewModel + ) + } + + ConversationOptions( + icon = R.drawable.baseline_format_list_bulleted_24, + text = R.string.nc_open_conversation_to_registered_users, + switch = { + Switch( + checked = isConversationAvailableForRegisteredUsers, + onCheckedChange = { + conversationCreationViewModel.isConversationAvailableForRegisteredUsers.value = it + } + ) + }, + showDialog = false, + conversationCreationViewModel = conversationCreationViewModel + ) + + if (isConversationAvailableForRegisteredUsers) { + ConversationOptions( + text = R.string.nc_open_to_guest_app_users, + switch = { + Switch( + checked = isOpenForGuestAppUsers, + onCheckedChange = { + conversationCreationViewModel.openForGuestAppUsers.value = it + } + ) + }, + showDialog = false, + conversationCreationViewModel = conversationCreationViewModel + ) + } +} + +@Composable +fun ConversationOptions( + icon: Int? = null, + text: Int, + switch: @Composable (() -> Unit)? = null, + showDialog: Boolean, + conversationCreationViewModel: ConversationCreationViewModel +) { + var showPasswordDialog by remember { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .then( + if (showDialog) { + Modifier.clickable { + showPasswordDialog = true + } + } else { + Modifier + } + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + } else { + Spacer(modifier = Modifier.width(40.dp)) + } + Text( + text = stringResource(id = text), + modifier = Modifier.weight(1f) + ) + if (switch != null) { + switch() + } + if (showPasswordDialog) { + ShowPasswordDialog( + onDismiss = { showPasswordDialog = false }, + conversationCreationViewModel = conversationCreationViewModel + ) + } + } +} + +@Composable +fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { + var password by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + Button(onClick = { + conversationCreationViewModel.updatePassword(password) + onDismiss() + }) { + Text(text = stringResource(id = R.string.save)) + } + }, + title = { Text(text = stringResource(id = R.string.nc_set_password)) }, + text = { + TextField( + value = password, + onValueChange = { + password = it + }, + label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) } + ) + }, + dismissButton = { + Button(onClick = { onDismiss() }) { + Text(text = stringResource(id = R.string.nc_cancel)) + } + } + ) +} + +@Composable +fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { + val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState() + Box( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp), + contentAlignment = Alignment.Center + ) { + Button( + onClick = { + conversationCreationViewModel.createRoomAndAddParticipants( + roomType = CompanionClass.ROOM_TYPE_GROUP, + conversationName = conversationCreationViewModel.roomName.value, + participants = selectedParticipants.toSet() + ) { roomToken -> + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + } + ) { + Text(text = stringResource(id = R.string.create_conversation)) + } + } +} +class CompanionClass { + companion object { + internal val TAG = ConversationCreationActivity::class.simpleName + internal const val ROOM_TYPE_GROUP = "2" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt new file mode 100644 index 000000000..6ebb6c350 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall + +interface ConversationCreationRepository { + + suspend fun allowGuests(token: String, allow: Boolean): GenericOverall + suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall + suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall + suspend fun openConversation(roomToken: String, scope: Int): GenericOverall + suspend fun addParticipants(conversationToken: String?, userId: String, sourceType: String): AddParticipantOverall + suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall + fun getImageUri(avatarId: String, requestBigSize: Boolean): String + suspend fun setPassword(roomToken: String, password: String): GenericOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt new file mode 100644 index 000000000..4e00f174c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt @@ -0,0 +1,150 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ApiUtils.getRetrofitBucketForAddParticipant +import com.nextcloud.talk.utils.ApiUtils.getRetrofitBucketForAddParticipantWithSource + +class ConversationCreationRepositoryImpl( + private val ncApiCoroutines: NcApiCoroutines, + private val userManager: UserManager +) : ConversationCreationRepository { + private val _currentUser = userManager.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) + val apiVersion = ApiUtils.getConversationApiVersion(_currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + + override suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall { + return ncApiCoroutines.renameRoom( + credentials, + ApiUtils.getUrlForRoom( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + roomNameNew + ) + } + + override suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall { + return ncApiCoroutines.setConversationDescription( + credentials, + ApiUtils.getUrlForConversationDescription( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + description + ) + } + + override suspend fun openConversation(roomToken: String, scope: Int): GenericOverall { + return ncApiCoroutines.openConversation( + credentials, + ApiUtils.getUrlForOpeningConversations( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + scope + ) + } + + override suspend fun addParticipants( + conversationToken: String?, + userId: String, + sourceType: String + ): AddParticipantOverall { + val retrofitBucket: RetrofitBucket = if (sourceType == "users") { + getRetrofitBucketForAddParticipant( + apiVersion, + _currentUser.baseUrl, + conversationToken, + userId + ) + } else { + getRetrofitBucketForAddParticipantWithSource( + apiVersion, + _currentUser.baseUrl, + conversationToken, + sourceType, + userId + ) + } + val participants = ncApiCoroutines.addParticipant(credentials, retrofitBucket.url, retrofitBucket.queryMap) + return participants + } + + override suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall { + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + apiVersion, + _currentUser.baseUrl, + roomType, + null, + null, + conversationName + ) + val response = ncApiCoroutines.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + return response + } + + override fun getImageUri(avatarId: String, requestBigSize: Boolean): String { + return ApiUtils.getUrlForAvatar( + _currentUser.baseUrl, + avatarId, + requestBigSize + ) + } + + override suspend fun setPassword(roomToken: String, password: String): GenericOverall { + val result = ncApiCoroutines.setPassword( + credentials, + ApiUtils.getUrlForRoomPassword( + apiVersion, + _currentUser.baseUrl!!, + roomToken + ), + password + ) + return result + } + + override suspend fun allowGuests(token: String, allow: Boolean): GenericOverall { + val url = ApiUtils.getUrlForRoomPublic( + apiVersion, + _currentUser.baseUrl!!, + token + ) + + val result: GenericOverall = if (allow) { + ncApiCoroutines.makeRoomPublic( + credentials, + url + ) + } else { + ncApiCoroutines.makeRoomPrivate( + credentials, + url + ) + } + + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt new file mode 100644 index 000000000..759637cde --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -0,0 +1,156 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import android.util.Log +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl.Companion.STATUS_CODE_OK +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ConversationCreationViewModel @Inject constructor( + private val repository: ConversationCreationRepository +) : ViewModel() { + private val _selectedParticipants = MutableStateFlow>(emptyList()) + val selectedParticipants: StateFlow> = _selectedParticipants + private val roomViewState = MutableStateFlow(RoomUIState.None) + + fun updateSelectedParticipants(participants: List) { + _selectedParticipants.value = participants + } + + private val _roomName = MutableStateFlow("") + val roomName: StateFlow = _roomName + private val _password = MutableStateFlow("") + val password: StateFlow = _password + private val _conversationDescription = MutableStateFlow("") + val conversationDescription: StateFlow = _conversationDescription + var isGuestsAllowed = mutableStateOf(false) + var isConversationAvailableForRegisteredUsers = mutableStateOf(false) + var openForGuestAppUsers = mutableStateOf(false) + private val addParticipantsViewState = MutableStateFlow(AddParticipantsUiState.None) + private val allowGuestsResult = MutableStateFlow(AllowGuestsUiState.None) + fun updateRoomName(roomName: String) { + _roomName.value = roomName + } + + fun updatePassword(password: String) { + _password.value = password + } + + fun updateConversationDescription(conversationDescription: String) { + _conversationDescription.value = conversationDescription + } + + fun createRoomAndAddParticipants( + roomType: String, + conversationName: String, + participants: Set, + onRoomCreated: (String) -> Unit + ) { + val scope = when { + isConversationAvailableForRegisteredUsers.value && !openForGuestAppUsers.value -> 1 + isConversationAvailableForRegisteredUsers.value && openForGuestAppUsers.value -> 2 + else -> 0 + } + viewModelScope.launch { + roomViewState.value = RoomUIState.None + try { + val roomResult = repository.createRoom(roomType, conversationName) + val conversation = roomResult.ocs?.data + + if (conversation != null) { + val token = conversation.token + if (token != null) { + try { + repository.setConversationDescription( + token, + _conversationDescription.value + ) + val allowGuestResultOverall = repository.allowGuests(token, isGuestsAllowed.value) + val statusCode: GenericMeta? = allowGuestResultOverall.ocs?.meta + val result = (statusCode?.statusCode == STATUS_CODE_OK) + if (result) { + allowGuestsResult.value = AllowGuestsUiState.Success(result) + for (participant in participants) { + if (participant.id != null) { + val participantOverall = repository.addParticipants( + token, + participant.id!!, + participant.source!! + ).ocs?.data + addParticipantsViewState.value = + AddParticipantsUiState.Success(participantOverall) + } + } + } + if (_password.value.isNotEmpty()) { + repository.setPassword(token, _password.value) + } + repository.openConversation(token, scope) + onRoomCreated(token) + } catch (exception: Exception) { + allowGuestsResult.value = AllowGuestsUiState.Error(exception.message ?: "") + } + } + roomViewState.value = RoomUIState.Success(conversation) + } else { + roomViewState.value = RoomUIState.Error("Conversation is null") + } + } catch (e: Exception) { + roomViewState.value = RoomUIState.Error(e.message ?: "Unknown error") + Log.e("ConversationCreationViewModel", "Error - ${e.message}") + } + } + } + + fun getImageUri(avatarId: String, requestBigSize: Boolean): String { + return repository.getImageUri(avatarId, requestBigSize) + } + + fun createRoom(roomType: String, conversationName: String?) { + viewModelScope.launch { + try { + val room = repository.createRoom( + roomType, + conversationName + ) + + val conversation: Conversation? = room.ocs?.data + roomViewState.value = RoomUIState.Success(conversation) + } catch (exception: Exception) { + roomViewState.value = RoomUIState.Error(exception.message ?: "") + } + } + } +} + +sealed class AllowGuestsUiState { + data object None : AllowGuestsUiState() + data class Success(val result: Boolean) : AllowGuestsUiState() + data class Error(val message: String) : AllowGuestsUiState() +} + +sealed class RoomUIState { + data object None : RoomUIState() + data class Success(val conversation: Conversation?) : RoomUIState() + data class Error(val message: String) : RoomUIState() +} + +sealed class AddParticipantsUiState { + data object None : AddParticipantsUiState() + data class Success(val participants: List?) : AddParticipantsUiState() + data class Error(val message: String) : AddParticipantsUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 4085d4114..f00c502ba 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -19,6 +19,8 @@ import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.contacts.ContactsRepositoryImpl import com.nextcloud.talk.conversation.repository.ConversationRepository import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl +import com.nextcloud.talk.conversationcreation.ConversationCreationRepository +import com.nextcloud.talk.conversationcreation.ConversationCreationRepositoryImpl import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository @@ -208,4 +210,12 @@ class RepositoryModule { fun provideContactsRepository(ncApiCoroutines: NcApiCoroutines, userManager: UserManager): ContactsRepository { return ContactsRepositoryImpl(ncApiCoroutines, userManager) } + + @Provides + fun provideConversationCreationRepository( + ncApiCoroutines: NcApiCoroutines, + userManager: UserManager + ): ConversationCreationRepository { + return ConversationCreationRepositoryImpl(ncApiCoroutines, userManager) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index 511e6d7e2..f1ac935fe 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -14,6 +14,7 @@ import com.nextcloud.talk.contacts.ContactsViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel +import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel @@ -156,4 +157,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(ContactsViewModel::class) abstract fun contactsViewModel(viewModel: ContactsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationCreationViewModel::class) + abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt index cb34a2372..30b856a6b 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -7,9 +7,9 @@ package com.nextcloud.talk.data.database.mappers -import com.nextcloud.talk.chat.data.model.ChatMessage -import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.chat.data.model.ChatMessage fun ChatMessageJson.asEntity(accountId: Long) = ChatMessageEntity( diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 28d4e4ff7..c0168c413 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -539,6 +539,10 @@ object ApiUtils { return getUrlForRoom(version, baseUrl, token) + "/description" } + fun getUrlForOpeningConversations(version: Int, baseUrl: String?, token: String): String { + return getUrlForRoom(version, baseUrl, token) + "/listable" + } + fun getUrlForTranslation(baseUrl: String): String { return "$baseUrl$OCS_API_VERSION/translation/translate" } diff --git a/app/src/main/res/drawable/baseline_tag_faces_24.xml b/app/src/main/res/drawable/baseline_tag_faces_24.xml new file mode 100644 index 000000000..3a4f45877 --- /dev/null +++ b/app/src/main/res/drawable/baseline_tag_faces_24.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fa26e80e..13ed2b442 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -242,6 +242,9 @@ How to translate with transifex: Remove from favorites Create a new conversation Join open conversations + Open conversation to registered users + Also open to guest app users + Visibility Added conversation %1$s to favorites Removed conversation %1$s from favorites @@ -386,6 +389,7 @@ How to translate with transifex: Close Icon Refresh Please check your internet connection + Visible Enter a message … @@ -429,6 +433,7 @@ How to translate with transifex: Allow guests Allow guests to share a public link to join this conversation. Cannot enable/disable guest access. + Set Password Password protection Set a password to restrict who can use the public link. Guest access password @@ -563,6 +568,7 @@ How to translate with transifex: No phone number integration due to missing permissions Chat via %s Account not found + Edit Save diff --git a/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt index 7bb905b43..8a9a56937 100644 --- a/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt +++ b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt @@ -81,7 +81,7 @@ class ContactsViewModelTest { @Test fun `update shareTypes`() { - viewModel.updateShareTypes(ShareType.Group.shareType) + viewModel.updateShareTypes(listOf(ShareType.Group.shareType)) assert(viewModel.shareTypeList.contains(ShareType.Group.shareType)) } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f1531e06f..f55002312 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4,13 +4,13 @@ true true - - - - + + + + @@ -145,7 +145,6 @@ - @@ -157,6 +156,7 @@ + @@ -246,6 +246,7 @@ + @@ -343,6 +344,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -359,6 +404,14 @@ + + + + + + + + @@ -372,6 +425,11 @@ + + + + + @@ -393,6 +451,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +