Merge pull request #4196 from nextcloud/user_avatar_selection

User avatar selection
This commit is contained in:
Marcel Hibbe 2024-09-12 17:30:24 +02:00 committed by GitHub
commit 946aa60409
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 179 additions and 34 deletions

View File

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

View File

@ -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: MultipartBody.Part
): RoomOverall
@DELETE
suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
}

View File

@ -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
@ -32,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
@ -57,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
@ -77,6 +81,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 +89,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 +98,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 +133,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<Uri?>(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<AutocompleteUser>("selectedParticipants")
?: emptyList()
val selectedParticipants =
data?.getParcelableArrayListExtra<AutocompleteUser>("selectedParticipants")
?: emptyList()
val participants = selectedParticipants.toMutableList()
conversationCreationViewModel.updateSelectedParticipants(participants)
}
@ -162,43 +202,75 @@ 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)
CreateConversation(conversationCreationViewModel, context)
CreateConversation(conversationCreationViewModel, context, selectedImageUri)
}
}
)
}
@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),
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)
)
}
}
}
@Composable
fun UploadAvatar() {
fun UploadAvatar(
pickImage: PickImage,
onImageSelected: (Uri) -> Unit,
imagePickerLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
remoteFilePickerLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
cameraLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
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,24 +279,28 @@ fun UploadAvatar() {
}
IconButton(onClick = {
pickImage.selectLocal(imagePickerLauncher)
}) {
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),
painter = painterResource(id = R.drawable.upload),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(
onClick = {
pickImage.selectRemote(remoteFilePickerLauncher)
}
) {
Icon(
painter = painterResource(id = R.drawable.ic_mimetype_folder),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(onClick = {
onDeleteImage()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_delete_grey600_24dp),
@ -502,7 +578,11 @@ 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
@ -515,7 +595,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)
@ -530,6 +611,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo
}
}
}
class CompanionClass {
companion object {
internal val TAG = ConversationCreationActivity::class.simpleName

View File

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

View File

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

View File

@ -7,26 +7,34 @@
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.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
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<List<AutocompleteUser>>(emptyList())
val selectedParticipants: StateFlow<List<AutocompleteUser>> = _selectedParticipants
private val roomViewState = MutableStateFlow<RoomUIState>(RoomUIState.None)
private val _currentUser = userManager.currentUser.blockingGet()
val currentUser: User = _currentUser
fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
_selectedParticipants.value = participants
}
@ -58,6 +66,7 @@ class ConversationCreationViewModel @Inject constructor(
roomType: String,
conversationName: String,
participants: Set<AutocompleteUser>,
selectedImageUri: Uri?,
onRoomCreated: (String) -> Unit
) {
val scope = when {
@ -100,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 ?: "")