diff --git a/app/build.gradle b/app/build.gradle index 3d91c9c5..6f38ab32 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,7 +63,7 @@ android { } dependencies { - implementation 'com.github.SimpleMobileTools:Simple-Commons:af11ea2e30' + implementation 'com.github.SimpleMobileTools:Simple-Commons:8a01bdee8c' implementation 'org.greenrobot:eventbus:3.3.1' implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61' implementation 'com.github.tibbi:android-smsmms:3581774c39' diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt index a736c0fe..c4ab1ac6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt @@ -257,7 +257,7 @@ class MainActivity : SimpleActivity() { } cachedConversations.forEach { cachedConv -> - val conv = conversations.find { it.threadId == cachedConv.threadId && !cachedConv.areContentsTheSame(it) } + val conv = conversations.find { it.threadId == cachedConv.threadId && !Conversation.areContentsTheSame(cachedConv, it) } if (conv != null) { val conversation = conv.copy(date = maxOf(cachedConv.date, conv.date)) conversationsDB.insertOrUpdate(conversation) @@ -299,7 +299,7 @@ class MainActivity : SimpleActivity() { val currAdapter = conversations_list.adapter if (currAdapter == null) { hideKeyboard() - ConversationsAdapter(this, sortedConversations, conversations_list) { + ConversationsAdapter(this, conversations_list) { Intent(this, ThreadActivity::class.java).apply { val conversation = it as Conversation putExtra(THREAD_ID, conversation.threadId) @@ -308,6 +308,7 @@ class MainActivity : SimpleActivity() { } }.apply { conversations_list.adapter = this + updateConversations(sortedConversations) } if (areSystemAnimationsEnabled) { @@ -315,12 +316,13 @@ class MainActivity : SimpleActivity() { } } else { try { - (currAdapter as ConversationsAdapter).updateConversations(sortedConversations) - if (currAdapter.conversations.isEmpty()) { - conversations_fastscroller.beGone() - no_conversations_placeholder.text = getString(R.string.no_conversations_found) - no_conversations_placeholder.beVisible() - no_conversations_placeholder_2.beVisible() + (currAdapter as ConversationsAdapter).updateConversations(sortedConversations) { + if (currAdapter.currentList.isEmpty()) { + conversations_fastscroller.beGone() + no_conversations_placeholder.text = getString(R.string.no_conversations_found) + no_conversations_placeholder.beVisible() + no_conversations_placeholder_2.beVisible() + } } } catch (ignored: Exception) { } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt index 3de1380e..f1a9d0d2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -55,6 +55,7 @@ import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.models.* +import com.simplemobiletools.smsmessenger.models.ThreadItem.* import kotlinx.android.synthetic.main.activity_thread.* import kotlinx.android.synthetic.main.item_selected_contact.view.* import kotlinx.android.synthetic.main.layout_attachment_picker.* @@ -116,8 +117,8 @@ class ThreadActivity : SimpleActivity() { bus = EventBus.getDefault() bus!!.register(this) - handlePermission(PERMISSION_READ_PHONE_STATE) { - if (it) { + handlePermission(PERMISSION_READ_PHONE_STATE) { granted -> + if (granted) { setupButtons() setupCachedMessages { val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L) @@ -263,7 +264,7 @@ class ThreadActivity : SimpleActivity() { } private fun setupThread() { - val privateCursor = getMyContactsCursor(false, true) + val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) ensureBackgroundThread { privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) @@ -329,33 +330,40 @@ class ThreadActivity : SimpleActivity() { } } + private fun getOrCreateThreadAdapter(): ThreadAdapter { + var currAdapter = thread_messages_list.adapter + if (currAdapter == null) { + currAdapter = ThreadAdapter( + activity = this, + recyclerView = thread_messages_list, + itemClick = { handleItemClick(it) }, + deleteMessages = { deleteMessages(it) } + ) + + thread_messages_list.adapter = currAdapter + thread_messages_list.endlessScrollListener = object : MyRecyclerView.EndlessScrollListener { + override fun updateBottom() {} + + override fun updateTop() { + fetchNextMessages() + } + } + } + return currAdapter as ThreadAdapter + } + private fun setupAdapter() { threadItems = getThreadItems() runOnUiThread { refreshMenuItems() - - val currAdapter = thread_messages_list.adapter - if (currAdapter == null) { - ThreadAdapter( - activity = this, - messages = threadItems, - recyclerView = thread_messages_list, - itemClick = { handleItemClick(it) }, - onThreadIdUpdate = { threadId = it } - ).apply { - thread_messages_list.adapter = this + getOrCreateThreadAdapter().apply { + val scrollPosition = if (currentList.lastOrNull() != threadItems.lastOrNull()) { + threadItems.lastIndex + } else { + -1 } - - thread_messages_list.endlessScrollListener = object : MyRecyclerView.EndlessScrollListener { - override fun updateBottom() {} - - override fun updateTop() { - fetchNextMessages() - } - } - } else { - (currAdapter as ThreadAdapter).updateMessages(threadItems) + updateMessages(threadItems, scrollPosition) } } @@ -387,6 +395,13 @@ class ThreadActivity : SimpleActivity() { } } + private fun scrollToBottom() { + val position = getOrCreateThreadAdapter().currentList.lastIndex + if (position >= 0) { + thread_messages_list.smoothScrollToPosition(position) + } + } + private fun handleItemClick(any: Any) { when { any is Message && any.isScheduled -> showScheduledMessageInfo(any) @@ -394,8 +409,53 @@ class ThreadActivity : SimpleActivity() { } } + private fun deleteMessages(messagesToRemove: List) { + val deletePosition = threadItems.indexOf(messagesToRemove.first()) + messages.removeAll(messagesToRemove.toSet()) + threadItems = getThreadItems() + + runOnUiThread { + if (messages.isEmpty()) { + finish() + } else { + getOrCreateThreadAdapter().apply { + updateMessages(threadItems, scrollPosition = deletePosition) + finishActMode() + } + } + } + + messagesToRemove.forEach { message -> + val messageId = message.id + if (message.isScheduled) { + deleteScheduledMessage(messageId) + cancelScheduleSendPendingIntent(messageId) + } else { + deleteMessage(messageId, message.isMMS) + } + } + updateLastConversationMessage(threadId) + + // move all scheduled messages to a temporary thread when there are no real messages left + if (messages.isNotEmpty() && messages.all { it.isScheduled }) { + val scheduledMessage = messages.last() + val fakeThreadId = generateRandomId() + createTemporaryThread(scheduledMessage, fakeThreadId) + updateScheduledMessagesThreadId(messages, fakeThreadId) + threadId = fakeThreadId + } + } + private fun fetchNextMessages() { if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) { + if (allMessagesFetched) { + getOrCreateThreadAdapter().apply { + val newList = currentList.toMutableList().apply { + removeAll { it is ThreadLoading } + } + updateMessages(newMessages = newList as ArrayList, scrollPosition = 0) + } + } return } @@ -414,16 +474,13 @@ class ThreadActivity : SimpleActivity() { .filter { message -> !messages.contains(message) } messages.addAll(0, olderMessages) - threadItems = getThreadItems() - allMessagesFetched = olderMessages.size < MESSAGES_LIMIT || olderMessages.isEmpty() + threadItems = getThreadItems() runOnUiThread { loadingOlderMessages = false val itemAtRefreshIndex = threadItems.indexOfFirst { it == firstItem } - (thread_messages_list.adapter as ThreadAdapter).apply { - updateMessages(threadItems, itemAtRefreshIndex) - } + getOrCreateThreadAdapter().updateMessages(threadItems, itemAtRefreshIndex) } } } @@ -433,7 +490,9 @@ class ThreadActivity : SimpleActivity() { val textColor = getProperTextColor() thread_send_message.apply { setTextColor(textColor) - compoundDrawables.forEach { it?.applyColorFilter(textColor) } + compoundDrawables.forEach { + it?.applyColorFilter(textColor) + } } confirm_manage_contacts.applyColorFilter(textColor) @@ -460,7 +519,11 @@ class ThreadActivity : SimpleActivity() { thread_send_message.isClickable = false thread_type_message.onTextChangeListener { checkSendMessageAvailability() - val messageString = if (config.useSimpleCharacters) it.normalizeString() else it + val messageString = if (config.useSimpleCharacters) { + it.normalizeString() + } else { + it + } val messageLength = SmsMessage.calculateLength(messageString, false) thread_character_counter.text = "${messageLength[2]}/${messageLength[0]}" } @@ -819,6 +882,11 @@ class ThreadActivity : SimpleActivity() { bus?.post(Events.RefreshMessages()) } + if (!allMessagesFetched && messages.size >= MESSAGES_LIMIT) { + val threadLoading = ThreadLoading(generateRandomId()) + items.add(0, threadLoading) + } + return items } @@ -988,6 +1056,7 @@ class ThreadActivity : SimpleActivity() { showErrorToast(getString(R.string.unknown_error_occurred)) return } + scrollToBottom() text = removeDiacriticsIfNeeded(text) @@ -1017,22 +1086,18 @@ class ThreadActivity : SimpleActivity() { threadId = message.threadId createTemporaryThread(message, message.threadId) } - messagesDB.insertOrUpdate(message) val conversation = conversationsDB.getConversationWithThreadId(threadId) if (conversation != null) { val nowSeconds = (System.currentTimeMillis() / 1000).toInt() - conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds)) + conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds, snippet = message.body)) } scheduleMessage(message) + insertOrUpdateMessage(message) runOnUiThread { clearCurrentMessage() hideScheduleSendUi() scheduledMessage = null - - if (!refreshedSinceSent) { - refreshMessages() - } } } } catch (e: Exception) { @@ -1047,11 +1112,16 @@ class ThreadActivity : SimpleActivity() { try { refreshedSinceSent = false sendMessage(text, addresses, subscriptionId, attachments) + ensureBackgroundThread { + val messageIds = messages.map { it.id } + val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds } + if (message != null) { + maybeUpdateMessageSubId(message) + insertOrUpdateMessage(message) + } + } clearCurrentMessage() - if (!refreshedSinceSent) { - refreshMessages() - } } catch (e: Exception) { showErrorToast(e) } catch (e: Error) { @@ -1065,6 +1135,24 @@ class ThreadActivity : SimpleActivity() { checkSendMessageAvailability() } + private fun insertOrUpdateMessage(message: Message) { + if (messages.map { it.id }.contains(message.id)) { + val messageToReplace = messages.find { it.id == message.id } + messages[messages.indexOf(messageToReplace)] = message + } else { + messages.add(message) + } + + val newItems = getThreadItems() + runOnUiThread { + getOrCreateThreadAdapter().updateMessages(newItems) + if (!refreshedSinceSent) { + refreshMessages() + } + } + messagesDB.insertOrUpdate(message) + } + // show selected contacts, properly split to new lines when appropriate // based on https://stackoverflow.com/a/13505029/1967672 private fun showSelectedContact(views: ArrayList) { @@ -1191,7 +1279,6 @@ class ThreadActivity : SimpleActivity() { } } - @SuppressLint("MissingPermission") @Subscribe(threadMode = ThreadMode.ASYNC) fun refreshMessages(event: Events.RefreshMessages) { refreshedSinceSent = true @@ -1202,29 +1289,24 @@ class ThreadActivity : SimpleActivity() { notificationManager.cancel(threadId.hashCode()) } + val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L val newThreadId = getThreadId(participants.getAddresses().toSet()) - val newMessages = getMessages(newThreadId, false) - messages = if (messages.all { it.isScheduled } && newMessages.isNotEmpty()) { - threadId = newThreadId + val newMessages = getMessages(newThreadId, getImageResolutions = true, includeScheduledMessages = false) + + if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) { // update scheduled messages with real thread id - updateScheduledMessagesThreadId(messages, newThreadId) - getMessages(newThreadId, true) - } else { - getMessages(threadId, true) + threadId = newThreadId + updateScheduledMessagesThreadId(messages = messages.filter { it.threadId != threadId }, threadId) } - val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L - - messages.filter { !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage -> - // subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually - if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) { - val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId - if (subscriptionId != null) { - updateMessageSubscriptionId(latestMessage.id, subscriptionId) - latestMessage.subscriptionId = subscriptionId - } - } + messages = newMessages.apply { + val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId) + .filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() } + addAll(scheduledMessages) + } + messages.filter { !it.isScheduled && !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage -> + maybeUpdateMessageSubId(latestMessage) messagesDB.insertOrIgnore(latestMessage) } @@ -1234,6 +1316,18 @@ class ThreadActivity : SimpleActivity() { } } + @SuppressLint("MissingPermission") + private fun maybeUpdateMessageSubId(message: Message) { + // subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually + if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) { + val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId + if (subscriptionId != null) { + updateMessageSubscriptionId(message.id, subscriptionId) + message.subscriptionId = subscriptionId + } + } + } + private fun isMmsMessage(text: String): Boolean { val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS @@ -1256,11 +1350,12 @@ class ThreadActivity : SimpleActivity() { RadioItem(TYPE_SEND, getString(R.string.send_now)), RadioItem(TYPE_DELETE, getString(R.string.delete)) ) - RadioGroupDialog(activity = this, items = items, titleId = R.string.scheduled_message) { - when (it as Int) { + RadioGroupDialog(activity = this, items = items, titleId = R.string.scheduled_message) { any -> + when (any as Int) { TYPE_DELETE -> cancelScheduledMessageAndRefresh(message.id) TYPE_EDIT -> editScheduledMessage(message) TYPE_SEND -> { + messages.removeAll { message.id == it.id } extractAttachments(message) sendNormalMessage(message.body, message.subscriptionId) cancelScheduledMessageAndRefresh(message.id) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt index 07912daa..880d96dc 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt @@ -2,15 +2,18 @@ package com.simplemobiletools.smsmessenger.adapters import android.content.Intent import android.graphics.Typeface +import android.os.Parcelable import android.text.TextUtils import android.util.TypedValue import android.view.Menu import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller -import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.FeatureLockedDialog import com.simplemobiletools.commons.extensions.* @@ -27,14 +30,23 @@ import com.simplemobiletools.smsmessenger.models.Conversation import kotlinx.android.synthetic.main.item_conversation.view.* class ConversationsAdapter( - activity: SimpleActivity, var conversations: ArrayList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit -) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate { + activity: SimpleActivity, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit +) : MyRecyclerViewListAdapter(activity, recyclerView, ConversationDiffCallback(), itemClick), RecyclerViewFastScroller.OnPopupTextUpdate { private var fontSize = activity.getTextSize() private var drafts = HashMap() + private var recyclerViewState: Parcelable? = null + init { setupDragListener(true) fetchDrafts(drafts) + setHasStableIds(true) + + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() = restoreRecyclerViewState() + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = restoreRecyclerViewState() + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = restoreRecyclerViewState() + }) } override fun getActionMenuId() = R.menu.cab_conversations @@ -73,13 +85,13 @@ class ConversationsAdapter( } } - override fun getSelectableItemCount() = conversations.size + override fun getSelectableItemCount() = itemCount override fun getIsItemSelectable(position: Int) = true - override fun getItemSelectionKey(position: Int) = conversations.getOrNull(position)?.hashCode() + override fun getItemSelectionKey(position: Int) = currentList.getOrNull(position)?.hashCode() - override fun getItemKeyPosition(key: Int) = conversations.indexOfFirst { it.hashCode() == key } + override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { it.hashCode() == key } override fun onActionModeCreated() {} @@ -88,14 +100,14 @@ class ConversationsAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent) override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val conversation = conversations[position] - holder.bindView(conversation, true, true) { itemView, layoutPosition -> + val conversation = getItem(position) + holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ -> setupView(itemView, conversation) } bindViewHolder(holder) } - override fun getItemCount() = conversations.size + override fun getItemId(position: Int) = getItem(position).threadId override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) @@ -128,8 +140,7 @@ class ConversationsAdapter( } val numbersToBlock = getSelectedItems() - val positions = getSelectedItemPositions() - conversations.removeAll(numbersToBlock) + val newList = currentList.toMutableList().apply { removeAll(numbersToBlock) } ensureBackgroundThread { numbersToBlock.map { it.phoneNumber }.forEach { number -> @@ -137,7 +148,7 @@ class ConversationsAdapter( } activity.runOnUiThread { - removeSelectedItems(positions) + submitList(newList) finishActMode() } } @@ -175,25 +186,25 @@ class ConversationsAdapter( return } - val conversationsToRemove = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList - val positions = getSelectedItemPositions() + val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList conversationsToRemove.forEach { activity.deleteConversation(it.threadId) activity.notificationManager.cancel(it.hashCode()) } - try { - conversations.removeAll(conversationsToRemove.toSet()) + val newList = try { + currentList.toMutableList().apply { removeAll(conversationsToRemove) } } catch (ignored: Exception) { + currentList.toMutableList() } activity.runOnUiThread { - if (conversationsToRemove.isEmpty()) { + if (newList.none { selectedKeys.contains(it.hashCode()) }) { refreshMessages() finishActMode() } else { - removeSelectedItems(positions) - if (conversations.isEmpty()) { + submitList(newList) + if (newList.isEmpty()) { refreshMessages() } } @@ -205,7 +216,7 @@ class ConversationsAdapter( return } - val conversationsMarkedAsRead = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList + val conversationsMarkedAsRead = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList ensureBackgroundThread { conversationsMarkedAsRead.filter { conversation -> !conversation.read }.forEach { activity.markThreadMessagesRead(it.threadId) @@ -223,7 +234,7 @@ class ConversationsAdapter( return } - val conversationsMarkedAsUnread = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList + val conversationsMarkedAsUnread = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList ensureBackgroundThread { conversationsMarkedAsUnread.filter { conversation -> conversation.read }.forEach { activity.markThreadMessagesUnread(it.threadId) @@ -246,7 +257,7 @@ class ConversationsAdapter( } } - private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList + private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList private fun pinConversation(pin: Boolean) { val conversations = getSelectedItems() @@ -285,14 +296,9 @@ class ConversationsAdapter( notifyDataSetChanged() } - fun updateConversations(newConversations: ArrayList) { - val latestConversations = newConversations.clone() as ArrayList - val oldHashCode = conversations.hashCode() - val newHashCode = latestConversations.hashCode() - if (newHashCode != oldHashCode) { - conversations = latestConversations - notifyDataSetChanged() - } + fun updateConversations(newConversations: ArrayList, commitCallback: (() -> Unit)? = null) { + saveRecyclerViewState() + submitList(newConversations.toList(), commitCallback) } fun updateDrafts() { @@ -356,5 +362,23 @@ class ConversationsAdapter( } } - override fun onChange(position: Int) = conversations.getOrNull(position)?.title ?: "" + override fun onChange(position: Int) = currentList.getOrNull(position)?.title ?: "" + + private fun saveRecyclerViewState() { + recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() + } + + private fun restoreRecyclerViewState() { + recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) + } + + private class ConversationDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Conversation, newItem: Conversation): Boolean { + return Conversation.areItemsTheSame(oldItem, newItem) + } + + override fun areContentsTheSame(oldItem: Conversation, newItem: Conversation): Boolean { + return Conversation.areContentsTheSame(oldItem, newItem) + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt index 63a484d3..0fa9e199 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt @@ -11,6 +11,7 @@ import android.util.TypedValue import android.view.Menu import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -21,7 +22,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target -import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.SimpleContactsHelper @@ -35,7 +36,10 @@ import com.simplemobiletools.smsmessenger.activities.VCardViewerActivity import com.simplemobiletools.smsmessenger.dialogs.SelectTextDialog import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.* -import com.simplemobiletools.smsmessenger.models.* +import com.simplemobiletools.smsmessenger.models.Attachment +import com.simplemobiletools.smsmessenger.models.Message +import com.simplemobiletools.smsmessenger.models.ThreadItem +import com.simplemobiletools.smsmessenger.models.ThreadItem.* import kotlinx.android.synthetic.main.item_attachment_image.view.* import kotlinx.android.synthetic.main.item_received_message.view.* import kotlinx.android.synthetic.main.item_received_message.view.thread_mesage_attachments_holder @@ -45,12 +49,16 @@ import kotlinx.android.synthetic.main.item_received_message.view.thread_message_ import kotlinx.android.synthetic.main.item_sent_message.view.* import kotlinx.android.synthetic.main.item_thread_date_time.view.* import kotlinx.android.synthetic.main.item_thread_error.view.* +import kotlinx.android.synthetic.main.item_thread_loading.view.* import kotlinx.android.synthetic.main.item_thread_sending.view.* import kotlinx.android.synthetic.main.item_thread_success.view.* class ThreadAdapter( - activity: SimpleActivity, var messages: ArrayList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit -) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { + activity: SimpleActivity, + recyclerView: MyRecyclerView, + itemClick: (Any) -> Unit, + val deleteMessages: (messages: List) -> Unit +) : MyRecyclerViewListAdapter(activity, recyclerView, ThreadItemDiffCallback(), itemClick) { private var fontSize = activity.getTextSize() @SuppressLint("MissingPermission") @@ -59,6 +67,7 @@ class ThreadAdapter( init { setupDragListener(true) + setHasStableIds(true) } override fun getActionMenuId() = R.menu.cab_thread @@ -92,13 +101,13 @@ class ThreadAdapter( } } - override fun getSelectableItemCount() = messages.filter { it is Message }.size + override fun getSelectableItemCount() = currentList.filterIsInstance().size override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position) - override fun getItemSelectionKey(position: Int) = (messages.getOrNull(position) as? Message)?.hashCode() + override fun getItemSelectionKey(position: Int) = (currentList.getOrNull(position) as? Message)?.hashCode() - override fun getItemKeyPosition(key: Int) = messages.indexOfFirst { (it as? Message)?.hashCode() == key } + override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { (it as? Message)?.hashCode() == key } override fun onActionModeCreated() {} @@ -106,6 +115,7 @@ class ThreadAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val layout = when (viewType) { + THREAD_LOADING -> R.layout.item_thread_loading THREAD_DATE_TIME -> R.layout.item_thread_date_time THREAD_RECEIVED_MESSAGE -> R.layout.item_received_message THREAD_SENT_MESSAGE_ERROR -> R.layout.item_thread_error @@ -117,32 +127,37 @@ class ThreadAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = messages[position] + val item = getItem(position) val isClickable = item is ThreadError || item is Message val isLongClickable = item is Message - holder.bindView(item, isClickable, isLongClickable) { itemView, layoutPosition -> + holder.bindView(item, isClickable, isLongClickable) { itemView, _ -> when (item) { + is ThreadLoading -> setupThreadLoading(itemView) is ThreadDateTime -> setupDateTime(itemView, item) - is ThreadSent -> setupThreadSuccess(itemView, item.delivered) is ThreadError -> setupThreadError(itemView) + is ThreadSent -> setupThreadSuccess(itemView, item.delivered) is ThreadSending -> setupThreadSending(itemView) - else -> setupView(holder, itemView, item as Message) + is Message -> setupView(holder, itemView, item) } } bindViewHolder(holder) } - override fun getItemCount() = messages.size + override fun getItemId(position: Int): Long { + return when (val item = getItem(position)) { + is Message -> Message.getStableId(item) + else -> item.hashCode().toLong() + } + } override fun getItemViewType(position: Int): Int { - val item = messages[position] - return when { - item is ThreadDateTime -> THREAD_DATE_TIME - (messages[position] as? Message)?.isReceivedMessage() == true -> THREAD_RECEIVED_MESSAGE - item is ThreadError -> THREAD_SENT_MESSAGE_ERROR - item is ThreadSent -> THREAD_SENT_MESSAGE_SENT - item is ThreadSending -> THREAD_SENT_MESSAGE_SENDING - else -> THREAD_SENT_MESSAGE + return when (val item = getItem(position)) { + is ThreadLoading -> THREAD_LOADING + is ThreadDateTime -> THREAD_DATE_TIME + is ThreadError -> THREAD_SENT_MESSAGE_ERROR + is ThreadSent -> THREAD_SENT_MESSAGE_SENT + is ThreadSending -> THREAD_SENT_MESSAGE_SENDING + is Message -> if (item.isReceivedMessage()) THREAD_RECEIVED_MESSAGE else THREAD_SENT_MESSAGE } } @@ -185,45 +200,14 @@ class ThreadAdapter( ConfirmationDialog(activity, question) { ensureBackgroundThread { - deleteMessages() + val messagesToRemove = getSelectedItems() + if (messagesToRemove.isNotEmpty()) { + deleteMessages(messagesToRemove.filterIsInstance()) + } } } } - private fun deleteMessages() { - val messagesToRemove = getSelectedItems() - if (messagesToRemove.isEmpty()) { - return - } - - val positions = getSelectedItemPositions() - val threadId = (messagesToRemove.firstOrNull() as? Message)?.threadId ?: return - messagesToRemove.forEach { - activity.deleteMessage((it as Message).id, it.isMMS) - } - messages.removeAll(messagesToRemove.toSet()) - activity.updateLastConversationMessage(threadId) - - val messages = messages.filterIsInstance() - if (messages.isNotEmpty() && messages.all { it.isScheduled }) { - // move all scheduled messages to a temporary thread as there are no real messages left - val message = messages.last() - val newThreadId = generateRandomId() - activity.createTemporaryThread(message, newThreadId) - activity.updateScheduledMessagesThreadId(messages, newThreadId) - onThreadIdUpdate(newThreadId) - } - - activity.runOnUiThread { - if (messages.isEmpty()) { - activity.finish() - } else { - removeSelectedItems(positions) - } - refreshMessages() - } - } - private fun forwardMessage() { val message = getSelectedItems().firstOrNull() as? Message ?: return val attachment = message.attachment?.attachments?.firstOrNull() @@ -239,18 +223,16 @@ class ThreadAdapter( } } - private fun getSelectedItems() = messages.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList + private fun getSelectedItems() = currentList.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList - private fun isThreadDateTime(position: Int) = messages.getOrNull(position) is ThreadDateTime + private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime - fun updateMessages(newMessages: ArrayList, scrollPosition: Int = newMessages.size - 1) { - val latestMessages = newMessages.clone() as ArrayList - val oldHashCode = messages.hashCode() - val newHashCode = latestMessages.hashCode() - if (newHashCode != oldHashCode) { - messages = latestMessages - notifyDataSetChanged() - recyclerView.scrollToPosition(scrollPosition) + fun updateMessages(newMessages: ArrayList, scrollPosition: Int = newMessages.lastIndex) { + val latestMessages = newMessages.toMutableList() + submitList(latestMessages) { + if (scrollPosition != -1) { + recyclerView.scrollToPosition(scrollPosition) + } } } @@ -484,6 +466,8 @@ class ThreadAdapter( } } + private fun setupThreadLoading(view: View) = view.thread_loading.setIndicatorColor(properPrimaryColor) + override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) if (!activity.isDestroyed && !activity.isFinishing && holder.itemView.thread_message_sender_photo != null) { @@ -491,3 +475,29 @@ class ThreadAdapter( } } } + +private class ThreadItemDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean { + if (oldItem::class.java != newItem::class.java) return false + return when (oldItem) { + is ThreadLoading -> oldItem.id == (newItem as ThreadLoading).id + is ThreadDateTime -> oldItem.date == (newItem as ThreadDateTime).date + is ThreadError -> oldItem.messageId == (newItem as ThreadError).messageId + is ThreadSent -> oldItem.messageId == (newItem as ThreadSent).messageId + is ThreadSending -> oldItem.messageId == (newItem as ThreadSending).messageId + is Message -> Message.areItemsTheSame(oldItem, newItem as Message) + } + } + + override fun areContentsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean { + if (oldItem::class.java != newItem::class.java) return false + return when (oldItem) { + is ThreadLoading, is ThreadSending -> true + is ThreadDateTime -> oldItem.simID == (newItem as ThreadDateTime).simID + is ThreadError -> oldItem.messageText == (newItem as ThreadError).messageText + is ThreadSent -> oldItem.delivered == (newItem as ThreadSent).delivered + is Message -> Message.areContentsTheSame(oldItem, newItem as Message) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt index 6b97672f..28190c1f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -58,7 +58,13 @@ val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB(). val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao() -fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom: Int = -1, includeScheduledMessages: Boolean = true): ArrayList { +fun Context.getMessages( + threadId: Long, + getImageResolutions: Boolean, + dateFrom: Int = -1, + includeScheduledMessages: Boolean = true, + limit: Int = MESSAGES_LIMIT +): ArrayList { val uri = Sms.CONTENT_URI val projection = arrayOf( Sms._ID, @@ -75,7 +81,7 @@ fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom: val rangeQuery = if (dateFrom == -1) "" else "AND ${Sms.DATE} < ${dateFrom.toLong() * 1000}" val selection = "${Sms.THREAD_ID} = ? $rangeQuery" val selectionArgs = arrayOf(threadId.toString()) - val sortOrder = "${Sms.DATE} DESC LIMIT $MESSAGES_LIMIT" + val sortOrder = "${Sms.DATE} DESC LIMIT $limit" val blockStatus = HashMap() val blockedNumbers = getBlockedNumbers() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt index 82d7eeb8..58aea416 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.view.View import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.SimpleContactsHelper +import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.extensions.* import kotlinx.android.synthetic.main.item_attachment_document.view.* @@ -25,12 +26,18 @@ fun View.setupDocumentPreview( filename.text = title } - try { - val size = context.getFileSizeFromUri(uri) - file_size.beVisible() - file_size.text = size.formatSize() - } catch (e: Exception) { - file_size.beGone() + ensureBackgroundThread { + try { + val size = context.getFileSizeFromUri(uri) + post { + file_size.beVisible() + file_size.text = size.formatSize() + } + } catch (e: Exception) { + post { + file_size.beGone() + } + } } val textColor = context.getProperTextColor() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt index 45c41f48..74b72a02 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -46,6 +46,7 @@ const val THREAD_SENT_MESSAGE = 3 const val THREAD_SENT_MESSAGE_ERROR = 4 const val THREAD_SENT_MESSAGE_SENT = 5 const val THREAD_SENT_MESSAGE_SENDING = 6 +const val THREAD_LOADING = 7 // view types for attachment list const val ATTACHMENT_DOCUMENT = 7 diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt index e4e2ce63..a29b01ae 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt @@ -4,8 +4,6 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Handler -import android.os.Looper import androidx.core.app.AlarmManagerCompat import com.klinker.android.send_message.Settings import com.klinker.android.send_message.Transaction @@ -71,12 +69,10 @@ fun Context.sendMessage(text: String, addresses: List, subscriptionId: I transaction.setExplicitBroadcastForSentSms(smsSentIntent) transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent) - Handler(Looper.getMainLooper()).post { - try { - transaction.sendNewMessage(message) - } catch (e: Exception) { - showErrorToast(e) - } + try { + transaction.sendNewMessage(message) + } catch (e: Exception) { + showErrorToast(e) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt index 86d09777..5da76a0f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt @@ -18,13 +18,19 @@ data class Conversation( @ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false ) { - fun areContentsTheSame(other: Conversation): Boolean { - return snippet == other.snippet && - date == other.date && - read == other.read && - title == other.title && - photoUri == other.photoUri && - isGroupConversation == other.isGroupConversation && - phoneNumber == other.phoneNumber + companion object { + fun areItemsTheSame(old: Conversation, new: Conversation): Boolean { + return old.threadId == new.threadId + } + + fun areContentsTheSame(old: Conversation, new: Conversation): Boolean { + return old.snippet == new.snippet && + old.date == new.date && + old.read == new.read && + old.title == new.title && + old.photoUri == new.photoUri && + old.isGroupConversation == new.isGroupConversation && + old.phoneNumber == new.phoneNumber + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt index 48a25679..8816d2d8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Message.kt @@ -27,4 +27,35 @@ data class Message( fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX fun millis() = date * 1000L + + companion object { + + fun getStableId(message: Message): Long { + var result = message.id.hashCode() + result = 31 * result + message.body.hashCode() + result = 31 * result + message.date.hashCode() + result = 31 * result + message.threadId.hashCode() + result = 31 * result + message.isMMS.hashCode() + result = 31 * result + (message.attachment?.hashCode() ?: 0) + result = 31 * result + message.senderName.hashCode() + result = 31 * result + message.senderPhotoUri.hashCode() + result = 31 * result + message.isScheduled.hashCode() + return result.toLong() + } + + fun areItemsTheSame(old: Message, new: Message): Boolean { + return old.id == new.id + } + + fun areContentsTheSame(old: Message, new: Message): Boolean { + return old.body == new.body && + old.threadId == new.threadId && + old.date == new.date && + old.isMMS == new.isMMS && + old.attachment == new.attachment && + old.senderName == new.senderName && + old.senderPhotoUri == new.senderPhotoUri && + old.isScheduled == new.isScheduled + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadDateTime.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadDateTime.kt deleted file mode 100644 index aae15f0f..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadDateTime.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.simplemobiletools.smsmessenger.models - -data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt deleted file mode 100644 index 9e32530d..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadError.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.simplemobiletools.smsmessenger.models - -data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItem.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItem.kt deleted file mode 100644 index 534397fa..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItem.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.simplemobiletools.smsmessenger.models - -open class ThreadItem diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItems.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItems.kt new file mode 100644 index 00000000..a26c0bf9 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadItems.kt @@ -0,0 +1,12 @@ +package com.simplemobiletools.smsmessenger.models + +/** + * Thread item representations for the main thread recyclerview. [Message] is also a [ThreadItem] + */ +sealed class ThreadItem { + data class ThreadLoading(val id: Long) : ThreadItem() + data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem() + data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem() + data class ThreadSent(val messageId: Long, val delivered: Boolean) : ThreadItem() + data class ThreadSending(val messageId: Long) : ThreadItem() +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt deleted file mode 100644 index 131f7386..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSending.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.simplemobiletools.smsmessenger.models - -data class ThreadSending(val messageId: Long) : ThreadItem() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSent.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSent.kt deleted file mode 100644 index 36faf7ab..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ThreadSent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.simplemobiletools.smsmessenger.models - -// show a check after the latest message, if it is a sent one and succeeded, -// show a double check if it is delivered -data class ThreadSent(val messageID: Long, val delivered: Boolean) : ThreadItem() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt index 53c9e020..1b32ca2c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt @@ -3,6 +3,8 @@ package com.simplemobiletools.smsmessenger.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Handler +import android.os.Looper import android.os.PowerManager import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.helpers.ensureBackgroundThread @@ -43,7 +45,9 @@ class ScheduledMessageReceiver : BroadcastReceiver() { val attachments = message.attachment?.attachments ?: emptyList() try { - context.sendMessage(message.body, addresses, message.subscriptionId, attachments) + Handler(Looper.getMainLooper()).post { + context.sendMessage(message.body, addresses, message.subscriptionId, attachments) + } // delete temporary conversation and message as it's already persisted to the telephony db now context.deleteScheduledMessage(messageId) diff --git a/app/src/main/res/drawable/item_received_background.xml b/app/src/main/res/drawable/item_received_background.xml index 3930c18e..8b0f65f8 100644 --- a/app/src/main/res/drawable/item_received_background.xml +++ b/app/src/main/res/drawable/item_received_background.xml @@ -3,9 +3,10 @@ android:shape="rectangle"> + android:bottomLeftRadius="@dimen/small_margin" + android:bottomRightRadius="@dimen/big_margin" + android:topLeftRadius="@dimen/big_margin" + android:topRightRadius="@dimen/big_margin" /> diff --git a/app/src/main/res/drawable/item_sent_background.xml b/app/src/main/res/drawable/item_sent_background.xml index d75f6f79..e4f45e2c 100644 --- a/app/src/main/res/drawable/item_sent_background.xml +++ b/app/src/main/res/drawable/item_sent_background.xml @@ -3,9 +3,10 @@ android:shape="rectangle"> + android:bottomLeftRadius="@dimen/big_margin" + android:bottomRightRadius="@dimen/small_margin" + android:topLeftRadius="@dimen/big_margin" + android:topRightRadius="@dimen/big_margin" /> diff --git a/app/src/main/res/layout/item_attachment_document.xml b/app/src/main/res/layout/item_attachment_document.xml index 603454b4..ba70eef4 100644 --- a/app/src/main/res/layout/item_attachment_document.xml +++ b/app/src/main/res/layout/item_attachment_document.xml @@ -29,6 +29,7 @@ @@ -48,6 +49,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="@dimen/normal_text_size" + android:visibility="gone" tools:text="2.18 KB" /> diff --git a/app/src/main/res/layout/item_thread_loading.xml b/app/src/main/res/layout/item_thread_loading.xml new file mode 100644 index 00000000..244cea63 --- /dev/null +++ b/app/src/main/res/layout/item_thread_loading.xml @@ -0,0 +1,18 @@ + + + + +