mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-19 14:28:44 -04:00
Merge pull request #385 from meshtastic/menu-messages
add action mode menu to messages (delete & select all)
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
@@ -14,11 +12,11 @@ import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
@@ -26,6 +24,7 @@ import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
@@ -37,7 +36,6 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
|
||||
if (actionId == receivedActionId) {
|
||||
func()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -45,6 +43,7 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
|
||||
@AndroidEntryPoint
|
||||
class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
|
||||
private var actionMode: ActionMode? = null
|
||||
private var _binding: MessagesFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
@@ -53,7 +52,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
// Allows textMultiline with IME_ACTION_SEND
|
||||
fun EditText.onActionSend(func: () -> Unit) {
|
||||
private fun EditText.onActionSend(func: () -> Unit) {
|
||||
setImeOptions(EditorInfo.IME_ACTION_SEND)
|
||||
setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
setOnEditorActionListener { _, actionId, _ ->
|
||||
@@ -61,7 +60,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
func()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -73,13 +71,12 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
|
||||
private fun getShortDateTime(time: Date): String {
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
val one_day = 60 * 60 * 24 * 1000
|
||||
if (System.currentTimeMillis() - time.time > one_day) {
|
||||
return dateTimeFormat.format(time)
|
||||
} else return timeFormat.format(time)
|
||||
val oneDayMsec = 60 * 60 * 24 * 1000L
|
||||
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
|
||||
dateTimeFormat.format(time)
|
||||
} else timeFormat.format(time)
|
||||
}
|
||||
|
||||
|
||||
// Provide a direct reference to each of the views within a data item
|
||||
// Used to cache the views within the item layout for fast access
|
||||
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
|
||||
@@ -119,8 +116,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
// Inflate the custom layout
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
|
||||
|
||||
@@ -128,6 +123,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
return ViewHolder(contactViewBinding)
|
||||
}
|
||||
|
||||
var messages = arrayOf<DataPacket>()
|
||||
var positionList = ArrayList<Int>()
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the data set held by the adapter.
|
||||
*
|
||||
@@ -167,30 +165,10 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
// Set cardview offset and color.
|
||||
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
|
||||
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
|
||||
holder.card.setOnLongClickListener {
|
||||
val deleteMessageDialog = AlertDialog.Builder(context)
|
||||
deleteMessageDialog.setMessage(R.string.delete_selected_message)
|
||||
deleteMessageDialog.setPositiveButton(
|
||||
R.string.delete
|
||||
) { _, _ ->
|
||||
model.messagesState.deleteMessage((messages[position]), position)
|
||||
}
|
||||
deleteMessageDialog.setNeutralButton(
|
||||
R.string.cancel
|
||||
) { _, _ ->
|
||||
}
|
||||
deleteMessageDialog.setNegativeButton(
|
||||
R.string.delete_all_messages
|
||||
) { _, _ ->
|
||||
model.messagesState.deleteAllMessages()
|
||||
}
|
||||
deleteMessageDialog.create()
|
||||
deleteMessageDialog.show()
|
||||
true
|
||||
}
|
||||
if (isMe) {
|
||||
marginParams.leftMargin = messageOffset
|
||||
marginParams.rightMargin = 0
|
||||
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
@@ -202,6 +180,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
} else {
|
||||
marginParams.rightMargin = messageOffset
|
||||
marginParams.leftMargin = 0
|
||||
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
@@ -243,9 +222,119 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
|
||||
} else
|
||||
holder.messageStatusIcon.visibility = View.INVISIBLE
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
if (actionMode == null) {
|
||||
actionMode = (activity as MainActivity).startActionMode(object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
||||
mode.title = "1"
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
clickItem(holder)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.deleteButton -> {
|
||||
val deleteMessagesString = resources.getQuantityString(
|
||||
R.plurals.delete_messages,
|
||||
positionList.size,
|
||||
positionList.size
|
||||
)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(deleteMessagesString)
|
||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||
debug("User clicked deleteButton")
|
||||
// all items selected --> deleteAllMessages()
|
||||
if (positionList.size == messages.size) {
|
||||
model.messagesState.deleteAllMessages()
|
||||
} else {
|
||||
// remove selectedList in reverse so we don't break the index
|
||||
positionList.sortedDescending().forEach {
|
||||
try {
|
||||
model.messagesState.deleteMessage((messages[it]), it)
|
||||
} catch (ex: ArrayIndexOutOfBoundsException) {
|
||||
errormsg("error deleting messages ${ex.message}")
|
||||
}
|
||||
}
|
||||
mode.finish()
|
||||
}
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
R.id.selectAllButton -> {
|
||||
// if all selected -> unselect all
|
||||
if (positionList.size == messages.size) {
|
||||
positionList.clear()
|
||||
mode.finish()
|
||||
} else {
|
||||
// else --> select all
|
||||
positionList.clear()
|
||||
positionList.addAll(messages.indices)
|
||||
}
|
||||
actionMode?.title = positionList.size.toString()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
positionList.clear()
|
||||
notifyDataSetChanged()
|
||||
actionMode = null
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// when action mode is enabled
|
||||
clickItem(holder)
|
||||
}
|
||||
true
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
if (actionMode != null) clickItem(holder)
|
||||
}
|
||||
|
||||
if (positionList.contains(position)) {
|
||||
holder.itemView.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 32f
|
||||
setColor(Color.rgb(127, 127, 127))
|
||||
}
|
||||
} else {
|
||||
holder.itemView.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 32f
|
||||
setColor(ContextCompat.getColor(holder.itemView.context, R.color.colorAdvancedBackground))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var messages = arrayOf<DataPacket>()
|
||||
private fun clickItem(holder: ViewHolder) {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (!positionList.contains(position)) {
|
||||
positionList.add(position)
|
||||
} else {
|
||||
positionList.remove(position)
|
||||
}
|
||||
if (positionList.isEmpty()) {
|
||||
// finish action mode when no items selected
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
// show total items selected on action mode title
|
||||
actionMode?.title = "${positionList.size}"
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
/// Called when our node DB changes
|
||||
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
|
||||
@@ -258,10 +347,15 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
actionMode?.finish()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -269,7 +363,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.sendButton.setOnClickListener {
|
||||
debug("sendButton click")
|
||||
debug("User clicked sendButton")
|
||||
|
||||
val str = binding.messageInputText.text.toString().trim()
|
||||
if (str.isNotEmpty())
|
||||
@@ -295,34 +389,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
layoutManager.stackFromEnd = true // We want the last rows to always be shown
|
||||
binding.messageListView.layoutManager = layoutManager
|
||||
|
||||
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
|
||||
model.messagesState.messages.observe(viewLifecycleOwner) {
|
||||
debug("New messages received: ${it.size}")
|
||||
messagesAdapter.onMessagesChanged(it)
|
||||
})
|
||||
}
|
||||
|
||||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
fun updateTextEnabled() {
|
||||
binding.textInputLayout.isEnabled =
|
||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
|
||||
model.isConnected.observe(viewLifecycleOwner) { connectionState ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
val connected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||
binding.textInputLayout.isEnabled = connected
|
||||
binding.sendButton.isEnabled = connected
|
||||
|
||||
// Just being connected is enough to allow sending texts I think
|
||||
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||
}
|
||||
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled()
|
||||
}) */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,9h8v10H8z"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillAlpha="0.3"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal file
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
|
||||
</vector>
|
||||
@@ -42,7 +42,6 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:autoLink="all"
|
||||
android:text="@string/sample_message"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/username"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
14
app/src/main/res/menu/menu_messages.xml
Normal file
14
app/src/main/res/menu/menu_messages.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/deleteButton"
|
||||
android:icon="@drawable/ic_twotone_delete_24"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/selectAllButton"
|
||||
android:icon="@drawable/ic_twotone_select_all_24"
|
||||
android:title="@string/select_all"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="action_settings">Configurações</string>
|
||||
<string name="channel_name">Nome do canal</string>
|
||||
<string name="channel_options">Opções do canal</string>
|
||||
@@ -113,8 +113,14 @@
|
||||
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
||||
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
||||
<string name="camera_required">Permissão da câmera</string>
|
||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string>
|
||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
|
||||
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
||||
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
||||
<string name="modem_config_slow_long">Longo alcance / lento</string>
|
||||
<plurals name="delete_messages">
|
||||
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
|
||||
<item quantity="other">Excluir %s mensagens?</item>
|
||||
</plurals>
|
||||
<string name="delete">Excluir</string>
|
||||
<string name="select_all">Selecionar tudo</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="action_settings">Configurações</string>
|
||||
<string name="channel_name">Nome do Canal</string>
|
||||
<string name="channel_options">Opções do Canal</string>
|
||||
@@ -112,9 +112,15 @@
|
||||
<string name="cancel_no_radio">Cancelar (sem acesso ao rádio)</string>
|
||||
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
||||
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string>
|
||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
|
||||
<string name="camera_required">Permissão da câmera</string>
|
||||
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
||||
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
||||
<string name="modem_config_slow_long">Longo alcance / lento</string>
|
||||
<plurals name="delete_messages">
|
||||
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
|
||||
<item quantity="other">Excluir %s mensagens?</item>
|
||||
</plurals>
|
||||
<string name="delete">Excluir</string>
|
||||
<string name="select_all">Selecionar tudo</string>
|
||||
</resources>
|
||||
|
||||
@@ -120,8 +120,11 @@
|
||||
<string name="why_camera_required">We must be granted access to the camera to read QR codes. No pictures or videos will be saved.</string>
|
||||
<string name="modem_config_slow_short">Short Range / Slow</string>
|
||||
<string name="modem_config_slow_medium">Medium Range / Slow</string>
|
||||
<string name="delete_selected_message">Delete selected message?</string>
|
||||
<plurals name="delete_messages">
|
||||
<item quantity="one">Delete message?</item>
|
||||
<item quantity="other">Delete %s messages?</item>
|
||||
</plurals>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="delete_all_messages">Delete All Messages</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="modem_config_slow_long">Long Range / Slow</string>
|
||||
</resources>
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:itemTextAppearance">@style/menu_item_color</item>
|
||||
|
||||
<item name="actionModeStyle">@style/MyActionMode</item>
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Spinner">
|
||||
@@ -73,6 +74,12 @@
|
||||
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="MyActionMode" parent="Base.Widget.AppCompat.ActionMode">
|
||||
<item name="background">@color/colorPrimary</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||
// Set the splash screen background, animated icon, and animation duration.
|
||||
<item name="windowSplashScreenBackground">@color/selectedColor</item>
|
||||
|
||||
Reference in New Issue
Block a user