Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.chatpostattachment.ChatPostAttachmentOvera
import com.nextcloud.talk.models.json.chatpostattachment.PostConversationAttachmentRequest
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ChatProbeAttachmentFolderOverall
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ProbeConversationAttachmentRequest
import com.nextcloud.talk.models.json.conversations.ConversationPresetsOverall
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
Expand Down Expand Up @@ -92,6 +93,12 @@ interface NcApiCoroutines {
@FieldMap options: Map<String, String>?
): RoomOverall

@GET
suspend fun getConversationPresets(
@Header("Authorization") authorization: String?,
@Url url: String
): ConversationPresetsOverall

@POST
suspend fun createRoomWithBody(
@Header("Authorization") authorization: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -42,11 +41,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.Chat
import androidx.compose.material.icons.outlined.Podcasts
import androidx.compose.material.icons.outlined.VolumeUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
Expand All @@ -59,7 +53,6 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand Down Expand Up @@ -101,6 +94,7 @@ import com.nextcloud.talk.components.AvatarEditPanelState
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.conversationcreation.ui.ConversationPresets
import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
Expand Down Expand Up @@ -234,7 +228,7 @@ fun ConversationCreationScreen(
},
onDeleteAvatar = {
conversationCreationViewModel.updateSelectedImageUri(null)
conversationCreationViewModel.updateSelectedEmoji(null)
conversationCreationViewModel.updateSelectedEmojiAvatar(null)
}
)
}
Expand Down Expand Up @@ -378,144 +372,6 @@ fun ConversationNameAndDescription(conversationCreationViewModel: ConversationCr
)
}

@Suppress("LongMethod")
@SuppressLint("SuspiciousIndentation")
@Composable
fun ConversationPresets(conversationCreationViewModel: ConversationCreationViewModel) {
val preset by conversationCreationViewModel.conversationPreset

Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SelectableCard(
modifier = Modifier.weight(1f),
title = stringResource(R.string.default_room),
subtitle = stringResource(R.string.default_room_preset),
icon = Icons.Outlined.Chat,
isSelected = preset == ConversationPreset.DEFAULT,
onClick = { conversationCreationViewModel.updateConversationPreset(ConversationPreset.DEFAULT) }
)

if (conversationCreationViewModel.canCreateVoiceRoom) {
SelectableCard(
modifier = Modifier.weight(1f),
title = stringResource(R.string.voice_room),
subtitle = stringResource(R.string.voice_room_preset),
icon = Icons.Outlined.VolumeUp,
isSelected = preset == ConversationPreset.VOICE_ROOM,
onClick = { conversationCreationViewModel.updateConversationPreset(ConversationPreset.VOICE_ROOM) }
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}

if (conversationCreationViewModel.canCreateChannel) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SelectableCard(
modifier = Modifier.weight(1f),
title = stringResource(R.string.nc_channel),
subtitle = stringResource(R.string.nc_channel_description),
icon = Icons.Outlined.Podcasts,
isSelected = preset == ConversationPreset.CHANNEL,
onClick = { conversationCreationViewModel.updateConversationPreset(ConversationPreset.CHANNEL) }
)

if (conversationCreationViewModel.canCreateAnnouncement) {
SelectableCard(
modifier = Modifier.weight(1f),
title = stringResource(R.string.nc_announcement),
subtitle = stringResource(R.string.nc_announcement_description),
icon = Icons.Outlined.Campaign,
isSelected = preset == ConversationPreset.ANNOUNCEMENT,
onClick = {
conversationCreationViewModel.updateConversationPreset(
ConversationPreset.ANNOUNCEMENT
)
}
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}

@Suppress("LongParameterList")
@Composable
fun SelectableCard(
modifier: Modifier = Modifier,
title: String,
subtitle: String,
icon: ImageVector,
isSelected: Boolean,
onClick: () -> Unit,
badgeText: String? = null
) {
val borderColor = if (isSelected) Color.LightGray else Color.Transparent
val borderWidth = 1.dp

Column(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.clickable { onClick() }
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(8.dp)
)
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Text(
text = title,
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
modifier = Modifier.weight(1f, fill = false)
)
}

if (badgeText != null) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(4.dp)
) {
Text(
text = badgeText,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}

Spacer(modifier = Modifier.height(12.dp))

Text(
text = subtitle,
fontSize = 13.sp,
lineHeight = 18.sp
)
}
}

@Suppress("LongMethod")
@SuppressLint("SuspiciousIndentation")
@Composable
Expand Down Expand Up @@ -616,6 +472,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
val isOpenForGuestAppUsers = conversationCreationViewModel.isOpenForGuestAppUsers

val isPasswordSet = conversationCreationViewModel.password.collectAsState().value.isNotEmpty()
val isListablePinned = ConversationParameter.LISTABLE in conversationCreationViewModel.pinnedParameters

Text(
text = stringResource(id = R.string.nc_new_conversation_visibility),
Expand Down Expand Up @@ -657,6 +514,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
switch = {
Switch(
checked = isConversationAvailableForRegisteredUsers,
enabled = !isListablePinned,
onCheckedChange = { conversationCreationViewModel.openConversationToRegisteredUsers(it) }
)
},
Expand All @@ -669,6 +527,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
switch = {
Switch(
checked = isOpenForGuestAppUsers,
enabled = !isListablePinned,
onCheckedChange = { conversationCreationViewModel.openConversationToGuestAppUsers(it) }
)
},
Expand Down Expand Up @@ -859,7 +718,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo
contentAlignment = Alignment.Center
) {
Button(
enabled = !isCreatingRoom,
enabled = !isCreatingRoom && !conversationCreationViewModel.isLoadingPresets,
onClick = {
conversationCreationViewModel.createRoomAndAddParticipants { roomToken ->
val bundle = Bundle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ data class NewConversation(
*/
class ConversationCreator @Inject constructor(private val repository: ConversationCreationRepository) {

@Suppress("Detekt.TooGenericExceptionCaught")
suspend fun create(user: User, newConversation: NewConversation): Conversation? {
val capabilities = user.capabilities?.spreedCapability
val credentials = ApiUtils.getCredentials(user.username, user.token)
Expand All @@ -67,7 +68,13 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
createWithFollowUpRequests(context, newConversation)
}

conversation?.token?.let { saveAvatar(context, newConversation, it) }
conversation?.token?.let { token ->
try {
saveAvatar(context, newConversation, token)
} catch (e: Exception) {
Log.e(TAG, "Failed to set the avatar of the created conversation", e)
}
}
return conversation
}

Expand All @@ -81,7 +88,7 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
val body = CreateRoomRequest().apply {
roomType = params.roomType.toString()
roomName = newConversation.name
preset = newConversation.preset.takeIf { it != ConversationPreset.DEFAULT }
preset = newConversation.preset.takeIf { it != ConversationPresetId.DEFAULT }
description = newConversation.description
readOnly = params.readOnly
listable = params.listable
Expand Down Expand Up @@ -116,7 +123,7 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
roomType = params.roomType.toString(),
baseUrl = context.user.baseUrl,
conversationName = newConversation.name,
preset = newConversation.preset.takeIf { it != ConversationPreset.DEFAULT }
preset = newConversation.preset.takeIf { it != ConversationPresetId.DEFAULT }
)
val conversation = repository.createRoom(context.credentials, retrofitBucket).ocs?.data
val token = conversation?.token?.takeIf { it.isNotEmpty() } ?: return conversation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.nextcloud.talk.conversationcreation

import com.nextcloud.talk.models.json.conversations.ConversationPreset

/**
* A conversation type offered by the server, with the parameters it applies on creation.
*/
data class ConversationPresetModel(
val identifier: String,
val name: String,
val description: String,
val parameters: Map<String, Int>
) {
companion object {
fun mapToConversationPresetModel(preset: ConversationPreset): ConversationPresetModel? {
val identifier = preset.identifier ?: return null
return ConversationPresetModel(
identifier = identifier,
name = preset.name.orEmpty(),
description = preset.description.orEmpty(),
parameters = preset.parameters.orEmpty()
)
}
}
}

/**
* Identifiers of the conversation types the server offers.
*/
object ConversationPresetId {
const val DEFAULT = "default"
const val FORCED = "forced"
const val VOICE_ROOM = "voiceroom"
const val PRESENTATION = "presentation"
const val WEBINAR = "webinar"
const val CLASSIFIED = "classified"
const val CHANNEL = "channel"
const val ANNOUNCEMENT = "announcement"
}

/**
* The presets that are offered for selection. The forced preset is applied by the server and is
* never a conversation type of its own.
*/
fun List<ConversationPresetModel>.selectable(): List<ConversationPresetModel> =
filterNot { it.identifier == ConversationPresetId.FORCED }

/**
* The parameters of a single preset, empty when the server does not offer it.
*/
fun List<ConversationPresetModel>.parametersOf(identifier: String): Map<String, Int> =
firstOrNull { it.identifier == identifier }?.parameters.orEmpty()

/**
* The parameters a conversation of the given type is created with: the administrator configured
* defaults, then the values of the selected preset, then the parameters the user chose, and finally
* the ones an administrator pinned, which the server enforces on top of any request anyway.
*/
fun List<ConversationPresetModel>.parametersFor(
identifier: String,
chosenByUser: Map<String, Int> = emptyMap()
): CreateConversationParams =
CreateConversationParams()
.withParameters(parametersOf(ConversationPresetId.DEFAULT))
.withParameters(parametersOf(identifier))
.withParameters(chosenByUser)
.withParameters(parametersOf(ConversationPresetId.FORCED))
Loading
Loading