mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Merge pull request #462 from Douile/dev-quick-chat
Add quick chat actions
This commit is contained in:
commit
79b3b1c024
20 changed files with 718 additions and 10 deletions
|
|
@ -0,0 +1,33 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class DragManageAdapter(var adapter: SwapAdapter, dragDirs: Int, swipeDirs: Int) :
|
||||
ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) {
|
||||
interface SwapAdapter {
|
||||
fun swapItems(fromPosition: Int, toPosition: Int)
|
||||
fun commitSwaps()
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
adapter.swapItems(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
adapter.commitSwaps()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -3,25 +3,25 @@ package com.geeksville.mesh.ui
|
|||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.allViews
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
|
|
@ -57,6 +57,8 @@ class MessagesFragment : Fragment(), Logging {
|
|||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private var isConnected = false
|
||||
|
||||
// Allows textMultiline with IME_ACTION_SEND
|
||||
private fun EditText.onActionSend(func: () -> Unit) {
|
||||
setOnEditorActionListener { _, actionId, _ ->
|
||||
|
|
@ -293,9 +295,45 @@ class MessagesFragment : Fragment(), Logging {
|
|||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
model.connectionState.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
|
||||
isConnected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||
binding.textInputLayout.isEnabled = isConnected
|
||||
binding.sendButton.isEnabled = isConnected
|
||||
for (subView: View in binding.quickChatLayout.allViews) {
|
||||
if (subView is Button) {
|
||||
subView.isEnabled = isConnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
|
||||
actions?.let {
|
||||
// This seems kinda hacky it might be better to replace with a recycler view
|
||||
binding.quickChatLayout.removeAllViews()
|
||||
for (action in actions) {
|
||||
val button = Button(context)
|
||||
button.setText(action.name)
|
||||
button.isEnabled = isConnected
|
||||
if (action.mode == QuickChatAction.Mode.Instant) {
|
||||
button.backgroundTintList = ContextCompat.getColorStateList(requireActivity(), R.color.colorMyMsg)
|
||||
}
|
||||
button.setOnClickListener {
|
||||
if (action.mode == QuickChatAction.Mode.Append) {
|
||||
val originalText = binding.messageInputText.text ?: ""
|
||||
val needsSpace = !originalText.endsWith(' ') && originalText.isNotEmpty()
|
||||
val newText = buildString {
|
||||
append(originalText)
|
||||
if (needsSpace) append(' ')
|
||||
append(action.message)
|
||||
}
|
||||
binding.messageInputText.setText(newText)
|
||||
binding.messageInputText.setSelection(newText.length)
|
||||
} else {
|
||||
model.messagesState.sendMessage(action.message, contactId)
|
||||
}
|
||||
}
|
||||
binding.quickChatLayout.addView(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
|
||||
class QuickChatActionAdapter internal constructor(
|
||||
private val context: Context,
|
||||
private val onEdit: (action: QuickChatAction) -> Unit,
|
||||
private val repositionAction: (fromPos: Int, toPos: Int) -> Unit,
|
||||
private val commitAction: () -> Unit,
|
||||
) : RecyclerView.Adapter<QuickChatActionAdapter.ActionViewHolder>(), DragManageAdapter.SwapAdapter {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var actions = emptyList<QuickChatAction>()
|
||||
|
||||
inner class ActionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val container: View = itemView.findViewById(R.id.quickChatActionContainer)
|
||||
val actionName: TextView = itemView.findViewById(R.id.quickChatActionName)
|
||||
val actionValue: TextView = itemView.findViewById(R.id.quickChatActionValue)
|
||||
val actionEdit: View = itemView.findViewById(R.id.quickChatActionEdit)
|
||||
val actionInstant: View = itemView.findViewById(R.id.quickChatActionInstant)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_quick_chat_action_layout, parent, false)
|
||||
return ActionViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ActionViewHolder, position: Int) {
|
||||
val current = actions[position]
|
||||
holder.actionName.text = current.name
|
||||
holder.actionValue.text = current.message
|
||||
val isInstant = current.mode == QuickChatAction.Mode.Instant
|
||||
holder.actionInstant.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
|
||||
if (isInstant) {
|
||||
holder.container.backgroundTintList = ContextCompat.getColorStateList(context, R.color.colorMyMsg)
|
||||
} else {
|
||||
holder.container.backgroundTintList = null
|
||||
}
|
||||
holder.actionEdit.setOnClickListener {
|
||||
onEdit(current)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setActions(actions: List<QuickChatAction>) {
|
||||
this.actions = actions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = actions.size
|
||||
|
||||
override fun swapItems(fromPosition: Int, toPosition: Int) {
|
||||
repositionAction(fromPosition, toPosition)
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun commitSwaps() {
|
||||
commitAction()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.QuickChatSettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
class QuickChatSettingsFragment : ScreenFragment("Quick Chat settings"), Logging {
|
||||
private var _binding: QuickChatSettingsFragmentBinding? = null
|
||||
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private lateinit var actions: List<QuickChatAction>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = QuickChatSettingsFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.quickChatSettingsCreateButton.setOnClickListener {
|
||||
val builder = createEditDialog(requireContext(), "New quick chat")
|
||||
|
||||
builder.builder.setPositiveButton("Add") { view, x ->
|
||||
|
||||
val name = builder.nameInput.text.toString().trim()
|
||||
val message = builder.messageInput.text.toString()
|
||||
if (builder.isNotEmpty())
|
||||
model.addQuickChatAction(
|
||||
name, message,
|
||||
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
|
||||
)
|
||||
}
|
||||
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
val quickChatActionAdapter =
|
||||
QuickChatActionAdapter(requireContext(), { action: QuickChatAction ->
|
||||
val builder = createEditDialog(requireContext(), "Edit quick chat")
|
||||
builder.nameInput.setText(action.name)
|
||||
builder.messageInput.setText(action.message)
|
||||
val isInstant = action.mode == QuickChatAction.Mode.Instant
|
||||
builder.modeSwitch.isChecked = isInstant
|
||||
builder.instantImage.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
builder.builder.setNegativeButton(R.string.delete) { _, _ ->
|
||||
model.deleteQuickChatAction(action)
|
||||
}
|
||||
builder.builder.setPositiveButton(R.string.save_btn) { _, _ ->
|
||||
if (builder.isNotEmpty()) {
|
||||
model.updateQuickChatAction(
|
||||
action,
|
||||
builder.nameInput.text.toString(),
|
||||
builder.messageInput.text.toString(),
|
||||
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
|
||||
)
|
||||
}
|
||||
}
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}, { fromPos, toPos ->
|
||||
Collections.swap(actions, fromPos, toPos)
|
||||
}, {
|
||||
model.updateActionPositions(actions)
|
||||
})
|
||||
|
||||
val dragCallback =
|
||||
DragManageAdapter(quickChatActionAdapter, ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||
val helper = ItemTouchHelper(dragCallback)
|
||||
|
||||
binding.quickChatSettingsView.apply {
|
||||
this.layoutManager = LinearLayoutManager(requireContext())
|
||||
this.adapter = quickChatActionAdapter
|
||||
helper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
|
||||
actions?.let {
|
||||
quickChatActionAdapter.setActions(actions)
|
||||
this.actions = actions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DialogBuilder(
|
||||
val builder: MaterialAlertDialogBuilder,
|
||||
val nameInput: EditText,
|
||||
val messageInput: EditText,
|
||||
val modeSwitch: SwitchMaterial,
|
||||
val instantImage: ImageView
|
||||
) {
|
||||
fun isNotEmpty(): Boolean = nameInput.text.isNotEmpty() and messageInput.text.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun getMessageName(message: String): String {
|
||||
return if (message.length <= 3) {
|
||||
message.uppercase()
|
||||
} else {
|
||||
buildString {
|
||||
append(message.first().uppercase())
|
||||
append(message[message.length / 2].uppercase())
|
||||
append(message.last().uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEditDialog(context: Context, title: String): DialogBuilder {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
|
||||
val layout =
|
||||
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_add_quick_chat, null)
|
||||
|
||||
val nameInput: EditText = layout.findViewById(R.id.addQuickChatName)
|
||||
val messageInput: EditText = layout.findViewById(R.id.addQuickChatMessage)
|
||||
val modeSwitch: SwitchMaterial = layout.findViewById(R.id.addQuickChatMode)
|
||||
val instantImage: ImageView = layout.findViewById(R.id.addQuickChatInsant)
|
||||
instantImage.visibility = if (modeSwitch.isChecked) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
var nameHasChanged = false
|
||||
|
||||
modeSwitch.setOnCheckedChangeListener { _, _ ->
|
||||
if (modeSwitch.isChecked) {
|
||||
modeSwitch.setText(R.string.mode_instant)
|
||||
instantImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
modeSwitch.setText(R.string.mode_append)
|
||||
instantImage.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.addTextChangedListener { text ->
|
||||
if (!nameHasChanged) {
|
||||
nameInput.setText(getMessageName(text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
nameInput.addTextChangedListener {
|
||||
if (nameInput.isFocused) nameHasChanged = true
|
||||
}
|
||||
|
||||
builder.setView(layout)
|
||||
|
||||
return DialogBuilder(builder, nameInput, messageInput, modeSwitch, instantImage)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue