Merge pull request #462 from Douile/dev-quick-chat

Add quick chat actions
This commit is contained in:
Andre Kirchhoff
2022-08-16 18:43:13 -03:00
committed by GitHub
20 changed files with 718 additions and 10 deletions

View File

@@ -1070,6 +1070,15 @@ class MainActivity : BaseActivity(), Logging,
chooseMapStyle()
return true
}
R.id.preferences_quick_chat -> {
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = QuickChatSettingsFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
return true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@@ -2,6 +2,7 @@ package com.geeksville.mesh.database
import android.app.Application
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -20,4 +21,9 @@ class DatabaseModule {
fun providePacketDao(database: MeshtasticDatabase): PacketDao {
return database.packetDao()
}
@Provides
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao {
return database.quickChatActionDao()
}
}

View File

@@ -5,11 +5,14 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
@Database(entities = [Packet::class], version = 1, exportSchema = false)
@Database(entities = [Packet::class, QuickChatAction::class], version = 2, exportSchema = false)
abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun packetDao(): PacketDao
abstract fun quickChatActionDao(): QuickChatActionDao
companion object {
fun getDatabase(context: Context): MeshtasticDatabase {

View File

@@ -0,0 +1,38 @@
package com.geeksville.mesh.database
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.QuickChatAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class QuickChatActionRepository @Inject constructor(private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>) {
private val quickChatActionDao by lazy {
quickChatDaoLazy.get()
}
suspend fun getAllActions(): Flow<List<QuickChatAction>> = withContext(Dispatchers.IO) {
quickChatActionDao.getAll()
}
suspend fun insert(action: QuickChatAction) = withContext(Dispatchers.IO) {
quickChatActionDao.insert(action)
}
suspend fun deleteAll() = withContext(Dispatchers.IO) {
quickChatActionDao.deleteAll()
}
suspend fun delete(action: QuickChatAction) = withContext(Dispatchers.IO) {
quickChatActionDao.delete(action)
}
suspend fun update(action: QuickChatAction) = withContext(Dispatchers.IO) {
quickChatActionDao.update(action)
}
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(Dispatchers.IO) {
quickChatActionDao.updateActionPosition(uuid, newPos)
}
}

View File

@@ -0,0 +1,37 @@
package com.geeksville.mesh.database.dao
import androidx.room.*
import com.geeksville.mesh.database.entity.QuickChatAction
import kotlinx.coroutines.flow.Flow
@Dao
interface QuickChatActionDao {
@Query("Select * from quick_chat order by position asc")
fun getAll(): Flow<List<QuickChatAction>>
@Insert
fun insert(action: QuickChatAction)
@Query("Delete from quick_chat")
fun deleteAll()
@Query("Delete from quick_chat where uuid=:uuid")
fun _delete(uuid: Long)
@Transaction
fun delete(action: QuickChatAction) {
_delete(action.uuid)
decrementPositionsAfter(action.position)
}
@Update
fun update(action: QuickChatAction)
@Query("Update quick_chat set position=:position WHERE uuid=:uuid")
fun updateActionPosition(uuid: Long, position: Int)
@Query("Update quick_chat set position=position-1 where position>=:position")
fun decrementPositionsAfter(position: Int)
}

View File

@@ -0,0 +1,19 @@
package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "quick_chat")
data class QuickChatAction(
@PrimaryKey(autoGenerate = true) val uuid: Long,
@ColumnInfo(name="name") val name: String,
@ColumnInfo(name="message") val message: String,
@ColumnInfo(name="mode") val mode: Mode,
@ColumnInfo(name="position") val position: Int) {
enum class Mode {
Append,
Instant,
}
}

View File

@@ -14,11 +14,14 @@ import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging
import com.geeksville.mesh.*
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.util.positionToMeter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -61,6 +64,7 @@ class UIViewModel @Inject constructor(
private val app: Application,
private val packetRepository: PacketRepository,
private val localConfigRepository: LocalConfigRepository,
private val quickChatActionRepository: QuickChatActionRepository,
private val preferences: SharedPreferences
) : ViewModel(), Logging {
@@ -70,6 +74,12 @@ class UIViewModel @Inject constructor(
private val _localConfig = MutableLiveData<LocalOnlyProtos.LocalConfig?>()
val localConfig: LiveData<LocalOnlyProtos.LocalConfig?> get() = _localConfig
private val _quickChatActions =
MutableStateFlow<List<com.geeksville.mesh.database.entity.QuickChatAction>>(
emptyList()
)
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
init {
viewModelScope.launch {
packetRepository.getAllPackets().collect { packets ->
@@ -81,6 +91,11 @@ class UIViewModel @Inject constructor(
_localConfig.value = config
}
}
viewModelScope.launch {
quickChatActionRepository.getAllActions().collect { actions ->
_quickChatActions.value = actions
}
}
debug("ViewModel created")
}
@@ -445,5 +460,43 @@ class UIViewModel @Inject constructor(
}
}
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
viewModelScope.launch(Dispatchers.Main) {
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
quickChatActionRepository.insert(action)
}
}
fun deleteQuickChatAction(action: QuickChatAction) {
viewModelScope.launch(Dispatchers.Main) {
quickChatActionRepository.delete(action)
}
}
fun updateQuickChatAction(
action: QuickChatAction,
name: String?,
message: String?,
mode: QuickChatAction.Mode?
) {
viewModelScope.launch(Dispatchers.Main) {
val newAction = QuickChatAction(
action.uuid,
name ?: action.name,
message ?: action.message,
mode ?: action.mode,
action.position
)
quickChatActionRepository.update(newAction)
}
}
fun updateActionPositions(actions: List<QuickChatAction>) {
viewModelScope.launch(Dispatchers.Main) {
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
}
}
}

View File

@@ -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")
}
}

View File

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

View File

@@ -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()
}
}

View File

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