Merge pull request #488 from Naveen3Singh/recyclerview_improvements

Recyclerview related improvements
This commit is contained in:
Tibor Kaputa 2022-11-19 20:37:12 +01:00 committed by GitHub
commit 322ccbd76a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 413 additions and 214 deletions

View File

@ -63,7 +63,7 @@ android {
} }
dependencies { dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:af11ea2e30' implementation 'com.github.SimpleMobileTools:Simple-Commons:8a01bdee8c'
implementation 'org.greenrobot:eventbus:3.3.1' implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61' implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61'
implementation 'com.github.tibbi:android-smsmms:3581774c39' implementation 'com.github.tibbi:android-smsmms:3581774c39'

View File

@ -257,7 +257,7 @@ class MainActivity : SimpleActivity() {
} }
cachedConversations.forEach { cachedConv -> 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) { if (conv != null) {
val conversation = conv.copy(date = maxOf(cachedConv.date, conv.date)) val conversation = conv.copy(date = maxOf(cachedConv.date, conv.date))
conversationsDB.insertOrUpdate(conversation) conversationsDB.insertOrUpdate(conversation)
@ -299,7 +299,7 @@ class MainActivity : SimpleActivity() {
val currAdapter = conversations_list.adapter val currAdapter = conversations_list.adapter
if (currAdapter == null) { if (currAdapter == null) {
hideKeyboard() hideKeyboard()
ConversationsAdapter(this, sortedConversations, conversations_list) { ConversationsAdapter(this, conversations_list) {
Intent(this, ThreadActivity::class.java).apply { Intent(this, ThreadActivity::class.java).apply {
val conversation = it as Conversation val conversation = it as Conversation
putExtra(THREAD_ID, conversation.threadId) putExtra(THREAD_ID, conversation.threadId)
@ -308,6 +308,7 @@ class MainActivity : SimpleActivity() {
} }
}.apply { }.apply {
conversations_list.adapter = this conversations_list.adapter = this
updateConversations(sortedConversations)
} }
if (areSystemAnimationsEnabled) { if (areSystemAnimationsEnabled) {
@ -315,12 +316,13 @@ class MainActivity : SimpleActivity() {
} }
} else { } else {
try { try {
(currAdapter as ConversationsAdapter).updateConversations(sortedConversations) (currAdapter as ConversationsAdapter).updateConversations(sortedConversations) {
if (currAdapter.conversations.isEmpty()) { if (currAdapter.currentList.isEmpty()) {
conversations_fastscroller.beGone() conversations_fastscroller.beGone()
no_conversations_placeholder.text = getString(R.string.no_conversations_found) no_conversations_placeholder.text = getString(R.string.no_conversations_found)
no_conversations_placeholder.beVisible() no_conversations_placeholder.beVisible()
no_conversations_placeholder_2.beVisible() no_conversations_placeholder_2.beVisible()
}
} }
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }

View File

@ -55,6 +55,7 @@ import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.* import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.models.ThreadItem.*
import kotlinx.android.synthetic.main.activity_thread.* import kotlinx.android.synthetic.main.activity_thread.*
import kotlinx.android.synthetic.main.item_selected_contact.view.* import kotlinx.android.synthetic.main.item_selected_contact.view.*
import kotlinx.android.synthetic.main.layout_attachment_picker.* import kotlinx.android.synthetic.main.layout_attachment_picker.*
@ -116,8 +117,8 @@ class ThreadActivity : SimpleActivity() {
bus = EventBus.getDefault() bus = EventBus.getDefault()
bus!!.register(this) bus!!.register(this)
handlePermission(PERMISSION_READ_PHONE_STATE) { handlePermission(PERMISSION_READ_PHONE_STATE) { granted ->
if (it) { if (granted) {
setupButtons() setupButtons()
setupCachedMessages { setupCachedMessages {
val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L) val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L)
@ -263,7 +264,7 @@ class ThreadActivity : SimpleActivity() {
} }
private fun setupThread() { private fun setupThread() {
val privateCursor = getMyContactsCursor(false, true) val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread { ensureBackgroundThread {
privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) 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() { private fun setupAdapter() {
threadItems = getThreadItems() threadItems = getThreadItems()
runOnUiThread { runOnUiThread {
refreshMenuItems() refreshMenuItems()
getOrCreateThreadAdapter().apply {
val currAdapter = thread_messages_list.adapter val scrollPosition = if (currentList.lastOrNull() != threadItems.lastOrNull()) {
if (currAdapter == null) { threadItems.lastIndex
ThreadAdapter( } else {
activity = this, -1
messages = threadItems,
recyclerView = thread_messages_list,
itemClick = { handleItemClick(it) },
onThreadIdUpdate = { threadId = it }
).apply {
thread_messages_list.adapter = this
} }
updateMessages(threadItems, scrollPosition)
thread_messages_list.endlessScrollListener = object : MyRecyclerView.EndlessScrollListener {
override fun updateBottom() {}
override fun updateTop() {
fetchNextMessages()
}
}
} else {
(currAdapter as ThreadAdapter).updateMessages(threadItems)
} }
} }
@ -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) { private fun handleItemClick(any: Any) {
when { when {
any is Message && any.isScheduled -> showScheduledMessageInfo(any) any is Message && any.isScheduled -> showScheduledMessageInfo(any)
@ -394,8 +409,53 @@ class ThreadActivity : SimpleActivity() {
} }
} }
private fun deleteMessages(messagesToRemove: List<Message>) {
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() { private fun fetchNextMessages() {
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) { if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) {
if (allMessagesFetched) {
getOrCreateThreadAdapter().apply {
val newList = currentList.toMutableList().apply {
removeAll { it is ThreadLoading }
}
updateMessages(newMessages = newList as ArrayList<ThreadItem>, scrollPosition = 0)
}
}
return return
} }
@ -414,16 +474,13 @@ class ThreadActivity : SimpleActivity() {
.filter { message -> !messages.contains(message) } .filter { message -> !messages.contains(message) }
messages.addAll(0, olderMessages) messages.addAll(0, olderMessages)
threadItems = getThreadItems()
allMessagesFetched = olderMessages.size < MESSAGES_LIMIT || olderMessages.isEmpty() allMessagesFetched = olderMessages.size < MESSAGES_LIMIT || olderMessages.isEmpty()
threadItems = getThreadItems()
runOnUiThread { runOnUiThread {
loadingOlderMessages = false loadingOlderMessages = false
val itemAtRefreshIndex = threadItems.indexOfFirst { it == firstItem } val itemAtRefreshIndex = threadItems.indexOfFirst { it == firstItem }
(thread_messages_list.adapter as ThreadAdapter).apply { getOrCreateThreadAdapter().updateMessages(threadItems, itemAtRefreshIndex)
updateMessages(threadItems, itemAtRefreshIndex)
}
} }
} }
} }
@ -433,7 +490,9 @@ class ThreadActivity : SimpleActivity() {
val textColor = getProperTextColor() val textColor = getProperTextColor()
thread_send_message.apply { thread_send_message.apply {
setTextColor(textColor) setTextColor(textColor)
compoundDrawables.forEach { it?.applyColorFilter(textColor) } compoundDrawables.forEach {
it?.applyColorFilter(textColor)
}
} }
confirm_manage_contacts.applyColorFilter(textColor) confirm_manage_contacts.applyColorFilter(textColor)
@ -460,7 +519,11 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.isClickable = false thread_send_message.isClickable = false
thread_type_message.onTextChangeListener { thread_type_message.onTextChangeListener {
checkSendMessageAvailability() 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) val messageLength = SmsMessage.calculateLength(messageString, false)
thread_character_counter.text = "${messageLength[2]}/${messageLength[0]}" thread_character_counter.text = "${messageLength[2]}/${messageLength[0]}"
} }
@ -819,6 +882,11 @@ class ThreadActivity : SimpleActivity() {
bus?.post(Events.RefreshMessages()) bus?.post(Events.RefreshMessages())
} }
if (!allMessagesFetched && messages.size >= MESSAGES_LIMIT) {
val threadLoading = ThreadLoading(generateRandomId())
items.add(0, threadLoading)
}
return items return items
} }
@ -988,6 +1056,7 @@ class ThreadActivity : SimpleActivity() {
showErrorToast(getString(R.string.unknown_error_occurred)) showErrorToast(getString(R.string.unknown_error_occurred))
return return
} }
scrollToBottom()
text = removeDiacriticsIfNeeded(text) text = removeDiacriticsIfNeeded(text)
@ -1017,22 +1086,18 @@ class ThreadActivity : SimpleActivity() {
threadId = message.threadId threadId = message.threadId
createTemporaryThread(message, message.threadId) createTemporaryThread(message, message.threadId)
} }
messagesDB.insertOrUpdate(message)
val conversation = conversationsDB.getConversationWithThreadId(threadId) val conversation = conversationsDB.getConversationWithThreadId(threadId)
if (conversation != null) { if (conversation != null) {
val nowSeconds = (System.currentTimeMillis() / 1000).toInt() val nowSeconds = (System.currentTimeMillis() / 1000).toInt()
conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds)) conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds, snippet = message.body))
} }
scheduleMessage(message) scheduleMessage(message)
insertOrUpdateMessage(message)
runOnUiThread { runOnUiThread {
clearCurrentMessage() clearCurrentMessage()
hideScheduleSendUi() hideScheduleSendUi()
scheduledMessage = null scheduledMessage = null
if (!refreshedSinceSent) {
refreshMessages()
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -1047,11 +1112,16 @@ class ThreadActivity : SimpleActivity() {
try { try {
refreshedSinceSent = false refreshedSinceSent = false
sendMessage(text, addresses, subscriptionId, attachments) 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() clearCurrentMessage()
if (!refreshedSinceSent) {
refreshMessages()
}
} catch (e: Exception) { } catch (e: Exception) {
showErrorToast(e) showErrorToast(e)
} catch (e: Error) { } catch (e: Error) {
@ -1065,6 +1135,24 @@ class ThreadActivity : SimpleActivity() {
checkSendMessageAvailability() 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 // show selected contacts, properly split to new lines when appropriate
// based on https://stackoverflow.com/a/13505029/1967672 // based on https://stackoverflow.com/a/13505029/1967672
private fun showSelectedContact(views: ArrayList<View>) { private fun showSelectedContact(views: ArrayList<View>) {
@ -1191,7 +1279,6 @@ class ThreadActivity : SimpleActivity() {
} }
} }
@SuppressLint("MissingPermission")
@Subscribe(threadMode = ThreadMode.ASYNC) @Subscribe(threadMode = ThreadMode.ASYNC)
fun refreshMessages(event: Events.RefreshMessages) { fun refreshMessages(event: Events.RefreshMessages) {
refreshedSinceSent = true refreshedSinceSent = true
@ -1202,29 +1289,24 @@ class ThreadActivity : SimpleActivity() {
notificationManager.cancel(threadId.hashCode()) notificationManager.cancel(threadId.hashCode())
} }
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
val newThreadId = getThreadId(participants.getAddresses().toSet()) val newThreadId = getThreadId(participants.getAddresses().toSet())
val newMessages = getMessages(newThreadId, false) val newMessages = getMessages(newThreadId, getImageResolutions = true, includeScheduledMessages = false)
messages = if (messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
threadId = newThreadId if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
// update scheduled messages with real thread id // update scheduled messages with real thread id
updateScheduledMessagesThreadId(messages, newThreadId) threadId = newThreadId
getMessages(newThreadId, true) updateScheduledMessagesThreadId(messages = messages.filter { it.threadId != threadId }, threadId)
} else {
getMessages(threadId, true)
} }
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L messages = newMessages.apply {
val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId)
messages.filter { !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage -> .filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() }
// subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually addAll(scheduledMessages)
if ((subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1) { }
val subscriptionId = availableSIMCards.getOrNull(currentSIMCardIndex)?.subscriptionId
if (subscriptionId != null) {
updateMessageSubscriptionId(latestMessage.id, subscriptionId)
latestMessage.subscriptionId = subscriptionId
}
}
messages.filter { !it.isScheduled && !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage ->
maybeUpdateMessageSubId(latestMessage)
messagesDB.insertOrIgnore(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 { private fun isMmsMessage(text: String): Boolean {
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS
@ -1256,11 +1350,12 @@ class ThreadActivity : SimpleActivity() {
RadioItem(TYPE_SEND, getString(R.string.send_now)), RadioItem(TYPE_SEND, getString(R.string.send_now)),
RadioItem(TYPE_DELETE, getString(R.string.delete)) RadioItem(TYPE_DELETE, getString(R.string.delete))
) )
RadioGroupDialog(activity = this, items = items, titleId = R.string.scheduled_message) { RadioGroupDialog(activity = this, items = items, titleId = R.string.scheduled_message) { any ->
when (it as Int) { when (any as Int) {
TYPE_DELETE -> cancelScheduledMessageAndRefresh(message.id) TYPE_DELETE -> cancelScheduledMessageAndRefresh(message.id)
TYPE_EDIT -> editScheduledMessage(message) TYPE_EDIT -> editScheduledMessage(message)
TYPE_SEND -> { TYPE_SEND -> {
messages.removeAll { message.id == it.id }
extractAttachments(message) extractAttachments(message)
sendNormalMessage(message.body, message.subscriptionId) sendNormalMessage(message.body, message.subscriptionId)
cancelScheduledMessageAndRefresh(message.id) cancelScheduledMessageAndRefresh(message.id)

View File

@ -2,15 +2,18 @@ package com.simplemobiletools.smsmessenger.adapters
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Parcelable
import android.text.TextUtils import android.text.TextUtils
import android.util.TypedValue import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller 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.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FeatureLockedDialog import com.simplemobiletools.commons.dialogs.FeatureLockedDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
@ -27,14 +30,23 @@ import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.* import kotlinx.android.synthetic.main.item_conversation.view.*
class ConversationsAdapter( class ConversationsAdapter(
activity: SimpleActivity, var conversations: ArrayList<Conversation>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit activity: SimpleActivity, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), RecyclerViewFastScroller.OnPopupTextUpdate { ) : MyRecyclerViewListAdapter<Conversation>(activity, recyclerView, ConversationDiffCallback(), itemClick), RecyclerViewFastScroller.OnPopupTextUpdate {
private var fontSize = activity.getTextSize() private var fontSize = activity.getTextSize()
private var drafts = HashMap<Long, String?>() private var drafts = HashMap<Long, String?>()
private var recyclerViewState: Parcelable? = null
init { init {
setupDragListener(true) setupDragListener(true)
fetchDrafts(drafts) 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 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 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() {} override fun onActionModeCreated() {}
@ -88,14 +100,14 @@ class ConversationsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val conversation = conversations[position] val conversation = getItem(position)
holder.bindView(conversation, true, true) { itemView, layoutPosition -> holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ ->
setupView(itemView, conversation) setupView(itemView, conversation)
} }
bindViewHolder(holder) bindViewHolder(holder)
} }
override fun getItemCount() = conversations.size override fun getItemId(position: Int) = getItem(position).threadId
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder) super.onViewRecycled(holder)
@ -128,8 +140,7 @@ class ConversationsAdapter(
} }
val numbersToBlock = getSelectedItems() val numbersToBlock = getSelectedItems()
val positions = getSelectedItemPositions() val newList = currentList.toMutableList().apply { removeAll(numbersToBlock) }
conversations.removeAll(numbersToBlock)
ensureBackgroundThread { ensureBackgroundThread {
numbersToBlock.map { it.phoneNumber }.forEach { number -> numbersToBlock.map { it.phoneNumber }.forEach { number ->
@ -137,7 +148,7 @@ class ConversationsAdapter(
} }
activity.runOnUiThread { activity.runOnUiThread {
removeSelectedItems(positions) submitList(newList)
finishActMode() finishActMode()
} }
} }
@ -175,25 +186,25 @@ class ConversationsAdapter(
return return
} }
val conversationsToRemove = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
val positions = getSelectedItemPositions()
conversationsToRemove.forEach { conversationsToRemove.forEach {
activity.deleteConversation(it.threadId) activity.deleteConversation(it.threadId)
activity.notificationManager.cancel(it.hashCode()) activity.notificationManager.cancel(it.hashCode())
} }
try { val newList = try {
conversations.removeAll(conversationsToRemove.toSet()) currentList.toMutableList().apply { removeAll(conversationsToRemove) }
} catch (ignored: Exception) { } catch (ignored: Exception) {
currentList.toMutableList()
} }
activity.runOnUiThread { activity.runOnUiThread {
if (conversationsToRemove.isEmpty()) { if (newList.none { selectedKeys.contains(it.hashCode()) }) {
refreshMessages() refreshMessages()
finishActMode() finishActMode()
} else { } else {
removeSelectedItems(positions) submitList(newList)
if (conversations.isEmpty()) { if (newList.isEmpty()) {
refreshMessages() refreshMessages()
} }
} }
@ -205,7 +216,7 @@ class ConversationsAdapter(
return return
} }
val conversationsMarkedAsRead = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> val conversationsMarkedAsRead = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
ensureBackgroundThread { ensureBackgroundThread {
conversationsMarkedAsRead.filter { conversation -> !conversation.read }.forEach { conversationsMarkedAsRead.filter { conversation -> !conversation.read }.forEach {
activity.markThreadMessagesRead(it.threadId) activity.markThreadMessagesRead(it.threadId)
@ -223,7 +234,7 @@ class ConversationsAdapter(
return return
} }
val conversationsMarkedAsUnread = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> val conversationsMarkedAsUnread = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
ensureBackgroundThread { ensureBackgroundThread {
conversationsMarkedAsUnread.filter { conversation -> conversation.read }.forEach { conversationsMarkedAsUnread.filter { conversation -> conversation.read }.forEach {
activity.markThreadMessagesUnread(it.threadId) activity.markThreadMessagesUnread(it.threadId)
@ -246,7 +257,7 @@ class ConversationsAdapter(
} }
} }
private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation> private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
private fun pinConversation(pin: Boolean) { private fun pinConversation(pin: Boolean) {
val conversations = getSelectedItems() val conversations = getSelectedItems()
@ -285,14 +296,9 @@ class ConversationsAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
fun updateConversations(newConversations: ArrayList<Conversation>) { fun updateConversations(newConversations: ArrayList<Conversation>, commitCallback: (() -> Unit)? = null) {
val latestConversations = newConversations.clone() as ArrayList<Conversation> saveRecyclerViewState()
val oldHashCode = conversations.hashCode() submitList(newConversations.toList(), commitCallback)
val newHashCode = latestConversations.hashCode()
if (newHashCode != oldHashCode) {
conversations = latestConversations
notifyDataSetChanged()
}
} }
fun updateDrafts() { 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<Conversation>() {
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)
}
}
} }

View File

@ -11,6 +11,7 @@ import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.RequestListener
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target 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.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SimpleContactsHelper 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.dialogs.SelectTextDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.* 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_attachment_image.view.*
import kotlinx.android.synthetic.main.item_received_message.view.* import kotlinx.android.synthetic.main.item_received_message.view.*
import kotlinx.android.synthetic.main.item_received_message.view.thread_mesage_attachments_holder 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_sent_message.view.*
import kotlinx.android.synthetic.main.item_thread_date_time.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_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_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.* import kotlinx.android.synthetic.main.item_thread_success.view.*
class ThreadAdapter( class ThreadAdapter(
activity: SimpleActivity, var messages: ArrayList<ThreadItem>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit activity: SimpleActivity,
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit,
val deleteMessages: (messages: List<Message>) -> Unit
) : MyRecyclerViewListAdapter<ThreadItem>(activity, recyclerView, ThreadItemDiffCallback(), itemClick) {
private var fontSize = activity.getTextSize() private var fontSize = activity.getTextSize()
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -59,6 +67,7 @@ class ThreadAdapter(
init { init {
setupDragListener(true) setupDragListener(true)
setHasStableIds(true)
} }
override fun getActionMenuId() = R.menu.cab_thread 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<Message>().size
override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position) 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() {} override fun onActionModeCreated() {}
@ -106,6 +115,7 @@ class ThreadAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layout = when (viewType) { val layout = when (viewType) {
THREAD_LOADING -> R.layout.item_thread_loading
THREAD_DATE_TIME -> R.layout.item_thread_date_time THREAD_DATE_TIME -> R.layout.item_thread_date_time
THREAD_RECEIVED_MESSAGE -> R.layout.item_received_message THREAD_RECEIVED_MESSAGE -> R.layout.item_received_message
THREAD_SENT_MESSAGE_ERROR -> R.layout.item_thread_error THREAD_SENT_MESSAGE_ERROR -> R.layout.item_thread_error
@ -117,32 +127,37 @@ class ThreadAdapter(
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = messages[position] val item = getItem(position)
val isClickable = item is ThreadError || item is Message val isClickable = item is ThreadError || item is Message
val isLongClickable = item is Message val isLongClickable = item is Message
holder.bindView(item, isClickable, isLongClickable) { itemView, layoutPosition -> holder.bindView(item, isClickable, isLongClickable) { itemView, _ ->
when (item) { when (item) {
is ThreadLoading -> setupThreadLoading(itemView)
is ThreadDateTime -> setupDateTime(itemView, item) is ThreadDateTime -> setupDateTime(itemView, item)
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
is ThreadError -> setupThreadError(itemView) is ThreadError -> setupThreadError(itemView)
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
is ThreadSending -> setupThreadSending(itemView) is ThreadSending -> setupThreadSending(itemView)
else -> setupView(holder, itemView, item as Message) is Message -> setupView(holder, itemView, item)
} }
} }
bindViewHolder(holder) 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 { override fun getItemViewType(position: Int): Int {
val item = messages[position] return when (val item = getItem(position)) {
return when { is ThreadLoading -> THREAD_LOADING
item is ThreadDateTime -> THREAD_DATE_TIME is ThreadDateTime -> THREAD_DATE_TIME
(messages[position] as? Message)?.isReceivedMessage() == true -> THREAD_RECEIVED_MESSAGE is ThreadError -> THREAD_SENT_MESSAGE_ERROR
item is ThreadError -> THREAD_SENT_MESSAGE_ERROR is ThreadSent -> THREAD_SENT_MESSAGE_SENT
item is ThreadSent -> THREAD_SENT_MESSAGE_SENT is ThreadSending -> THREAD_SENT_MESSAGE_SENDING
item is ThreadSending -> THREAD_SENT_MESSAGE_SENDING is Message -> if (item.isReceivedMessage()) THREAD_RECEIVED_MESSAGE else THREAD_SENT_MESSAGE
else -> THREAD_SENT_MESSAGE
} }
} }
@ -185,45 +200,14 @@ class ThreadAdapter(
ConfirmationDialog(activity, question) { ConfirmationDialog(activity, question) {
ensureBackgroundThread { ensureBackgroundThread {
deleteMessages() val messagesToRemove = getSelectedItems()
if (messagesToRemove.isNotEmpty()) {
deleteMessages(messagesToRemove.filterIsInstance<Message>())
}
} }
} }
} }
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<Message>()
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() { private fun forwardMessage() {
val message = getSelectedItems().firstOrNull() as? Message ?: return val message = getSelectedItems().firstOrNull() as? Message ?: return
val attachment = message.attachment?.attachments?.firstOrNull() 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<ThreadItem> private fun getSelectedItems() = currentList.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList<ThreadItem>
private fun isThreadDateTime(position: Int) = messages.getOrNull(position) is ThreadDateTime private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime
fun updateMessages(newMessages: ArrayList<ThreadItem>, scrollPosition: Int = newMessages.size - 1) { fun updateMessages(newMessages: ArrayList<ThreadItem>, scrollPosition: Int = newMessages.lastIndex) {
val latestMessages = newMessages.clone() as ArrayList<ThreadItem> val latestMessages = newMessages.toMutableList()
val oldHashCode = messages.hashCode() submitList(latestMessages) {
val newHashCode = latestMessages.hashCode() if (scrollPosition != -1) {
if (newHashCode != oldHashCode) { recyclerView.scrollToPosition(scrollPosition)
messages = latestMessages }
notifyDataSetChanged()
recyclerView.scrollToPosition(scrollPosition)
} }
} }
@ -484,6 +466,8 @@ class ThreadAdapter(
} }
} }
private fun setupThreadLoading(view: View) = view.thread_loading.setIndicatorColor(properPrimaryColor)
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder) super.onViewRecycled(holder)
if (!activity.isDestroyed && !activity.isFinishing && holder.itemView.thread_message_sender_photo != null) { if (!activity.isDestroyed && !activity.isFinishing && holder.itemView.thread_message_sender_photo != null) {
@ -491,3 +475,29 @@ class ThreadAdapter(
} }
} }
} }
private class ThreadItemDiffCallback : DiffUtil.ItemCallback<ThreadItem>() {
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)
}
}
}

View File

@ -58,7 +58,13 @@ val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB().
val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao() val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao()
fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom: Int = -1, includeScheduledMessages: Boolean = true): ArrayList<Message> { fun Context.getMessages(
threadId: Long,
getImageResolutions: Boolean,
dateFrom: Int = -1,
includeScheduledMessages: Boolean = true,
limit: Int = MESSAGES_LIMIT
): ArrayList<Message> {
val uri = Sms.CONTENT_URI val uri = Sms.CONTENT_URI
val projection = arrayOf( val projection = arrayOf(
Sms._ID, 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 rangeQuery = if (dateFrom == -1) "" else "AND ${Sms.DATE} < ${dateFrom.toLong() * 1000}"
val selection = "${Sms.THREAD_ID} = ? $rangeQuery" val selection = "${Sms.THREAD_ID} = ? $rangeQuery"
val selectionArgs = arrayOf(threadId.toString()) val selectionArgs = arrayOf(threadId.toString())
val sortOrder = "${Sms.DATE} DESC LIMIT $MESSAGES_LIMIT" val sortOrder = "${Sms.DATE} DESC LIMIT $limit"
val blockStatus = HashMap<String, Boolean>() val blockStatus = HashMap<String, Boolean>()
val blockedNumbers = getBlockedNumbers() val blockedNumbers = getBlockedNumbers()

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.view.View import android.view.View
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SimpleContactsHelper import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import kotlinx.android.synthetic.main.item_attachment_document.view.* import kotlinx.android.synthetic.main.item_attachment_document.view.*
@ -25,12 +26,18 @@ fun View.setupDocumentPreview(
filename.text = title filename.text = title
} }
try { ensureBackgroundThread {
val size = context.getFileSizeFromUri(uri) try {
file_size.beVisible() val size = context.getFileSizeFromUri(uri)
file_size.text = size.formatSize() post {
} catch (e: Exception) { file_size.beVisible()
file_size.beGone() file_size.text = size.formatSize()
}
} catch (e: Exception) {
post {
file_size.beGone()
}
}
} }
val textColor = context.getProperTextColor() val textColor = context.getProperTextColor()

View File

@ -46,6 +46,7 @@ const val THREAD_SENT_MESSAGE = 3
const val THREAD_SENT_MESSAGE_ERROR = 4 const val THREAD_SENT_MESSAGE_ERROR = 4
const val THREAD_SENT_MESSAGE_SENT = 5 const val THREAD_SENT_MESSAGE_SENT = 5
const val THREAD_SENT_MESSAGE_SENDING = 6 const val THREAD_SENT_MESSAGE_SENDING = 6
const val THREAD_LOADING = 7
// view types for attachment list // view types for attachment list
const val ATTACHMENT_DOCUMENT = 7 const val ATTACHMENT_DOCUMENT = 7

View File

@ -4,8 +4,6 @@ import android.app.AlarmManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.core.app.AlarmManagerCompat import androidx.core.app.AlarmManagerCompat
import com.klinker.android.send_message.Settings import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction import com.klinker.android.send_message.Transaction
@ -71,12 +69,10 @@ fun Context.sendMessage(text: String, addresses: List<String>, subscriptionId: I
transaction.setExplicitBroadcastForSentSms(smsSentIntent) transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent) transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
Handler(Looper.getMainLooper()).post { try {
try { transaction.sendNewMessage(message)
transaction.sendNewMessage(message) } catch (e: Exception) {
} catch (e: Exception) { showErrorToast(e)
showErrorToast(e)
}
} }
} }

View File

@ -18,13 +18,19 @@ data class Conversation(
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false @ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false
) { ) {
fun areContentsTheSame(other: Conversation): Boolean { companion object {
return snippet == other.snippet && fun areItemsTheSame(old: Conversation, new: Conversation): Boolean {
date == other.date && return old.threadId == new.threadId
read == other.read && }
title == other.title &&
photoUri == other.photoUri && fun areContentsTheSame(old: Conversation, new: Conversation): Boolean {
isGroupConversation == other.isGroupConversation && return old.snippet == new.snippet &&
phoneNumber == other.phoneNumber old.date == new.date &&
old.read == new.read &&
old.title == new.title &&
old.photoUri == new.photoUri &&
old.isGroupConversation == new.isGroupConversation &&
old.phoneNumber == new.phoneNumber
}
} }
} }

View File

@ -27,4 +27,35 @@ data class Message(
fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX
fun millis() = date * 1000L 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
}
}
} }

View File

@ -1,3 +0,0 @@
package com.simplemobiletools.smsmessenger.models
data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem()

View File

@ -1,3 +0,0 @@
package com.simplemobiletools.smsmessenger.models
data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem()

View File

@ -1,3 +0,0 @@
package com.simplemobiletools.smsmessenger.models
open class ThreadItem

View File

@ -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()
}

View File

@ -1,3 +0,0 @@
package com.simplemobiletools.smsmessenger.models
data class ThreadSending(val messageId: Long) : ThreadItem()

View File

@ -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()

View File

@ -3,6 +3,8 @@ package com.simplemobiletools.smsmessenger.receivers
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
@ -43,7 +45,9 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
val attachments = message.attachment?.attachments ?: emptyList() val attachments = message.attachment?.attachments ?: emptyList()
try { 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 // delete temporary conversation and message as it's already persisted to the telephony db now
context.deleteScheduledMessage(messageId) context.deleteScheduledMessage(messageId)

View File

@ -3,9 +3,10 @@
android:shape="rectangle"> android:shape="rectangle">
<corners <corners
android:bottomRightRadius="@dimen/normal_margin" android:bottomLeftRadius="@dimen/small_margin"
android:topLeftRadius="@dimen/normal_margin" android:bottomRightRadius="@dimen/big_margin"
android:topRightRadius="@dimen/normal_margin" /> android:topLeftRadius="@dimen/big_margin"
android:topRightRadius="@dimen/big_margin" />
<solid android:color="@color/activated_item_foreground" /> <solid android:color="@color/activated_item_foreground" />

View File

@ -3,9 +3,10 @@
android:shape="rectangle"> android:shape="rectangle">
<corners <corners
android:bottomLeftRadius="@dimen/normal_margin" android:bottomLeftRadius="@dimen/big_margin"
android:topLeftRadius="@dimen/normal_margin" android:bottomRightRadius="@dimen/small_margin"
android:topRightRadius="@dimen/normal_margin" /> android:topLeftRadius="@dimen/big_margin"
android:topRightRadius="@dimen/big_margin" />
<solid android:color="@color/color_primary" /> <solid android:color="@color/color_primary" />

View File

@ -29,6 +29,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="@dimen/medium_margin" android:layout_marginStart="@dimen/medium_margin"
android:orientation="vertical"> android:orientation="vertical">
@ -48,6 +49,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="@dimen/normal_text_size" android:textSize="@dimen/normal_text_size"
android:visibility="gone"
tools:text="2.18 KB" /> tools:text="2.18 KB" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="@dimen/normal_margin">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/thread_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/small_margin"
android:indeterminate="true"
app:indicatorSize="@dimen/big_margin"
app:trackCornerRadius="@dimen/normal_margin" />
</LinearLayout>