mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 13:08:25 +00:00
Add expiration subtitle to Delete option in message context menu
This commit is contained in:
parent
b8aa46912a
commit
b56c3bd6c5
@ -5,10 +5,10 @@ import androidx.annotation.AttrRes
|
||||
/**
|
||||
* Represents an action to be rendered
|
||||
*/
|
||||
data class ActionItem @JvmOverloads constructor(
|
||||
data class ActionItem(
|
||||
@AttrRes val iconRes: Int,
|
||||
val title: CharSequence,
|
||||
val title: Int,
|
||||
val action: Runnable,
|
||||
val contentDescription: String? = null,
|
||||
val subtitle: String? = null
|
||||
val contentDescription: Int? = null,
|
||||
val subtitle: (() -> CharSequence?)? = null
|
||||
)
|
||||
|
@ -5,9 +5,15 @@ import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@ -52,13 +58,9 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean = this == newItem
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean = this == newItem
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
@ -69,6 +71,7 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
itemView: View,
|
||||
private val onItemClick: () -> Unit,
|
||||
) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
private var subtitleJob: Job? = null
|
||||
val icon: ImageView = itemView.findViewById(R.id.context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.context_menu_item_title)
|
||||
val subtitle: TextView = itemView.findViewById(R.id.context_menu_item_subtitle)
|
||||
@ -79,21 +82,45 @@ class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
context.theme.resolveAttribute(model.item.iconRes, typedValue, true)
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId))
|
||||
}
|
||||
itemView.contentDescription = model.item.contentDescription
|
||||
title.text = model.item.title
|
||||
subtitle.text = model.item.subtitle
|
||||
subtitle.isVisible = model.item.subtitle != null
|
||||
model.item.contentDescription?.let(context.resources::getString)?.let { itemView.contentDescription = it }
|
||||
title.setText(model.item.title)
|
||||
subtitle.isGone = true
|
||||
model.item.subtitle?.let {
|
||||
startSubtitleJob(subtitle, it)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_only)
|
||||
DisplayType.TOP -> R.drawable.context_menu_item_background_top
|
||||
DisplayType.BOTTOM -> R.drawable.context_menu_item_background_bottom
|
||||
DisplayType.MIDDLE -> R.drawable.context_menu_item_background_middle
|
||||
DisplayType.ONLY -> R.drawable.context_menu_item_background_only
|
||||
}.let(itemView::setBackgroundResource)
|
||||
}
|
||||
|
||||
private fun startSubtitleJob(textView: TextView, getSubtitle: () -> CharSequence?) {
|
||||
fun updateText() = getSubtitle().let {
|
||||
textView.isGone = it == null
|
||||
textView.text = it
|
||||
}
|
||||
updateText()
|
||||
|
||||
subtitleJob?.cancel()
|
||||
subtitleJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (true) {
|
||||
updateText()
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
// naive job cancellation, will break if many items are added to context menu.
|
||||
subtitleJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
@ -37,7 +38,12 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class ConversationReactionOverlay : FrameLayout {
|
||||
private val emojiViewGlobalRect = Rect()
|
||||
@ -501,54 +507,51 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
?: return emptyList()
|
||||
val userPublicKey = getLocalNumber(context)!!
|
||||
// Select message
|
||||
items += ActionItem(R.attr.menu_select_icon, context.resources.getString(R.string.conversation_context__menu_select), { handleActionItemClicked(Action.SELECT) },
|
||||
context.resources.getString(R.string.AccessibilityId_select))
|
||||
items += ActionItem(R.attr.menu_select_icon, R.string.conversation_context__menu_select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select)
|
||||
// Reply
|
||||
val canWrite = openGroup == null || openGroup.canWrite
|
||||
if (canWrite && !message.isPending && !message.isFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_reply), { handleActionItemClicked(Action.REPLY) },
|
||||
context.resources.getString(R.string.AccessibilityId_reply_message))
|
||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply_message)
|
||||
}
|
||||
// Copy message text
|
||||
if (!containsControlMessage && hasText) {
|
||||
items += ActionItem(R.attr.menu_copy_icon, context.resources.getString(R.string.copy), { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||
}
|
||||
// Copy Session ID
|
||||
if (recipient.isGroupRecipient && !recipient.isOpenGroupRecipient && message.recipient.address.toString() != userPublicKey) {
|
||||
items += ActionItem(R.attr.menu_copy_icon, context.resources.getString(R.string.activity_conversation_menu_copy_session_id), { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
||||
items += ActionItem(R.attr.menu_copy_icon, R.string.activity_conversation_menu_copy_session_id, { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
||||
}
|
||||
// Delete message
|
||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(
|
||||
R.attr.menu_trash_icon,
|
||||
context.resources.getString(R.string.delete),
|
||||
{ handleActionItemClicked(Action.DELETE) },
|
||||
context.resources.getString(R.string.AccessibilityId_delete_message),
|
||||
message.takeIf { it.expireStarted > 0 }?.run { expiresIn.milliseconds }?.let { "Auto-deletes in $it" }
|
||||
)
|
||||
val subtitle = { message.takeIf { it.expireStarted > 0 }
|
||||
?.run { expiresIn - (SnodeAPI.nowWithOffset - expireStarted) }
|
||||
?.coerceAtLeast(0L)
|
||||
?.milliseconds
|
||||
?.to2partString()
|
||||
?.let { context.getString(R.string.auto_deletes_in, it) } }
|
||||
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) }, R.string.AccessibilityId_delete_message, subtitle)
|
||||
}
|
||||
// Ban user
|
||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_block_icon, context.resources.getString(R.string.conversation_context__menu_ban_user), { handleActionItemClicked(Action.BAN_USER) })
|
||||
items += ActionItem(R.attr.menu_block_icon, R.string.conversation_context__menu_ban_user, { handleActionItemClicked(Action.BAN_USER) })
|
||||
}
|
||||
// Ban and delete all
|
||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_trash_icon, context.resources.getString(R.string.conversation_context__menu_ban_and_delete_all), { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
|
||||
items += ActionItem(R.attr.menu_trash_icon, R.string.conversation_context__menu_ban_and_delete_all, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
|
||||
}
|
||||
// Message detail
|
||||
items += ActionItem(R.attr.menu_info_icon, context.resources.getString(R.string.conversation_context__menu_message_details), { handleActionItemClicked(Action.VIEW_INFO) })
|
||||
items += ActionItem(R.attr.menu_info_icon, R.string.conversation_context__menu_message_details, { handleActionItemClicked(Action.VIEW_INFO) })
|
||||
// Resend
|
||||
if (message.isFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_resend_message), { handleActionItemClicked(Action.RESEND) })
|
||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resend_message, { handleActionItemClicked(Action.RESEND) })
|
||||
}
|
||||
// Resync
|
||||
if (message.isSyncFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_resync_message), { handleActionItemClicked(Action.RESYNC) })
|
||||
items += ActionItem(R.attr.menu_reply_icon, R.string.conversation_context__menu_resync_message, { handleActionItemClicked(Action.RESYNC) })
|
||||
}
|
||||
// Save media
|
||||
if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
|
||||
items += ActionItem(R.attr.menu_save_icon, context.resources.getString(R.string.conversation_context_image__save_attachment), { handleActionItemClicked(Action.DOWNLOAD) },
|
||||
context.resources.getString(R.string.AccessibilityId_save_attachment))
|
||||
items += ActionItem(R.attr.menu_save_icon, R.string.conversation_context_image__save_attachment, { handleActionItemClicked(Action.DOWNLOAD) }, R.string.AccessibilityId_save_attachment)
|
||||
}
|
||||
backgroundView.visibility = VISIBLE
|
||||
foregroundView.visibility = VISIBLE
|
||||
@ -585,16 +588,14 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
revealAnimatorSet.playTogether(reveals)
|
||||
}
|
||||
|
||||
private fun newHideAnimatorSet(): AnimatorSet {
|
||||
val set = AnimatorSet()
|
||||
set.addListener(object : AnimationCompleteListener() {
|
||||
private fun newHideAnimatorSet() = AnimatorSet().apply {
|
||||
addListener(object : AnimationCompleteListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
visibility = GONE
|
||||
}
|
||||
})
|
||||
set.interpolator = INTERPOLATOR
|
||||
set.playTogether(newHideAnimators())
|
||||
return set
|
||||
interpolator = INTERPOLATOR
|
||||
playTogether(newHideAnimators())
|
||||
}
|
||||
|
||||
private fun newHideAnimators(): List<Animator> {
|
||||
@ -644,26 +645,17 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
fun onActionSelected(action: Action)
|
||||
}
|
||||
|
||||
private class Boundary {
|
||||
private var min = 0f
|
||||
private var max = 0f
|
||||
|
||||
internal constructor()
|
||||
internal constructor(min: Float, max: Float) {
|
||||
update(min, max)
|
||||
}
|
||||
private class Boundary(private var min: Float = 0f, private var max: Float = 0f) {
|
||||
|
||||
fun update(min: Float, max: Float) {
|
||||
this.min = min
|
||||
this.max = max
|
||||
}
|
||||
|
||||
operator fun contains(value: Float): Boolean {
|
||||
return if (min < max) {
|
||||
min < value && max > value
|
||||
} else {
|
||||
min > value && max < value
|
||||
}
|
||||
operator fun contains(value: Float) = if (min < max) {
|
||||
min < value && max > value
|
||||
} else {
|
||||
min > value && max < value
|
||||
}
|
||||
}
|
||||
|
||||
@ -693,4 +685,8 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
const val LONG_PRESS_SCALE_FACTOR = 0.95f
|
||||
private val INTERPOLATOR: Interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Duration.to2partString(): String? =
|
||||
toComponents { days, hours, minutes, seconds, nanoseconds -> listOf(days.days, hours.hours, minutes.minutes, seconds.seconds) }
|
||||
.filter { it.inWholeSeconds > 0L }.take(2).takeIf { it.isNotEmpty() }?.joinToString(" ")
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/context_menu_item_subtitle"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textSize="@dimen/tiny_font_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -1077,5 +1077,6 @@
|
||||
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>
|
||||
|
||||
<string name="unread_marker">Unread Messages</string>
|
||||
<string name="auto_deletes_in">Auto-deletes in %1$s</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user