Remove EntryMenuController and OperationMenuController

The "operation view" at the bottom is replaced by snackbars.

I have removed the join via public link feature for now. This was buggy, complex and incomplete. This feature must be reimplemented in a more useful place (login screen, so you can use it as a guest without using an existing instance).

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2023-09-11 17:41:07 +02:00
parent da541d3fc3
commit 32dbe70399
No known key found for this signature in database
GPG key ID: C793F8B59F43CE7B
29 changed files with 1663 additions and 2063 deletions

View file

@ -41,6 +41,7 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuItemCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
@ -51,9 +52,10 @@ import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum
import com.nextcloud.talk.conversation.CreateConversationDialogFragment
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityContactsBinding
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.events.OpenConversationEvent
import com.nextcloud.talk.jobs.AddParticipantsToConversation
import com.nextcloud.talk.models.RetrofitBucket
@ -64,9 +66,9 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
import com.nextcloud.talk.ui.dialog.ContactsBottomDialog
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.UserIdUtils.getIdForUser
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -82,7 +84,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.parceler.Parcels
import java.io.IOException
import java.util.Collections
import java.util.Locale
import javax.inject.Inject
@ -120,7 +121,6 @@ class ContactsActivity :
private var existingParticipants: List<String>? = null
private var isAddingParticipantsView = false
private var conversationToken: String? = null
private var contactsBottomDialog: ContactsBottomDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -160,12 +160,8 @@ class ContactsActivity :
toggleConversationPrivacyLayout(!isPublicCall)
}
if (isAddingParticipantsView) {
binding.joinConversationViaLink.visibility = View.GONE
binding.callHeaderLayout.visibility = View.GONE
} else {
binding.joinConversationViaLink.setOnClickListener {
joinConversationViaLink()
}
binding.listOpenConversations.setOnClickListener {
listOpenConversations()
}
@ -228,7 +224,7 @@ class ContactsActivity :
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
super.onPrepareOptionsMenu(menu)
if (searchItem != null) {
binding?.titleTextView?.let {
binding.titleTextView.let {
viewThemeUtils.platform.colorToolbarMenuIcon(
it.context,
searchItem!!
@ -270,7 +266,10 @@ class ContactsActivity :
}
private fun selectionDone() {
if (!isAddingParticipantsView) {
if (isAddingParticipantsView) {
addParticipantsToConversation()
} else {
// if there is only 1 participant, directly add him while creating room (which can only add 'one')
if (!isPublicCall && selectedCircleIds.size + selectedGroupIds.size + selectedUserIds.size == 1) {
val userId: String
var sourceType: String? = null
@ -290,8 +289,9 @@ class ContactsActivity :
}
}
createRoom(roomType, sourceType, userId)
// if there are more participants to add, ask for roomName and add them one after another
} else {
val bundle = Bundle()
val roomType: Conversation.ConversationType = if (isPublicCall) {
Conversation.ConversationType.ROOM_PUBLIC_CALL
} else {
@ -301,16 +301,19 @@ class ContactsActivity :
val groupIdsArray = ArrayList(selectedGroupIds)
val emailsArray = ArrayList(selectedEmails)
val circleIdsArray = ArrayList(selectedCircleIds)
bundle.putParcelable(BundleKeys.KEY_CONVERSATION_TYPE, Parcels.wrap(roomType))
bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_EMAIL, emailsArray)
bundle.putStringArrayList(BundleKeys.KEY_INVITED_CIRCLE, circleIdsArray)
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_INVITE_USERS)
prepareAndShowBottomSheetWithBundle(bundle)
val createConversationDialog = CreateConversationDialogFragment.newInstance(
userIdsArray,
groupIdsArray,
emailsArray,
circleIdsArray,
Parcels.wrap(roomType)
)
createConversationDialog.show(
supportFragmentManager,
TAG
)
}
} else {
addParticipantsToConversation()
}
}
@ -373,7 +376,38 @@ class ContactsActivity :
AddParticipantsToConversation::class.java
).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
finish()
WorkManager.getInstance(context).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")
eventBus.post(
EventStatus(
getIdForUser(currentUser),
EventStatus.EventType.PARTICIPANTS_UPDATE,
true
)
)
finish()
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "failed AddParticipantsToConversation")
}
else -> {
}
}
}
}
}
private fun initSearchView() {
@ -401,14 +435,14 @@ class ContactsActivity :
private fun fetchData() {
dispose(null)
alreadyFetching = true
userHeaderItems = HashMap<String, GenericTextHeaderItem>()
val query = adapter!!.getFilter(String::class.java) as String?
userHeaderItems = HashMap()
val query = adapter!!.getFilter(String::class.java)
val retrofitBucket: RetrofitBucket =
ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query)
val modifiedQueryMap: HashMap<String, Any?> = HashMap<String, Any?>(retrofitBucket.queryMap)
modifiedQueryMap.put("limit", CONTACTS_BATCH_SIZE)
val modifiedQueryMap: HashMap<String, Any?> = HashMap(retrofitBucket.queryMap)
modifiedQueryMap["limit"] = CONTACTS_BATCH_SIZE
if (isAddingParticipantsView) {
modifiedQueryMap.put("itemId", conversationToken)
modifiedQueryMap["itemId"] = conversationToken
}
val shareTypesList: ArrayList<String> = ArrayList()
// users
@ -426,7 +460,7 @@ class ContactsActivity :
// circles
shareTypesList.add("7")
}
modifiedQueryMap.put("shareTypes[]", shareTypesList)
modifiedQueryMap["shareTypes[]"] = shareTypesList
ncApi.getContactsWithSearchParam(
credentials,
retrofitBucket.url,
@ -444,7 +478,7 @@ class ContactsActivity :
override fun onNext(responseBody: ResponseBody) {
val newUserItemList = processAutocompleteUserList(responseBody)
userHeaderItems = HashMap<String, GenericTextHeaderItem>()
userHeaderItems = HashMap()
contactItems!!.addAll(newUserItemList)
sortUserItems(newUserItemList)
@ -455,16 +489,16 @@ class ContactsActivity :
adapter?.filterItems()
}
binding?.controllerGenericRv?.swipeRefreshLayout?.isRefreshing = false
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
}
override fun onError(e: Throwable) {
binding?.controllerGenericRv?.swipeRefreshLayout?.isRefreshing = false
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
dispose(contactsQueryDisposable)
}
override fun onComplete() {
binding?.controllerGenericRv?.swipeRefreshLayout?.isRefreshing = false
binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
dispose(contactsQueryDisposable)
alreadyFetching = false
disengageProgressBar()
@ -474,18 +508,18 @@ class ContactsActivity :
private fun processAutocompleteUserList(responseBody: ResponseBody): MutableList<AbstractFlexibleItem<*>> {
try {
val autocompleteOverall: AutocompleteOverall = LoganSquare.parse<AutocompleteOverall>(
val autocompleteOverall: AutocompleteOverall = LoganSquare.parse(
responseBody.string(),
AutocompleteOverall::class.java
)
val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList<AutocompleteUser>()
val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList()
autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!)
return processAutocompleteUserList(autocompleteUsersList)
} catch (ioe: IOException) {
Log.e(TAG, "Parsing response body failed while getting contacts", ioe)
}
return ArrayList<AbstractFlexibleItem<*>>()
return ArrayList()
}
private fun processAutocompleteUserList(
@ -493,7 +527,7 @@ class ContactsActivity :
): MutableList<AbstractFlexibleItem<*>> {
var participant: Participant
val actorTypeConverter = EnumActorTypeConverter()
val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList<AbstractFlexibleItem<*>>()
val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList()
for (autocompleteUser in autocompleteUsersList) {
if (autocompleteUser.id != null &&
autocompleteUser.id != currentUser!!.userId &&
@ -529,7 +563,7 @@ class ContactsActivity :
resources!!.getString(R.string.nc_circles)
}
else -> {
participant.displayName!!.substring(0, 1).toUpperCase(Locale.getDefault())
participant.displayName!!.substring(0, 1).uppercase(Locale.getDefault())
}
}
}
@ -547,76 +581,72 @@ class ContactsActivity :
return participant
}
@Suppress("LongMethod")
private fun sortUserItems(newUserItemList: MutableList<AbstractFlexibleItem<*>>) {
Collections.sort(
newUserItemList,
{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
(o1 as ContactItem).model.displayName!!
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
(o2 as ContactItem).model.displayName!!
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
val firstSource: String = (o1 as ContactItem).model.source!!
val secondSource: String = (o2 as ContactItem).model.source!!
if (firstSource == secondSource) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
// First users
if ("users" == firstSource) {
return@sort -1
} else if ("users" == secondSource) {
return@sort 1
}
// Then groups
if ("groups" == firstSource) {
return@sort -1
} else if ("groups" == secondSource) {
return@sort 1
}
// Then circles
if ("circles" == firstSource) {
return@sort -1
} else if ("circles" == secondSource) {
return@sort 1
}
// Otherwise fall back to name sorting
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
firstName.compareTo(secondName, ignoreCase = true)
}
)
Collections.sort(
contactItems
) { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
newUserItemList.sortWith sort@{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
(o1 as ContactItem).model.displayName!!
o1.model.displayName!!
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
(o2 as ContactItem).model.displayName!!
o2.model.displayName!!
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
if ("groups" == (o1 as ContactItem).model.source &&
"groups" == (o2 as ContactItem).model.source
val firstSource: String = o1.model.source!!
val secondSource: String = o2.model.source!!
if (firstSource == secondSource) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
// First users
if ("users" == firstSource) {
return@sort -1
} else if ("users" == secondSource) {
return@sort 1
}
// Then groups
if ("groups" == firstSource) {
return@sort -1
} else if ("groups" == secondSource) {
return@sort 1
}
// Then circles
if ("circles" == firstSource) {
return@sort -1
} else if ("circles" == secondSource) {
return@sort 1
}
// Otherwise fall back to name sorting
return@sort firstName.compareTo(secondName, ignoreCase = true)
}
firstName.compareTo(secondName, ignoreCase = true)
}
contactItems?.sortWith sort@{ o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
val firstName: String = if (o1 is ContactItem) {
o1.model.displayName!!
} else {
(o1 as GenericTextHeaderItem).model
}
val secondName: String = if (o2 is ContactItem) {
o2.model.displayName!!
} else {
(o2 as GenericTextHeaderItem).model
}
if (o1 is ContactItem && o2 is ContactItem) {
if ("groups" == o1.model.source &&
"groups" == o2.model.source
) {
return@sort firstName.compareTo(secondName, ignoreCase = true)
} else if ("groups" == (o1 as ContactItem).model.source) {
} else if ("groups" == o1.model.source) {
return@sort -1
} else if ("groups" == (o2 as ContactItem).model.source) {
} else if ("groups" == o2.model.source) {
return@sort 1
}
}
@ -626,24 +656,19 @@ class ContactsActivity :
private fun prepareViews() {
layoutManager = SmoothScrollLinearLayoutManager(this)
binding?.controllerGenericRv?.recyclerView?.layoutManager = layoutManager
binding?.controllerGenericRv?.recyclerView?.setHasFixedSize(true)
binding?.controllerGenericRv?.recyclerView?.adapter = adapter
binding?.controllerGenericRv?.swipeRefreshLayout?.setOnRefreshListener { fetchData() }
binding.controllerGenericRv.recyclerView.layoutManager = layoutManager
binding.controllerGenericRv.recyclerView.setHasFixedSize(true)
binding.controllerGenericRv.recyclerView.adapter = adapter
binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() }
binding?.controllerGenericRv?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it.swipeRefreshLayout) }
binding.controllerGenericRv.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it.swipeRefreshLayout) }
binding.listOpenConversationsImage.background?.setColorFilter(
ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
PorterDuff.Mode.SRC_IN
)
binding.joinConversationViaLinkImage.background?.setColorFilter(
ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
PorterDuff.Mode.SRC_IN
)
binding?.let {
binding.let {
viewThemeUtils.platform.colorImageViewBackgroundAndIcon(it.publicCallLink)
}
disengageProgressBar()
@ -655,7 +680,6 @@ class ContactsActivity :
binding.controllerGenericRv.root.visibility = View.VISIBLE
if (isNewConversationView) {
binding.callHeaderLayout.visibility = View.VISIBLE
binding.joinConversationViaLink.visibility = View.VISIBLE
}
}
}
@ -708,21 +732,12 @@ class ContactsActivity :
}
}
private fun prepareAndShowBottomSheetWithBundle(bundle: Bundle) {
// 11: create conversation-enter name for new conversation
// 10: get&join room when enter link
contactsBottomDialog = ContactsBottomDialog(this, bundle)
contactsBottomDialog?.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(openConversationEvent: OpenConversationEvent) {
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(openConversationEvent.bundle!!)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
contactsBottomDialog?.dismiss()
}
override fun onItemClick(view: View, position: Int): Boolean {
@ -730,7 +745,6 @@ class ContactsActivity :
if (!isNewConversationView && !isAddingParticipantsView) {
createRoom(adapter?.getItem(position) as ContactItem)
} else {
val participant: Participant = (adapter?.getItem(position) as ContactItem).model
updateSelection((adapter?.getItem(position) as ContactItem))
}
}
@ -840,12 +854,6 @@ class ContactsActivity :
return "groups" == contactItem.model.source && participant.selected && adapter?.selectedItemCount!! > 1
}
private fun joinConversationViaLink() {
val bundle = Bundle()
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM)
prepareAndShowBottomSheetWithBundle(bundle)
}
private fun listOpenConversations() {
val intent = Intent(this, ListOpenConversationsActivity::class.java)
startActivity(intent)
@ -854,30 +862,11 @@ class ContactsActivity :
private fun toggleCallHeader() {
toggleConversationPrivacyLayout(isPublicCall)
isPublicCall = !isPublicCall
toggleConversationViaLinkVisibility(isPublicCall)
enableContactForNonPublicCall()
checkAndHandleDoneMenuItem()
adapter?.notifyDataSetChanged()
}
private fun updateGroupParticipantSelection() {
val currentItems: List<AbstractFlexibleItem<*>> = adapter?.currentItems as
List<AbstractFlexibleItem<*>>
var internalParticipant: Participant
for (i in currentItems.indices) {
if (currentItems[i] is ContactItem) {
internalParticipant = (currentItems[i] as ContactItem).model
if (internalParticipant.calculatedActorType == Participant.ActorType.GROUPS &&
internalParticipant.selected
) {
internalParticipant.selected = false
selectedGroupIds.remove(internalParticipant.calculatedActorId)
}
}
}
}
private fun enableContactForNonPublicCall() {
for (i in 0 until adapter!!.itemCount) {
if (adapter?.getItem(i) is ContactItem) {
@ -891,20 +880,12 @@ class ContactsActivity :
private fun toggleConversationPrivacyLayout(showInitialLayout: Boolean) {
if (showInitialLayout) {
binding.initialRelativeLayout.visibility = View.VISIBLE
binding.secondaryRelativeLayout.visibility = View.GONE
binding.publicConversationCreate.visibility = View.VISIBLE
binding.publicConversationInfo.visibility = View.GONE
} else {
binding.initialRelativeLayout.visibility = View.GONE
binding.secondaryRelativeLayout.visibility = View.VISIBLE
}
}
private fun toggleConversationViaLinkVisibility(isPublicCall: Boolean) {
if (isPublicCall) {
binding.joinConversationViaLink.visibility = View.GONE
updateGroupParticipantSelection()
} else {
binding.joinConversationViaLink.visibility = View.VISIBLE
binding.publicConversationCreate.visibility = View.GONE
binding.publicConversationInfo.visibility = View.VISIBLE
binding.listOpenConversations.visibility = View.GONE
}
}

View file

@ -1,32 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet
enum class ConversationOperationEnum {
OPS_CODE_RENAME_ROOM,
OPS_CODE_GET_AND_JOIN_ROOM,
OPS_CODE_INVITE_USERS,
OPS_CODE_MARK_AS_READ,
OPS_CODE_MARK_AS_UNREAD,
OPS_CODE_REMOVE_FAVORITE,
OPS_CODE_ADD_FAVORITE,
OPS_CODE_JOIN_ROOM
}

View file

@ -1,377 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* @author Andy Scherzinger
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.text.TextUtils
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.content.res.ResourcesCompat
import autodagger.AutoInjector
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.google.android.material.textfield.TextInputLayout
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ControllerEntryMenuBinding
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.vanniktech.emoji.EmojiPopup
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.internal.immutableListOf
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class EntryMenuController(args: Bundle) :
BaseController(
R.layout.controller_entry_menu,
args
) {
private val binding: ControllerEntryMenuBinding? by viewBinding(ControllerEntryMenuBinding::bind)
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var eventBus: EventBus
@Inject
lateinit var userManager: UserManager
private val operation: ConversationOperationEnum
private var conversation: Conversation? = null
private val packageName: String
private val name: String
private var emojiPopup: EmojiPopup? = null
private val originalBundle: Bundle
private var currentUser: User? = null
private val roomToken: String
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.SEARCH_BAR
override fun onAttach(view: View) {
super.onAttach(view)
if (ApplicationWideMessageHolder.MessageType.CALL_PASSWORD_WRONG ==
ApplicationWideMessageHolder.getInstance().messageType
) {
binding?.textInputLayout?.error = resources?.getString(R.string.nc_wrong_password)
ApplicationWideMessageHolder.getInstance().messageType = null
if (binding?.okButton?.isEnabled == true) {
binding?.okButton?.isEnabled = false
binding?.okButton?.alpha = OPACITY_BUTTON_DISABLED
}
}
emojiPopup = binding?.let {
EmojiPopup(
rootView = view,
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(" ")
}
)
}
}
override fun onViewBound(view: View) {
super.onViewBound(view)
currentUser = userManager.currentUser.blockingGet()
if (operation == ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) {
binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
textEditAddChangedListener()
binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) }
binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) }
binding?.textInputLayout?.hint = resources!!.getString(R.string.nc_conversation_link)
binding?.textInputLayout?.requestFocus()
binding?.smileyButton?.setOnClickListener { onSmileyClick() }
binding?.okButton?.setOnClickListener { onOkButtonClick() }
} else if (operation == ConversationOperationEnum.OPS_CODE_INVITE_USERS) {
binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT
textEditAddChangedListener()
binding?.smileyButton?.visibility = View.VISIBLE
binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) }
binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) }
binding?.textInputLayout?.requestFocus()
binding?.smileyButton?.setOnClickListener { onSmileyClick() }
binding?.okButton?.setOnClickListener { onOkButtonClick() }
} else {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
ncApi.getRoom(
ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
ApiUtils.getUrlForRoom(apiVersion, currentUser!!.baseUrl, roomToken)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
@Suppress("Detekt.LongMethod")
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
if (conversation != null && operation === ConversationOperationEnum.OPS_CODE_RENAME_ROOM) {
binding?.textEdit?.setText(conversation!!.name)
}
binding?.textEdit?.setOnEditorActionListener { v, actionId, event ->
@Suppress("IMPLICIT_BOXING_IN_IDENTITY_EQUALS")
if (actionId === EditorInfo.IME_ACTION_DONE && binding?.okButton?.isEnabled == true) {
binding?.okButton?.callOnClick()
return@setOnEditorActionListener true
}
false
}
textEditAddChangedListener()
var labelText = ""
when (operation) {
ConversationOperationEnum.OPS_CODE_RENAME_ROOM -> {
labelText = resources!!.getString(R.string.nc_call_name)
binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT
binding?.smileyButton?.visibility = View.VISIBLE
}
ConversationOperationEnum.OPS_CODE_JOIN_ROOM -> {
// 99 is joining a conversation via password
labelText = resources!!.getString(R.string.nc_password)
binding?.textEdit?.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
else -> {
}
}
if (PASSWORD_ENTRY_OPERATIONS.contains(operation)) {
binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
} else {
binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_NONE
}
binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) }
binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) }
binding?.textInputLayout?.hint = labelText
binding?.textInputLayout?.requestFocus()
binding?.smileyButton?.setOnClickListener { onSmileyClick() }
binding?.okButton?.setOnClickListener { onOkButtonClick() }
}
override fun onError(e: Throwable) {
Log.e("EntryMenuController", "error")
}
override fun onComplete() {
// unused atm
}
})
}
}
private fun textEditAddChangedListener() {
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) {
if (!TextUtils.isEmpty(s)) {
if (operation === ConversationOperationEnum.OPS_CODE_RENAME_ROOM) {
if (conversation!!.name == null || !conversation!!.name.equals(s.toString())) {
if (!binding?.okButton?.isEnabled!!) {
binding?.okButton?.isEnabled = true
binding?.okButton?.alpha = OPACITY_ENABLED
}
binding?.textInputLayout?.isErrorEnabled = false
} else {
if (binding?.okButton?.isEnabled == true) {
binding?.okButton?.isEnabled = false
binding?.okButton?.alpha = OPACITY_DISABLED
}
binding?.textInputLayout?.error = resources?.getString(R.string.nc_call_name_is_same)
}
} else if (operation !== ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) {
if (!binding?.okButton?.isEnabled!!) {
binding?.okButton?.isEnabled = true
binding?.okButton?.alpha = OPACITY_ENABLED
}
binding?.textInputLayout?.isErrorEnabled = false
} else if (
UriUtils.hasHttpProtocolPrefixed(binding?.textEdit?.text.toString()) &&
binding?.textEdit?.text.toString().contains("/call/")
) {
if (!binding?.okButton?.isEnabled!!) {
binding?.okButton?.isEnabled = true
binding?.okButton?.alpha = OPACITY_ENABLED
}
binding?.textInputLayout?.isErrorEnabled = false
} else {
if (binding?.okButton?.isEnabled == true) {
binding?.okButton?.isEnabled = false
binding?.okButton?.alpha = OPACITY_DISABLED
}
binding?.textInputLayout?.error = resources?.getString(R.string.nc_wrong_link)
}
} else {
if (binding?.okButton?.isEnabled == true) {
binding?.okButton?.isEnabled = false
binding?.okButton?.alpha = OPACITY_DISABLED
}
binding?.textInputLayout?.isErrorEnabled = false
}
}
})
}
private fun onSmileyClick() {
emojiPopup?.toggle()
}
private fun onOkButtonClick() {
if (operation === ConversationOperationEnum.OPS_CODE_JOIN_ROOM) {
joinRoom()
} else if (
operation !== ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM &&
operation !== ConversationOperationEnum.OPS_CODE_INVITE_USERS
) {
val bundle = Bundle()
conversation!!.name = binding?.textEdit?.text.toString()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
bundle.putString(BundleKeys.KEY_NEW_ROOM_NAME, binding?.textEdit?.text.toString())
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, operation)
router.pushController(
RouterTransaction.with(OperationsMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
} else if (operation !== ConversationOperationEnum.OPS_CODE_INVITE_USERS) {
val bundle = Bundle()
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, operation)
bundle.putString(BundleKeys.KEY_CALL_URL, binding?.textEdit?.text.toString())
router.pushController(
RouterTransaction.with(OperationsMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
} else if (operation === ConversationOperationEnum.OPS_CODE_INVITE_USERS) {
originalBundle.putString(BundleKeys.KEY_CONVERSATION_NAME, binding?.textEdit?.text.toString())
router.pushController(
RouterTransaction.with(
OperationsMenuController(
originalBundle
)
)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
}
private fun joinRoom() {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, binding?.textEdit?.text.toString())
bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, operation)
router.pushController(
RouterTransaction.with(OperationsMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
init {
sharedApplication!!.componentApplication.inject(this)
originalBundle = args
operation = args.getSerializable(BundleKeys.KEY_OPERATION_CODE) as ConversationOperationEnum
roomToken = args.getString(KEY_ROOM_TOKEN, "")
name = args.getString(BundleKeys.KEY_APP_ITEM_NAME, "")
packageName = args.getString(BundleKeys.KEY_APP_ITEM_PACKAGE_NAME, "")
// callUrl = args.getString(BundleKeys.KEY_CALL_URL, "")
}
companion object {
private val PASSWORD_ENTRY_OPERATIONS: List<ConversationOperationEnum> =
immutableListOf(
ConversationOperationEnum.OPS_CODE_JOIN_ROOM
)
const val OPACITY_DISABLED = 0.38f
const val OPACITY_BUTTON_DISABLED = 0.7f
const val OPACITY_ENABLED = 1.0f
}
}

View file

@ -1,753 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import autodagger.AutoInjector
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ControllerOperationsMenuBinding
import com.nextcloud.talk.events.ConversationsListFetchDataEvent
import com.nextcloud.talk.events.OpenConversationEvent
import com.nextcloud.talk.models.RetrofitBucket
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
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 com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.NoSupportedApiException
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_TYPE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INVITED_GROUP
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INVITED_PARTICIPANTS
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_ROOM_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPERATION_CODE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.parceler.Parcels
import retrofit2.HttpException
import java.io.IOException
import java.util.Collections
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class OperationsMenuController(args: Bundle) : BaseController(
R.layout.controller_operations_menu,
args
) {
private val binding: ControllerOperationsMenuBinding? by viewBinding(ControllerOperationsMenuBinding::bind)
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var eventBus: EventBus
private val operation: ConversationOperationEnum?
private var conversation: Conversation? = null
private var currentUser: User? = null
private val callPassword: String
private val callUrl: String
private var roomToken: String
private val roomNameNew: String
private var baseUrl: String? = null
private var conversationToken: String? = null
private var disposable: Disposable? = null
private var conversationType: ConversationType? = null
private var invitedUsers: ArrayList<String>? = ArrayList()
private var invitedGroups: ArrayList<String>? = ArrayList()
private var credentials: String? = null
private val conversationName: String
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.SEARCH_BAR
override fun onViewBound(view: View) {
super.onViewBound(view)
sharedApplication!!.componentApplication.inject(this)
currentUser = userManager.currentUser.blockingGet()
binding?.progressBar?.let { viewThemeUtils.platform.colorCircularProgressBar(it, ColorRole.PRIMARY) }
if (!TextUtils.isEmpty(callUrl) && callUrl.contains("/call")) {
conversationToken = callUrl.substring(callUrl.lastIndexOf("/") + 1)
if (callUrl.contains("/index.php")) {
baseUrl = callUrl.substring(0, callUrl.indexOf("/index.php"))
} else {
baseUrl = callUrl.substring(0, callUrl.indexOf("/call"))
}
}
if (roomToken.isNotEmpty()) {
val apiVersion = apiVersion()
ncApi.getRoom(
credentials,
ApiUtils.getUrlForRoom(apiVersion, currentUser!!.baseUrl, roomToken)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
if (!TextUtils.isEmpty(baseUrl) && baseUrl != currentUser!!.baseUrl) {
fetchCapabilitiesForGuest()
} else {
processOperation()
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while fetching room", e)
}
override fun onComplete() {
// unused atm
}
})
} else {
processOperation()
}
}
private fun fetchCapabilitiesForGuest() {
ncApi.getCapabilities(null, ApiUtils.getUrlForCapabilities(baseUrl))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<CapabilitiesOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
currentUser = User()
currentUser!!.baseUrl = baseUrl
currentUser!!.userId = "?"
try {
currentUser!!.capabilities = capabilitiesOverall.ocs!!.data!!.capabilities
} catch (e: IOException) {
Log.e("OperationsMenu", "Failed to serialize capabilities")
}
try {
checkCapabilities(currentUser!!)
processOperation()
} catch (e: NoSupportedApiException) {
showResultImage(everythingOK = false, isGuestSupportError = false)
Log.d(TAG, "No supported server version found", e)
}
}
override fun onError(e: Throwable) {
showResultImage(everythingOK = false, isGuestSupportError = false)
Log.e(TAG, "Error fetching capabilities for guest", e)
}
override fun onComplete() {
// unused atm
}
})
}
@Suppress("Detekt.ComplexMethod")
private fun processOperation() {
if (currentUser == null) {
showResultImage(everythingOK = false, isGuestSupportError = true)
Log.e(TAG, "Ended up in processOperation without a valid currentUser")
return
}
credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
when (operation) {
ConversationOperationEnum.OPS_CODE_RENAME_ROOM -> operationRenameRoom()
ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM -> operationGetAndJoinRoom()
ConversationOperationEnum.OPS_CODE_INVITE_USERS -> operationInviteUsers()
ConversationOperationEnum.OPS_CODE_MARK_AS_READ -> operationMarkAsRead()
ConversationOperationEnum.OPS_CODE_MARK_AS_UNREAD -> operationMarkAsUnread()
ConversationOperationEnum.OPS_CODE_REMOVE_FAVORITE,
ConversationOperationEnum.OPS_CODE_ADD_FAVORITE -> operationToggleFavorite()
ConversationOperationEnum.OPS_CODE_JOIN_ROOM -> operationJoinRoom()
else -> {
}
}
}
private fun apiVersion(): Int {
return ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
}
private fun chatApiVersion(): Int {
return ApiUtils.getChatApiVersion(currentUser, intArrayOf(ApiUtils.APIv1))
}
private fun operationJoinRoom() {
ncApi.joinRoom(
credentials,
ApiUtils.getUrlForParticipantsActive(
apiVersion(),
baseUrl,
conversationToken
),
callPassword
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(RoomOperationsObserver())
}
private fun operationMarkAsRead() {
ncApi.setChatReadMarker(
credentials,
ApiUtils.getUrlForChatReadMarker(
chatApiVersion(),
currentUser!!.baseUrl,
conversation!!.token
),
conversation!!.lastMessage!!.jsonMessageId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(GenericOperationsObserver())
}
private fun operationMarkAsUnread() {
ncApi.markRoomAsUnread(
credentials,
ApiUtils.getUrlForChatReadMarker(
chatApiVersion(),
currentUser!!.baseUrl,
conversation!!.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(GenericOperationsObserver())
}
private fun operationRenameRoom() {
ncApi.renameRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion(),
currentUser!!.baseUrl,
conversation!!.token
),
roomNameNew
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(GenericOperationsObserver())
}
private fun operationToggleFavorite() {
val genericOperationsObserver = GenericOperationsObserver()
val apiVersion = apiVersion()
if (operation === ConversationOperationEnum.OPS_CODE_REMOVE_FAVORITE) {
ncApi.removeConversationFromFavorites(
credentials,
ApiUtils.getUrlForRoomFavorite(
apiVersion,
currentUser!!.baseUrl,
conversation!!.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(genericOperationsObserver)
} else {
ncApi.addConversationToFavorites(
credentials,
ApiUtils.getUrlForRoomFavorite(
apiVersion,
currentUser!!.baseUrl,
conversation!!.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(genericOperationsObserver)
}
}
private fun operationInviteUsers() {
val retrofitBucket: RetrofitBucket
val apiVersion = apiVersion()
var invite: String? = null
if (invitedGroups!!.size > 0) {
invite = invitedGroups!![0]
}
retrofitBucket = if (conversationType == ConversationType.ROOM_PUBLIC_CALL) {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl,
"3",
null,
invite,
conversationName
)
} else {
ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
currentUser!!.baseUrl,
"2",
null,
invite,
conversationName
)
}
ncApi.createRoom(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
ncApi.getRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion,
currentUser!!.baseUrl,
conversation!!.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(
roomOverall: RoomOverall
) {
conversation = roomOverall.ocs!!.data
inviteUsersToAConversation()
}
override fun onError(e: Throwable) {
showResultImage(everythingOK = false, isGuestSupportError = false)
dispose()
}
override fun onComplete() {
// unused atm
}
})
}
override fun onError(e: Throwable) {
showResultImage(everythingOK = false, isGuestSupportError = false)
dispose()
}
override fun onComplete() {
dispose()
}
})
}
private fun operationGetAndJoinRoom() {
val apiVersion = apiVersion()
ncApi.getRoom(
credentials,
ApiUtils.getUrlForRoom(apiVersion, baseUrl, conversationToken)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
if (conversation!!.hasPassword && conversation!!.isGuest) {
eventBus.post(ConversationsListFetchDataEvent())
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
bundle.putSerializable(KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_JOIN_ROOM)
router.pushController(
RouterTransaction.with(EntryMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
} else if (conversation!!.isGuest) {
ncApi.joinRoom(
credentials,
ApiUtils.getUrlForParticipantsActive(
apiVersion,
baseUrl,
conversationToken
),
null
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
initiateConversation()
}
override fun onError(e: Throwable) {
showResultImage(everythingOK = false, isGuestSupportError = false)
dispose()
}
override fun onComplete() {
// unused atm
}
})
} else {
initiateConversation()
}
}
override fun onError(e: Throwable) {
showResultImage(everythingOK = false, isGuestSupportError = false)
dispose()
}
override fun onComplete() {
dispose()
}
})
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun showResultImage(everythingOK: Boolean, isGuestSupportError: Boolean) {
try {
binding?.progressBar?.visibility = View.GONE
if (resources != null) {
if (everythingOK) {
binding?.resultImageView?.setImageDrawable(
DisplayUtils.getTintedDrawable(
resources,
R.drawable.ic_check_circle_black_24dp,
R.color.nc_darkGreen
)
)
} else {
binding?.resultImageView?.setImageDrawable(
DisplayUtils.getTintedDrawable(
resources,
R.drawable.ic_cancel_black_24dp,
R.color.nc_darkRed
)
)
}
}
binding?.resultImageView?.visibility = View.VISIBLE
if (everythingOK) {
binding?.resultTextView?.setText(R.string.nc_all_ok_operation)
} else {
binding?.resultTextView?.setTextColor(resources!!.getColor(R.color.nc_darkRed, null))
binding?.resultTextView?.setText(R.string.nc_failed_to_perform_operation)
}
binding?.resultTextView?.visibility = View.VISIBLE
if (everythingOK) {
eventBus.post(ConversationsListFetchDataEvent())
} else {
binding?.resultImageView?.setImageDrawable(
DisplayUtils.getTintedDrawable(
resources,
R.drawable.ic_cancel_black_24dp,
R.color.nc_darkRed
)
)
binding?.okButton?.setOnClickListener { v: View? -> eventBus.post(ConversationsListFetchDataEvent()) }
binding?.okButton?.visibility = View.VISIBLE
}
} catch (npe: NullPointerException) {
Log.i(TAG, "Controller already closed", npe)
}
}
private fun dispose() {
if (disposable != null && !disposable!!.isDisposed) {
disposable!!.dispose()
}
disposable = null
}
public override fun onDestroy() {
super.onDestroy()
dispose()
}
@kotlin.Throws(NoSupportedApiException::class)
private fun checkCapabilities(currentUser: User) {
ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
ApiUtils.getCallApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
ApiUtils.getChatApiVersion(currentUser, intArrayOf(1))
ApiUtils.getSignalingApiVersion(currentUser, intArrayOf(ApiUtils.APIv3, 2, 1))
}
private fun inviteUsersToAConversation() {
val localInvitedUsers = invitedUsers
val localInvitedGroups = invitedGroups
if (localInvitedGroups!!.size > 0) {
localInvitedGroups.removeAt(0)
}
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, API_CONVERSATION_VERSIONS)
if (localInvitedUsers!!.size > 0 || localInvitedGroups.size > 0 &&
CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")
) {
addGroupsToConversation(localInvitedUsers, localInvitedGroups, apiVersion)
addUsersToConversation(localInvitedUsers, localInvitedGroups, apiVersion)
} else {
initiateConversation()
}
}
private fun addUsersToConversation(
localInvitedUsers: ArrayList<String>?,
localInvitedGroups: ArrayList<String>?,
apiVersion: Int
) {
var retrofitBucket: RetrofitBucket
for (i in localInvitedUsers!!.indices) {
val userId = invitedUsers!![i]
retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(
apiVersion,
currentUser!!.baseUrl,
conversation!!.token,
userId
)
ncApi.addParticipant(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<AddParticipantOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(addParticipantOverall: AddParticipantOverall) {
// unused atm
}
override fun onError(e: Throwable) {
dispose()
}
override fun onComplete() {
Collections.synchronizedList(localInvitedUsers).remove(userId)
if (localInvitedGroups!!.size == 0 && localInvitedUsers.size == 0) {
initiateConversation()
}
dispose()
}
})
}
}
private fun addGroupsToConversation(
localInvitedUsers: ArrayList<String>?,
localInvitedGroups: ArrayList<String>?,
apiVersion: Int
) {
var retrofitBucket: RetrofitBucket
if (localInvitedGroups!!.size > 0 &&
CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")
) {
for (i in localInvitedGroups.indices) {
val groupId = localInvitedGroups[i]
retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource(
apiVersion,
currentUser!!.baseUrl,
conversation!!.token,
"groups",
groupId
)
ncApi.addParticipant(credentials, retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<AddParticipantOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(addParticipantOverall: AddParticipantOverall) {
// unused atm
}
override fun onError(e: Throwable) {
dispose()
}
override fun onComplete() {
Collections.synchronizedList(localInvitedGroups).remove(groupId)
if (localInvitedGroups.size == 0 && localInvitedUsers!!.size == 0) {
initiateConversation()
}
dispose()
}
})
}
}
}
private fun initiateConversation() {
eventBus.post(ConversationsListFetchDataEvent())
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, conversation!!.token)
bundle.putString(KEY_ROOM_ID, conversation!!.roomId)
bundle.putString(KEY_CONVERSATION_NAME, conversation!!.displayName)
bundle.putString(KEY_CONVERSATION_PASSWORD, callPassword)
eventBus.post(OpenConversationEvent(conversation, bundle))
}
private fun handleObserverError(e: Throwable) {
if (operation !== ConversationOperationEnum.OPS_CODE_JOIN_ROOM || e !is HttpException) {
showResultImage(everythingOK = false, isGuestSupportError = false)
} else {
val response = e.response()
if (response != null && response.code() == FORBIDDEN) {
ApplicationWideMessageHolder.getInstance()
.setMessageType(ApplicationWideMessageHolder.MessageType.CALL_PASSWORD_WRONG)
router.popCurrentController()
} else {
showResultImage(everythingOK = false, isGuestSupportError = false)
}
}
dispose()
}
private inner class GenericOperationsObserver : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onNext(genericOverall: GenericOverall) {
if (operation !== ConversationOperationEnum.OPS_CODE_JOIN_ROOM) {
showResultImage(everythingOK = true, isGuestSupportError = false)
} else {
throw IllegalArgumentException("Unsupported operation code observed!")
}
}
override fun onError(e: Throwable) {
handleObserverError(e)
}
override fun onComplete() {
dispose()
}
}
private inner class RoomOperationsObserver : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs!!.data
if (operation !== ConversationOperationEnum.OPS_CODE_JOIN_ROOM) {
showResultImage(everythingOK = true, isGuestSupportError = false)
} else {
conversation = roomOverall.ocs!!.data
initiateConversation()
}
}
override fun onError(e: Throwable) {
handleObserverError(e)
}
override fun onComplete() {
dispose()
}
}
companion object {
private const val TAG = "OperationsMenu"
private const val FORBIDDEN = 403
private val API_CONVERSATION_VERSIONS = intArrayOf(4, 1)
}
init {
operation = args.getSerializable(KEY_OPERATION_CODE) as ConversationOperationEnum?
callPassword = args.getString(KEY_CONVERSATION_PASSWORD, "")
callUrl = args.getString(KEY_CALL_URL, "")
roomToken = args.getString(KEY_ROOM_TOKEN, "")
roomNameNew = args.getString(KEY_NEW_ROOM_NAME, "")
if (args.containsKey(KEY_INVITED_PARTICIPANTS)) {
invitedUsers = args.getStringArrayList(KEY_INVITED_PARTICIPANTS)
}
if (args.containsKey(KEY_INVITED_GROUP)) {
invitedGroups = args.getStringArrayList(KEY_INVITED_GROUP)
}
if (args.containsKey(KEY_CONVERSATION_TYPE)) {
conversationType = Parcels.unwrap(args.getParcelable(KEY_CONVERSATION_TYPE))
}
conversationName = args.getString(KEY_CONVERSATION_NAME, "")
}
}

View file

@ -0,0 +1,310 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.Conversation
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: Conversation.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.from(context))
val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context)
.setTitle(resources.getString(R.string.nc_call_name))
// 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
)
}
}
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) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
val chatIntent = Intent(context, 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

@ -0,0 +1,229 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.conversation
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.res.ColorStateList
import android.os.Bundle
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 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.conversation.viewmodel.RenameConversationViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.databinding.DialogRenameConversationBinding
import com.nextcloud.talk.events.ConversationsListFetchDataEvent
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.vanniktech.emoji.EmojiPopup
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class RenameConversationDialogFragment : DialogFragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var eventBus: EventBus
private lateinit var binding: DialogRenameConversationBinding
private lateinit var viewModel: RenameConversationViewModel
private var emojiPopup: EmojiPopup? = null
private var roomToken = ""
private var initialName = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
viewModel = ViewModelProvider(this, viewModelFactory)[RenameConversationViewModel::class.java]
roomToken = arguments?.getString(KEY_ROOM_TOKEN)!!
initialName = arguments?.getString(INITIAL_NAME)!!
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogRenameConversationBinding.inflate(LayoutInflater.from(context))
val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context)
.setTitle(resources.getString(R.string.nc_call_name))
// listener is null for now to avoid closing after button was clicked.
// listener is set later in onStart
.setPositiveButton(R.string.nc_rename_confirm, 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()
binding.textEdit.setText(initialName)
val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.isEnabled = false
positiveButton.setOnClickListener {
viewModel.renameConversation(roomToken, binding.textEdit.text.toString())
}
}
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 (initialName == s.toString()) {
positiveButton.isEnabled = false
} else if (!positiveButton.isEnabled) {
positiveButton.isEnabled = true
}
} else {
if (positiveButton.isEnabled) {
positiveButton.isEnabled = false
}
}
}
})
}
private fun setupStateObserver() {
viewModel.viewState.observe(viewLifecycleOwner) { state ->
when (state) {
is RenameConversationViewModel.InitialState -> {}
is RenameConversationViewModel.RenamingState -> {}
is RenameConversationViewModel.RenamingSuccessState -> handleSuccess()
is RenameConversationViewModel.RenamingFailedState -> showError()
else -> {}
}
}
}
private fun handleSuccess() {
eventBus.post(ConversationsListFetchDataEvent())
context?.resources?.let {
String.format(
it.getString(R.string.renamed_conversation),
initialName
)
}?.let {
(activity as ConversationsListActivity?)?.showSnackbar(
it
)
}
dismiss()
}
private fun showError() {
dismiss()
Log.e(TAG, "Failed to rename conversation")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
/**
* Fragment creator
*/
companion object {
private val TAG = RenameConversationDialogFragment::class.java.simpleName
private const val KEY_ROOM_TOKEN = "keyRoomToken"
private const val INITIAL_NAME = "initialName"
@JvmStatic
fun newInstance(roomTokenParam: String, initialName: String): RenameConversationDialogFragment {
val args = Bundle()
args.putString(KEY_ROOM_TOKEN, roomTokenParam)
args.putString(INITIAL_NAME, initialName)
val fragment = RenameConversationDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.conversation.repository
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable
interface ConversationRepository {
fun renameConversation(
roomToken: String,
roomNameNew: String
): Observable<GenericOverall>
fun createConversation(
roomName: String,
conversationType: Conversation.ConversationType?
): Observable<RoomOverall>
}

View file

@ -0,0 +1,97 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
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 renameConversation(
roomToken: String,
roomNameNew: String
): Observable<GenericOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
return ncApi.renameRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion,
currentUser.baseUrl,
roomToken
),
roomNameNew
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(API_RETRIES)
}
override fun createConversation(
roomName: String,
conversationType: Conversation.ConversationType?
): Observable<RoomOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
val retrofitBucket: RetrofitBucket = if (conversationType == Conversation.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"
const val API_RETRIES: Long = 3
}
}

View file

@ -0,0 +1,95 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.Conversation
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: Conversation.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

@ -0,0 +1,84 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.conversation.viewmodel
import android.util.Log
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.generic.GenericOverall
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 RenameConversationViewModel @Inject constructor(private val repository: ConversationRepository) : ViewModel() {
sealed class ViewState
object InitialState : ViewState()
object RenamingState : ViewState()
object RenamingSuccessState : ViewState()
object RenamingFailedState : ViewState()
private val _viewState: MutableLiveData<ViewState> = MutableLiveData(
InitialState
)
val viewState: LiveData<ViewState>
get() = _viewState
fun renameConversation(roomToken: String, roomNameNew: String) {
_viewState.value = RenamingState
repository.renameConversation(
roomToken,
roomNameNew
)
.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(RenameConversationObserver())
}
inner class RenameConversationObserver : Observer<GenericOverall> {
lateinit var genericOverall: GenericOverall
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(response: GenericOverall) {
genericOverall = response
}
override fun onError(e: Throwable) {
Log.e(TAG, "Failed to rename conversation", e)
_viewState.value = RenamingFailedState
}
override fun onComplete() {
_viewState.value = RenamingSuccessState
}
}
companion object {
private val TAG = RenameConversationViewModel::class.java.simpleName
}
}

View file

@ -58,6 +58,7 @@ import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import coil.imageLoader
@ -86,7 +87,6 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ControllerConversationsRvBinding
import com.nextcloud.talk.events.ConversationsListFetchDataEvent
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.interfaces.ConversationMenuInterface
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.run
import com.nextcloud.talk.jobs.DeleteConversationWorker
@ -143,8 +143,7 @@ import javax.inject.Inject
class ConversationsListActivity :
BaseActivity(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
ConversationMenuInterface {
FlexibleAdapter.OnItemLongClickListener {
private lateinit var binding: ControllerConversationsRvBinding
@ -181,7 +180,6 @@ class ConversationsListActivity :
private var credentials: String? = null
private var adapterWasNull = true
private var isRefreshing = false
private var conversationMenuBundle: Bundle? = null
private var showShareToScreen = false
private var filesToShare: ArrayList<String>? = null
private var selectedConversation: Conversation? = null
@ -600,6 +598,9 @@ class ConversationsListActivity :
searchItem!!.expandActionView()
}
fun showSnackbar(text: String) {
Snackbar.make(binding.root, text, Snackbar.LENGTH_LONG).show()
}
fun fetchRooms() {
val includeStatus = isUserStatusAvailable(userManager.currentUser.blockingGet())
@ -999,7 +1000,7 @@ class ConversationsListActivity :
@SuppressLint("CheckResult") // handled by helper
private fun loadMoreMessages() {
binding?.swipeRefreshLayoutView?.isRefreshing = true
binding.swipeRefreshLayoutView.isRefreshing = true
val observable = searchHelper!!.loadMore()
observable?.observeOn(AndroidSchedulers.mainThread())
?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable ->
@ -1302,50 +1303,33 @@ class ConversationsListActivity :
}, BOTTOM_SHEET_DELAY)
}
override fun showDeleteConversationDialog(bundle: Bundle) {
conversationMenuBundle = bundle
if (conversationMenuBundle != null &&
isInternalUserEqualsCurrentUser(currentUser, conversationMenuBundle)
) {
binding?.floatingActionButton?.let {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
val data = Data.Builder()
data.putLong(
KEY_INTERNAL_USER_ID,
conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID)
)
data.putString(KEY_ROOM_TOKEN, bundle.getString(KEY_ROOM_TOKEN))
conversationMenuBundle = null
deleteConversation(data.build())
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
conversationMenuBundle = null
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
fun showDeleteConversationDialog(conversation: Conversation) {
binding.floatingActionButton.let {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
}
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
deleteConversation(conversation)
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
}
}
private fun isInternalUserEqualsCurrentUser(currentUser: User?, conversationMenuBundle: Bundle?): Boolean {
return currentUser != null && conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) == currentUser.id
}
private fun showUnauthorizedDialog() {
binding?.floatingActionButton?.let {
binding.floatingActionButton.let {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog.colorMaterialAlertDialogIcon(
@ -1517,10 +1501,40 @@ class ConversationsListActivity :
Runtime.getRuntime().exit(0)
}
private fun deleteConversation(data: Data) {
private fun deleteConversation(conversation: Conversation) {
val data = Data.Builder()
data.putLong(
KEY_INTERNAL_USER_ID,
currentUser?.id!!
)
data.putString(KEY_ROOM_TOKEN, conversation.token)
val deleteConversationWorker =
OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data).build()
OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(deleteConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
showSnackbar(
String.format(
context.resources.getString(R.string.deleted_conversation),
conversation.displayName
)
)
}
WorkInfo.State.FAILED -> {
showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
}
else -> {
}
}
}
}
}
private fun onMessageSearchResult(results: MessageSearchResults) {

View file

@ -28,6 +28,8 @@ package com.nextcloud.talk.dagger.modules
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.chat.data.ChatRepository
import com.nextcloud.talk.chat.data.ChatRepositoryImpl
import com.nextcloud.talk.conversation.repository.ConversationRepository
import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository
import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl
import com.nextcloud.talk.data.source.local.TalkDatabase
@ -139,4 +141,10 @@ class RepositoryModule {
ConversationInfoEditRepository {
return ConversationInfoEditRepositoryImpl(ncApi, userProvider)
}
@Provides
fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew):
ConversationRepository {
return ConversationRepositoryImpl(ncApi, userProvider)
}
}

View file

@ -25,6 +25,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.callnotification.viewmodel.CallNotificationViewModel
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
import com.nextcloud.talk.messagesearch.MessageSearchViewModel
import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel
@ -132,4 +134,14 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(ConversationInfoEditViewModel::class)
abstract fun conversationInfoEditViewModel(viewModel: ConversationInfoEditViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(RenameConversationViewModel::class)
abstract fun renameConversationViewModel(viewModel: RenameConversationViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ConversationViewModel::class)
abstract fun conversationViewModel(viewModel: ConversationViewModel): ViewModel
}

View file

@ -25,11 +25,9 @@ import android.content.Context;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.events.EventStatus;
import com.nextcloud.talk.models.RetrofitBucket;
import com.nextcloud.talk.users.UserManager;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.UserIdUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import org.greenrobot.eventbus.EventBus;
@ -138,9 +136,6 @@ public class AddParticipantsToConversation extends Worker {
}
}
eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user),
EventStatus.EventType.PARTICIPANTS_UPDATE,
true));
return Result.success();
}
}

View file

@ -1,86 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import autodagger.AutoInjector
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController
import com.nextcloud.talk.databinding.DialogBottomContactsBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactsBottomDialog(
val activity: Activity,
val bundle: Bundle
) : BottomSheetDialog(activity) {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
private var dialogRouter: Router? = null
private lateinit var binding: DialogBottomContactsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
binding = DialogBottomContactsBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
viewThemeUtils.platform.themeDialog(binding.root)
executeEntryMenuController(bundle)
}
private fun executeEntryMenuController(bundle: Bundle) {
dialogRouter = Conductor.attachRouter(activity, binding.root, null)
dialogRouter!!.pushController(
RouterTransaction.with(EntryMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
override fun onStart() {
super.onStart()
val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from(bottomSheet as View)
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
companion object {
private const val TAG = "ContactsBottomDialog"
}
}

View file

@ -26,36 +26,31 @@ import android.view.View
import android.view.ViewGroup
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_ADD_FAVORITE
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_MARK_AS_READ
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_MARK_AS_UNREAD
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_REMOVE_FAVORITE
import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_RENAME_ROOM
import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController
import com.nextcloud.talk.controllers.bottomsheet.OperationsMenuController
import com.nextcloud.talk.conversation.RenameConversationDialogFragment
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogConversationOperationsBinding
import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPERATION_CODE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -65,8 +60,6 @@ class ConversationsListBottomDialog(
val conversation: Conversation
) : BottomSheetDialog(activity) {
private var dialogRouter: Router? = null
private lateinit var binding: DialogConversationOperationsBinding
@Inject
@ -78,6 +71,8 @@ class ConversationsListBottomDialog(
@Inject
lateinit var userManager: UserManager
lateinit var credentials: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
@ -90,6 +85,15 @@ class ConversationsListBottomDialog(
initHeaderDescription()
initItemsVisibility()
initClickListeners()
credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
}
override fun onStart() {
super.onStart()
val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from(bottomSheet as View)
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
private fun initHeaderDescription() {
@ -104,18 +108,18 @@ class ConversationsListBottomDialog(
val hasFavoritesCapability = CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "favorites")
val canModerate = conversation.canModerate(currentUser)
binding.conversationOperationRemoveFavorite.visibility = setVisibleIf(
binding.conversationRemoveFromFavorites.visibility = setVisibleIf(
hasFavoritesCapability && conversation.favorite
)
binding.conversationOperationAddFavorite.visibility = setVisibleIf(
binding.conversationAddToFavorites.visibility = setVisibleIf(
hasFavoritesCapability && !conversation.favorite
)
binding.conversationOperationMarkAsRead.visibility = setVisibleIf(
binding.conversationMarkAsRead.visibility = setVisibleIf(
conversation.unreadMessages > 0 && CapabilitiesUtilNew.canSetChatReadMarker(currentUser)
)
binding.conversationOperationMarkAsUnread.visibility = setVisibleIf(
binding.conversationMarkAsUnread.visibility = setVisibleIf(
conversation.unreadMessages <= 0 && CapabilitiesUtilNew.canMarkRoomAsUnread(currentUser)
)
@ -144,98 +148,258 @@ class ConversationsListBottomDialog(
}
private fun initClickListeners() {
binding.conversationOperationAddFavorite.setOnClickListener {
executeOperationsMenuController(OPS_CODE_ADD_FAVORITE)
binding.conversationAddToFavorites.setOnClickListener {
addConversationToFavorites()
}
binding.conversationOperationRemoveFavorite.setOnClickListener {
executeOperationsMenuController(OPS_CODE_REMOVE_FAVORITE)
binding.conversationRemoveFromFavorites.setOnClickListener {
removeConversationFromFavorites()
}
binding.conversationOperationLeave.setOnClickListener {
val dataBuilder = Data.Builder()
dataBuilder.putString(KEY_ROOM_TOKEN, conversation.token)
dataBuilder.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
val data = dataBuilder.build()
val leaveConversationWorker =
OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java).setInputData(
data
).build()
WorkManager.getInstance().enqueue(leaveConversationWorker)
dismiss()
binding.conversationMarkAsRead.setOnClickListener {
markConversationAsRead()
}
binding.conversationOperationDelete.setOnClickListener {
if (!TextUtils.isEmpty(conversation.token)) {
val bundle = Bundle()
bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
bundle.putString(KEY_ROOM_TOKEN, conversation.token)
activity.showDeleteConversationDialog(bundle)
}
dismiss()
binding.conversationMarkAsUnread.setOnClickListener {
markConversationAsUnread()
}
binding.conversationOperationRename.setOnClickListener {
executeEntryMenuController(OPS_CODE_RENAME_ROOM)
renameConversation()
}
binding.conversationOperationMarkAsRead.setOnClickListener {
executeOperationsMenuController(OPS_CODE_MARK_AS_READ)
binding.conversationOperationLeave.setOnClickListener {
leaveConversation()
}
binding.conversationOperationMarkAsUnread.setOnClickListener {
executeOperationsMenuController(OPS_CODE_MARK_AS_UNREAD)
binding.conversationOperationDelete.setOnClickListener {
deleteConversation()
}
}
private fun executeOperationsMenuController(operation: ConversationOperationEnum) {
val bundle = Bundle()
bundle.putSerializable(KEY_OPERATION_CODE, operation)
bundle.putString(KEY_ROOM_TOKEN, conversation.token)
binding.operationItemsLayout.visibility = View.GONE
dialogRouter = Conductor.attachRouter(activity, binding.root, null)
dialogRouter!!.pushController(
RouterTransaction.with(OperationsMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
private fun addConversationToFavorites() {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
ncApi.addConversationToFavorites(
credentials,
ApiUtils.getUrlForRoomFavorite(
apiVersion,
currentUser.baseUrl,
conversation.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
activity.fetchRooms()
override fun onNext(genericOverall: GenericOverall) {
activity.fetchRooms()
activity.showSnackbar(
String.format(
context.resources.getString(R.string.added_to_favorites),
conversation.displayName
)
)
dismiss()
}
override fun onError(e: Throwable) {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
dismiss()
}
override fun onComplete() {
// unused atm
}
})
}
private fun executeEntryMenuController(operation: ConversationOperationEnum) {
val bundle = Bundle()
bundle.putSerializable(KEY_OPERATION_CODE, operation)
bundle.putString(KEY_ROOM_TOKEN, conversation.token)
binding.operationItemsLayout.visibility = View.GONE
dialogRouter = Conductor.attachRouter(activity, binding.root, null)
dialogRouter!!.pushController(
// TODO refresh conversation list after EntryMenuController finished (throw event? / pass controller
// into EntryMenuController to execute fetch data... ?!)
// for example if you set a password, the dialog items should be refreshed for the next time you open it
// without to manually have to refresh the conversations list
// also see BottomSheetLockEvent ??
RouterTransaction.with(EntryMenuController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
private fun removeConversationFromFavorites() {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
ncApi.removeConversationFromFavorites(
credentials,
ApiUtils.getUrlForRoomFavorite(
apiVersion,
currentUser.baseUrl,
conversation.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(genericOverall: GenericOverall) {
activity.fetchRooms()
activity.showSnackbar(
String.format(
context.resources.getString(R.string.removed_from_favorites),
conversation.displayName
)
)
dismiss()
}
override fun onError(e: Throwable) {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
dismiss()
}
override fun onComplete() {
// unused atm
}
})
}
override fun onStart() {
super.onStart()
val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from(bottomSheet as View)
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
private fun markConversationAsUnread() {
ncApi.markRoomAsUnread(
credentials,
ApiUtils.getUrlForChatReadMarker(
chatApiVersion(),
currentUser.baseUrl,
conversation.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(genericOverall: GenericOverall) {
activity.fetchRooms()
activity.showSnackbar(
String.format(
context.resources.getString(R.string.marked_as_unread),
conversation.displayName
)
)
dismiss()
}
override fun onError(e: Throwable) {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
dismiss()
}
override fun onComplete() {
// unused atm
}
})
}
private fun markConversationAsRead() {
ncApi.setChatReadMarker(
credentials,
ApiUtils.getUrlForChatReadMarker(
chatApiVersion(),
currentUser.baseUrl,
conversation.token
),
conversation.lastMessage!!.jsonMessageId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry(1)
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(genericOverall: GenericOverall) {
activity.fetchRooms()
activity.showSnackbar(
String.format(
context.resources.getString(R.string.marked_as_read),
conversation.displayName
)
)
dismiss()
}
override fun onError(e: Throwable) {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
dismiss()
}
override fun onComplete() {
// unused atm
}
})
}
private fun renameConversation() {
if (!TextUtils.isEmpty(conversation.token)) {
dismiss()
val conversationDialog = RenameConversationDialogFragment.newInstance(
conversation.token!!,
conversation.displayName!!
)
conversationDialog.show(
activity.supportFragmentManager,
TAG
)
}
}
private fun leaveConversation() {
val dataBuilder = Data.Builder()
dataBuilder.putString(KEY_ROOM_TOKEN, conversation.token)
dataBuilder.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
val data = dataBuilder.build()
val leaveConversationWorker =
OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java).setInputData(
data
).build()
WorkManager.getInstance().enqueue(leaveConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(leaveConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
activity.showSnackbar(
String.format(
context.resources.getString(R.string.left_conversation),
conversation.displayName
)
)
}
WorkInfo.State.FAILED -> {
activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry))
}
else -> {
}
}
}
}
dismiss()
}
private fun deleteConversation() {
if (!TextUtils.isEmpty(conversation.token)) {
activity.showDeleteConversationDialog(conversation)
}
dismiss()
}
private fun chatApiVersion(): Int {
return ApiUtils.getChatApiVersion(currentUser, intArrayOf(ApiUtils.APIv1))
}
companion object {
val TAG = ConversationsListBottomDialog::class.simpleName
}
}

View file

@ -1,12 +0,0 @@
package com.nextcloud.talk.utils.remapchat
import android.os.Bundle
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.Router
data class RemapChatModel(
val router: Router,
val controllerChangeHandler: ControllerChangeHandler,
val chatControllerTag: String,
val bundle: Bundle
)

View file

@ -1,25 +0,0 @@
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View file

@ -1,25 +0,0 @@
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="16.0" android:viewportWidth="16.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="m9.236,2.166 l-3.182,3.184c-0.707,0.707 -1.038,1.618 -0.988,2.457 0.05,0.839 0.433,1.584 0.988,2.139l1.412,-1.416c-0.567,-0.567 -0.544,-1.219 0.002,-1.766l3.181,-3.182c0.525,-0.525 1.251,-0.523 1.772,-0.002 0.482,0.556 0.527,1.238 -0.004,1.77l-0.82,0.82c0.555,0.785 0.645,1.366 0.593,2.234l1.641,-1.641c1.237,-1.237 1.237,-3.365 0,-4.602 -1.236,-1.236 -3.342,-1.211 -4.596,0.004zM9.943,6.051 L8.529,7.469c0,0 0.003,0 0.004,0 0.55,0.55 0.507,1.258 -0.004,1.77l-3.182,3.182c-0.696,0.592 -1.298,0.471 -1.77,0 -0.626,-0.626 -0.5,-1.268 0,-1.768l0.85,-0.847c-0.556,-0.784 -0.648,-1.365 -0.598,-2.232l-1.666,1.666c-1.239,1.239 -1.236,3.36 0,4.596 1.235,1.235 3.362,1.236 4.598,0l3.182,-3.182c0.709,-0.708 1.04,-1.618 0.991,-2.459 -0.048,-0.84 -0.432,-1.586 -0.989,-2.141z"/>
</vector>

View file

@ -93,7 +93,7 @@
android:orientation="vertical">
<RelativeLayout
android:id="@+id/initial_relative_layout"
android:id="@+id/public_conversation_create"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -124,7 +124,7 @@
</RelativeLayout>
<RelativeLayout
android:id="@+id/secondary_relative_layout"
android:id="@+id/public_conversation_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
@ -178,40 +178,6 @@
android:textAppearance="@style/ListItem" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/join_conversation_via_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:orientation="vertical">
<ImageView
android:id="@+id/join_conversation_via_link_image"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/standard_margin"
android:background="@drawable/round_bgnd"
android:contentDescription="@null"
android:padding="@dimen/standard_half_padding"
android:src="@drawable/ic_public_black_24px"
app:tint="@color/white" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/join_conversation_via_link_image"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/nc_join_via_link"
android:textAlignment="viewStart"
android:textAppearance="@style/ListItem" />
</RelativeLayout>
<include
android:id="@+id/controller_generic_rv"
layout="@layout/controller_generic_rv" />

View file

@ -75,36 +75,36 @@
android:orientation="vertical">
<RelativeLayout
android:id="@+id/conversation_info_name"
android:layout_width="match_parent"
android:id="@+id/conversation_info_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:animateLayoutChanges="true"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/avatar_image"
android:layout_width="@dimen/avatar_size_big"
android:layout_height="@dimen/avatar_size_big"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/standard_margin"
android:contentDescription="@string/avatar"
tools:src="@drawable/account_circle_48dp" />
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/display_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:animateLayoutChanges="true"
android:visibility="gone"
tools:visibility="visible"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin">
android:layout_below="@id/avatar_image"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:textSize="@dimen/headline_text_size"
tools:text="Jane Doe" />
<ImageView
android:id="@+id/avatar_image"
android:layout_width="@dimen/avatar_size_big"
android:layout_height="@dimen/avatar_size_big"
android:layout_marginTop="@dimen/standard_margin"
android:layout_centerHorizontal="true"
android:contentDescription="@string/avatar"
tools:src="@drawable/account_circle_48dp" />
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/display_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/avatar_image"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:textSize="@dimen/headline_text_size"
tools:text="Jane Doe" />
</RelativeLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/conversation_description"
@ -119,43 +119,41 @@
android:id="@+id/description_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:autoLink="web"
tools:text="Hello world!" />
</LinearLayout>
<LinearLayout
android:id="@+id/add_to_favorites_button"
android:layout_width="match_parent"
android:id="@+id/add_to_favorites_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_half_padding"
android:visibility="gone"
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_half_padding"
android:visibility="gone"
tools:visibility="gone">
android:background="@color/transparent"
app:icon="@drawable/ic_star_black_24dp"
app:iconGravity="textStart"
app:iconSize="@dimen/sm_icon_height"
app:iconTint="@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_add_to_favorites"
android:textSize="@dimen/two_line_primary_text_size" />
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/transparent"
app:icon="@drawable/ic_star_black_24dp"
app:iconGravity="textStart"
app:iconSize="@dimen/sm_icon_height"
app:iconTint="@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_add_to_favorites"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/settings"
@ -187,251 +185,244 @@
</LinearLayout>
<LinearLayout
android:id="@+id/shared_items"
android:id="@+id/shared_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/shared_items_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical"
>
android:padding="@dimen/standard_padding"
android:text="@string/nc_shared_items"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/shared_items_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_folder_multiple_image"
app:tint="@color/grey_600" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/shared_items_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nc_shared_items"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold"
android:padding="@dimen/standard_padding"/>
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_shared_items_description"
android:textSize="@dimen/two_line_primary_text_size" />
<LinearLayout
android:id="@+id/shared_items_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_folder_multiple_image"
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_shared_items_description"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_settings"
android:id="@+id/conversation_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/conversation_settings_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical"
>
android:padding="@dimen/standard_padding"
android:text="@string/nc_conversation_settings"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/conversation_settings_title"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/conversation_info_chat_settings_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_half_margin"
android:hint="@string/nc_expire_messages">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/conversation_settings_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nc_conversation_settings"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold"
android:padding="@dimen/standard_padding"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/conversation_info_chat_settings_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginHorizontal="@dimen/standard_margin"
android:hint="@string/nc_expire_messages">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/conversation_settings_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:lines="1"
android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu"
android:text="" />
android:inputType="none"
android:lines="1"
android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu"
android:text="" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/conversation_info_expire_messages_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:text="@string/nc_expire_messages_explanation"
android:textColor="@color/disabled_text"
android:textSize="@dimen/supporting_text_text_size"/>
</LinearLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/conversation_info_expire_messages_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:text="@string/nc_expire_messages_explanation"
android:textColor="@color/disabled_text"
android:textSize="@dimen/supporting_text_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/participants"
android:id="@+id/participants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical"
android:visibility="gone"
tools:ignore="UnknownIdInLayout"
tools:visibility="visible">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/participants_list_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/standard_padding"
android:text="@string/nc_participants"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/addParticipantsAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:visibility="gone"
tools:ignore="UnknownIdInLayout"
tools:visibility="visible"
android:orientation="vertical">
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_account_plus"
app:tint="@color/grey_600" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/participants_list_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/standard_padding"
android:text="@string/nc_participants"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold" />
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_participants_add"
android:textSize="@dimen/two_line_primary_text_size" />
<LinearLayout
android:id="@+id/addParticipantsAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_account_plus"
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_participants_add"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/rv_item_conversation_info_participant" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/rv_item_conversation_info_participant" />
</LinearLayout>
<LinearLayout
android:id="@+id/danger_zone_options"
android:id="@+id/danger_zone_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/standard_padding"
android:text="@string/danger_zone"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/leaveConversationAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="vertical"
>
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_exit_to_app_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/danger_zone"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_leave"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold"
android:padding="@dimen/standard_padding"/>
<LinearLayout
android:id="@+id/leaveConversationAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_exit_to_app_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_leave"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/clearConversationHistory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_delete_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_clear_history"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/deleteConversationAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_quarter_margin"
android:orientation="horizontal"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_delete_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_delete_call"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/clearConversationHistory"
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">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_delete_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_clear_history"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/deleteConversationAction"
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">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_delete_black_24dp"
app:tint="@color/design_default_color_error" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_delete_call"
android:textColor="@color/design_default_color_error"
android:textSize="@dimen/two_line_primary_text_size" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/controller_operations_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/result_image_view"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="8dp"
android:contentDescription="@null"
android:tintMode="src_in"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerInParent="true"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:indeterminateTintMode="src_in"
android:keepScreenOn="true" />
<TextView
android:id="@+id/result_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/result_image_view"
android:layout_centerHorizontal="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="12dp"
android:maxLines="3"
android:textAlignment="center"
android:textColor="@color/colorPrimary"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ok_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/result_text_view"
android:layout_alignParentEnd="true"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:background="@color/bg_inverse"
android:text="@string/nc_ok"
android:textColor="@color/colorPrimary"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/web_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/result_text_view"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:layout_toStartOf="@id/ok_button"
android:background="@color/bg_inverse"
android:text="@string/nc_join_via_web"
android:textColor="@color/nc_darkGreen"
android:visibility="gone" />
</RelativeLayout>

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ @author Andy Scherzinger
~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_half_padding">
</LinearLayout>

View file

@ -47,7 +47,7 @@
tools:text="conversation name" />
<LinearLayout
android:id="@+id/conversation_operation_remove_favorite"
android:id="@+id/conversation_remove_from_favorites"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
@ -77,7 +77,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_add_favorite"
android:id="@+id/conversation_add_to_favorites"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
@ -107,7 +107,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_mark_as_read"
android:id="@+id/conversation_mark_as_read"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
@ -137,7 +137,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_mark_as_unread"
android:id="@+id/conversation_mark_as_unread"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"

View file

@ -27,22 +27,6 @@
android:paddingTop="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_half_padding">
<com.google.android.material.button.MaterialButton
android:id="@+id/ok_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text_input_layout"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/standard_half_margin"
android:layout_marginBottom="@dimen/standard_half_margin"
android:alpha="0.7"
android:background="@color/bg_default"
android:enabled="false"
android:text="@string/nc_proceed"
android:textAllCaps="false"
android:textColor="@color/colorPrimary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
@ -77,7 +61,6 @@
android:background="@color/transparent"
android:contentDescription="@string/nc_add_emojis"
android:src="@drawable/ic_insert_emoticon_black_24dp"
android:visibility="gone"
app:tint="@color/medium_emphasis_text"
tools:visibility="visible" />

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Marcel Hibbe
~ Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
~ Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_padding"
android:paddingTop="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_half_padding">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_half_margin"
android:layout_toStartOf="@id/smileyButton"
app:errorTextAppearance="@style/ErrorAppearance"
app:passwordToggleTint="@color/grey_600"
app:boxStrokeColor="@color/colorPrimary"
app:hintTextColor="@color/colorPrimary">
<com.nextcloud.talk.utils.EmojiTextInputEditText
android:id="@+id/text_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="textUri"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/smileyButton"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_alignBottom="@id/text_input_layout"
android:layout_alignParentEnd="true"
android:layout_marginStart="-4dp"
android:background="@color/transparent"
android:contentDescription="@string/nc_add_emojis"
android:src="@drawable/ic_insert_emoticon_black_24dp"
app:tint="@color/medium_emphasis_text"
tools:visibility="visible" />
</RelativeLayout>

View file

@ -44,7 +44,6 @@
<color name="bg_default">#121212</color>
<color name="bg_default_semitransparent">#99121212</color>
<color name="bg_inverse">@color/grey950</color>
<color name="fg_default">#FFFFFF</color>
<color name="fg_inverse">#121212</color>

View file

@ -74,7 +74,6 @@
<color name="bg_default">#FFFFFF</color>
<color name="bg_default_semitransparent">#99FFFFFF</color>
<color name="bg_inverse">@color/grey950</color>
<color name="bg_dark_mention_chips">#333333</color>
<color name="bg_message_list_incoming_bubble">#EFEFEF</color>

View file

@ -49,6 +49,8 @@ How to translate with transifex:
<string name="nc_common_set">Set</string>
<string name="nc_common_dismiss">Dismiss</string>
<string name="nc_common_error_sorry">Sorry, something went wrong!</string>
<string name="nc_common_create">Create</string>
<!-- Bottom Navigation -->
<string name="nc_settings">Settings</string>
@ -168,10 +170,6 @@ How to translate with transifex:
<string name="nc_cancel">Cancel</string>
<string name="nc_no_proxy">No proxy</string>
<string name="nc_password">Password</string>
<string name="nc_conversation_link">Conversation link</string>
<string name="nc_new_password">New password</string>
<string name="nc_wrong_password">Wrong password</string>
<string name="nc_about">About</string>
<string name="nc_privacy">Privacy</string>
<string name="nc_get_source_code">Get source code</string>
@ -190,20 +188,27 @@ How to translate with transifex:
<string name="nc_clear_history_warning">Do you really want to delete all messages in this conversation?</string>
<string name="nc_clear_history_success">All messages were deleted</string>
<string name="nc_rename">Rename conversation</string>
<string name="nc_rename_confirm">Rename</string>
<string name="nc_delete_call">Delete conversation</string>
<string name="nc_delete">Delete</string>
<string name="nc_delete_all">Delete all</string>
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_new_conversation">New conversation</string>
<string name="nc_join_via_link">Join with a link</string>
<string name="nc_list_open_conversations">List open conversations</string>
<string name="nc_join_via_web">Join via web</string>
<string name="nc_mark_as_read">Mark as read</string>
<string name="nc_mark_as_unread">Mark as unread</string>
<string name="nc_add_to_favorites">Add to favorites</string>
<string name="nc_remove_from_favorites">Remove from favorites</string>
<string name="added_to_favorites">Added conversation %1$s to favorites</string>
<string name="removed_from_favorites">Removed conversation %1$s from favorites</string>
<string name="marked_as_unread">Marked conversation %1$s as unread</string>
<string name="marked_as_read">Marked conversation %1$s as read</string>
<string name="deleted_conversation">Deleted conversation %1$s</string>
<string name="left_conversation">You left the conversation %1$s</string>
<string name="renamed_conversation">Conversation %1$s was renamed</string>
<string name="nc_forward_to_three_dots">Forward to …</string>
<!-- Open conversations -->
@ -279,16 +284,10 @@ How to translate with transifex:
<string name="nc_important_conversation">Important conversation</string>
<string name="nc_important_conversation_desc">Notifications in this conversation will override Do Not Disturb settings</string>
<!-- Bottom sheet menu -->
<string name="nc_failed_to_perform_operation">Sorry, something went wrong!</string>
<string name="nc_failed_signaling_settings">Target server does not support joining public conversations via mobile phones. You may attempt to join the conversation via web browser.</string>
<string name="nc_all_ok_operation">OK, all done!</string>
<string name="nc_ok">OK</string>
<string name="nc_call_name">Conversation name</string>
<string name="nc_proceed">Proceed</string>
<string name="nc_add_emojis">Add emojis</string>
<string name="nc_call_name_is_same">The name you entered is the same as the existing one</string>
<string name="nc_wrong_link">Conversation link is not valid</string>
<string name="nc_share_text">Join the conversation at %1$s/index.php/call/%2$s</string>
<string name="nc_share_subject">%1$s invitation</string>
<string name="nc_share_text_pass">\nPassword: %1$s</string>