From 7c2e6a2d2c68f7f575144abdcb057694392e769a Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Sun, 6 May 2018 17:12:56 -0400 Subject: [PATCH] #42 - Multi selection for messages --- .../java/repository/MessageRepositoryImpl.kt | 17 ++-- .../java/interactor/CancelDelayedMessage.kt | 2 +- .../{DeleteMessage.kt => DeleteMessages.kt} | 18 ++-- domain/src/main/java/interactor/ReceiveMms.kt | 2 +- .../main/java/repository/MessageRepository.kt | 2 +- .../main/java/common/base/QkRealmAdapter.kt | 34 +++++++ .../java/feature/compose/ComposeActivity.kt | 43 ++++----- .../main/java/feature/compose/ComposeState.kt | 3 +- .../main/java/feature/compose/ComposeView.kt | 10 +- .../java/feature/compose/ComposeViewModel.kt | 92 ++++++++++++------- .../java/feature/compose/MessagesAdapter.kt | 40 +++++--- .../drawable/ic_content_copy_black_24dp.xml | 9 ++ .../src/main/res/layout/compose_activity.xml | 3 +- presentation/src/main/res/menu/compose.xml | 25 ++++- presentation/src/main/res/values/strings.xml | 1 + 15 files changed, 203 insertions(+), 98 deletions(-) rename domain/src/main/java/interactor/{DeleteMessage.kt => DeleteMessages.kt} (65%) create mode 100644 presentation/src/main/res/drawable/ic_content_copy_black_24dp.xml diff --git a/data/src/main/java/repository/MessageRepositoryImpl.kt b/data/src/main/java/repository/MessageRepositoryImpl.kt index 6baf96d4..6c671965 100644 --- a/data/src/main/java/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/repository/MessageRepositoryImpl.kt @@ -612,14 +612,19 @@ class MessageRepositoryImpl @Inject constructor( } } - override fun deleteMessage(messageId: Long) { + override fun deleteMessages(vararg messageIds: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() - realm.where(Message::class.java).equalTo("id", messageId).findFirst()?.let { message -> - val uri = message.getUri() - realm.executeTransaction { message.deleteFromRealm() } - context.contentResolver.delete(uri, null, null) - } + + val messages = realm.where(Message::class.java) + .anyOf("id", messageIds) + .findAll() + + val uris = messages.map { it.getUri() } + + realm.executeTransaction { messages.deleteAllFromRealm() } + + uris.forEach { uri -> context.contentResolver.delete(uri, null, null) } } } diff --git a/domain/src/main/java/interactor/CancelDelayedMessage.kt b/domain/src/main/java/interactor/CancelDelayedMessage.kt index 2021ff26..33442f74 100644 --- a/domain/src/main/java/interactor/CancelDelayedMessage.kt +++ b/domain/src/main/java/interactor/CancelDelayedMessage.kt @@ -27,7 +27,7 @@ class CancelDelayedMessage @Inject constructor(private val messageRepo: MessageR override fun buildObservable(params: Long): Flowable<*> { return Flowable.just(params) .doOnNext { id -> messageRepo.cancelDelayedSms(id) } - .doOnNext { id -> messageRepo.deleteMessage(id) } + .doOnNext { id -> messageRepo.deleteMessages(id) } } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/DeleteMessage.kt b/domain/src/main/java/interactor/DeleteMessages.kt similarity index 65% rename from domain/src/main/java/interactor/DeleteMessage.kt rename to domain/src/main/java/interactor/DeleteMessages.kt index 0590cb7b..1c6b6353 100644 --- a/domain/src/main/java/interactor/DeleteMessage.kt +++ b/domain/src/main/java/interactor/DeleteMessages.kt @@ -22,19 +22,17 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class DeleteMessage @Inject constructor( +class DeleteMessages @Inject constructor( private val messageRepo: MessageRepository, private val updateBadge: UpdateBadge -) : Interactor() { +) : Interactor() { - override fun buildObservable(params: Long): Flowable<*> { - return Flowable.just(params) - .doOnNext { messageId -> - val threadId = messageRepo.getMessage(messageId)?.threadId - messageRepo.deleteMessage(messageId) - threadId?.let { messageRepo.updateConversations(it) } - } - .doOnNext { messageRepo.updateConversations(params) } // Update the conversation + data class Params(val messageIds: List, val threadId: Long? = null) + + override fun buildObservable(params: Params): Flowable<*> { + return Flowable.just(params.messageIds.toLongArray()) + .doOnNext { messageIds -> messageRepo.deleteMessages(*messageIds) } // Delete the messages + .doOnNext { params.threadId?.let { messageRepo.updateConversations(it) } } // Update the conversation .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/ReceiveMms.kt b/domain/src/main/java/interactor/ReceiveMms.kt index e34ef5e2..26c5e5f3 100644 --- a/domain/src/main/java/interactor/ReceiveMms.kt +++ b/domain/src/main/java/interactor/ReceiveMms.kt @@ -45,7 +45,7 @@ class ReceiveMms @Inject constructor( // turns out that it should be blocked, then delete it // TODO Don't store blocked messages in the first place !externalBlockingManager.shouldBlock(message.address).blockingGet().also { blocked -> - if (blocked) messageRepo.deleteMessage(message.id) + if (blocked) messageRepo.deleteMessages(message.id) } } .doOnNext { message -> messageRepo.updateConversations(message.threadId) } // Update the conversation diff --git a/domain/src/main/java/repository/MessageRepository.kt b/domain/src/main/java/repository/MessageRepository.kt index 87bab362..2bf2323d 100644 --- a/domain/src/main/java/repository/MessageRepository.kt +++ b/domain/src/main/java/repository/MessageRepository.kt @@ -120,7 +120,7 @@ interface MessageRepository { fun markDeliveryFailed(id: Long, resultCode: Int) - fun deleteMessage(messageId: Long) + fun deleteMessages(vararg messageIds: Long) fun deleteConversations(vararg threadIds: Long) diff --git a/presentation/src/main/java/common/base/QkRealmAdapter.kt b/presentation/src/main/java/common/base/QkRealmAdapter.kt index 86f1eccd..092af102 100644 --- a/presentation/src/main/java/common/base/QkRealmAdapter.kt +++ b/presentation/src/main/java/common/base/QkRealmAdapter.kt @@ -3,6 +3,8 @@ package common.base import android.support.v7.widget.RecyclerView import android.view.View import common.util.extensions.setVisible +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import io.realm.OrderedRealmCollection import io.realm.RealmList import io.realm.RealmModel @@ -21,6 +23,38 @@ abstract class QkRealmAdapter : RealmRecyclerViewAdapter> = BehaviorSubject.create() + + private val selection = mutableListOf() + + /** + * Toggles the selected state for a particular view + * + * If we are currently in selection mode (we have an active selection), then the state will + * toggle. If we are not in selection mode, then we will only toggle if [force] + */ + protected fun toggleSelection(id: Long, force: Boolean = true): Boolean { + if (!force && selection.isEmpty()) return false + + when (selection.contains(id)) { + true -> selection.remove(id) + false -> selection.add(id) + } + + selectionChanges.onNext(selection) + return true + } + + protected fun isSelected(id: Long): Boolean { + return selection.contains(id) + } + + fun clearSelection() { + selection.clear() + selectionChanges.onNext(selection) + notifyDataSetChanged() + } + override fun updateData(data: OrderedRealmCollection?) { removeListener(getData()) addListener(data) diff --git a/presentation/src/main/java/feature/compose/ComposeActivity.kt b/presentation/src/main/java/feature/compose/ComposeActivity.kt index 11a4cb01..0b76384b 100644 --- a/presentation/src/main/java/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/feature/compose/ComposeActivity.kt @@ -30,7 +30,6 @@ import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable -import common.QkDialog import common.base.QkThemedActivity import common.util.extensions.autoScrollToStart import common.util.extensions.setBackgroundTint @@ -57,12 +56,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val chipSelectedIntent: Subject by lazy { contactsAdapter.contactSelected } override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } - override val callIntent: Subject = PublishSubject.create() - override val infoIntent: Subject = PublishSubject.create() + override val optionsItemIntent: Subject = PublishSubject.create() override val messageClickIntent: Subject by lazy { messageAdapter.clicks } - override val messageLongClickIntent: Subject by lazy { messageAdapter.longClicks } + override val messagesSelectedIntent by lazy { messageAdapter.selectionChanges } override val cancelSendingIntent: Subject by lazy { messageAdapter.cancelSending } - override val menuItemIntent: Subject by lazy { dialog.adapter.menuItemClicks } override val attachmentDeletedIntent: Subject by lazy { attachmentAdapter.attachmentDeleted } override val textChangedIntent by lazy { message.textChanges() } override val attachIntent by lazy { attach.clicks() } @@ -70,10 +67,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val galleryIntent by lazy { gallery.clicks() } override val inputContentIntent by lazy { message.inputContentSelected } override val sendIntent by lazy { send.clicks() } + override val backPressedIntent: Subject = PublishSubject.create() @Inject lateinit var chipsAdapter: ChipsAdapter @Inject lateinit var contactsAdapter: ContactAdapter - @Inject lateinit var dialog: QkDialog @Inject lateinit var messageAdapter: MessagesAdapter @Inject lateinit var attachmentAdapter: AttachmentAdapter @@ -173,6 +170,11 @@ class ComposeActivity : QkThemedActivity(), ComposeView { threadId.onNext(state.selectedConversation) + title = when (state.selectedMessages) { + 0 -> state.conversationtitle + else -> getString(R.string.compose_title_selected, state.selectedMessages) + } + toolbarTitle.setVisible(!state.editingMode) chips.setVisible(state.editingMode) contacts.setVisible(state.contactsVisible) @@ -182,8 +184,11 @@ class ComposeActivity : QkThemedActivity(), ComposeView { if (state.editingMode && chips.adapter == null) chips.adapter = chipsAdapter if (state.editingMode && contacts.adapter == null) contacts.adapter = contactsAdapter - toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode - toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode + toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0 + toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0 + toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages == 1 + toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && state.selectedMessages > 0 + toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1 if (chipsAdapter.data.isEmpty() && state.selectedContacts.isNotEmpty()) { message.showKeyboard() @@ -200,21 +205,18 @@ class ComposeActivity : QkThemedActivity(), ComposeView { camera.setVisible(state.attaching) gallery.setVisible(state.attaching) - if (title != state.title) title = state.title - counter.text = state.remaining counter.setVisible(counter.text.isNotBlank()) send.isEnabled = state.canSend } - override fun setDraft(draft: String) { - message.setText(draft) + override fun clearSelection() { + messageAdapter.clearSelection() } - override fun showMenu(menuItems: List) { - dialog.adapter.data = menuItems - dialog.show(this) + override fun setDraft(draft: String) { + message.setText(draft) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -223,13 +225,12 @@ class ComposeActivity : QkThemedActivity(), ComposeView { } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.call -> callIntent.onNext(Unit) - R.id.info -> infoIntent.onNext(Unit) - else -> return super.onOptionsItemSelected(item) - } - + optionsItemIntent.onNext(item.itemId) return true } + override fun onBackPressed() { + backPressedIntent.onNext(Unit) + } + } \ No newline at end of file diff --git a/presentation/src/main/java/feature/compose/ComposeState.kt b/presentation/src/main/java/feature/compose/ComposeState.kt index 4f0eb6c5..25fc76ab 100644 --- a/presentation/src/main/java/feature/compose/ComposeState.kt +++ b/presentation/src/main/java/feature/compose/ComposeState.kt @@ -30,8 +30,9 @@ data class ComposeState( val contactsVisible: Boolean = false, val selectedConversation: Long = 0, val selectedContacts: List = ArrayList(), - val title: String = "", + val conversationtitle: String = "", val messages: Pair>? = null, + val selectedMessages: Int = 0, val attachments: List = ArrayList(), val attaching: Boolean = false, val remaining: String = "", diff --git a/presentation/src/main/java/feature/compose/ComposeView.kt b/presentation/src/main/java/feature/compose/ComposeView.kt index 79e4ea85..e9ef292c 100644 --- a/presentation/src/main/java/feature/compose/ComposeView.kt +++ b/presentation/src/main/java/feature/compose/ComposeView.kt @@ -19,7 +19,6 @@ package feature.compose import android.support.v13.view.inputmethod.InputContentInfoCompat -import common.MenuItem import common.base.QkView import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -35,12 +34,10 @@ interface ComposeView : QkView { val chipSelectedIntent: Subject val chipDeletedIntent: Subject val menuReadyIntent: Observable - val callIntent: Subject - val infoIntent: Subject + val optionsItemIntent: Observable val messageClickIntent: Subject - val messageLongClickIntent: Subject + val messagesSelectedIntent: Observable> val cancelSendingIntent: Subject - val menuItemIntent: Subject val attachmentDeletedIntent: Subject val textChangedIntent: Observable val attachIntent: Observable @@ -49,7 +46,8 @@ interface ComposeView : QkView { val inputContentIntent: Observable val sendIntent: Observable - fun showMenu(menuItems: List) + fun clearSelection() fun setDraft(draft: String) + val backPressedIntent: Observable } \ No newline at end of file diff --git a/presentation/src/main/java/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/feature/compose/ComposeViewModel.kt index f5112c21..61118bfa 100644 --- a/presentation/src/main/java/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/feature/compose/ComposeViewModel.kt @@ -29,7 +29,6 @@ import com.mlsdev.rximagepicker.Sources import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable -import common.MenuItem import common.Navigator import common.base.QkViewModel import common.util.ClipboardUtils @@ -38,7 +37,7 @@ import common.util.filter.ContactFilter import injection.appComponent import interactor.CancelDelayedMessage import interactor.ContactSync -import interactor.DeleteMessage +import interactor.DeleteMessages import interactor.MarkRead import interactor.RetrySending import interactor.SendMessage @@ -79,7 +78,7 @@ class ComposeViewModel(intent: Intent) : QkViewModel( @Inject lateinit var sendMessage: SendMessage @Inject lateinit var retrySending: RetrySending @Inject lateinit var markRead: MarkRead - @Inject lateinit var deleteMessage: DeleteMessage + @Inject lateinit var deleteMessages: DeleteMessages private var sharedText: String = "" private val attachments: Subject> = BehaviorSubject.createDefault(ArrayList()) @@ -89,10 +88,6 @@ class ComposeViewModel(intent: Intent) : QkViewModel( private val conversation: Observable private val messages: Observable> - private val menuCopy by lazy { MenuItem(context.getString(R.string.compose_menu_copy), 0) } - private val menuForward by lazy { MenuItem(context.getString(R.string.compose_menu_forward), 1) } - private val menuDelete by lazy { MenuItem(context.getString(R.string.compose_menu_delete), 2) } - init { appComponent.inject(this) @@ -178,7 +173,7 @@ class ComposeViewModel(intent: Intent) : QkViewModel( disposables += conversation .distinctUntilChanged { conversation -> conversation.getTitle() } .subscribe { conversation -> - newState { it.copy(title = conversation.getTitle()) } + newState { it.copy(conversationtitle = conversation.getTitle()) } } disposables += attachments @@ -276,7 +271,8 @@ class ComposeViewModel(intent: Intent) : QkViewModel( .subscribe { newState { it.copy() } } // Open the phone dialer if the call button is clicked - view.callIntent + view.optionsItemIntent + .filter { it == R.id.call } .withLatestFrom(conversation, { _, conversation -> conversation }) .map { conversation -> conversation.recipients.first() } .map { recipient -> recipient.address } @@ -284,11 +280,45 @@ class ComposeViewModel(intent: Intent) : QkViewModel( .subscribe { address -> navigator.makePhoneCall(address) } // Open the conversation settings if info button is clicked - view.infoIntent + view.optionsItemIntent + .filter { it == R.id.info } .withLatestFrom(conversation, { _, conversation -> conversation }) .autoDisposable(view.scope()) .subscribe { conversation -> navigator.showConversationInfo(conversation.id) } + // Copy the message contents + view.optionsItemIntent + .filter { it == R.id.copy } + .withLatestFrom(view.messagesSelectedIntent, { _, messages -> + messages?.firstOrNull()?.let { messageRepo.getMessage(it) }?.let { message -> + ClipboardUtils.copy(context, message.getText()) + context.makeToast(R.string.toast_copied) + } + }) + .autoDisposable(view.scope()) + .subscribe { view.clearSelection() } + + // Delete the messages + view.optionsItemIntent + .filter { it == R.id.delete } + .withLatestFrom(view.messagesSelectedIntent, conversation, { _, messages, conversation -> + deleteMessages.execute(DeleteMessages.Params(messages, conversation.id)) + }) + .autoDisposable(view.scope()) + .subscribe { view.clearSelection() } + + // Forward the message + view.optionsItemIntent + .filter { it == R.id.forward } + .withLatestFrom(view.messagesSelectedIntent, { _, messages -> + messages?.firstOrNull()?.let { messageRepo.getMessage(it) }?.let { message -> + val images = message.parts.filter { it.isImage() }.mapNotNull { it.getUri() } + navigator.showCompose(message.body, images) + } + }) + .autoDisposable(view.scope()) + .subscribe { view.clearSelection() } + // Retry sending view.messageClickIntent .filter { message -> message.isFailedMessage() } @@ -296,10 +326,11 @@ class ComposeViewModel(intent: Intent) : QkViewModel( .autoDisposable(view.scope()) .subscribe() - // Show menu - view.messageLongClickIntent + // Update the State when the message selected count changes + view.messagesSelectedIntent + .map { selection -> selection.size } .autoDisposable(view.scope()) - .subscribe { view.showMenu(listOf(menuCopy, menuForward, menuDelete)) } + .subscribe { messages -> newState { it.copy(selectedMessages = messages, editingMode = false) } } // Cancel sending a message view.cancelSendingIntent @@ -307,26 +338,6 @@ class ComposeViewModel(intent: Intent) : QkViewModel( .autoDisposable(view.scope()) .subscribe { message -> cancelMessage.execute(message.id) } - // Handle long-press menu item click - view.menuItemIntent - .withLatestFrom(view.messageLongClickIntent, { actionId, message -> - when (actionId) { - menuCopy.actionId -> { - ClipboardUtils.copy(context, message.getText()) - context.makeToast(R.string.toast_copied) - } - - menuForward.actionId -> { - val images = message.parts.filter { it.isImage() }.mapNotNull { it.getUri() } - navigator.showCompose(message.body, images) - } - - menuDelete.actionId -> deleteMessage.execute(message.id) - } - }) - .autoDisposable(view.scope()) - .subscribe() - // Save draft when the activity goes into the background view.activityVisibleIntent .filter { visible -> !visible } @@ -446,6 +457,21 @@ class ComposeViewModel(intent: Intent) : QkViewModel( }) .autoDisposable(view.scope()) .subscribe() + + // Navigate back + view.optionsItemIntent + .filter { it == android.R.id.home } + .map { Unit } + .mergeWith(view.backPressedIntent) + .withLatestFrom(state, { _, state -> + when { + state.selectedMessages > 0 -> view.clearSelection() + else -> newState { it.copy(hasError = true) } + } + }) + .autoDisposable(view.scope()) + .subscribe() + } } \ No newline at end of file diff --git a/presentation/src/main/java/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/feature/compose/MessagesAdapter.kt index a7cbf60d..470980bf 100644 --- a/presentation/src/main/java/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/feature/compose/MessagesAdapter.kt @@ -32,8 +32,8 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar -import com.jakewharton.rxbinding2.view.RxView import com.jakewharton.rxbinding2.view.clicks +import com.jakewharton.rxbinding2.view.longClicks import com.moez.QKSMS.R import common.Navigator import common.base.QkRealmAdapter @@ -80,7 +80,6 @@ class MessagesAdapter @Inject constructor( } val clicks: Subject = PublishSubject.create() - val longClicks: Subject = PublishSubject.create() val cancelSending: Subject = PublishSubject.create() var data: Pair>? = null @@ -106,7 +105,7 @@ class MessagesAdapter @Inject constructor( private val layoutInflater = LayoutInflater.from(context) private val contactCache = ContactCache() - private val selected = HashMap() + private val expanded = HashMap() private val disposables = CompositeDisposable() private var theme = colors.theme @@ -143,6 +142,9 @@ class MessagesAdapter @Inject constructor( .subscribe { color -> view.body.setBackgroundTint(color) } } + disposables += colors.ripple + .subscribe { res -> view.setBackgroundResource(res) } + view.body.forwardTouches(view) return QkViewHolder(view) @@ -159,12 +161,24 @@ class MessagesAdapter @Inject constructor( val next = if (position == itemCount - 1) null else getItem(position + 1) val view = viewHolder.itemView - RxView.clicks(view).subscribe { - clicks.onNext(message) - selected[message.id] = view.status.visibility != View.VISIBLE - bindStatus(viewHolder, position) + view.clicks().subscribe { + when (toggleSelection(message.id, false)) { + true -> view.isSelected = isSelected(message.id) + false -> { + clicks.onNext(message) + expanded[message.id] = view.status.visibility != View.VISIBLE + bindStatus(viewHolder, message, next) + } + } } - RxView.longClicks(view).subscribe { longClicks.onNext(message) } + view.longClicks().subscribe { + toggleSelection(message.id) + view.isSelected = isSelected(message.id) + } + + + // Update the selected state + view.isSelected = isSelected(message.id) // Bind the cancel view @@ -191,7 +205,7 @@ class MessagesAdapter @Inject constructor( // Bind the message status - bindStatus(viewHolder, position) + bindStatus(viewHolder, message, next) // Bind the timestamp @@ -284,9 +298,7 @@ class MessagesAdapter @Inject constructor( } } - private fun bindStatus(viewHolder: QkViewHolder, position: Int) { - val message = getItem(position)!! - val next = if (position == itemCount - 1) null else getItem(position + 1) + private fun bindStatus(viewHolder: QkViewHolder, message: Message, next: Message?) { val view = viewHolder.itemView val age = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - message.date) @@ -301,10 +313,10 @@ class MessagesAdapter @Inject constructor( } view.status.setVisible(when { - selected[message.id] == true -> true + expanded[message.id] == true -> true message.isSending() -> true message.isFailedMessage() -> true - selected[message.id] == false -> false + expanded[message.id] == false -> false conversation?.recipients?.size ?: 0 > 1 && !message.isMe() && next?.compareSender(message) != true -> true message.isDelivered() && next?.isDelivered() != true && age <= TIMESTAMP_THRESHOLD -> true else -> false diff --git a/presentation/src/main/res/drawable/ic_content_copy_black_24dp.xml b/presentation/src/main/res/drawable/ic_content_copy_black_24dp.xml new file mode 100644 index 00000000..8a894a3b --- /dev/null +++ b/presentation/src/main/res/drawable/ic_content_copy_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml index 6f63024d..12c25dea 100644 --- a/presentation/src/main/res/layout/compose_activity.xml +++ b/presentation/src/main/res/layout/compose_activity.xml @@ -1,5 +1,4 @@ - -