Merge pull request #4064 from nextcloud/create_new_conversation

Create new conversation
This commit is contained in:
Marcel Hibbe 2024-09-05 15:50:33 +02:00 committed by GitHub
commit 38c1b6b007
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1398 additions and 162 deletions

View File

@ -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'

View File

@ -129,6 +129,9 @@
<activity android:name=".contacts.ContactsActivityCompose"
android:theme="@style/AppTheme"/>
<activity android:name=".conversationcreation.ConversationCreationActivity"
android:theme="@style/AppTheme"/>
<activity
android:name=".account.AccountVerificationActivity"
android:theme="@style/AppTheme" />

View File

@ -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<String, String>?
): 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<String, String>?
): 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
}

View File

@ -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<AutocompleteUser>
) {
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<AutocompleteUser>
) {
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<AutocompleteUser>, contactsViewModel: ContactsViewModel, context: Context) {
fun ContactsItem(
contacts: List<AutocompleteUser>,
contactsViewModel: ContactsViewModel,
context: Context,
selectedParticipants: MutableList<AutocompleteUser>
) {
val groupedContacts: Map<String, List<AutocompleteUser>> = contacts.groupBy { contact ->
(
if (contact.source == "users") {
@ -166,11 +425,16 @@ fun ContactsItem(contacts: List<AutocompleteUser>, 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

View File

@ -30,6 +30,10 @@ class ContactsViewModel @Inject constructor(
val shareTypeList: List<String> = shareTypes
private val _searchState = MutableStateFlow(false)
val searchState: StateFlow<Boolean> = _searchState
private val selectedParticipants = MutableStateFlow<List<AutocompleteUser>>(emptyList())
val selectedParticipantsList: StateFlow<List<AutocompleteUser>> = selectedParticipants
private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView
init {
getContactsFromSearchParams()
@ -39,12 +43,19 @@ class ContactsViewModel @Inject constructor(
_searchQuery.value = query
}
fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
selectedParticipants.value = participants
}
fun updateSearchState(searchState: Boolean) {
_searchState.value = searchState
}
fun updateShareTypes(value: String) {
shareTypes.add(value)
fun updateShareTypes(value: List<String>) {
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 {

View File

@ -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
),

View File

@ -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")
}

View File

@ -0,0 +1,535 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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<AutocompleteUser>("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<Intent, ActivityResult>,
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<AutocompleteUser>
)
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"
}
}

View File

@ -0,0 +1,24 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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
}

View File

@ -0,0 +1,150 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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
}
}

View File

@ -0,0 +1,156 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* 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<List<AutocompleteUser>>(emptyList())
val selectedParticipants: StateFlow<List<AutocompleteUser>> = _selectedParticipants
private val roomViewState = MutableStateFlow<RoomUIState>(RoomUIState.None)
fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
_selectedParticipants.value = participants
}
private val _roomName = MutableStateFlow("")
val roomName: StateFlow<String> = _roomName
private val _password = MutableStateFlow("")
val password: StateFlow<String> = _password
private val _conversationDescription = MutableStateFlow("")
val conversationDescription: StateFlow<String> = _conversationDescription
var isGuestsAllowed = mutableStateOf(false)
var isConversationAvailableForRegisteredUsers = mutableStateOf(false)
var openForGuestAppUsers = mutableStateOf(false)
private val addParticipantsViewState = MutableStateFlow<AddParticipantsUiState>(AddParticipantsUiState.None)
private val allowGuestsResult = MutableStateFlow<AllowGuestsUiState>(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<AutocompleteUser>,
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<Conversation>?) : AddParticipantsUiState()
data class Error(val message: String) : AddParticipantsUiState()
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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(

View File

@ -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"
}

View File

@ -0,0 +1,18 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/>
</vector>

View File

@ -242,6 +242,9 @@ How to translate with transifex:
<string name="nc_remove_from_favorites">Remove from favorites</string>
<string name="nc_create_new_conversation">Create a new conversation</string>
<string name="nc_join_open_conversations">Join open conversations</string>
<string name="nc_open_conversation_to_registered_users">Open conversation to registered users</string>
<string name="nc_open_to_guest_app_users">Also open to guest app users</string>
<string name="nc_new_conversation_visibility">Visibility</string>
<string name="added_to_favorites">Added conversation %1$s to favorites</string>
<string name="removed_from_favorites">Removed conversation %1$s from favorites</string>
@ -386,6 +389,7 @@ How to translate with transifex:
<string name="close_icon">Close Icon</string>
<string name="nc_refresh">Refresh</string>
<string name="nc_check_your_internet">Please check your internet connection</string>
<string name="nc_visible">Visible</string>
<!-- Chat -->
<string name="nc_hint_enter_a_message">Enter a message …</string>
@ -429,6 +433,7 @@ How to translate with transifex:
<string name="nc_guest_access_allow_title">Allow guests</string>
<string name="nc_guest_access_allow_summary">Allow guests to share a public link to join this conversation.</string>
<string name="nc_guest_access_allow_failed">Cannot enable/disable guest access.</string>
<string name="nc_set_password">Set Password</string>
<string name="nc_guest_access_password_title">Password protection</string>
<string name="nc_guest_access_password_summary">Set a password to restrict who can use the public link.</string>
<string name="nc_guest_access_password_dialog_title">Guest access password</string>
@ -563,6 +568,7 @@ How to translate with transifex:
<string name="no_phone_book_integration_due_to_permissions">No phone number integration due to missing permissions</string>
<string name="nc_phone_book_integration_chat_via">Chat via %s</string>
<string name="nc_phone_book_integration_account_not_found">Account not found</string>
<string name= "nc_edit">Edit</string>
<!-- save feature -->
<string name="nc_save_message">Save</string>

View File

@ -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))
}

View File

@ -4,13 +4,13 @@
<verify-metadata>true</verify-metadata>
<verify-signatures>true</verify-signatures>
<trusted-artifacts>
<trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
<trust group="androidx.fragment"/>
<trust group="com.android.tools.build" name="aapt2" version="8.4.1-11315950" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
<trust group="com.github.nextcloud-deps" name="android-talk-webrtc" version="110.5481.0" reason="ships OS specific artifacts (win/linux) - temp global trust"/>
<trust group="com.google.dagger"/>
<trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
<trust file=".*-sources[.]jar" regex="true"/>
<trust file="tensorflow-lite-metadata-0.1.0-rc2.pom" reason="differing hash on every CI run - temp global trust"/>
<trust group="com.google.dagger" />
<trust group="org.javassist" name="javassist" version="3.26.0-GA" reason="java assist"/>
<trust group="androidx.fragment"/>
</trusted-artifacts>
<ignored-keys>
<ignored-key id="0AA3E5C3D232E79B" reason="Key couldn't be downloaded from any key server"/>
@ -145,7 +145,6 @@
<trusting group="androidx.annotation"/>
<trusting group="androidx.camera"/>
<trusting group="androidx.collection"/>
<trusting group="androidx.compose.foundation"/>
<trusting group="androidx.compose.material3"/>
<trusting group="androidx.core"/>
<trusting group="androidx.emoji2"/>
@ -157,6 +156,7 @@
<trusting group="androidx.sqlite"/>
<trusting group="androidx.webkit"/>
<trusting group="androidx.work"/>
<trusting group="androidx.compose.foundation"/>
</trusted-key>
<trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1">
<trusting group="org.apache" name="apache"/>
@ -246,6 +246,7 @@
<trusted-key id="E4AC7874F3479A0F1F8ECF9960BB45F36B649F22" group="fr.dudie" name="nominatim-api" version="3.4"/>
<trusted-key id="E77417AC194160A3FABD04969A259C7EE636C5ED" group="^com[.]google($|([.].*))" regex="true"/>
<trusted-key id="E7DC75FC24FB3C8DFE8086AD3D5839A2262CBBFB" group="org.jetbrains.kotlinx"/>
<trusted-key id="64B9B09F164AA0BF88742EB61188B69F6D6259CA" group="com.google.accompanist"/>
<trusted-key id="E82D2EAF2E83830CE1F7F6BE571A5291E827E1C7" group="net.java" name="jvnet-parent" version="3"/>
<trusted-key id="E85AED155021AF8A6C6B7A4A7C7D8456294423BA" group="org.objenesis"/>
<trusted-key id="EAA526B91DD83BA3E1B9636FA730529CA355A63E" group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2.1"/>
@ -343,6 +344,50 @@
<sha256 value="9516c2ae44284ea0bd3d0eade0ee638879b708cbe31e3af92ba96c300604ebc3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.6">
<artifact name="exifinterface-1.3.6.aar">
<sha256 value="1804105e9e05fdd8f760413bad5de498c381aa329f4f9d94c851bc891ac654c6" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="exifinterface-1.3.6.module">
<sha256 value="5e9fd84ca3fd3b7706f6856fa4383107de8676bf7c42b7d4b8108949414d6201" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.1.0">
<artifact name="core-1.1.0.pom">
<sha256 value="dae46132cdcd46b798425f7cb78fd65890869b6d26101ccdcd43461a4f51754c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.3.2">
<artifact name="core-1.3.2.pom">
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.test.ext" name="junit" version="1.1.5">
<artifact name="junit-1.1.5.aar">
<sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="junit-1.1.5.pom">
<sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.8.0">
<artifact name="core-ktx-1.8.0.module">
<sha256 value="a91bc3e02f209f643dd8275345a9e3003ce20d64fc0760eccf479c1709842f72" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
<artifact name="annotation-experimental-1.3.0.aar">
<sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="annotation-experimental-1.3.0.module">
<sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.5.0">
<artifact name="annotation-1.5.0.jar">
<sha256 value="261fb7c0210858500bab66d34354972a75166ab4182add283780b05513d6ec4a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -359,6 +404,14 @@
<sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.exifinterface" name="exifinterface" version="1.3.2">
<artifact name="exifinterface-1.3.2.aar">
<sha256 value="8770c180103e0b8c04a07eb4c59153af639b09eca25deae9bdcdaf869d1e5b6b" origin="Generated by Gradle"/>
</artifact>
<artifact name="exifinterface-1.3.2.module">
<sha256 value="10ba5b5cbea7f5c8758be4fdaec60a3545e891a1130d830a442b88cf5336a885" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.0.0">
<artifact name="annotation-experimental-1.0.0.pom">
<sha256 value="6b73ff6608f4b1d6cbab620b65708a382d0b39901cf4e6b0d16f84a1b04d7732" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -372,6 +425,11 @@
<sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.1.0-rc01">
<artifact name="annotation-experimental-1.1.0-rc01.module">
<sha256 value="d45ac493e84d968aabb2bea2b7744031a98cf5074447c0f3b862d600fc44b55c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -393,6 +451,76 @@
<sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.3.0">
<artifact name="annotation-experimental-1.3.0.aar">
<sha256 value="abfd29c8556e5bd0325a9f769ab9e9d154ff4a5515c476cdd5a2a8285b1b19dc" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="annotation-experimental-1.3.0.module">
<sha256 value="5eebeaff01d042e06dcf292abf8964ad391e4b0159f0090f16253d6045d38da0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose" name="compose-bom" version="2024.06.00">
<artifact name="compose-bom-2024.06.00.pom">
<sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.activity" name="activity-compose" version="1.7.0">
<artifact name="activity-compose-1.7.0.aar">
<sha256 value="caa72885d1ce7979c1d6c59a8b255c6097b770780d4d4da95d56979a348646cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="activity-compose-1.7.0.module">
<sha256 value="f7a29bcba338575dcf89a553cff9cfad3f140340eaf2b56fd0193244da602c0a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-1.0.1.module">
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose" name="compose-bom" version="2024.06.00">
<artifact name="compose-bom-2024.06.00.pom">
<sha256 value="1b391a969ff81c0bb43b3711e92d977e8bfa72457a11d8a37910a7051bdc3045" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.1.0">
<artifact name="appcompat-1.1.0.pom">
<sha256 value="340d617121f8ef8e02a6680c8f357aa3e542276d0c8a1cdcb6fd98984b2cb7b9" origin="Generated by Gradle" reason="Artifact is not signed"/>