add action mode menu to messages (delete & select all)

1.2-legacy
andrekir 2022-03-02 11:39:07 -03:00
rodzic 7395cc5583
commit 01f8154189
9 zmienionych plików z 102 dodań i 82 usunięć

Wyświetl plik

@ -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,22 +71,21 @@ 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) :
RecyclerView.ViewHolder(itemView.root) {
val card: CardView = itemView.Card
val username: Chip = itemView.username
val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime
val messageStatusIcon: ImageView = itemView.messageStatusIcon
val card: CardView = itemView.Card
}
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
@ -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)
@ -159,7 +154,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val msg = messages[position]
val nodes = model.nodeDB.nodes.value!!
val node = nodes.get(msg.from)
// Determine if this is my message (originated on this device).
// val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
val isMe = msg.from == "^local"
@ -167,48 +162,19 @@ 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) {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMyMsg
)
)
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg))
}
} else {
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
context?.let {
holder.card.setCardBackgroundColor(
ContextCompat.getColor(
it,
R.color.colorMsg
)
)
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg))
}
}
// Hide the username chip for my messages
@ -217,11 +183,14 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
} else {
holder.username.visibility = View.VISIBLE
// If we can't find the sender, just use the ID
val node = nodes[msg.from]
val user = node?.user
holder.username.text = user?.shortName ?: msg.from
}
if (msg.errorMessage != null) {
context?.let { holder.card.setCardBackgroundColor(Color.RED) }
holder.itemView.context?.let {
holder.card.setCardBackgroundColor(Color.RED)
}
holder.messageText.text = msg.errorMessage
} else {
holder.messageText.text = msg.text
@ -258,10 +227,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 +243,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 +269,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()
}) */
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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" />

Wyświetl plik

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

Wyświetl plik

@ -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,7 +113,13 @@
<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>
<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>

Wyświetl plik

@ -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,8 +112,14 @@
<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>
<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>

Wyświetl plik

@ -120,7 +120,10 @@
<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>
</resources>

Wyświetl plik

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