From df6d54d51c30f15267d1c6b4a2b73a18c18c2f36 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 22 May 2024 16:09:12 +0200 Subject: [PATCH 1/6] Add endpoints upload and delete Conversation Avatar Signed-off-by: sowjanyakch --- .../com/nextcloud/talk/api/NcApiCoroutines.kt | 14 ++ .../contacts/ContactsActivityViewModel.kt | 104 +++++++++++++ .../ConversationCreationRepository.kt | 4 + .../ConversationCreationRepositoryImpl.kt | 33 +++++ .../ConversationCreationViewModel.kt | 42 ++++++ .../repository/FakeRepositorySuccess.kt | 4 + gradle/verification-metadata.xml | 138 +++++++++++++++++- 7 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt 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 d08d2b317..b5cb33a13 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -11,13 +11,16 @@ 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 okhttp3.MultipartBody import retrofit2.http.DELETE import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT +import retrofit2.http.Part import retrofit2.http.Query import retrofit2.http.QueryMap import retrofit2.http.Url @@ -96,4 +99,15 @@ interface NcApiCoroutines { @Url url: String?, @Field("password") password: String? ): GenericOverall + + @Multipart + @POST + suspend fun uploadConversationAvatar( + @Header("Authorization") authorization: String, + @Url url: String, + @Part("attachment") attachment: MultipartBody.Part + ): RoomOverall + + @DELETE + suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall } diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt new file mode 100644 index 000000000..6c87c6608 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt @@ -0,0 +1,104 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ContactsActivityViewModel @Inject constructor( + private val repository: ContactsRepository, + private val userManager: UserManager +) : ViewModel() { + + private val _contactsViewState = MutableStateFlow(ContactsUiState.None) + val contactsViewState: StateFlow = _contactsViewState + private val _roomViewState = MutableStateFlow(RoomUiState.None) + val roomViewState: StateFlow = _roomViewState + private val _currentUser = userManager.currentUser.blockingGet() + val currentUser: User = _currentUser + private val _searchQuery = MutableStateFlow("") + val searchQuery: StateFlow = _searchQuery + private val shareTypes: MutableList = mutableListOf(ShareType.User.shareType) + val shareTypeList: List = shareTypes + + init { + getContactsFromSearchParams() + } + + fun updateSearchQuery(query: String) { + _searchQuery.value = query + } + + fun updateShareTypes(value: String) { + shareTypes.add(value) + } + + fun getContactsFromSearchParams() { + _contactsViewState.value = ContactsUiState.Loading + viewModelScope.launch { + try { + val contacts = repository.getContacts( + searchQuery.value, + shareTypeList + ) + val contactsList: List? = contacts.ocs!!.data + _contactsViewState.value = ContactsUiState.Success(contactsList) + } catch (exception: Exception) { + _contactsViewState.value = ContactsUiState.Error(exception.message ?: "") + } + } + } + + fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?) { + viewModelScope.launch { + try { + val room = repository.createRoom( + roomType, + sourceType, + userId, + conversationName + ) + + val conversation: Conversation? = room.ocs?.data + _roomViewState.value = RoomUiState.Success(conversation) + } catch (exception: Exception) { + _roomViewState.value = RoomUiState.Error(exception.message ?: "") + } + } + } + + fun getImageUri(avatarId: String, requestBigSize: Boolean): String { + return ApiUtils.getUrlForAvatar( + _currentUser.baseUrl, + avatarId, + requestBigSize + ) + } +} + +sealed class ContactsUiState { + data object None : ContactsUiState() + data object Loading : ContactsUiState() + data class Success(val contacts: List?) : ContactsUiState() + data class Error(val message: String) : ContactsUiState() +} + +sealed class RoomUiState { + data object None : RoomUiState() + data class Success(val conversation: Conversation?) : RoomUiState() + data class Error(val message: String) : RoomUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt index 6ebb6c350..12c9dbad8 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt @@ -7,9 +7,11 @@ package com.nextcloud.talk.conversationcreation +import com.nextcloud.talk.models.domain.ConversationModel 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 java.io.File interface ConversationCreationRepository { @@ -21,4 +23,6 @@ interface ConversationCreationRepository { suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall fun getImageUri(avatarId: String, requestBigSize: Boolean): String suspend fun setPassword(roomToken: String, password: String): GenericOverall + suspend fun uploadConversationAvatar(file: File, roomToken: String): ConversationModel + suspend fun deleteConversationAvatar(roomToken: String): ConversationModel } diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt index 4e00f174c..1f804e3ad 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt @@ -10,6 +10,7 @@ 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.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall @@ -17,6 +18,11 @@ 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 +import com.nextcloud.talk.utils.Mimetype +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File class ConversationCreationRepositoryImpl( private val ncApiCoroutines: NcApiCoroutines, @@ -126,6 +132,33 @@ class ConversationCreationRepositoryImpl( return result } + override suspend fun uploadConversationAvatar(file: File, roomToken: String): ConversationModel { + val builder = MultipartBody.Builder() + builder.setType(MultipartBody.FORM) + builder.addFormDataPart( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) + ) + val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_JPG.toMediaTypeOrNull()) + ) + val response = ncApiCoroutines.uploadConversationAvatar( + credentials!!, + ApiUtils.getUrlForConversationAvatar(1, _currentUser.baseUrl!!, roomToken), + filePart + ) + return ConversationModel.mapToConversationModel(response.ocs?.data!!, _currentUser) + } + + override suspend fun deleteConversationAvatar(roomToken: String): ConversationModel { + val url = ApiUtils.getUrlForConversationAvatar(1, _currentUser.baseUrl!!, roomToken) + val response = ncApiCoroutines.deleteConversationAvatar(credentials!!, url) + return ConversationModel.mapToConversationModel(response.ocs?.data!!, _currentUser) + } + override suspend fun allowGuests(token: String, allow: Boolean): GenericOverall { val url = ApiUtils.getUrlForRoomPublic( apiVersion, diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index 759637cde..2b9213383 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -11,6 +11,7 @@ import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericMeta @@ -18,6 +19,7 @@ import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.io.File import javax.inject.Inject class ConversationCreationViewModel @Inject constructor( @@ -27,6 +29,12 @@ class ConversationCreationViewModel @Inject constructor( val selectedParticipants: StateFlow> = _selectedParticipants private val roomViewState = MutableStateFlow(RoomUIState.None) + private val _uploadState = MutableStateFlow(UploadAvatarState.Loading) + val uploadState: StateFlow = _uploadState + + private val _deleteState = MutableStateFlow(DeleteAvatarState.Loading) + val deleteState: StateFlow = _deleteState + fun updateSelectedParticipants(participants: List) { _selectedParticipants.value = participants } @@ -116,6 +124,28 @@ class ConversationCreationViewModel @Inject constructor( } } + fun uploadConversationAvatar(file: File, roomToken: String) { + viewModelScope.launch { + try { + val response = repository.uploadConversationAvatar(file, roomToken) + _uploadState.value = UploadAvatarState.Success(response) + } catch (e: Exception) { + _uploadState.value = UploadAvatarState.Error(e) + } + } + } + + fun deleteConversationAvatar(roomToken: String) { + viewModelScope.launch { + try { + val result = repository.deleteConversationAvatar(roomToken) + _deleteState.value = DeleteAvatarState.Success(result) + } catch (e: Exception) { + _deleteState.value = DeleteAvatarState.Error(e) + } + } + } + fun getImageUri(avatarId: String, requestBigSize: Boolean): String { return repository.getImageUri(avatarId, requestBigSize) } @@ -154,3 +184,15 @@ sealed class AddParticipantsUiState { data class Success(val participants: List?) : AddParticipantsUiState() data class Error(val message: String) : AddParticipantsUiState() } + +sealed class UploadAvatarState { + object Loading : UploadAvatarState() + data class Success(val roomOverall: ConversationModel) : UploadAvatarState() + data class Error(val exception: Exception) : UploadAvatarState() +} + +sealed class DeleteAvatarState { + object Loading : DeleteAvatarState() + data class Success(val roomOverall: ConversationModel) : DeleteAvatarState() + data class Error(val exception: Exception) : DeleteAvatarState() +} diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt index f5d87d919..9022ba059 100644 --- a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt @@ -29,4 +29,8 @@ class FakeRepositorySuccess : ContactsRepository { override fun getImageUri(avatarId: String, requestBigSize: Boolean): String { return "https://mydomain.com/index.php/avatar/$avatarId/512" } + + override fun getImageUri(avatarId: String, requestBigSize: Boolean): String { + TODO("Not yet implemented") + } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 634c94ad3..e1c53723b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4,13 +4,13 @@ true true - - - - + + + + @@ -151,7 +151,6 @@ - @@ -163,6 +162,7 @@ + @@ -252,6 +252,7 @@ + @@ -349,6 +350,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -365,6 +410,14 @@ + + + + + + + + @@ -378,6 +431,11 @@ + + + + + @@ -399,6 +457,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fa014aab3a9a6d78b434683dbe4b464c182067e5 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Wed, 11 Sep 2024 17:58:52 +0200 Subject: [PATCH 2/6] Make pickImage work Signed-off-by: sowjanyakch --- .../ConversationCreationActivity.kt | 114 ++++++++++++++---- .../ConversationCreationViewModel.kt | 8 +- 2 files changed, 99 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index 021f6e9cf..e2bf8e5ea 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -13,6 +13,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult @@ -77,6 +78,7 @@ 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.PickImage import com.nextcloud.talk.utils.bundle.BundleKeys import javax.inject.Inject @@ -84,7 +86,7 @@ import javax.inject.Inject class ConversationCreationActivity : BaseActivity() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - + private lateinit var pickImage: PickImage override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -93,13 +95,16 @@ class ConversationCreationActivity : BaseActivity() { this, viewModelFactory )[ConversationCreationViewModel::class.java] + val conversationUser = conversationCreationViewModel.currentUser + pickImage = PickImage(this, conversationUser) + setContent { val colorScheme = viewThemeUtils.getColorScheme(this) val context = LocalContext.current MaterialTheme( colorScheme = colorScheme ) { - ConversationCreationScreen(conversationCreationViewModel, context) + ConversationCreationScreen(conversationCreationViewModel, context, pickImage) } SetStatusBarColor() } @@ -125,15 +130,47 @@ private fun SetStatusBarColor() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConversationCreationScreen(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { +fun ConversationCreationScreen( + conversationCreationViewModel: ConversationCreationViewModel, + context: Context, + pickImage: PickImage +) { + var selectedImageUri by remember { mutableStateOf(null) } + + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onImagePickerResult(result.data) { uri -> + selectedImageUri = uri + } + } + } + + val remoteFilePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onSelectRemoteFilesResult(imagePickerLauncher, result.data) + } + } + + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onTakePictureResult(imagePickerLauncher, result.data) + } + } + 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 selectedParticipants = + data?.getParcelableArrayListExtra("selectedParticipants") + ?: emptyList() val participants = selectedParticipants.toMutableList() conversationCreationViewModel.updateSelectedParticipants(participants) } @@ -162,8 +199,16 @@ fun ConversationCreationScreen(conversationCreationViewModel: ConversationCreati .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { - DefaultUserAvatar() - UploadAvatar() + DefaultUserAvatar(selectedImageUri) + UploadAvatar( + pickImage = pickImage, + onImageSelected = { uri -> selectedImageUri = uri }, + imagePickerLauncher = imagePickerLauncher, + remoteFilePickerLauncher = remoteFilePickerLauncher, + cameraLauncher = cameraLauncher, + onDeleteImage = { selectedImageUri = null } + ) + ConversationNameAndDescription(conversationCreationViewModel) AddParticipants(launcher, context, conversationCreationViewModel) RoomCreationOptions(conversationCreationViewModel) @@ -174,31 +219,51 @@ fun ConversationCreationScreen(conversationCreationViewModel: ConversationCreati } @Composable -fun DefaultUserAvatar() { +fun DefaultUserAvatar(selectedImageUri: Uri?) { 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) - ) + if (selectedImageUri != null) { + AsyncImage( + model = selectedImageUri, + contentDescription = stringResource(id = R.string.user_avatar), + modifier = Modifier + .size(84.dp) + .padding(top = 8.dp) + ) + } else { + AsyncImage( + model = R.drawable.ic_circular_group, + contentDescription = stringResource(id = R.string.user_avatar), + modifier = Modifier + .size(84.dp) + .padding(top = 8.dp) + ) + } } } @Composable -fun UploadAvatar() { +fun UploadAvatar( + pickImage: PickImage, + onImageSelected: (Uri) -> Unit, + imagePickerLauncher: ManagedActivityResultLauncher, + remoteFilePickerLauncher: ManagedActivityResultLauncher, + cameraLauncher: ManagedActivityResultLauncher, + onDeleteImage: () -> Unit +) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement = Arrangement.Center ) { - IconButton(onClick = { - }) { + IconButton( + onClick = { + pickImage.takePicture(cameraLauncher) + } + ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_photo_camera_24), contentDescription = null, @@ -207,6 +272,7 @@ fun UploadAvatar() { } IconButton(onClick = { + pickImage.selectLocal(imagePickerLauncher) }) { Icon( painter = painterResource(id = R.drawable.ic_folder_multiple_image), @@ -214,9 +280,11 @@ fun UploadAvatar() { modifier = Modifier.size(24.dp) ) } - - IconButton(onClick = { - }) { + IconButton( + onClick = { + pickImage.selectRemote(remoteFilePickerLauncher) + } + ) { Icon( painter = painterResource(id = R.drawable.baseline_tag_faces_24), contentDescription = null, @@ -225,6 +293,7 @@ fun UploadAvatar() { } IconButton(onClick = { + onDeleteImage() }) { Icon( painter = painterResource(id = R.drawable.ic_delete_grey600_24dp), @@ -530,6 +599,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo } } } + class CompanionClass { companion object { internal val TAG = ConversationCreationActivity::class.simpleName diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index 2b9213383..e710b3b3e 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -11,11 +11,13 @@ import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel 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 com.nextcloud.talk.users.UserManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -23,7 +25,8 @@ import java.io.File import javax.inject.Inject class ConversationCreationViewModel @Inject constructor( - private val repository: ConversationCreationRepository + private val repository: ConversationCreationRepository, + private val userManager: UserManager ) : ViewModel() { private val _selectedParticipants = MutableStateFlow>(emptyList()) val selectedParticipants: StateFlow> = _selectedParticipants @@ -35,6 +38,9 @@ class ConversationCreationViewModel @Inject constructor( private val _deleteState = MutableStateFlow(DeleteAvatarState.Loading) val deleteState: StateFlow = _deleteState + private val _currentUser = userManager.currentUser.blockingGet() + val currentUser: User = _currentUser + fun updateSelectedParticipants(participants: List) { _selectedParticipants.value = participants } From f8bc4f2e96761cf30025ee4a34cd4fb0915ca0e6 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Thu, 12 Sep 2024 09:21:07 +0200 Subject: [PATCH 3/6] upload conversation avatar Signed-off-by: sowjanyakch --- .../com/nextcloud/talk/api/NcApiCoroutines.kt | 2 +- .../ConversationCreationActivity.kt | 11 +++-- .../ConversationCreationViewModel.kt | 47 +++---------------- 3 files changed, 13 insertions(+), 47 deletions(-) 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 b5cb33a13..537bdeb14 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -105,7 +105,7 @@ interface NcApiCoroutines { suspend fun uploadConversationAvatar( @Header("Authorization") authorization: String, @Url url: String, - @Part("attachment") attachment: MultipartBody.Part + @Part attachment: MultipartBody.Part ): RoomOverall @DELETE diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index e2bf8e5ea..0ba7aee21 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -212,7 +212,7 @@ fun ConversationCreationScreen( ConversationNameAndDescription(conversationCreationViewModel) AddParticipants(launcher, context, conversationCreationViewModel) RoomCreationOptions(conversationCreationViewModel) - CreateConversation(conversationCreationViewModel, context) + CreateConversation(conversationCreationViewModel, context,selectedImageUri) } } ) @@ -275,7 +275,7 @@ fun UploadAvatar( pickImage.selectLocal(imagePickerLauncher) }) { Icon( - painter = painterResource(id = R.drawable.ic_folder_multiple_image), + painter = painterResource(id = R.drawable.upload), contentDescription = null, modifier = Modifier.size(24.dp) ) @@ -286,7 +286,7 @@ fun UploadAvatar( } ) { Icon( - painter = painterResource(id = R.drawable.baseline_tag_faces_24), + painter = painterResource(id = R.drawable.ic_mimetype_folder), contentDescription = null, modifier = Modifier.size(24.dp) ) @@ -571,7 +571,7 @@ fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: Con } @Composable -fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { +fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context,selectedImageUri: Uri?) { val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState() Box( modifier = Modifier @@ -584,7 +584,8 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo conversationCreationViewModel.createRoomAndAddParticipants( roomType = CompanionClass.ROOM_TYPE_GROUP, conversationName = conversationCreationViewModel.roomName.value, - participants = selectedParticipants.toSet() + participants = selectedParticipants.toSet(), + selectedImageUri = selectedImageUri ) { roomToken -> val bundle = Bundle() bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index e710b3b3e..13fc607f5 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -7,12 +7,13 @@ package com.nextcloud.talk.conversationcreation +import android.net.Uri import android.util.Log import androidx.compose.runtime.mutableStateOf +import androidx.core.net.toFile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericMeta @@ -21,7 +22,6 @@ import com.nextcloud.talk.users.UserManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import java.io.File import javax.inject.Inject class ConversationCreationViewModel @Inject constructor( @@ -32,12 +32,6 @@ class ConversationCreationViewModel @Inject constructor( val selectedParticipants: StateFlow> = _selectedParticipants private val roomViewState = MutableStateFlow(RoomUIState.None) - private val _uploadState = MutableStateFlow(UploadAvatarState.Loading) - val uploadState: StateFlow = _uploadState - - private val _deleteState = MutableStateFlow(DeleteAvatarState.Loading) - val deleteState: StateFlow = _deleteState - private val _currentUser = userManager.currentUser.blockingGet() val currentUser: User = _currentUser @@ -72,6 +66,7 @@ class ConversationCreationViewModel @Inject constructor( roomType: String, conversationName: String, participants: Set, + selectedImageUri: Uri?, onRoomCreated: (String) -> Unit ) { val scope = when { @@ -114,6 +109,9 @@ class ConversationCreationViewModel @Inject constructor( repository.setPassword(token, _password.value) } repository.openConversation(token, scope) + if(selectedImageUri!= null){ + repository.uploadConversationAvatar(selectedImageUri.toFile(), token) + } onRoomCreated(token) } catch (exception: Exception) { allowGuestsResult.value = AllowGuestsUiState.Error(exception.message ?: "") @@ -130,28 +128,6 @@ class ConversationCreationViewModel @Inject constructor( } } - fun uploadConversationAvatar(file: File, roomToken: String) { - viewModelScope.launch { - try { - val response = repository.uploadConversationAvatar(file, roomToken) - _uploadState.value = UploadAvatarState.Success(response) - } catch (e: Exception) { - _uploadState.value = UploadAvatarState.Error(e) - } - } - } - - fun deleteConversationAvatar(roomToken: String) { - viewModelScope.launch { - try { - val result = repository.deleteConversationAvatar(roomToken) - _deleteState.value = DeleteAvatarState.Success(result) - } catch (e: Exception) { - _deleteState.value = DeleteAvatarState.Error(e) - } - } - } - fun getImageUri(avatarId: String, requestBigSize: Boolean): String { return repository.getImageUri(avatarId, requestBigSize) } @@ -191,14 +167,3 @@ sealed class AddParticipantsUiState { data class Error(val message: String) : AddParticipantsUiState() } -sealed class UploadAvatarState { - object Loading : UploadAvatarState() - data class Success(val roomOverall: ConversationModel) : UploadAvatarState() - data class Error(val exception: Exception) : UploadAvatarState() -} - -sealed class DeleteAvatarState { - object Loading : DeleteAvatarState() - data class Success(val roomOverall: ConversationModel) : DeleteAvatarState() - data class Error(val exception: Exception) : DeleteAvatarState() -} From 3946240e4f44cc03f0b661acdc814281816f8104 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Thu, 12 Sep 2024 10:03:49 +0200 Subject: [PATCH 4/6] Add verification metadata Signed-off-by: sowjanyakch --- .../contacts/ContactsActivityViewModel.kt | 104 ------------- gradle/verification-metadata.xml | 138 +----------------- 2 files changed, 5 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt deleted file mode 100644 index 6c87c6608..000000000 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivityViewModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.contacts - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.users.UserManager -import com.nextcloud.talk.utils.ApiUtils -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -class ContactsActivityViewModel @Inject constructor( - private val repository: ContactsRepository, - private val userManager: UserManager -) : ViewModel() { - - private val _contactsViewState = MutableStateFlow(ContactsUiState.None) - val contactsViewState: StateFlow = _contactsViewState - private val _roomViewState = MutableStateFlow(RoomUiState.None) - val roomViewState: StateFlow = _roomViewState - private val _currentUser = userManager.currentUser.blockingGet() - val currentUser: User = _currentUser - private val _searchQuery = MutableStateFlow("") - val searchQuery: StateFlow = _searchQuery - private val shareTypes: MutableList = mutableListOf(ShareType.User.shareType) - val shareTypeList: List = shareTypes - - init { - getContactsFromSearchParams() - } - - fun updateSearchQuery(query: String) { - _searchQuery.value = query - } - - fun updateShareTypes(value: String) { - shareTypes.add(value) - } - - fun getContactsFromSearchParams() { - _contactsViewState.value = ContactsUiState.Loading - viewModelScope.launch { - try { - val contacts = repository.getContacts( - searchQuery.value, - shareTypeList - ) - val contactsList: List? = contacts.ocs!!.data - _contactsViewState.value = ContactsUiState.Success(contactsList) - } catch (exception: Exception) { - _contactsViewState.value = ContactsUiState.Error(exception.message ?: "") - } - } - } - - fun createRoom(roomType: String, sourceType: String, userId: String, conversationName: String?) { - viewModelScope.launch { - try { - val room = repository.createRoom( - roomType, - sourceType, - userId, - conversationName - ) - - val conversation: Conversation? = room.ocs?.data - _roomViewState.value = RoomUiState.Success(conversation) - } catch (exception: Exception) { - _roomViewState.value = RoomUiState.Error(exception.message ?: "") - } - } - } - - fun getImageUri(avatarId: String, requestBigSize: Boolean): String { - return ApiUtils.getUrlForAvatar( - _currentUser.baseUrl, - avatarId, - requestBigSize - ) - } -} - -sealed class ContactsUiState { - data object None : ContactsUiState() - data object Loading : ContactsUiState() - data class Success(val contacts: List?) : ContactsUiState() - data class Error(val message: String) : ContactsUiState() -} - -sealed class RoomUiState { - data object None : RoomUiState() - data class Success(val conversation: Conversation?) : RoomUiState() - data class Error(val message: String) : RoomUiState() -} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e1c53723b..634c94ad3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4,13 +4,13 @@ true true + + + + - - - - @@ -151,6 +151,7 @@ + @@ -162,7 +163,6 @@ - @@ -252,7 +252,6 @@ - @@ -350,50 +349,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -410,14 +365,6 @@ - - - - - - - - @@ -431,11 +378,6 @@ - - - - - @@ -457,76 +399,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From f20ec95b5f17f3c7d92288672a416def587f2c50 Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Thu, 12 Sep 2024 10:10:01 +0200 Subject: [PATCH 5/6] Avoid conflicting overloads Signed-off-by: sowjanyakch --- .../talk/contacts/repository/FakeRepositorySuccess.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt index 9022ba059..f5d87d919 100644 --- a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt @@ -29,8 +29,4 @@ class FakeRepositorySuccess : ContactsRepository { override fun getImageUri(avatarId: String, requestBigSize: Boolean): String { return "https://mydomain.com/index.php/avatar/$avatarId/512" } - - override fun getImageUri(avatarId: String, requestBigSize: Boolean): String { - TODO("Not yet implemented") - } } From 6bc3b1a6e9398dfeaa9b5aba04f0888e6a8ff51a Mon Sep 17 00:00:00 2001 From: sowjanyakch Date: Thu, 12 Sep 2024 14:52:33 +0200 Subject: [PATCH 6/6] Make avatar circular Signed-off-by: sowjanyakch --- app/build.gradle | 2 +- .../ConversationCreationActivity.kt | 15 +++++++++++++-- .../ConversationCreationViewModel.kt | 3 +-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 99498baa6..a5aa0ebb4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,7 +15,7 @@ import com.github.spotbugs.snom.SpotBugsTask plugins { id "org.jetbrains.kotlin.plugin.compose" version "2.0.20" id "org.jetbrains.kotlin.kapt" - id 'com.google.devtools.ksp' version '2.0.20-1.0.25' + id 'com.google.devtools.ksp' version '2.0.20-1.0.24' } apply plugin: 'com.android.application' diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt index 0ba7aee21..89acf6c85 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -33,6 +33,7 @@ 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.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -58,7 +59,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.colorResource @@ -212,7 +215,7 @@ fun ConversationCreationScreen( ConversationNameAndDescription(conversationCreationViewModel) AddParticipants(launcher, context, conversationCreationViewModel) RoomCreationOptions(conversationCreationViewModel) - CreateConversation(conversationCreationViewModel, context,selectedImageUri) + CreateConversation(conversationCreationViewModel, context, selectedImageUri) } } ) @@ -228,17 +231,21 @@ fun DefaultUserAvatar(selectedImageUri: Uri?) { AsyncImage( model = selectedImageUri, contentDescription = stringResource(id = R.string.user_avatar), + contentScale = ContentScale.Crop, modifier = Modifier .size(84.dp) .padding(top = 8.dp) + .clip(CircleShape) ) } else { AsyncImage( model = R.drawable.ic_circular_group, contentDescription = stringResource(id = R.string.user_avatar), + contentScale = ContentScale.Crop, modifier = Modifier .size(84.dp) .padding(top = 8.dp) + .clip(CircleShape) ) } } @@ -571,7 +578,11 @@ fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: Con } @Composable -fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context,selectedImageUri: Uri?) { +fun CreateConversation( + conversationCreationViewModel: ConversationCreationViewModel, + context: Context, + selectedImageUri: Uri? +) { val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState() Box( modifier = Modifier diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt index 13fc607f5..38586a6e3 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -109,7 +109,7 @@ class ConversationCreationViewModel @Inject constructor( repository.setPassword(token, _password.value) } repository.openConversation(token, scope) - if(selectedImageUri!= null){ + if (selectedImageUri != null) { repository.uploadConversationAvatar(selectedImageUri.toFile(), token) } onRoomCreated(token) @@ -166,4 +166,3 @@ sealed class AddParticipantsUiState { data class Success(val participants: List?) : AddParticipantsUiState() data class Error(val message: String) : AddParticipantsUiState() } -