Merge pull request #4826 from nextcloud/feature/4712/addParticipantTo1to1

Feature/4712/add participant to1to1
This commit is contained in:
Sowjanya Kota 2025-04-17 12:47:19 +02:00 committed by GitHub
commit a9090d4e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 443 additions and 618 deletions

View File

@ -186,12 +186,10 @@ class MainActivity : BaseActivity(), ActionBarProvider {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1))
val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser?.baseUrl!!,
roomType,
null,
userId,
null
version = apiVersion,
baseUrl = currentUser?.baseUrl!!,
roomType = roomType,
invite = userId
)
ncApi.createRoom(

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.ChatOverall
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
@ -57,6 +59,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

@ -45,6 +45,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
@ -1597,19 +1598,17 @@ class ChatActivity :
private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
if (conversationUser != null) {
runOnUiThread {
if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
Snackbar.make(
binding.root,
context.resources.getString(R.string.switch_to_main_room),
Snackbar.LENGTH_LONG
).show()
val toastInfo = if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
context.resources.getString(R.string.switch_to_main_room)
} else {
Snackbar.make(
binding.root,
context.resources.getString(R.string.switch_to_breakout_room),
Snackbar.LENGTH_LONG
).show()
context.resources.getString(R.string.switch_to_breakout_room)
}
// do not replace with snackbar, as it would disappear with the activity switch
Toast.makeText(
context,
toastInfo,
Toast.LENGTH_LONG
).show()
}
val bundle = Bundle()
@ -3167,12 +3166,10 @@ class ChatActivity :
val apiVersion =
ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
conversationUser?.baseUrl!!,
"1",
null,
message?.user?.id?.substring(INVITE_LENGTH),
null
version = apiVersion,
baseUrl = conversationUser?.baseUrl!!,
roomType = "1",
invite = message?.user?.id?.substring(INVITE_LENGTH)
)
chatViewModel.createRoom(
credentials!!,
@ -3600,12 +3597,10 @@ class ChatActivity :
}
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
conversationUser?.baseUrl!!,
"1",
null,
userMentionClickEvent.userId,
null
version = apiVersion,
baseUrl = conversationUser?.baseUrl!!,
roomType = "1",
invite = userMentionClickEvent.userId
)
chatViewModel.createRoom(
@ -3712,12 +3707,11 @@ class ChatActivity :
val apiVersion =
ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
conversationUser?.baseUrl!!,
ROOM_TYPE_ONE_TO_ONE,
ACTOR_TYPE,
userId,
null
version = apiVersion,
baseUrl = conversationUser?.baseUrl!!,
roomType = ROOM_TYPE_ONE_TO_ONE,
source = ACTOR_TYPE,
invite = userId
)
chatViewModel.createRoom(
credentials!!,

View File

@ -71,12 +71,12 @@ class ContactsRepositoryImpl @Inject constructor(
}
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
_currentUser.baseUrl,
roomType,
sourceType,
userId,
conversationName
version = apiVersion,
baseUrl = _currentUser.baseUrl,
roomType = roomType,
source = sourceType,
invite = userId,
conversationName = conversationName
)
val response = ncApiCoroutines.createRoom(
credentials,

View File

@ -1,307 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversation
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
import com.nextcloud.talk.databinding.DialogCreateConversationBinding
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.vanniktech.emoji.EmojiPopup
import org.greenrobot.eventbus.EventBus
import org.parceler.Parcels
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class CreateConversationDialogFragment : DialogFragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var eventBus: EventBus
@Inject
lateinit var currentUserProvider: CurrentUserProviderNew
private lateinit var binding: DialogCreateConversationBinding
private lateinit var viewModel: ConversationViewModel
private var emojiPopup: EmojiPopup? = null
private var conversationType: ConversationEnums.ConversationType? = null
private var usersToInvite: ArrayList<String> = ArrayList()
private var groupsToInvite: ArrayList<String> = ArrayList()
private var emailsToInvite: ArrayList<String> = ArrayList()
private var circlesToInvite: ArrayList<String> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
viewModel = ViewModelProvider(this, viewModelFactory)[ConversationViewModel::class.java]
if (arguments?.containsKey(USERS_TO_INVITE) == true) {
usersToInvite = arguments?.getStringArrayList(USERS_TO_INVITE)!!
}
if (arguments?.containsKey(GROUPS_TO_INVITE) == true) {
groupsToInvite = arguments?.getStringArrayList(GROUPS_TO_INVITE)!!
}
if (arguments?.containsKey(EMAILS_TO_INVITE) == true) {
emailsToInvite = arguments?.getStringArrayList(EMAILS_TO_INVITE)!!
}
if (arguments?.containsKey(CIRCLES_TO_INVITE) == true) {
circlesToInvite = arguments?.getStringArrayList(CIRCLES_TO_INVITE)!!
}
if (arguments?.containsKey(KEY_CONVERSATION_TYPE) == true) {
conversationType = Parcels.unwrap(arguments?.getParcelable(KEY_CONVERSATION_TYPE))
}
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogCreateConversationBinding.inflate(layoutInflater)
val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context)
.setTitle(resources.getString(R.string.create_conversation))
// listener is null for now to avoid closing after button was clicked.
// listener is set later in onStart
.setPositiveButton(R.string.nc_common_create, null)
.setNegativeButton(R.string.nc_common_dismiss, null)
.setView(binding.root)
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.root.context, dialogBuilder)
return dialogBuilder.create()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
setupStateObserver()
setupEmojiPopup()
}
override fun onStart() {
super.onStart()
val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.isEnabled = false
positiveButton.setOnClickListener {
viewModel.createConversation(
binding.textEdit.text.toString(),
conversationType
)
}
themeDialog()
}
private fun themeDialog() {
viewThemeUtils.platform.themeDialog(binding.root)
viewThemeUtils.platform.colorTextButtons((dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE))
viewThemeUtils.platform.colorTextButtons((dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE))
viewThemeUtils.material.colorTextInputLayout(binding.textInputLayout)
}
private fun setupEmojiPopup() {
emojiPopup = binding.let {
EmojiPopup(
rootView = requireView(),
editText = it.textEdit,
onEmojiPopupShownListener = {
viewThemeUtils.platform.colorImageView(it.smileyButton, ColorRole.PRIMARY)
},
onEmojiPopupDismissListener = {
it.smileyButton.imageTintList = ColorStateList.valueOf(
ResourcesCompat.getColor(
resources,
R.color.medium_emphasis_text,
context?.theme
)
)
},
onEmojiClickListener = {
binding.textEdit.editableText?.append(" ")
}
)
}
}
private fun setupListeners() {
binding.smileyButton.setOnClickListener { emojiPopup?.toggle() }
binding.textEdit.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// unused atm
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// unused atm
}
override fun afterTextChanged(s: Editable) {
val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
if (!TextUtils.isEmpty(s)) {
if (!positiveButton.isEnabled) {
positiveButton.isEnabled = true
}
} else {
if (positiveButton.isEnabled) {
positiveButton.isEnabled = false
}
}
}
})
}
private fun setupStateObserver() {
viewModel.viewState.observe(viewLifecycleOwner) { state ->
when (state) {
is ConversationViewModel.InitialState -> {}
is ConversationViewModel.CreatingState -> {}
is ConversationViewModel.CreatingSuccessState -> addParticipants(state.roomToken)
is ConversationViewModel.CreatingFailedState -> {
Log.e(TAG, "Failed to create conversation")
showError()
}
else -> {}
}
}
}
private fun addParticipants(roomToken: String) {
val data = Data.Builder()
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!)
data.putString(BundleKeys.KEY_TOKEN, roomToken)
data.putStringArray(BundleKeys.KEY_SELECTED_USERS, usersToInvite.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupsToInvite.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailsToInvite.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circlesToInvite.toTypedArray())
val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
AddParticipantsToConversation::class.java
)
.setInputData(data.build())
.build()
WorkManager.getInstance(requireContext()).enqueue(addParticipantsToConversationWorker)
WorkManager.getInstance(requireContext()).getWorkInfoByIdLiveData(addParticipantsToConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.RUNNING -> {
Log.d(TAG, "running AddParticipantsToConversation")
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "success AddParticipantsToConversation")
initiateConversation(roomToken)
}
WorkInfo.State.FAILED -> {
Log.e(TAG, "failed to AddParticipantsToConversation")
showError()
}
else -> {
}
}
}
}
}
private fun initiateConversation(roomToken: String) {
activity?.let {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
val chatIntent = Intent(it, ChatActivity::class.java)
chatIntent.putExtras(bundle)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
dismiss()
}
private fun showError() {
dismiss()
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
/**
* Fragment creator
*/
companion object {
private val TAG = CreateConversationDialogFragment::class.java.simpleName
private const val USERS_TO_INVITE = "usersToInvite"
private const val GROUPS_TO_INVITE = "groupsToInvite"
private const val EMAILS_TO_INVITE = "emailsToInvite"
private const val CIRCLES_TO_INVITE = "circlesToInvite"
private const val KEY_CONVERSATION_TYPE = "keyConversationType"
@JvmStatic
fun newInstance(
usersToInvite: ArrayList<String>?,
groupsToInvite: ArrayList<String>?,
emailsToInvite: ArrayList<String>?,
circlesToInvite: ArrayList<String>?,
conversationType: Parcelable
): CreateConversationDialogFragment {
val args = Bundle()
args.putStringArrayList(USERS_TO_INVITE, usersToInvite)
args.putStringArrayList(GROUPS_TO_INVITE, groupsToInvite)
args.putStringArrayList(EMAILS_TO_INVITE, emailsToInvite)
args.putStringArrayList(CIRCLES_TO_INVITE, circlesToInvite)
args.putParcelable(KEY_CONVERSATION_TYPE, conversationType)
val fragment = CreateConversationDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,19 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import io.reactivex.Observable
interface ConversationRepository {
fun createConversation(
roomName: String,
conversationType: ConversationEnums.ConversationType?
): Observable<RoomOverall>
}

View File

@ -1,62 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) :
ConversationRepository {
val currentUser: User = currentUserProvider.currentUser.blockingGet()
val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
override fun createConversation(
roomName: String,
conversationType: ConversationEnums.ConversationType?
): Observable<RoomOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val retrofitBucket: RetrofitBucket =
if (conversationType == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_PUBLIC,
null,
null,
roomName
)
} else {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser.baseUrl!!,
ROOM_TYPE_GROUP,
null,
null,
roomName
)
}
return ncApi.createRoom(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
}
companion object {
private const val ROOM_TYPE_PUBLIC = "3"
private const val ROOM_TYPE_GROUP = "2"
}
}

View File

@ -1,78 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.conversation.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.nextcloud.talk.conversation.repository.ConversationRepository
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.conversations.RoomOverall
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class ConversationViewModel @Inject constructor(private val repository: ConversationRepository) : ViewModel() {
sealed class ViewState
object InitialState : ViewState()
object CreatingState : ViewState()
class CreatingSuccessState(val roomToken: String) : ViewState()
object CreatingFailedState : ViewState()
private val _viewState: MutableLiveData<ViewState> = MutableLiveData(
InitialState
)
val viewState: LiveData<ViewState>
get() = _viewState
private var disposable: Disposable? = null
override fun onCleared() {
super.onCleared()
disposable?.dispose()
}
fun createConversation(roomName: String, conversationType: ConversationEnums.ConversationType?) {
_viewState.value = CreatingState
repository.createConversation(
roomName,
conversationType
)
.doOnSubscribe { disposable = it }
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(CreateConversationObserver())
}
inner class CreateConversationObserver : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
val conversation = roomOverall.ocs!!.data
_viewState.value = CreatingSuccessState(conversation?.token!!)
}
override fun onError(e: Throwable) {
// dispose()
}
override fun onComplete() {
// dispose()
}
}
companion object {
private val TAG = ConversationViewModel::class.java.simpleName
}
}

View File

@ -97,12 +97,10 @@ class ConversationCreationRepositoryImpl @Inject constructor(
override suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall {
val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
_currentUser.baseUrl,
roomType,
null,
null,
conversationName
version = apiVersion,
baseUrl = _currentUser.baseUrl,
roomType = roomType,
conversationName = conversationName
)
val response = ncApiCoroutines.createRoom(
credentials,

View File

@ -142,23 +142,6 @@ class ConversationCreationViewModel @Inject constructor(
fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return repository.getImageUri(avatarId, requestBigSize)
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun createRoom(roomType: String, conversationName: String?) {
viewModelScope.launch {
try {
val room = repository.createRoom(
roomType,
conversationName
)
val conversation: Conversation? = room.ocs?.data
roomViewState.value = RoomUIState.Success(conversation)
} catch (exception: Exception) {
roomViewState.value = RoomUIState.Error(exception.message ?: "")
}
}
}
}
sealed class AllowGuestsUiState {

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
@ -66,7 +65,7 @@ import com.nextcloud.talk.extensions.loadConversationAvatar
import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
import com.nextcloud.talk.extensions.loadSystemAvatar
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.jobs.AddParticipantsToConversationWorker
import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.domain.ConversationModel
@ -93,6 +92,7 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
@ -142,6 +142,8 @@ class ConversationInfoActivity :
private var adapter: FlexibleAdapter<ParticipantItem>? = null
private var userItems: MutableList<ParticipantItem> = ArrayList()
private var startGroupChat: Boolean = false
private lateinit var optionsMenu: Menu
private val workerData: Data?
@ -157,13 +159,22 @@ class ConversationInfoActivity :
private val addParticipantsResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
) { it ->
executeIfResultOk(it) { intent ->
val selectedParticipants =
val selectedAutocompleteUsers =
intent?.getParcelableArrayListExtraProvider<AutocompleteUser>("selectedParticipants")
?: emptyList()
val participants = selectedParticipants.toMutableList()
addParticipantsToConversation(participants)
if (startGroupChat) {
viewModel.createRoomFromOneToOne(
conversationUser,
userItems.map { it.model },
selectedAutocompleteUsers,
conversationToken
)
} else {
addParticipantsToConversation(selectedAutocompleteUsers)
}
}
}
@ -206,7 +217,14 @@ class ConversationInfoActivity :
binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog() }
binding.leaveConversationAction.setOnClickListener { leaveConversation() }
binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() }
binding.addParticipantsAction.setOnClickListener { selectParticipantsToAdd() }
binding.addParticipantsAction.setOnClickListener {
startGroupChat = false
selectParticipantsToAdd()
}
binding.startGroupChat.setOnClickListener {
startGroupChat = true
selectParticipantsToAdd()
}
binding.listBansButton.setOnClickListener { listBans() }
viewModel.getRoom(conversationUser, conversationToken)
@ -222,47 +240,14 @@ class ConversationInfoActivity :
private fun initObservers() {
initViewStateObserver()
viewModel.getCapabilitiesViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.GetCapabilitiesSuccessState -> {
spreedCapabilities = state.spreedCapabilities
handleConversation()
}
else -> {}
}
}
viewModel.getBanActorState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.BanActorSuccessState -> {
getListOfParticipants() // Refresh the list of participants
}
ConversationInfoViewModel.BanActorErrorState -> {
Snackbar.make(binding.root, "Error banning actor", Snackbar.LENGTH_SHORT).show()
}
else -> {}
}
}
viewModel.getConversationReadOnlyState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> {
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> {
Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show()
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> {
}
}
initCapabilitiesObersver()
initRoomOberserver()
initBanActorObserver()
initConversationReadOnlyObserver()
initClearChatHistoryObserver()
}
private fun initClearChatHistoryObserver() {
viewModel.clearChatHistoryViewState.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.ClearChatHistoryViewState.None -> {
@ -284,6 +269,73 @@ class ConversationInfoActivity :
}
}
private fun initConversationReadOnlyObserver() {
viewModel.getConversationReadOnlyState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> {
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> {
Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show()
}
is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> {
}
}
}
}
private fun initBanActorObserver() {
viewModel.getBanActorState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.BanActorSuccessState -> {
getListOfParticipants() // Refresh the list of participants
}
ConversationInfoViewModel.BanActorErrorState -> {
Snackbar.make(binding.root, "Error banning actor", Snackbar.LENGTH_SHORT).show()
}
else -> {}
}
}
}
private fun initRoomOberserver() {
viewModel.createRoomViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.CreateRoomUIState.Success -> {
state.room.ocs?.data?.token?.let { token ->
val chatIntent = Intent(context, ChatActivity::class.java).apply {
putExtra(KEY_ROOM_TOKEN, token)
}
startActivity(chatIntent)
}
}
is ConversationInfoViewModel.CreateRoomUIState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
else -> {}
}
}
}
private fun initCapabilitiesObersver() {
viewModel.getCapabilitiesViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.GetCapabilitiesSuccessState -> {
spreedCapabilities = state.spreedCapabilities
handleConversation()
}
else -> {}
}
}
}
private fun initViewStateObserver() {
viewModel.viewState.observe(this) { state ->
when (state) {
@ -673,7 +725,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")
@ -706,17 +758,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!!)
}
}
@ -729,7 +781,7 @@ class ConversationInfoActivity :
data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailIdsArray.toTypedArray())
data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray.toTypedArray())
val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
AddParticipantsToConversation::class.java
AddParticipantsToConversationWorker::class.java
).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
@ -865,7 +917,12 @@ class ConversationInfoActivity :
binding.sharedItems.visibility = GONE
}
if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CONVERSATION_CREATION_ALL)
) {
binding.addParticipantsAction.visibility = GONE
binding.startGroupChat.visibility = VISIBLE
} else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE
if (hasSpreedFeatureCapability(
spreedCapabilities,

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,
0,
0,
0,
"",
"",
"",
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,23 @@ 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
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 com.nextcloud.talk.utils.DisplayUtils
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -112,6 +123,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,6 +135,69 @@ class ConversationInfoViewModel @Inject constructor(
?.subscribe(GetRoomObserver())
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun createRoomFromOneToOne(
user: User,
userItems: List<Participant>,
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(
userItems.map { it.displayName },
autocompleteUsers.map { it.label }
),
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 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) {
_getCapabilitiesViewState.value = GetCapabilitiesStartState
@ -280,6 +358,26 @@ 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
fun createConversationNameByParticipants(
originalParticipants: List<String?>,
allParticipants: List<String?>
): String {
fun List<String?>.sortedJoined() =
sortedBy { it?.lowercase() }
.joinToString(NEW_CONVERSATION_PARTICIPANTS_SEPARATOR)
val addedParticipants = allParticipants - originalParticipants.toSet()
val conversationName = originalParticipants.mapNotNull { it }.sortedJoined() +
NEW_CONVERSATION_PARTICIPANTS_SEPARATOR +
addedParticipants.mapNotNull { it }.sortedJoined()
return DisplayUtils.ellipsize(conversationName, MAX_ROOM_NAME_LENGTH)
}
}
sealed class ClearChatHistoryViewState {
@ -300,6 +398,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

@ -17,8 +17,6 @@ import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.contacts.ContactsRepositoryImpl
import com.nextcloud.talk.conversation.repository.ConversationRepository
import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl
import com.nextcloud.talk.conversationcreation.ConversationCreationRepository
import com.nextcloud.talk.conversationcreation.ConversationCreationRepositoryImpl
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository
@ -142,10 +140,6 @@ class RepositoryModule {
return ConversationInfoEditRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
}
@Provides
fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository =
ConversationRepositoryImpl(ncApi, userProvider)
@Provides
fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi)

View File

@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
@ -135,11 +134,6 @@ abstract class ViewModelModule {
@ViewModelKey(ConversationInfoEditViewModel::class)
abstract fun conversationInfoEditViewModel(viewModel: ConversationInfoEditViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ConversationViewModel::class)
abstract fun conversationViewModel(viewModel: ConversationViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(InvitationsViewModel::class)

View File

@ -28,7 +28,7 @@ import autodagger.AutoInjector;
import io.reactivex.schedulers.Schedulers;
@AutoInjector(NextcloudTalkApplication.class)
public class AddParticipantsToConversation extends Worker {
public class AddParticipantsToConversationWorker extends Worker {
@Inject
NcApi ncApi;
@ -38,7 +38,7 @@ public class AddParticipantsToConversation extends Worker {
@Inject
EventBus eventBus;
public AddParticipantsToConversation(@NonNull Context context, @NonNull WorkerParameters workerParams) {
public AddParticipantsToConversationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
}

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,

View File

@ -118,12 +118,10 @@ class ProfileBottomSheet(val ncApi: NcApi, val userModel: User, val viewThemeUti
val apiVersion =
ApiUtils.getConversationApiVersion(userModel, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
userModel.baseUrl!!,
"1",
null,
userId,
null
version = apiVersion,
baseUrl = userModel.baseUrl!!,
roomType = "1",
invite = userId
)
val credentials = ApiUtils.getCredentials(userModel.username, userModel.token)
ncApi.createRoom(

View File

@ -298,25 +298,19 @@ object ApiUtils {
@Suppress("LongParameterList")
fun getRetrofitBucketForCreateRoom(
version: Int,
baseUrl: String?,
roomType: String,
source: String?,
invite: String?,
conversationName: String?
baseUrl: String? = null,
source: String? = null,
invite: String? = null,
conversationName: String? = null
): RetrofitBucket {
val retrofitBucket = RetrofitBucket()
retrofitBucket.url = getUrlForRooms(version, baseUrl)
val queryMap: MutableMap<String, String> = HashMap()
queryMap["roomType"] = roomType
if (invite != null) {
queryMap["invite"] = invite
}
if (source != null) {
queryMap["source"] = source
}
if (conversationName != null) {
queryMap["roomName"] = conversationName
}
invite?.let { queryMap["invite"] = it }
source?.let { queryMap["source"] = it }
conversationName?.let { queryMap["roomName"] = it }
retrofitBucket.queryMap = queryMap
return retrofitBucket
}

View File

@ -56,7 +56,8 @@ enum class SpreedFeatures(val value: String) {
DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"),
BAN_V1("ban-v1"),
EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"),
ARCHIVE_CONVERSATIONS("archived-conversations-v2")
ARCHIVE_CONVERSATIONS("archived-conversations-v2"),
CONVERSATION_CREATION_ALL("conversation-creation-all")
}
@Suppress("TooManyFunctions")

View File

@ -392,6 +392,34 @@
</LinearLayout>
<LinearLayout
android:id="@+id/startGroupChat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_people_group_black_24px"
app:tint="@color/grey_600" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_start_group_chat"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"

View File

@ -0,0 +1,25 @@
/*
* 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.viewmodel
import org.junit.Test
import org.junit.Assert.assertEquals
class ConversationInfoViewModelTest {
@Test
fun `createConversationNameByParticipants should combine names correctly`() {
val original = listOf("Dave", null, "Charlie")
val all = listOf("Bob", "Charlie", "Dave", "Alice", null, "Simon")
val expectedName = "Charlie, Dave, Alice, Bob, Simon"
val result = ConversationInfoViewModel.createConversationNameByParticipants(original, all)
assertEquals(expectedName, result)
}
}