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 af291d4cf..21e9d92ad 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -2,11 +2,13 @@ * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe * 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? ): RoomOverall + @POST + suspend fun createRoomWithBody( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Body roomRequest: CreateRoomRequest + ): RoomOverall + /* QueryMap items are as follows: - "roomName" : "newName" diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index a964cc913..d3e46080a 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -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("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) { + private fun addParticipantsToConversation(autocompleteUsers: List) { val groupIdsArray: MutableSet = HashSet() val emailIdsArray: MutableSet = HashSet() val circleIdsArray: MutableSet = HashSet() val userIdsArray: MutableSet = 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!!) } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt new file mode 100644 index 000000000..0729c53f9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * 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() + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt new file mode 100644 index 000000000..1f1e8c834 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * 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 = arrayListOf(), + @JsonField(name = ["federated_users"]) + var federatedUsers: MutableList = arrayListOf(), + @JsonField(name = ["groups"]) + var groups: MutableList = arrayListOf(), + @JsonField(name = ["emails"]) + var emails: MutableList = arrayListOf(), + @JsonField(name = ["phones"]) + var phones: MutableList = arrayListOf(), + @JsonField(name = ["teams"]) + var teams: MutableList = arrayListOf() +) diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index b6e8aab07..d13e45d58 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -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 get() = _getConversationReadOnlyState + private val _createRoomViewState = MutableLiveData(CreateRoomUIState.None) + val createRoomViewState: LiveData + 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, 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): 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): 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() diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt index df4350504..c68b581d6 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt @@ -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 } diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt index 5732b07cd..92886680e 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt @@ -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,