diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index 6db3555bc9b..a318d20d796 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -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 + newUserItemList.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) { + val firstSource: String = o1.model.source!! + val secondSource: String = o2.model.source!! + if (firstSource == secondSource) { + return@sort firstName.compareTo(secondName, ignoreCase = true) } - 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 - } + // First users + if ("users" == firstSource) { + return@sort -1 + } else if ("users" == secondSource) { + return@sort 1 + } - // Then circles - if ("circles" == firstSource) { - return@sort -1 - } else if ("circles" == secondSource) { - return@sort 1 - } + // Then groups + if ("groups" == firstSource) { + return@sort -1 + } else if ("groups" == secondSource) { + return@sort 1 + } - // Otherwise fall back to name sorting - return@sort firstName.compareTo(secondName, ignoreCase = true) + // Then circles + if ("circles" == firstSource) { + return@sort -1 + } else if ("circles" == secondSource) { + return@sort 1 } - firstName.compareTo(secondName, ignoreCase = true) + + // 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<*> -> + contactItems?.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 + 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 - } else { - binding.initialRelativeLayout.visibility = View.GONE - binding.secondaryRelativeLayout.visibility = View.VISIBLE - } - } - - private fun toggleConversationViaLinkVisibility(isPublicCall: Boolean) { - if (isPublicCall) { - binding.joinConversationViaLink.visibility = View.GONE - updateGroupParticipantSelection() + binding.publicConversationCreate.visibility = View.VISIBLE + binding.publicConversationInfo.visibility = View.GONE } else { - binding.joinConversationViaLink.visibility = View.VISIBLE + binding.publicConversationCreate.visibility = View.GONE + binding.publicConversationInfo.visibility = View.VISIBLE + binding.listOpenConversations.visibility = View.GONE } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/ConversationOperationEnum.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/ConversationOperationEnum.kt deleted file mode 100644 index 1e5979d7b55..00000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/ConversationOperationEnum.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt deleted file mode 100644 index 391e0a2f9cb..00000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt deleted file mode 100644 index e6b083710d0..00000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt +++ /dev/null @@ -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, "") - } -} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt new file mode 100644 index 00000000000..142dc6e2745 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/CreateConversationDialogFragment.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt new file mode 100644 index 00000000000..6c0d3f08c9a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt new file mode 100644 index 00000000000..4e085b55675 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt @@ -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> +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt new file mode 100644 index 00000000000..6db12b27fc1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt @@ -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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt new file mode 100644 index 00000000000..c35f31ced88 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt @@ -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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/RenameConversationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/RenameConversationViewModel.kt new file mode 100644 index 00000000000..ff992d70750 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/RenameConversationViewModel.kt @@ -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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 082af2eaa61..66666301158 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -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) { _, _ -> + } - private fun isInternalUserEqualsCurrentUser(currentUser: User?, conversationMenuBundle: Bundle?): Boolean { - return currentUser != null && conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) == currentUser.id + 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 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) { diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 5949effb21f..9d93b2a5514 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -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) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index b2a26758cff..596fb30d232 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -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 } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java index 4417b096c73..407ce8bca2b 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java @@ -25,11 +25,9 @@ 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 Result doWork() { } } - eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), - EventStatus.EventType.PARTICIPANTS_UPDATE, - true)); return Result.success(); } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ContactsBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ContactsBottomDialog.kt deleted file mode 100644 index 0e94c877cd4..00000000000 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ContactsBottomDialog.kt +++ /dev/null @@ -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" - } -} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt index 41ccf6d9840..b64e7d61981 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt @@ -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) + 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 + } + + 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 + } + }) + } - dialogRouter!!.pushController( - RouterTransaction.with(OperationsMenuController(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 + } + }) + } - activity.fetchRooms() + 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 executeEntryMenuController(operation: ConversationOperationEnum) { - val bundle = Bundle() - bundle.putSerializable(KEY_OPERATION_CODE, operation) - bundle.putString(KEY_ROOM_TOKEN, conversation.token) + 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 + } + }) + } - binding.operationItemsLayout.visibility = View.GONE + 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 -> { + } + } + } + } - dialogRouter = Conductor.attachRouter(activity, binding.root, null) + dismiss() + } - dialogRouter!!.pushController( + private fun deleteConversation() { + if (!TextUtils.isEmpty(conversation.token)) { + activity.showDeleteConversationDialog(conversation) + } - // 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 ?? + dismiss() + } - RouterTransaction.with(EntryMenuController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + private fun chatApiVersion(): Int { + return ApiUtils.getChatApiVersion(currentUser, intArrayOf(ApiUtils.APIv1)) } - 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 { + val TAG = ConversationsListBottomDialog::class.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/remapchat/RemapChatModel.kt b/app/src/main/java/com/nextcloud/talk/utils/remapchat/RemapChatModel.kt deleted file mode 100644 index 6f4e42e4660..00000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/remapchat/RemapChatModel.kt +++ /dev/null @@ -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 -) diff --git a/app/src/main/res/drawable/ic_check_circle_black_24dp.xml b/app/src/main/res/drawable/ic_check_circle_black_24dp.xml deleted file mode 100644 index 00195584ded..00000000000 --- a/app/src/main/res/drawable/ic_check_circle_black_24dp.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/drawable/ic_public_black_24px.xml b/app/src/main/res/drawable/ic_public_black_24px.xml deleted file mode 100644 index 49c84ae8817..00000000000 --- a/app/src/main/res/drawable/ic_public_black_24px.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/layout/activity_contacts.xml b/app/src/main/res/layout/activity_contacts.xml index 6d7d87207a7..f318fce3026 100644 --- a/app/src/main/res/layout/activity_contacts.xml +++ b/app/src/main/res/layout/activity_contacts.xml @@ -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" /> diff --git a/app/src/main/res/layout/activity_conversation_info.xml b/app/src/main/res/layout/activity_conversation_info.xml index f25f8a84399..f92306cca54 100644 --- a/app/src/main/res/layout/activity_conversation_info.xml +++ b/app/src/main/res/layout/activity_conversation_info.xml @@ -75,36 +75,36 @@ android:orientation="vertical"> <RelativeLayout - android:id="@+id/conversation_info_name" - android:layout_width="match_parent" - 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: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_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" /> + <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_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: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:background="@color/transparent" - app:icon="@drawable/ic_star_black_24dp" - app:iconGravity="textStart" - app:iconSize="@dimen/sm_icon_height" - app:iconTint="@color/grey_600" /> + 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.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"/> - - <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> + 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 - 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.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.textview.MaterialTextView - android:id="@+id/conversation_settings_title" - 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" + <com.google.android.material.textfield.MaterialAutoCompleteTextView + android:id="@+id/conversation_settings_dropdown" 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"> + android:inputType="none" + android:lines="1" + android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu" + android:text="" /> - <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="" /> + </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> + + <LinearLayout + 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: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.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"/> + 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> + <LinearLayout - android:id="@+id/participants" + 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: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_exit_to_app_black_24dp" + app:tint="@color/design_default_color_error" /> <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: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" /> + 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/danger_zone_options" + <LinearLayout + android:id="@+id/clearConversationHistory" 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_delete_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_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/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> </LinearLayout> </ScrollView> </LinearLayout> diff --git a/app/src/main/res/layout/controller_operations_menu.xml b/app/src/main/res/layout/controller_operations_menu.xml deleted file mode 100644 index b5e63641908..00000000000 --- a/app/src/main/res/layout/controller_operations_menu.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/layout/dialog_bottom_contacts.xml b/app/src/main/res/layout/dialog_bottom_contacts.xml deleted file mode 100644 index cbccc2e9300..00000000000 --- a/app/src/main/res/layout/dialog_bottom_contacts.xml +++ /dev/null @@ -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> diff --git a/app/src/main/res/layout/dialog_conversation_operations.xml b/app/src/main/res/layout/dialog_conversation_operations.xml index 7e05f329fac..65df06d174a 100644 --- a/app/src/main/res/layout/dialog_conversation_operations.xml +++ b/app/src/main/res/layout/dialog_conversation_operations.xml @@ -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" diff --git a/app/src/main/res/layout/controller_entry_menu.xml b/app/src/main/res/layout/dialog_create_conversation.xml similarity index 79% rename from app/src/main/res/layout/controller_entry_menu.xml rename to app/src/main/res/layout/dialog_create_conversation.xml index 4c851cc3f0c..6aafea7c235 100644 --- a/app/src/main/res/layout/controller_entry_menu.xml +++ b/app/src/main/res/layout/dialog_create_conversation.xml @@ -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" /> diff --git a/app/src/main/res/layout/dialog_rename_conversation.xml b/app/src/main/res/layout/dialog_rename_conversation.xml new file mode 100644 index 00000000000..27ed22aca4d --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_conversation.xml @@ -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> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 4d64f32ef71..7a1e8a6b85d 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -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> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2a33bdb2a2a..5d7ec0cf371 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fa0ef657e9..07e9ef70f7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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>