WIP use new api endpoint to create group conversation

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-03-26 16:06:10 +01:00
parent 77fe2ad024
commit fa5570e901
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
7 changed files with 243 additions and 12 deletions

View File

@ -2,11 +2,13 @@
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.api
import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.conversations.RoomOverall
@ -56,6 +58,13 @@ interface NcApiCoroutines {
@QueryMap options: Map<String, String>?
): RoomOverall
@POST
suspend fun createRoomWithBody(
@Header("Authorization") authorization: String?,
@Url url: String?,
@Body roomRequest: CreateRoomRequest
): RoomOverall
/*
QueryMap items are as follows:
- "roomName" : "newName"

View File

@ -11,7 +11,6 @@
package com.nextcloud.talk.conversationinfo
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
@ -161,16 +160,18 @@ class ConversationInfoActivity :
ActivityResultContracts.StartActivityForResult()
) {
executeIfResultOk(it) { intent ->
val selectedParticipants =
val selectedAutocompleteUsers =
intent?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList()
val participants = selectedParticipants.toMutableList()
if (startGroupChat) {
Snackbar.make(binding.root, "TODO: start group chat...", Snackbar.LENGTH_LONG).show()
viewModel.createRoom()
viewModel.createRoomFromOneToOne(
conversationUser,
selectedAutocompleteUsers,
conversationToken
)
} else {
addParticipantsToConversation(participants)
addParticipantsToConversation(selectedAutocompleteUsers)
}
}
}
@ -250,6 +251,21 @@ class ConversationInfoActivity :
}
}
viewModel.createRoomViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.CreateRoomUIState.Success -> {
// for now noting is done here.
// the breakout room signaling message should be triggered and conversation should be switched.
}
is ConversationInfoViewModel.CreateRoomUIState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
else -> {}
}
}
viewModel.getBanActorState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.BanActorSuccessState -> {
@ -688,7 +704,7 @@ class ConversationInfoActivity :
}
private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
onResult(result.data)
} else {
Log.e(ChatActivity.TAG, "resultCode for received intent was != ok")
@ -721,17 +737,17 @@ class ConversationInfoActivity :
addParticipantsResult.launch(intent)
}
private fun addParticipantsToConversation(participants: List<AutocompleteUser>) {
private fun addParticipantsToConversation(autocompleteUsers: List<AutocompleteUser>) {
val groupIdsArray: MutableSet<String> = HashSet()
val emailIdsArray: MutableSet<String> = HashSet()
val circleIdsArray: MutableSet<String> = HashSet()
val userIdsArray: MutableSet<String> = HashSet()
participants.forEach { participant ->
autocompleteUsers.forEach { participant ->
when (participant.source) {
Participant.ActorType.GROUPS.name.lowercase() -> groupIdsArray.add(participant.id!!)
GROUPS.name.lowercase() -> groupIdsArray.add(participant.id!!)
Participant.ActorType.EMAILS.name.lowercase() -> emailIdsArray.add(participant.id!!)
Participant.ActorType.CIRCLES.name.lowercase() -> circleIdsArray.add(participant.id!!)
CIRCLES.name.lowercase() -> circleIdsArray.add(participant.id!!)
else -> userIdsArray.add(participant.id!!)
}
}

View File

@ -0,0 +1,72 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversationinfo
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
@JsonObject
data class CreateRoomRequest(
@JsonField(name = ["roomType"])
var roomType: String,
@JsonField(name = ["roomName"])
var roomName: String? = null,
@JsonField(name = ["objectType"])
var objectType: String? = null,
@JsonField(name = ["objectId"])
var objectId: String? = null,
@JsonField(name = ["password"])
var password: String? = null,
@JsonField(name = ["readOnly"])
var readOnly: Int,
@JsonField(name = ["listable"])
var listable: Int,
@JsonField(name = ["messageExpiration"])
var messageExpiration: Int? = null,
@JsonField(name = ["lobbyState"])
var lobbyState: Int? = null,
@JsonField(name = ["lobbyTimer"])
var lobbyTimer: Int,
@JsonField(name = ["sipEnabled"])
var sipEnabled: Int,
@JsonField(name = ["permissions"])
var permissions: Int,
@JsonField(name = ["recordingConsent"])
var recordingConsent: Int,
@JsonField(name = ["mentionPermissions"])
var mentionPermissions: Int,
@JsonField(name = ["description"])
var description: String? = null,
@JsonField(name = ["emoji"])
var emoji: String? = null,
@JsonField(name = ["avatarColor"])
var avatarColor: String? = null,
@JsonField(name = ["participants"])
var participants: Participants? = null
) {
constructor() : this(
0.toString(),
"",
"",
"",
"",
0,
0,
0,
0,
0,
0,
255,
0,
0,
"",
"string",
"string",
Participants()
)
}

View File

@ -0,0 +1,27 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversationinfo
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
@JsonObject
data class Participants(
@JsonField(name = ["users"])
var users: MutableList<String> = arrayListOf(),
@JsonField(name = ["federated_users"])
var federatedUsers: MutableList<String> = arrayListOf(),
@JsonField(name = ["groups"])
var groups: MutableList<String> = arrayListOf(),
@JsonField(name = ["emails"])
var emails: MutableList<String> = arrayListOf(),
@JsonField(name = ["phones"])
var phones: MutableList<String> = arrayListOf(),
@JsonField(name = ["teams"])
var teams: MutableList<String> = arrayListOf()
)

View File

@ -15,12 +15,21 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.conversationinfo.Participants
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.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES
import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS
import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED
import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -112,6 +121,10 @@ class ConversationInfoViewModel @Inject constructor(
val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState>
get() = _getConversationReadOnlyState
private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None)
val createRoomViewState: LiveData<CreateRoomUIState>
get() = _createRoomViewState
fun getRoom(user: User, token: String) {
_viewState.value = GetRoomStartState
chatNetworkDataSource.getRoom(user, token)
@ -120,7 +133,76 @@ class ConversationInfoViewModel @Inject constructor(
?.subscribe(GetRoomObserver())
}
fun createRoom() {
@Suppress("Detekt.TooGenericExceptionCaught")
fun createRoomFromOneToOne(user: User, autocompleteUsers: List<AutocompleteUser>, roomToken: String) {
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1))
val url = getUrlForRooms(apiVersion, user.baseUrl!!)
val credentials = ApiUtils.getCredentials(user.username, user.token)!!
val participantsBody = convertAutocompleteUserToParticipant(autocompleteUsers)
val body = CreateRoomRequest(
roomName = createConversationNameByParticipants(autocompleteUsers),
roomType = GROUP_CONVERSATION_TYPE,
readOnly = 0,
listable = 1,
lobbyTimer = 0,
sipEnabled = 0,
permissions = 0,
recordingConsent = 0,
mentionPermissions = 0,
participants = participantsBody,
objectType = EXTENDED_CONVERSATION,
objectId = roomToken
)
viewModelScope.launch {
try {
val roomOverall = conversationsRepository.createRoom(
credentials,
url,
body
)
_createRoomViewState.value = CreateRoomUIState.Success(roomOverall)
} catch (e: Exception) {
Log.e(TAG, "Failed to create room", e)
_createRoomViewState.value = CreateRoomUIState.Error(e)
}
}
}
private fun createConversationNameByParticipants(autocompleteUsers: List<AutocompleteUser>): String {
fun cutOffString(input: String, maxLength: Int): String {
return if (input.length > maxLength) {
input.take(maxLength)
} else {
input
}
}
val conversationName = autocompleteUsers
.sortedBy { it.label?.lowercase() }
.mapNotNull { it.label }
.joinToString(NEW_CONVERSATION_PARTICIPANTS_SEPARATOR)
return cutOffString(conversationName, MAX_ROOM_NAME_LENGTH)
}
private fun convertAutocompleteUserToParticipant(autocompleteUsers: List<AutocompleteUser>): Participants {
val participants = Participants()
autocompleteUsers.forEach { autocompleteUser ->
when (autocompleteUser.source) {
GROUPS.name.lowercase() -> participants.groups.add(autocompleteUser.id!!)
EMAILS.name.lowercase() -> participants.emails.add(autocompleteUser.id!!)
CIRCLES.name.lowercase() -> participants.teams.add(autocompleteUser.id!!)
FEDERATED.name.lowercase() -> participants.federatedUsers.add(autocompleteUser.id!!)
"phones".lowercase() -> participants.phones.add(autocompleteUser.id!!)
else -> participants.users.add(autocompleteUser.id!!)
}
}
return participants
}
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
@ -283,6 +365,10 @@ class ConversationInfoViewModel @Inject constructor(
companion object {
private val TAG = ConversationInfoViewModel::class.simpleName
private const val NEW_CONVERSATION_PARTICIPANTS_SEPARATOR = ", "
private const val EXTENDED_CONVERSATION = "extended_conversation"
private const val GROUP_CONVERSATION_TYPE = "2"
private const val MAX_ROOM_NAME_LENGTH = 255
}
sealed class ClearChatHistoryViewState {
@ -303,6 +389,12 @@ class ConversationInfoViewModel @Inject constructor(
data class Error(val exception: Exception) : AllowGuestsUIState()
}
sealed class CreateRoomUIState {
data object None : CreateRoomUIState()
data class Success(val room: RoomOverall) : CreateRoomUIState()
data class Error(val exception: Exception) : CreateRoomUIState()
}
sealed class PasswordUiState {
data object None : PasswordUiState()
data object Success : PasswordUiState()

View File

@ -7,6 +7,8 @@
*/
package com.nextcloud.talk.repositories.conversations
import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import io.reactivex.Observable
@ -42,4 +44,6 @@ interface ConversationsRepository {
suspend fun setConversationReadOnly(roomToken: String, state: Int): GenericOverall
suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall
suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall
}

View File

@ -9,7 +9,9 @@ package com.nextcloud.talk.repositories.conversations
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
@ -105,6 +107,15 @@ class ConversationsRepositoryImpl(
)
}
override suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall {
val response = coroutineApi.createRoomWithBody(
credentials,
url,
body
)
return response
}
override suspend fun banActor(
credentials: String,
url: String,