Rename package to org.fossify.messages
This commit is contained in:
parent
d71db351ca
commit
e2f83f49da
106 changed files with 417 additions and 418 deletions
|
|
@ -0,0 +1,96 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.view.Menu
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.extensions.notificationManager
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.extensions.deleteConversation
|
||||
import org.fossify.messages.extensions.updateConversationArchivedStatus
|
||||
import org.fossify.messages.helpers.refreshMessages
|
||||
import org.fossify.messages.models.Conversation
|
||||
|
||||
class ArchivedConversationsAdapter(
|
||||
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
|
||||
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
|
||||
override fun getActionMenuId() = R.menu.cab_archived_conversations
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {}
|
||||
|
||||
override fun actionItemPressed(id: Int) {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
when (id) {
|
||||
R.id.cab_delete -> askConfirmDelete()
|
||||
R.id.cab_unarchive -> unarchiveConversation()
|
||||
R.id.cab_select_all -> selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmDelete() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
|
||||
|
||||
val baseString = org.fossify.commons.R.string.deletion_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
deleteConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversations() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToRemove.forEach {
|
||||
activity.deleteConversation(it.threadId)
|
||||
activity.notificationManager.cancel(it.threadId.hashCode())
|
||||
}
|
||||
|
||||
removeConversationsFromList(conversationsToRemove)
|
||||
}
|
||||
|
||||
private fun unarchiveConversation() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBackgroundThread {
|
||||
val conversationsToUnarchive = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToUnarchive.forEach {
|
||||
activity.updateConversationArchivedStatus(it.threadId, false)
|
||||
}
|
||||
|
||||
removeConversationsFromList(conversationsToUnarchive)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeConversationsFromList(removedConversations: List<Conversation>) {
|
||||
val newList = try {
|
||||
currentList.toMutableList().apply { removeAll(removedConversations) }
|
||||
} catch (ignored: Exception) {
|
||||
currentList.toMutableList()
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
|
||||
refreshMessages()
|
||||
finishActMode()
|
||||
} else {
|
||||
submitList(newList)
|
||||
if (newList.isEmpty()) {
|
||||
refreshMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
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 org.fossify.commons.activities.BaseSimpleActivity
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.VCardViewerActivity
|
||||
import org.fossify.messages.databinding.ItemAttachmentDocumentPreviewBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentMediaPreviewBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentVcardPreviewBinding
|
||||
import org.fossify.messages.extensions.*
|
||||
import org.fossify.messages.helpers.*
|
||||
import org.fossify.messages.models.AttachmentSelection
|
||||
|
||||
class AttachmentsAdapter(
|
||||
val activity: BaseSimpleActivity,
|
||||
val recyclerView: RecyclerView,
|
||||
val onAttachmentsRemoved: () -> Unit,
|
||||
val onReady: (() -> Unit)
|
||||
) : ListAdapter<AttachmentSelection, AttachmentsAdapter.AttachmentsViewHolder>(AttachmentDiffCallback()) {
|
||||
|
||||
private val config = activity.config
|
||||
private val resources = activity.resources
|
||||
private val primaryColor = activity.getProperPrimaryColor()
|
||||
private val imageCompressor by lazy { ImageCompressor(activity) }
|
||||
|
||||
val attachments = mutableListOf<AttachmentSelection>()
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return getItem(position).viewType
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AttachmentsViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = when (viewType) {
|
||||
ATTACHMENT_DOCUMENT -> ItemAttachmentDocumentPreviewBinding.inflate(inflater, parent, false)
|
||||
ATTACHMENT_VCARD -> ItemAttachmentVcardPreviewBinding.inflate(inflater, parent, false)
|
||||
ATTACHMENT_MEDIA -> ItemAttachmentMediaPreviewBinding.inflate(inflater, parent, false)
|
||||
else -> throw IllegalArgumentException("Unknown view type: $viewType")
|
||||
}
|
||||
|
||||
return AttachmentsViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AttachmentsViewHolder, position: Int) {
|
||||
val attachment = getItem(position)
|
||||
holder.bindView { binding, _ ->
|
||||
when (attachment.viewType) {
|
||||
ATTACHMENT_DOCUMENT -> {
|
||||
(binding as ItemAttachmentDocumentPreviewBinding).setupDocumentPreview(
|
||||
uri = attachment.uri,
|
||||
title = attachment.filename,
|
||||
mimeType = attachment.mimetype,
|
||||
onClick = { activity.launchViewIntent(attachment.uri, attachment.mimetype, attachment.filename) },
|
||||
onRemoveButtonClicked = { removeAttachment(attachment) }
|
||||
)
|
||||
}
|
||||
|
||||
ATTACHMENT_VCARD -> {
|
||||
(binding as ItemAttachmentVcardPreviewBinding).setupVCardPreview(
|
||||
activity = activity,
|
||||
uri = attachment.uri,
|
||||
onClick = {
|
||||
val intent = Intent(activity, VCardViewerActivity::class.java).also {
|
||||
it.putExtra(EXTRA_VCARD_URI, attachment.uri)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
},
|
||||
onRemoveButtonClicked = { removeAttachment(attachment) }
|
||||
)
|
||||
}
|
||||
|
||||
ATTACHMENT_MEDIA -> setupMediaPreview(
|
||||
binding = binding as ItemAttachmentMediaPreviewBinding,
|
||||
attachment = attachment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
attachments.clear()
|
||||
submitList(emptyList())
|
||||
recyclerView.onGlobalLayout {
|
||||
onAttachmentsRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
fun addAttachment(attachment: AttachmentSelection) {
|
||||
attachments.removeAll { AttachmentSelection.areItemsTheSame(it, attachment) }
|
||||
attachments.add(attachment)
|
||||
submitList(attachments.toList())
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: AttachmentSelection) {
|
||||
attachments.removeAll { AttachmentSelection.areItemsTheSame(it, attachment) }
|
||||
if (attachments.isEmpty()) {
|
||||
clear()
|
||||
} else {
|
||||
submitList(attachments.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMediaPreview(binding: ItemAttachmentMediaPreviewBinding, attachment: AttachmentSelection) {
|
||||
binding.apply {
|
||||
mediaAttachmentHolder.background.applyColorFilter(primaryColor.darkenColor())
|
||||
mediaAttachmentHolder.setOnClickListener {
|
||||
activity.launchViewIntent(attachment.uri, attachment.mimetype, attachment.filename)
|
||||
}
|
||||
|
||||
removeAttachmentButtonHolder.removeAttachmentButton.apply {
|
||||
beVisible()
|
||||
background.applyColorFilter(primaryColor)
|
||||
setOnClickListener {
|
||||
removeAttachment(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
val compressImage = attachment.mimetype.isImageMimeType() && !attachment.mimetype.isGifMimeType()
|
||||
if (compressImage && attachment.isPending && config.mmsFileSizeLimit != FILE_SIZE_NONE) {
|
||||
thumbnail.beGone()
|
||||
compressionProgress.beVisible()
|
||||
|
||||
imageCompressor.compressImage(attachment.uri, config.mmsFileSizeLimit) { compressedUri ->
|
||||
activity.runOnUiThread {
|
||||
when (compressedUri) {
|
||||
attachment.uri -> {
|
||||
attachments.find { it.uri == attachment.uri }?.isPending = false
|
||||
loadMediaPreview(this, attachment)
|
||||
}
|
||||
|
||||
null -> {
|
||||
activity.toast(R.string.compress_error)
|
||||
removeAttachment(attachment)
|
||||
}
|
||||
|
||||
else -> {
|
||||
attachments.remove(attachment)
|
||||
addAttachment(attachment.copy(uri = compressedUri, isPending = false))
|
||||
}
|
||||
}
|
||||
onReady()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadMediaPreview(this, attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaPreview(binding: ItemAttachmentMediaPreviewBinding, attachment: AttachmentSelection) {
|
||||
val roundedCornersRadius = resources.getDimension(org.fossify.commons.R.dimen.activity_margin).toInt()
|
||||
val size = resources.getDimension(R.dimen.attachment_preview_size).toInt()
|
||||
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transform(CenterCrop(), RoundedCorners(roundedCornersRadius))
|
||||
|
||||
Glide.with(binding.thumbnail)
|
||||
.load(attachment.uri)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.override(size, size)
|
||||
.apply(options)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||
removeAttachment(attachment)
|
||||
activity.toast(org.fossify.commons.R.string.unknown_error_occurred)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(dr: Drawable, a: Any, t: Target<Drawable>, d: DataSource, i: Boolean): Boolean {
|
||||
binding.thumbnail.beVisible()
|
||||
binding.playIcon.beVisibleIf(attachment.mimetype.isVideoMimeType())
|
||||
binding.compressionProgress.beGone()
|
||||
return false
|
||||
}
|
||||
})
|
||||
.into(binding.thumbnail)
|
||||
}
|
||||
|
||||
inner class AttachmentsViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bindView(callback: (binding: ViewBinding, adapterPosition: Int) -> Unit) {
|
||||
callback(binding, adapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentDiffCallback : DiffUtil.ItemCallback<AttachmentSelection>() {
|
||||
override fun areItemsTheSame(oldItem: AttachmentSelection, newItem: AttachmentSelection): Boolean {
|
||||
return AttachmentSelection.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AttachmentSelection, newItem: AttachmentSelection): Boolean {
|
||||
return AttachmentSelection.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import org.fossify.commons.databinding.ItemContactWithNumberBinding
|
||||
import org.fossify.commons.extensions.darkenColor
|
||||
import org.fossify.commons.extensions.getContrastColor
|
||||
import org.fossify.commons.extensions.getProperBackgroundColor
|
||||
import org.fossify.commons.extensions.normalizeString
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
|
||||
class AutoCompleteTextViewAdapter(val activity: SimpleActivity, val contacts: ArrayList<SimpleContact>) : ArrayAdapter<SimpleContact>(activity, 0, contacts) {
|
||||
var resultList = ArrayList<SimpleContact>()
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val contact = resultList.getOrNull(position)
|
||||
var listItem = convertView
|
||||
if (listItem == null || listItem.tag != contact?.name?.isNotEmpty()) {
|
||||
listItem = ItemContactWithNumberBinding.inflate(LayoutInflater.from(activity), parent, false).root
|
||||
}
|
||||
|
||||
listItem.tag = contact?.name?.isNotEmpty()
|
||||
ItemContactWithNumberBinding.bind(listItem).apply {
|
||||
// clickable and focusable properties seem to break Autocomplete clicking, so remove them
|
||||
itemContactFrame.apply {
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
val backgroundColor = activity.getProperBackgroundColor()
|
||||
itemContactFrame.setBackgroundColor(backgroundColor.darkenColor())
|
||||
itemContactName.setTextColor(backgroundColor.getContrastColor())
|
||||
itemContactNumber.setTextColor(backgroundColor.getContrastColor())
|
||||
|
||||
if (contact != null) {
|
||||
itemContactName.text = contact.name
|
||||
itemContactNumber.text = contact.phoneNumbers.first().normalizedNumber
|
||||
SimpleContactsHelper(context).loadContactImage(contact.photoUri, itemContactImage, contact.name)
|
||||
}
|
||||
}
|
||||
|
||||
return listItem
|
||||
}
|
||||
|
||||
override fun getFilter() = object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
if (constraint != null) {
|
||||
val results = mutableListOf<SimpleContact>()
|
||||
val searchString = constraint.toString().normalizeString()
|
||||
contacts.forEach {
|
||||
if (it.doesContainPhoneNumber(searchString) || it.name.contains(searchString, true)) {
|
||||
results.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
results.sortWith(compareBy { !it.name.startsWith(searchString, true) })
|
||||
|
||||
filterResults.values = results
|
||||
filterResults.count = results.size
|
||||
}
|
||||
return filterResults
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||
if (results != null && results.count > 0) {
|
||||
resultList.clear()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
resultList.addAll(results.values as List<SimpleContact>)
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertResultToString(resultValue: Any?) = (resultValue as? SimpleContact)?.name
|
||||
}
|
||||
|
||||
override fun getItem(index: Int) = resultList[index]
|
||||
|
||||
override fun getCount() = resultList.size
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Parcelable
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||
import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.databinding.ItemConversationBinding
|
||||
import org.fossify.messages.extensions.config
|
||||
import org.fossify.messages.extensions.getAllDrafts
|
||||
import org.fossify.messages.models.Conversation
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
abstract class BaseConversationsAdapter(
|
||||
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
|
||||
) : MyRecyclerViewListAdapter<Conversation>(activity, recyclerView, ConversationDiffCallback(), itemClick, onRefresh),
|
||||
RecyclerViewFastScroller.OnPopupTextUpdate {
|
||||
private var fontSize = activity.getTextSize()
|
||||
private var drafts = HashMap<Long, String?>()
|
||||
|
||||
private var recyclerViewState: Parcelable? = null
|
||||
|
||||
init {
|
||||
setupDragListener(true)
|
||||
ensureBackgroundThread {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
fun updateFontSize() {
|
||||
fontSize = activity.getTextSize()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateConversations(newConversations: ArrayList<Conversation>, commitCallback: (() -> Unit)? = null) {
|
||||
saveRecyclerViewState()
|
||||
submitList(newConversations.toList(), commitCallback)
|
||||
}
|
||||
|
||||
fun updateDrafts() {
|
||||
ensureBackgroundThread {
|
||||
val newDrafts = HashMap<Long, String?>()
|
||||
fetchDrafts(newDrafts)
|
||||
if (drafts.hashCode() != newDrafts.hashCode()) {
|
||||
drafts = newDrafts
|
||||
activity.runOnUiThread {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSelectableItemCount() = itemCount
|
||||
|
||||
protected fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
|
||||
override fun getIsItemSelectable(position: Int) = true
|
||||
|
||||
override fun getItemSelectionKey(position: Int) = currentList.getOrNull(position)?.hashCode()
|
||||
|
||||
override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { it.hashCode() == key }
|
||||
|
||||
override fun onActionModeCreated() {}
|
||||
|
||||
override fun onActionModeDestroyed() {}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ItemConversationBinding.inflate(layoutInflater, parent, false)
|
||||
return createViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val conversation = getItem(position)
|
||||
holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ ->
|
||||
setupView(itemView, conversation)
|
||||
}
|
||||
bindViewHolder(holder)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = getItem(position).threadId
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
val itemView = ItemConversationBinding.bind(holder.itemView)
|
||||
Glide.with(activity).clear(itemView.conversationImage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchDrafts(drafts: HashMap<Long, String?>) {
|
||||
drafts.clear()
|
||||
for ((threadId, draft) in activity.getAllDrafts()) {
|
||||
drafts[threadId] = draft
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupView(view: View, conversation: Conversation) {
|
||||
ItemConversationBinding.bind(view).apply {
|
||||
root.setupViewBackground(activity)
|
||||
val smsDraft = drafts[conversation.threadId]
|
||||
draftIndicator.beVisibleIf(smsDraft != null)
|
||||
draftIndicator.setTextColor(properPrimaryColor)
|
||||
|
||||
pinIndicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString()))
|
||||
pinIndicator.applyColorFilter(textColor)
|
||||
|
||||
conversationFrame.isSelected = selectedKeys.contains(conversation.hashCode())
|
||||
|
||||
conversationAddress.apply {
|
||||
text = conversation.title
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
|
||||
}
|
||||
|
||||
conversationBodyShort.apply {
|
||||
text = smsDraft ?: conversation.snippet
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f)
|
||||
}
|
||||
|
||||
conversationDate.apply {
|
||||
text = conversation.date.formatDateOrTime(context, true, false)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
|
||||
}
|
||||
|
||||
val style = if (conversation.read) {
|
||||
conversationBodyShort.alpha = 0.7f
|
||||
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
|
||||
} else {
|
||||
conversationBodyShort.alpha = 1f
|
||||
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
|
||||
|
||||
}
|
||||
conversationAddress.setTypeface(null, style)
|
||||
conversationBodyShort.setTypeface(null, style)
|
||||
|
||||
arrayListOf(conversationAddress, conversationBodyShort, conversationDate).forEach {
|
||||
it.setTextColor(textColor)
|
||||
}
|
||||
|
||||
// at group conversations we use an icon as the placeholder, not any letter
|
||||
val placeholder = if (conversation.isGroupConversation) {
|
||||
SimpleContactsHelper(activity).getColoredGroupIcon(conversation.title)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
SimpleContactsHelper(activity).loadContactImage(conversation.photoUri, conversationImage, conversation.title, placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import org.fossify.commons.adapters.MyRecyclerViewAdapter
|
||||
import org.fossify.commons.databinding.ItemContactWithNumberBinding
|
||||
import org.fossify.commons.extensions.getTextSize
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
|
||||
class ContactsAdapter(
|
||||
activity: SimpleActivity, var contacts: ArrayList<SimpleContact>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
|
||||
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
|
||||
private var fontSize = activity.getTextSize()
|
||||
|
||||
override fun getActionMenuId() = 0
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {}
|
||||
|
||||
override fun actionItemPressed(id: Int) {}
|
||||
|
||||
override fun getSelectableItemCount() = contacts.size
|
||||
|
||||
override fun getIsItemSelectable(position: Int) = true
|
||||
|
||||
override fun getItemSelectionKey(position: Int) = contacts.getOrNull(position)?.rawId
|
||||
|
||||
override fun getItemKeyPosition(key: Int) = contacts.indexOfFirst { it.rawId == key }
|
||||
|
||||
override fun onActionModeCreated() {}
|
||||
|
||||
override fun onActionModeDestroyed() {}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ItemContactWithNumberBinding.inflate(layoutInflater, parent, false)
|
||||
return createViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val contact = contacts[position]
|
||||
holder.bindView(contact, allowSingleClick = true, allowLongClick = false) { itemView, _ ->
|
||||
setupView(itemView, contact)
|
||||
}
|
||||
bindViewHolder(holder)
|
||||
}
|
||||
|
||||
override fun getItemCount() = contacts.size
|
||||
|
||||
fun updateContacts(newContacts: ArrayList<SimpleContact>) {
|
||||
val oldHashCode = contacts.hashCode()
|
||||
val newHashCode = newContacts.hashCode()
|
||||
if (newHashCode != oldHashCode) {
|
||||
contacts = newContacts
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupView(view: View, contact: SimpleContact) {
|
||||
ItemContactWithNumberBinding.bind(view).apply {
|
||||
itemContactName.apply {
|
||||
text = contact.name
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
|
||||
}
|
||||
|
||||
itemContactNumber.apply {
|
||||
text = TextUtils.join(", ", contact.phoneNumbers.map { it.normalizedNumber })
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||
}
|
||||
|
||||
SimpleContactsHelper(activity).loadContactImage(contact.photoUri, itemContactImage, contact.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
val binding = ItemContactWithNumberBinding.bind(holder.itemView)
|
||||
Glide.with(activity).clear(binding.itemContactImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.dialogs.FeatureLockedDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.KEY_PHONE
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.helpers.isNougatPlus
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.dialogs.RenameConversationDialog
|
||||
import org.fossify.messages.extensions.*
|
||||
import org.fossify.messages.helpers.refreshMessages
|
||||
import org.fossify.messages.messaging.isShortCodeWithLetters
|
||||
import org.fossify.messages.models.Conversation
|
||||
|
||||
class ConversationsAdapter(
|
||||
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
|
||||
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
|
||||
override fun getActionMenuId() = R.menu.cab_conversations
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {
|
||||
val selectedItems = getSelectedItems()
|
||||
val isSingleSelection = isOneItemSelected()
|
||||
val selectedConversation = selectedItems.firstOrNull() ?: return
|
||||
val isGroupConversation = selectedConversation.isGroupConversation
|
||||
val archiveAvailable = activity.config.isArchiveAvailable
|
||||
|
||||
menu.apply {
|
||||
findItem(R.id.cab_block_number).title = activity.addLockedLabelIfNeeded(org.fossify.commons.R.string.block_number)
|
||||
findItem(R.id.cab_block_number).isVisible = isNougatPlus()
|
||||
findItem(R.id.cab_add_number_to_contact).isVisible = isSingleSelection && !isGroupConversation
|
||||
findItem(R.id.cab_dial_number).isVisible = isSingleSelection && !isGroupConversation && !isShortCodeWithLetters(selectedConversation.phoneNumber)
|
||||
findItem(R.id.cab_copy_number).isVisible = isSingleSelection && !isGroupConversation
|
||||
findItem(R.id.cab_rename_conversation).isVisible = isSingleSelection && isGroupConversation
|
||||
findItem(R.id.cab_mark_as_read).isVisible = selectedItems.any { !it.read }
|
||||
findItem(R.id.cab_mark_as_unread).isVisible = selectedItems.any { it.read }
|
||||
findItem(R.id.cab_archive).isVisible = archiveAvailable
|
||||
checkPinBtnVisibility(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionItemPressed(id: Int) {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
when (id) {
|
||||
R.id.cab_add_number_to_contact -> addNumberToContact()
|
||||
R.id.cab_block_number -> tryBlocking()
|
||||
R.id.cab_dial_number -> dialNumber()
|
||||
R.id.cab_copy_number -> copyNumberToClipboard()
|
||||
R.id.cab_delete -> askConfirmDelete()
|
||||
R.id.cab_archive -> askConfirmArchive()
|
||||
R.id.cab_rename_conversation -> renameConversation(getSelectedItems().first())
|
||||
R.id.cab_mark_as_read -> markAsRead()
|
||||
R.id.cab_mark_as_unread -> markAsUnread()
|
||||
R.id.cab_pin_conversation -> pinConversation(true)
|
||||
R.id.cab_unpin_conversation -> pinConversation(false)
|
||||
R.id.cab_select_all -> selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryBlocking() {
|
||||
if (activity.isOrWasThankYouInstalled()) {
|
||||
askConfirmBlock()
|
||||
} else {
|
||||
FeatureLockedDialog(activity) { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmBlock() {
|
||||
val numbers = getSelectedItems().distinctBy { it.phoneNumber }.map { it.phoneNumber }
|
||||
val numbersString = TextUtils.join(", ", numbers)
|
||||
val question = String.format(resources.getString(org.fossify.commons.R.string.block_confirmation), numbersString)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
blockNumbers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun blockNumbers() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val numbersToBlock = getSelectedItems()
|
||||
val newList = currentList.toMutableList().apply { removeAll(numbersToBlock) }
|
||||
|
||||
ensureBackgroundThread {
|
||||
numbersToBlock.map { it.phoneNumber }.forEach { number ->
|
||||
activity.addBlockedNumber(number)
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
submitList(newList)
|
||||
finishActMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dialNumber() {
|
||||
val conversation = getSelectedItems().firstOrNull() ?: return
|
||||
activity.dialNumber(conversation.phoneNumber) {
|
||||
finishActMode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyNumberToClipboard() {
|
||||
val conversation = getSelectedItems().firstOrNull() ?: return
|
||||
activity.copyToClipboard(conversation.phoneNumber)
|
||||
finishActMode()
|
||||
}
|
||||
|
||||
private fun askConfirmDelete() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
|
||||
|
||||
val baseString = org.fossify.commons.R.string.deletion_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
deleteConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmArchive() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
|
||||
|
||||
val baseString = R.string.archive_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
archiveConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun archiveConversations() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToRemove.forEach {
|
||||
activity.updateConversationArchivedStatus(it.threadId, true)
|
||||
activity.notificationManager.cancel(it.threadId.hashCode())
|
||||
}
|
||||
|
||||
val newList = try {
|
||||
currentList.toMutableList().apply { removeAll(conversationsToRemove) }
|
||||
} catch (ignored: Exception) {
|
||||
currentList.toMutableList()
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
|
||||
refreshMessages()
|
||||
finishActMode()
|
||||
} else {
|
||||
submitList(newList)
|
||||
if (newList.isEmpty()) {
|
||||
refreshMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversations() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToRemove.forEach {
|
||||
activity.deleteConversation(it.threadId)
|
||||
activity.notificationManager.cancel(it.threadId.hashCode())
|
||||
}
|
||||
|
||||
val newList = try {
|
||||
currentList.toMutableList().apply { removeAll(conversationsToRemove) }
|
||||
} catch (ignored: Exception) {
|
||||
currentList.toMutableList()
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
|
||||
refreshMessages()
|
||||
finishActMode()
|
||||
} else {
|
||||
submitList(newList)
|
||||
if (newList.isEmpty()) {
|
||||
refreshMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameConversation(conversation: Conversation) {
|
||||
RenameConversationDialog(activity, conversation) {
|
||||
ensureBackgroundThread {
|
||||
val updatedConv = activity.renameConversation(conversation, newTitle = it)
|
||||
activity.runOnUiThread {
|
||||
finishActMode()
|
||||
currentList.toMutableList().apply {
|
||||
set(indexOf(conversation), updatedConv)
|
||||
updateConversations(this as ArrayList<Conversation>)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsRead() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsMarkedAsRead = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
ensureBackgroundThread {
|
||||
conversationsMarkedAsRead.filter { conversation -> !conversation.read }.forEach {
|
||||
activity.markThreadMessagesRead(it.threadId)
|
||||
}
|
||||
|
||||
refreshConversations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsUnread() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsMarkedAsUnread = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
ensureBackgroundThread {
|
||||
conversationsMarkedAsUnread.filter { conversation -> conversation.read }.forEach {
|
||||
activity.markThreadMessagesUnread(it.threadId)
|
||||
}
|
||||
|
||||
refreshConversations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNumberToContact() {
|
||||
val conversation = getSelectedItems().firstOrNull() ?: return
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_INSERT_OR_EDIT
|
||||
type = "vnd.android.cursor.item/contact"
|
||||
putExtra(KEY_PHONE, conversation.phoneNumber)
|
||||
activity.launchActivityIntent(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pinConversation(pin: Boolean) {
|
||||
val conversations = getSelectedItems()
|
||||
if (conversations.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pin) {
|
||||
activity.config.addPinnedConversations(conversations)
|
||||
} else {
|
||||
activity.config.removePinnedConversations(conversations)
|
||||
}
|
||||
|
||||
getSelectedItemPositions().forEach {
|
||||
notifyItemChanged(it)
|
||||
}
|
||||
refreshConversations()
|
||||
}
|
||||
|
||||
private fun checkPinBtnVisibility(menu: Menu) {
|
||||
val pinnedConversations = activity.config.pinnedConversations
|
||||
val selectedConversations = getSelectedItems()
|
||||
menu.findItem(R.id.cab_pin_conversation).isVisible = selectedConversations.any { !pinnedConversations.contains(it.threadId.toString()) }
|
||||
menu.findItem(R.id.cab_unpin_conversation).isVisible = selectedConversations.any { pinnedConversations.contains(it.threadId.toString()) }
|
||||
}
|
||||
|
||||
private fun refreshConversations() {
|
||||
activity.runOnUiThread {
|
||||
refreshMessages()
|
||||
finishActMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.view.Menu
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.extensions.notificationManager
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.extensions.deleteConversation
|
||||
import org.fossify.messages.extensions.restoreAllMessagesFromRecycleBinForConversation
|
||||
import org.fossify.messages.helpers.refreshMessages
|
||||
import org.fossify.messages.models.Conversation
|
||||
|
||||
class RecycleBinConversationsAdapter(
|
||||
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
|
||||
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
|
||||
override fun getActionMenuId() = R.menu.cab_recycle_bin_conversations
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {}
|
||||
|
||||
override fun actionItemPressed(id: Int) {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
when (id) {
|
||||
R.id.cab_delete -> askConfirmDelete()
|
||||
R.id.cab_restore -> askConfirmRestore()
|
||||
R.id.cab_select_all -> selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmDelete() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
|
||||
|
||||
val baseString = org.fossify.commons.R.string.deletion_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
deleteConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversations() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToRemove.forEach {
|
||||
activity.deleteConversation(it.threadId)
|
||||
activity.notificationManager.cancel(it.threadId.hashCode())
|
||||
}
|
||||
|
||||
removeConversationsFromList(conversationsToRemove)
|
||||
}
|
||||
|
||||
private fun askConfirmRestore() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
|
||||
|
||||
val baseString = R.string.restore_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
restoreConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreConversations() {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
|
||||
conversationsToRemove.forEach {
|
||||
activity.restoreAllMessagesFromRecycleBinForConversation(it.threadId)
|
||||
}
|
||||
|
||||
removeConversationsFromList(conversationsToRemove)
|
||||
}
|
||||
|
||||
private fun removeConversationsFromList(removedConversations: List<Conversation>) {
|
||||
val newList = try {
|
||||
currentList.toMutableList().apply { removeAll(removedConversations) }
|
||||
} catch (ignored: Exception) {
|
||||
currentList.toMutableList()
|
||||
}
|
||||
|
||||
activity.runOnUiThread {
|
||||
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
|
||||
refreshMessages()
|
||||
finishActMode()
|
||||
} else {
|
||||
submitList(newList)
|
||||
if (newList.isEmpty()) {
|
||||
refreshMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import org.fossify.commons.adapters.MyRecyclerViewAdapter
|
||||
import org.fossify.commons.extensions.getTextSize
|
||||
import org.fossify.commons.extensions.highlightTextPart
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.databinding.ItemSearchResultBinding
|
||||
import org.fossify.messages.models.SearchResult
|
||||
|
||||
class SearchResultsAdapter(
|
||||
activity: SimpleActivity, var searchResults: ArrayList<SearchResult>, recyclerView: MyRecyclerView, highlightText: String, itemClick: (Any) -> Unit
|
||||
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
|
||||
|
||||
private var fontSize = activity.getTextSize()
|
||||
private var textToHighlight = highlightText
|
||||
|
||||
override fun getActionMenuId() = 0
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {}
|
||||
|
||||
override fun actionItemPressed(id: Int) {}
|
||||
|
||||
override fun getSelectableItemCount() = searchResults.size
|
||||
|
||||
override fun getIsItemSelectable(position: Int) = false
|
||||
|
||||
override fun getItemSelectionKey(position: Int) = searchResults.getOrNull(position)?.hashCode()
|
||||
|
||||
override fun getItemKeyPosition(key: Int) = searchResults.indexOfFirst { it.hashCode() == key }
|
||||
|
||||
override fun onActionModeCreated() {}
|
||||
|
||||
override fun onActionModeDestroyed() {}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false)
|
||||
return createViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val searchResult = searchResults[position]
|
||||
holder.bindView(searchResult, allowSingleClick = true, allowLongClick = false) { itemView, _ ->
|
||||
setupView(itemView, searchResult)
|
||||
}
|
||||
bindViewHolder(holder)
|
||||
}
|
||||
|
||||
override fun getItemCount() = searchResults.size
|
||||
|
||||
fun updateItems(newItems: ArrayList<SearchResult>, highlightText: String = "") {
|
||||
if (newItems.hashCode() != searchResults.hashCode()) {
|
||||
searchResults = newItems.clone() as ArrayList<SearchResult>
|
||||
textToHighlight = highlightText
|
||||
notifyDataSetChanged()
|
||||
} else if (textToHighlight != highlightText) {
|
||||
textToHighlight = highlightText
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupView(view: View, searchResult: SearchResult) {
|
||||
ItemSearchResultBinding.bind(view).apply {
|
||||
searchResultTitle.apply {
|
||||
text = searchResult.title.highlightTextPart(textToHighlight, properPrimaryColor)
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
|
||||
}
|
||||
|
||||
searchResultSnippet.apply {
|
||||
text = searchResult.snippet.highlightTextPart(textToHighlight, properPrimaryColor)
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f)
|
||||
}
|
||||
|
||||
searchResultDate.apply {
|
||||
text = searchResult.date
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
|
||||
}
|
||||
|
||||
SimpleContactsHelper(activity).loadContactImage(searchResult.photoUri, searchResultImage, searchResult.title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
val binding = ItemSearchResultBinding.bind(holder.itemView)
|
||||
Glide.with(activity).clear(binding.searchResultImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,585 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Size
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.NewConversationActivity
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.activities.ThreadActivity
|
||||
import org.fossify.messages.activities.VCardViewerActivity
|
||||
import org.fossify.messages.databinding.*
|
||||
import org.fossify.messages.dialogs.DeleteConfirmationDialog
|
||||
import org.fossify.messages.dialogs.MessageDetailsDialog
|
||||
import org.fossify.messages.dialogs.SelectTextDialog
|
||||
import org.fossify.messages.extensions.*
|
||||
import org.fossify.messages.helpers.*
|
||||
import org.fossify.messages.models.Attachment
|
||||
import org.fossify.messages.models.Message
|
||||
import org.fossify.messages.models.ThreadItem
|
||||
import org.fossify.messages.models.ThreadItem.*
|
||||
|
||||
class ThreadAdapter(
|
||||
activity: SimpleActivity,
|
||||
recyclerView: MyRecyclerView,
|
||||
itemClick: (Any) -> Unit,
|
||||
val isRecycleBin: Boolean,
|
||||
val deleteMessages: (messages: List<Message>, toRecycleBin: Boolean, fromRecycleBin: Boolean) -> Unit
|
||||
) : MyRecyclerViewListAdapter<ThreadItem>(activity, recyclerView, ThreadItemDiffCallback(), itemClick) {
|
||||
private var fontSize = activity.getTextSize()
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val hasMultipleSIMCards = (activity.subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1
|
||||
private val maxChatBubbleWidth = activity.usableScreenSize.x * 0.8f
|
||||
|
||||
init {
|
||||
setupDragListener(true)
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getActionMenuId() = R.menu.cab_thread
|
||||
|
||||
override fun prepareActionMode(menu: Menu) {
|
||||
val isOneItemSelected = isOneItemSelected()
|
||||
val selectedItem = getSelectedItems().firstOrNull() as? Message
|
||||
val hasText = selectedItem?.body != null && selectedItem.body != ""
|
||||
menu.apply {
|
||||
findItem(R.id.cab_copy_to_clipboard).isVisible = isOneItemSelected && hasText
|
||||
findItem(R.id.cab_save_as).isVisible = isOneItemSelected && selectedItem?.attachment?.attachments?.size == 1
|
||||
findItem(R.id.cab_share).isVisible = isOneItemSelected && hasText
|
||||
findItem(R.id.cab_forward_message).isVisible = isOneItemSelected
|
||||
findItem(R.id.cab_select_text).isVisible = isOneItemSelected && hasText
|
||||
findItem(R.id.cab_properties).isVisible = isOneItemSelected
|
||||
findItem(R.id.cab_restore).isVisible = isRecycleBin
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionItemPressed(id: Int) {
|
||||
if (selectedKeys.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
when (id) {
|
||||
R.id.cab_copy_to_clipboard -> copyToClipboard()
|
||||
R.id.cab_save_as -> saveAs()
|
||||
R.id.cab_share -> shareText()
|
||||
R.id.cab_forward_message -> forwardMessage()
|
||||
R.id.cab_select_text -> selectText()
|
||||
R.id.cab_delete -> askConfirmDelete()
|
||||
R.id.cab_restore -> askConfirmRestore()
|
||||
R.id.cab_select_all -> selectAll()
|
||||
R.id.cab_properties -> showMessageDetails()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSelectableItemCount() = currentList.filterIsInstance<Message>().size
|
||||
|
||||
override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position)
|
||||
|
||||
override fun getItemSelectionKey(position: Int) = (currentList.getOrNull(position) as? Message)?.hashCode()
|
||||
|
||||
override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { (it as? Message)?.hashCode() == key }
|
||||
|
||||
override fun onActionModeCreated() {}
|
||||
|
||||
override fun onActionModeDestroyed() {}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = when (viewType) {
|
||||
THREAD_LOADING -> ItemThreadLoadingBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_DATE_TIME -> ItemThreadDateTimeBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_SENT_MESSAGE_ERROR -> ItemThreadErrorBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_SENT_MESSAGE_SENT -> ItemThreadSuccessBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_SENT_MESSAGE_SENDING -> ItemThreadSendingBinding.inflate(layoutInflater, parent, false)
|
||||
else -> ItemMessageBinding.inflate(layoutInflater, parent, false)
|
||||
}
|
||||
|
||||
return ThreadViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val isClickable = item is ThreadError || item is Message
|
||||
val isLongClickable = item is Message
|
||||
holder.bindView(item, isClickable, isLongClickable) { itemView, _ ->
|
||||
when (item) {
|
||||
is ThreadLoading -> setupThreadLoading(itemView)
|
||||
is ThreadDateTime -> setupDateTime(itemView, item)
|
||||
is ThreadError -> setupThreadError(itemView)
|
||||
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
|
||||
is ThreadSending -> setupThreadSending(itemView)
|
||||
is Message -> setupView(holder, itemView, item)
|
||||
}
|
||||
}
|
||||
bindViewHolder(holder)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard() {
|
||||
val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
activity.copyToClipboard(firstItem.body)
|
||||
}
|
||||
|
||||
private fun saveAs() {
|
||||
val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
val attachment = firstItem.attachment?.attachments?.first() ?: return
|
||||
(activity as ThreadActivity).saveMMS(attachment.mimetype, attachment.uriString)
|
||||
}
|
||||
|
||||
private fun shareText() {
|
||||
val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
activity.shareTextIntent(firstItem.body)
|
||||
}
|
||||
|
||||
private fun selectText() {
|
||||
val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
if (firstItem.body.trim().isNotEmpty()) {
|
||||
SelectTextDialog(activity, firstItem.body)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMessageDetails() {
|
||||
val message = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
MessageDetailsDialog(activity, message)
|
||||
}
|
||||
|
||||
private fun askConfirmDelete() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
|
||||
// not sure how we can get UnknownFormatConversionException here, so show the error and hope that someone reports it
|
||||
val items = try {
|
||||
resources.getQuantityString(R.plurals.delete_messages, itemsCnt, itemsCnt)
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
return
|
||||
}
|
||||
|
||||
val baseString = if (activity.config.useRecycleBin && !isRecycleBin) {
|
||||
org.fossify.commons.R.string.move_to_recycle_bin_confirmation
|
||||
} else {
|
||||
org.fossify.commons.R.string.deletion_confirmation
|
||||
}
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin && !isRecycleBin) { skipRecycleBin ->
|
||||
ensureBackgroundThread {
|
||||
val messagesToRemove = getSelectedItems()
|
||||
if (messagesToRemove.isNotEmpty()) {
|
||||
val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin && !isRecycleBin
|
||||
deleteMessages(messagesToRemove.filterIsInstance<Message>(), toRecycleBin, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmRestore() {
|
||||
val itemsCnt = selectedKeys.size
|
||||
|
||||
// not sure how we can get UnknownFormatConversionException here, so show the error and hope that someone reports it
|
||||
val items = try {
|
||||
resources.getQuantityString(R.plurals.delete_messages, itemsCnt, itemsCnt)
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
return
|
||||
}
|
||||
|
||||
val baseString = R.string.restore_confirmation
|
||||
val question = String.format(resources.getString(baseString), items)
|
||||
|
||||
ConfirmationDialog(activity, question) {
|
||||
ensureBackgroundThread {
|
||||
val messagesToRestore = getSelectedItems()
|
||||
if (messagesToRestore.isNotEmpty()) {
|
||||
deleteMessages(messagesToRestore.filterIsInstance<Message>(), false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardMessage() {
|
||||
val message = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
val attachment = message.attachment?.attachments?.firstOrNull()
|
||||
Intent(activity, NewConversationActivity::class.java).apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, message.body)
|
||||
|
||||
if (attachment != null) {
|
||||
putExtra(Intent.EXTRA_STREAM, attachment.getUri())
|
||||
}
|
||||
|
||||
activity.startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedItems() = currentList.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList<ThreadItem>
|
||||
|
||||
private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime
|
||||
|
||||
fun updateMessages(newMessages: ArrayList<ThreadItem>, scrollPosition: Int = -1) {
|
||||
val latestMessages = newMessages.toMutableList()
|
||||
submitList(latestMessages) {
|
||||
if (scrollPosition != -1) {
|
||||
recyclerView.scrollToPosition(scrollPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupView(holder: ViewHolder, view: View, message: Message) {
|
||||
ItemMessageBinding.bind(view).apply {
|
||||
threadMessageHolder.isSelected = selectedKeys.contains(message.hashCode())
|
||||
threadMessageBody.apply {
|
||||
text = message.body
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||
beVisibleIf(message.body.isNotEmpty())
|
||||
setOnLongClickListener {
|
||||
holder.viewLongClicked()
|
||||
true
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
holder.viewClicked(message)
|
||||
}
|
||||
}
|
||||
|
||||
if (message.isReceivedMessage()) {
|
||||
setupReceivedMessageView(messageBinding = this, message = message)
|
||||
} else {
|
||||
setupSentMessageView(messageBinding = this, message = message)
|
||||
}
|
||||
|
||||
if (message.attachment?.attachments?.isNotEmpty() == true) {
|
||||
threadMessageAttachmentsHolder.beVisible()
|
||||
threadMessageAttachmentsHolder.removeAllViews()
|
||||
for (attachment in message.attachment.attachments) {
|
||||
val mimetype = attachment.mimetype
|
||||
when {
|
||||
mimetype.isImageMimeType() || mimetype.isVideoMimeType() -> setupImageView(holder, binding = this, message, attachment)
|
||||
mimetype.isVCardMimeType() -> setupVCardView(holder, threadMessageAttachmentsHolder, message, attachment)
|
||||
else -> setupFileView(holder, threadMessageAttachmentsHolder, message, attachment)
|
||||
}
|
||||
|
||||
threadMessagePlayOutline.beVisibleIf(mimetype.startsWith("video/"))
|
||||
}
|
||||
} else {
|
||||
threadMessageAttachmentsHolder.beGone()
|
||||
threadMessagePlayOutline.beGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupReceivedMessageView(messageBinding: ItemMessageBinding, message: Message) {
|
||||
messageBinding.apply {
|
||||
with(ConstraintSet()) {
|
||||
clone(threadMessageHolder)
|
||||
clear(threadMessageWrapper.id, ConstraintSet.END)
|
||||
connect(threadMessageWrapper.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
|
||||
applyTo(threadMessageHolder)
|
||||
}
|
||||
|
||||
threadMessageSenderPhoto.beVisible()
|
||||
threadMessageSenderPhoto.setOnClickListener {
|
||||
val contact = message.getSender()!!
|
||||
activity.getContactFromAddress(contact.phoneNumbers.first().normalizedNumber) {
|
||||
if (it != null) {
|
||||
activity.startContactDetailsIntent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threadMessageBody.apply {
|
||||
background = AppCompatResources.getDrawable(activity, R.drawable.item_received_background)
|
||||
setTextColor(textColor)
|
||||
setLinkTextColor(activity.getProperPrimaryColor())
|
||||
}
|
||||
|
||||
if (!activity.isFinishing && !activity.isDestroyed) {
|
||||
val contactLetterIcon = SimpleContactsHelper(activity).getContactLetterIcon(message.senderName)
|
||||
val placeholder = BitmapDrawable(activity.resources, contactLetterIcon)
|
||||
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.error(placeholder)
|
||||
.centerCrop()
|
||||
|
||||
Glide.with(activity)
|
||||
.load(message.senderPhotoUri)
|
||||
.placeholder(placeholder)
|
||||
.apply(options)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(threadMessageSenderPhoto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSentMessageView(messageBinding: ItemMessageBinding, message: Message) {
|
||||
messageBinding.apply {
|
||||
with(ConstraintSet()) {
|
||||
clone(threadMessageHolder)
|
||||
clear(threadMessageWrapper.id, ConstraintSet.START)
|
||||
connect(threadMessageWrapper.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
|
||||
applyTo(threadMessageHolder)
|
||||
}
|
||||
|
||||
val primaryColor = activity.getProperPrimaryColor()
|
||||
val contrastColor = primaryColor.getContrastColor()
|
||||
|
||||
threadMessageBody.apply {
|
||||
updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
removeRule(RelativeLayout.END_OF)
|
||||
addRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
}
|
||||
|
||||
background = AppCompatResources.getDrawable(activity, R.drawable.item_sent_background)
|
||||
background.applyColorFilter(primaryColor)
|
||||
setTextColor(contrastColor)
|
||||
setLinkTextColor(contrastColor)
|
||||
|
||||
if (message.isScheduled) {
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
|
||||
val scheduledDrawable = AppCompatResources.getDrawable(activity, org.fossify.commons.R.drawable.ic_clock_vector)?.apply {
|
||||
applyColorFilter(contrastColor)
|
||||
val size = lineHeight
|
||||
setBounds(0, 0, size, size)
|
||||
}
|
||||
|
||||
setCompoundDrawables(null, null, scheduledDrawable, null)
|
||||
} else {
|
||||
typeface = Typeface.DEFAULT
|
||||
setCompoundDrawables(null, null, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupImageView(holder: ViewHolder, binding: ItemMessageBinding, message: Message, attachment: Attachment) = binding.apply {
|
||||
val mimetype = attachment.mimetype
|
||||
val uri = attachment.getUri()
|
||||
|
||||
val imageView = ItemAttachmentImageBinding.inflate(layoutInflater)
|
||||
threadMessageAttachmentsHolder.addView(imageView.root)
|
||||
|
||||
val placeholderDrawable = ColorDrawable(Color.TRANSPARENT)
|
||||
val isTallImage = attachment.height > attachment.width
|
||||
val transformation = if (isTallImage) CenterCrop() else FitCenter()
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.placeholder(placeholderDrawable)
|
||||
.transform(transformation)
|
||||
|
||||
var builder = Glide.with(root.context)
|
||||
.load(uri)
|
||||
.apply(options)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||
threadMessagePlayOutline.beGone()
|
||||
threadMessageAttachmentsHolder.removeView(imageView.root)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(dr: Drawable, a: Any, t: Target<Drawable>, d: DataSource, i: Boolean) = false
|
||||
})
|
||||
|
||||
// limit attachment sizes to avoid causing OOM
|
||||
var wantedAttachmentSize = Size(attachment.width, attachment.height)
|
||||
if (wantedAttachmentSize.width > maxChatBubbleWidth) {
|
||||
val newHeight = wantedAttachmentSize.height / (wantedAttachmentSize.width / maxChatBubbleWidth)
|
||||
wantedAttachmentSize = Size(maxChatBubbleWidth.toInt(), newHeight.toInt())
|
||||
}
|
||||
|
||||
builder = if (isTallImage) {
|
||||
builder.override(wantedAttachmentSize.width, wantedAttachmentSize.width)
|
||||
} else {
|
||||
builder.override(wantedAttachmentSize.width, wantedAttachmentSize.height)
|
||||
}
|
||||
|
||||
try {
|
||||
builder.into(imageView.attachmentImage)
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
|
||||
imageView.attachmentImage.setOnClickListener {
|
||||
if (actModeCallback.isSelectable) {
|
||||
holder.viewClicked(message)
|
||||
} else {
|
||||
activity.launchViewIntent(uri, mimetype, attachment.filename)
|
||||
}
|
||||
}
|
||||
imageView.root.setOnLongClickListener {
|
||||
holder.viewLongClicked()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupVCardView(holder: ViewHolder, parent: LinearLayout, message: Message, attachment: Attachment) {
|
||||
val uri = attachment.getUri()
|
||||
val vCardView = ItemAttachmentVcardBinding.inflate(layoutInflater).apply {
|
||||
setupVCardPreview(
|
||||
activity = activity,
|
||||
uri = uri,
|
||||
onClick = {
|
||||
if (actModeCallback.isSelectable) {
|
||||
holder.viewClicked(message)
|
||||
} else {
|
||||
val intent = Intent(activity, VCardViewerActivity::class.java).also {
|
||||
it.putExtra(EXTRA_VCARD_URI, uri)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
},
|
||||
onLongClick = { holder.viewLongClicked() }
|
||||
)
|
||||
}.root
|
||||
|
||||
parent.addView(vCardView)
|
||||
}
|
||||
|
||||
private fun setupFileView(holder: ViewHolder, parent: LinearLayout, message: Message, attachment: Attachment) {
|
||||
val mimetype = attachment.mimetype
|
||||
val uri = attachment.getUri()
|
||||
val attachmentView = ItemAttachmentDocumentBinding.inflate(layoutInflater).apply {
|
||||
setupDocumentPreview(
|
||||
uri = uri,
|
||||
title = attachment.filename,
|
||||
mimeType = attachment.mimetype,
|
||||
onClick = {
|
||||
if (actModeCallback.isSelectable) {
|
||||
holder.viewClicked(message)
|
||||
} else {
|
||||
activity.launchViewIntent(uri, mimetype, attachment.filename)
|
||||
}
|
||||
},
|
||||
onLongClick = { holder.viewLongClicked() }
|
||||
)
|
||||
}.root
|
||||
|
||||
parent.addView(attachmentView)
|
||||
}
|
||||
|
||||
private fun setupDateTime(view: View, dateTime: ThreadDateTime) {
|
||||
ItemThreadDateTimeBinding.bind(view).apply {
|
||||
threadDateTime.apply {
|
||||
text = dateTime.date.formatDateOrTime(context, hideTimeAtOtherDays = false, showYearEvenIfCurrent = false)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||
}
|
||||
threadDateTime.setTextColor(textColor)
|
||||
|
||||
threadSimIcon.beVisibleIf(hasMultipleSIMCards)
|
||||
threadSimNumber.beVisibleIf(hasMultipleSIMCards)
|
||||
if (hasMultipleSIMCards) {
|
||||
threadSimNumber.text = dateTime.simID
|
||||
threadSimNumber.setTextColor(textColor.getContrastColor())
|
||||
threadSimIcon.applyColorFilter(textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupThreadSuccess(view: View, isDelivered: Boolean) {
|
||||
ItemThreadSuccessBinding.bind(view).apply {
|
||||
threadSuccess.setImageResource(if (isDelivered) R.drawable.ic_check_double_vector else org.fossify.commons.R.drawable.ic_check_vector)
|
||||
threadSuccess.applyColorFilter(textColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupThreadError(view: View) {
|
||||
val binding = ItemThreadErrorBinding.bind(view)
|
||||
binding.threadError.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize - 4)
|
||||
}
|
||||
|
||||
private fun setupThreadSending(view: View) {
|
||||
ItemThreadSendingBinding.bind(view).threadSending.apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||
setTextColor(textColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupThreadLoading(view: View) {
|
||||
val binding = ItemThreadLoadingBinding.bind(view)
|
||||
binding.threadLoading.setIndicatorColor(properPrimaryColor)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
val binding = (holder as ThreadViewHolder).binding
|
||||
if (binding is ItemMessageBinding) {
|
||||
Glide.with(activity).clear(binding.threadMessageSenderPhoto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ThreadViewHolder(val binding: ViewBinding) : ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package org.fossify.messages.adapters
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.databinding.ItemVcardContactBinding
|
||||
import org.fossify.messages.databinding.ItemVcardContactPropertyBinding
|
||||
import org.fossify.messages.models.VCardPropertyWrapper
|
||||
import org.fossify.messages.models.VCardWrapper
|
||||
|
||||
private const val TYPE_VCARD_CONTACT = 1
|
||||
private const val TYPE_VCARD_CONTACT_PROPERTY = 2
|
||||
|
||||
class VCardViewerAdapter(
|
||||
activity: SimpleActivity, private var items: MutableList<Any>, private val itemClick: (Any) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var fontSize = activity.getTextSize()
|
||||
private var textColor = activity.getProperTextColor()
|
||||
private val layoutInflater = activity.layoutInflater
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (val item = items[position]) {
|
||||
is VCardWrapper -> TYPE_VCARD_CONTACT
|
||||
is VCardPropertyWrapper -> TYPE_VCARD_CONTACT_PROPERTY
|
||||
else -> throw IllegalArgumentException("Unexpected type: ${item::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
TYPE_VCARD_CONTACT -> VCardContactViewHolder(
|
||||
binding = ItemVcardContactBinding.inflate(layoutInflater, parent, false)
|
||||
)
|
||||
|
||||
TYPE_VCARD_CONTACT_PROPERTY -> VCardPropertyViewHolder(
|
||||
binding = ItemVcardContactPropertyBinding.inflate(layoutInflater, parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Unexpected type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
when (holder) {
|
||||
is VCardContactViewHolder -> holder.bindView(item as VCardWrapper)
|
||||
is VCardPropertyViewHolder -> holder.bindView(item as VCardPropertyWrapper)
|
||||
}
|
||||
}
|
||||
|
||||
inner class VCardContactViewHolder(val binding: ItemVcardContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bindView(item: VCardWrapper) {
|
||||
val name = item.fullName
|
||||
binding.apply {
|
||||
itemContactName.apply {
|
||||
text = name
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f)
|
||||
}
|
||||
itemContactImage.apply {
|
||||
val photo = item.vCard.photos.firstOrNull()
|
||||
val placeholder = if (name != null) {
|
||||
SimpleContactsHelper(context).getContactLetterIcon(name).toDrawable(resources)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val roundingRadius = resources.getDimensionPixelSize(org.fossify.commons.R.dimen.big_margin)
|
||||
val transformation = RoundedCorners(roundingRadius)
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.placeholder(placeholder)
|
||||
.transform(transformation)
|
||||
Glide.with(this)
|
||||
.load(photo?.data ?: photo?.url)
|
||||
.apply(options)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(this)
|
||||
}
|
||||
expandCollapseIcon.apply {
|
||||
val expandCollapseDrawable = if (item.expanded) {
|
||||
R.drawable.ic_collapse_up
|
||||
} else {
|
||||
R.drawable.ic_expand_down
|
||||
}
|
||||
setImageResource(expandCollapseDrawable)
|
||||
applyColorFilter(textColor)
|
||||
}
|
||||
|
||||
if (items.size > 1) {
|
||||
root.setOnClickListener {
|
||||
expandOrCollapseRow(item)
|
||||
}
|
||||
}
|
||||
root.onGlobalLayout {
|
||||
if (items.size == 1) {
|
||||
expandOrCollapseRow(item)
|
||||
expandCollapseIcon.beGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun expandOrCollapseRow(item: VCardWrapper) {
|
||||
val properties = item.properties
|
||||
if (item.expanded) {
|
||||
collapseRow(properties, item)
|
||||
} else {
|
||||
expandRow(properties, item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expandRow(properties: List<VCardPropertyWrapper>, vCardWrapper: VCardWrapper) {
|
||||
vCardWrapper.expanded = true
|
||||
val nextPosition = items.indexOf(vCardWrapper) + 1
|
||||
items.addAll(nextPosition, properties)
|
||||
notifyItemRangeInserted(nextPosition, properties.size)
|
||||
binding.expandCollapseIcon.setImageResource(R.drawable.ic_collapse_up)
|
||||
}
|
||||
|
||||
private fun collapseRow(properties: List<VCardPropertyWrapper>, vCardWrapper: VCardWrapper) {
|
||||
vCardWrapper.expanded = false
|
||||
val nextPosition = items.indexOf(vCardWrapper) + 1
|
||||
repeat(properties.size) {
|
||||
items.removeAt(nextPosition)
|
||||
}
|
||||
notifyItemRangeRemoved(nextPosition, properties.size)
|
||||
binding.expandCollapseIcon.setImageResource(R.drawable.ic_expand_down)
|
||||
}
|
||||
}
|
||||
|
||||
inner class VCardPropertyViewHolder(val binding: ItemVcardContactPropertyBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bindView(item: VCardPropertyWrapper) {
|
||||
binding.apply {
|
||||
itemVcardPropertyTitle.apply {
|
||||
text = item.value
|
||||
setTextColor(textColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f)
|
||||
}
|
||||
itemVcardPropertySubtitle.apply {
|
||||
text = item.type
|
||||
setTextColor(textColor)
|
||||
}
|
||||
root.setOnClickListener {
|
||||
itemClick(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue