kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: migrate `MessagesFragment` to Compose (#1444)
rodzic
5d3b36532f
commit
3c581f81a8
|
@ -195,9 +195,6 @@ class UIViewModel @Inject constructor(
|
||||||
val quickChatActions get() = quickChatActionRepository.getAllActions()
|
val quickChatActions get() = quickChatActionRepository.getAllActions()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||||
|
|
||||||
private val _focusedNode = MutableStateFlow<NodeEntity?>(null)
|
|
||||||
val focusedNode: StateFlow<NodeEntity?> = _focusedNode
|
|
||||||
|
|
||||||
private val nodeFilterText = MutableStateFlow("")
|
private val nodeFilterText = MutableStateFlow("")
|
||||||
private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD)
|
private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD)
|
||||||
private val includeUnknown = MutableStateFlow(preferences.getBoolean("include-unknown", false))
|
private val includeUnknown = MutableStateFlow(preferences.getBoolean("include-unknown", false))
|
||||||
|
@ -729,11 +726,6 @@ class UIViewModel @Inject constructor(
|
||||||
_currentTab.value = tab
|
_currentTab.value = tab
|
||||||
}
|
}
|
||||||
|
|
||||||
fun focusUserNode(node: NodeEntity?) {
|
|
||||||
_currentTab.value = 1
|
|
||||||
_focusedNode.value = node
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNodeFilterText(text: String) {
|
fun setNodeFilterText(text: String) {
|
||||||
nodeFilterText.value = text
|
nodeFilterText.value = text
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,8 +126,6 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
|
||||||
private inner class ActionModeCallback : ActionMode.Callback {
|
private inner class ActionModeCallback : ActionMode.Callback {
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
||||||
menu.findItem(R.id.resendButton).isVisible = false
|
|
||||||
menu.findItem(R.id.copyButton).isVisible = false
|
|
||||||
mode.title = "1"
|
mode.title = "1"
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
@ -24,6 +25,7 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
@ -41,12 +43,12 @@ import kotlinx.coroutines.flow.debounce
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessageListView(
|
internal fun MessageListView(
|
||||||
messages: List<Message>,
|
messages: List<Message>,
|
||||||
selectedList: List<Message>,
|
selectedIds: MutableState<Set<Long>>,
|
||||||
onClick: (Message) -> Unit,
|
|
||||||
onLongClick: (Message) -> Unit,
|
|
||||||
onChipClick: (Message) -> Unit,
|
|
||||||
onUnreadChanged: (Long) -> Unit,
|
onUnreadChanged: (Long) -> Unit,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onClick: (Message) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
|
||||||
val listState = rememberLazyListState(
|
val listState = rememberLazyListState(
|
||||||
initialFirstVisibleItemIndex = messages.indexOfLast { !it.read }.coerceAtLeast(0)
|
initialFirstVisibleItemIndex = messages.indexOfLast { !it.read }.coerceAtLeast(0)
|
||||||
)
|
)
|
||||||
|
@ -60,14 +62,20 @@ internal fun MessageListView(
|
||||||
SimpleAlertDialog(title = title, text = text) { showStatusDialog = null }
|
SimpleAlertDialog(title = title, text = text) { showStatusDialog = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggle(uuid: Long) = if (selectedIds.value.contains(uuid)) {
|
||||||
|
selectedIds.value -= uuid
|
||||||
|
} else {
|
||||||
|
selectedIds.value += uuid
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
// contentPadding = PaddingValues(8.dp)
|
contentPadding = contentPadding
|
||||||
) {
|
) {
|
||||||
items(messages, key = { it.uuid }) { msg ->
|
items(messages, key = { it.uuid }) { msg ->
|
||||||
val selected by remember { derivedStateOf { selectedList.contains(msg) } }
|
val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } }
|
||||||
|
|
||||||
MessageItem(
|
MessageItem(
|
||||||
shortName = msg.user.shortName.takeIf { msg.user.id != DataPacket.ID_LOCAL },
|
shortName = msg.user.shortName.takeIf { msg.user.id != DataPacket.ID_LOCAL },
|
||||||
|
@ -75,9 +83,9 @@ internal fun MessageListView(
|
||||||
messageTime = msg.time,
|
messageTime = msg.time,
|
||||||
messageStatus = msg.status,
|
messageStatus = msg.status,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = { onClick(msg) },
|
onClick = { if (inSelectionMode) toggle(msg.uuid) },
|
||||||
onLongClick = { onLongClick(msg) },
|
onLongClick = { toggle(msg.uuid) },
|
||||||
onChipClick = { onChipClick(msg) },
|
onChipClick = { onClick(msg) },
|
||||||
onStatusClick = { showStatusDialog = msg }
|
onStatusClick = { showStatusDialog = msg }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,55 +17,85 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import androidx.compose.foundation.background
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.AlertDialog
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.allViews
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
import com.geeksville.mesh.android.Logging
|
||||||
import com.geeksville.mesh.android.toast
|
|
||||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
|
||||||
import com.geeksville.mesh.model.Message
|
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.model.getChannel
|
import com.geeksville.mesh.model.getChannel
|
||||||
|
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import com.geeksville.mesh.util.Utf8ByteLengthFilter
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal fun FragmentManager.navigateToMessages(contactKey: String) {
|
internal fun FragmentManager.navigateToMessages(contactKey: String, message: String = "") {
|
||||||
val messagesFragment = MessagesFragment().apply {
|
|
||||||
arguments = bundleOf("contactKey" to contactKey)
|
|
||||||
}
|
|
||||||
beginTransaction()
|
|
||||||
.add(R.id.mainActivityLayout, messagesFragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
internal fun FragmentManager.navigateToPreInitMessages(contactKey: String, message: String) {
|
|
||||||
val messagesFragment = MessagesFragment().apply {
|
val messagesFragment = MessagesFragment().apply {
|
||||||
arguments = bundleOf("contactKey" to contactKey, "message" to message)
|
arguments = bundleOf("contactKey" to contactKey, "message" to message)
|
||||||
}
|
}
|
||||||
|
@ -77,256 +107,345 @@ internal fun FragmentManager.navigateToPreInitMessages(contactKey: String, messa
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MessagesFragment : Fragment(), Logging {
|
class MessagesFragment : Fragment(), Logging {
|
||||||
|
|
||||||
private val actionModeCallback: ActionModeCallback = ActionModeCallback()
|
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
private var _binding: MessagesFragmentBinding? = null
|
|
||||||
|
|
||||||
// This property is only valid between onCreateView and onDestroyView.
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
private val model: UIViewModel by activityViewModels()
|
||||||
|
|
||||||
private lateinit var contactKey: String
|
|
||||||
|
|
||||||
private val selectedList = emptyList<Message>().toMutableStateList()
|
|
||||||
|
|
||||||
private fun onClick(message: Message) {
|
|
||||||
if (actionMode != null) {
|
|
||||||
onLongClick(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLongClick(message: Message) {
|
|
||||||
if (actionMode == null) {
|
|
||||||
actionMode = (activity as AppCompatActivity).startSupportActionMode(actionModeCallback)
|
|
||||||
}
|
|
||||||
selectedList.apply {
|
|
||||||
if (contains(message)) remove(message) else add(message)
|
|
||||||
}
|
|
||||||
if (selectedList.isEmpty()) {
|
|
||||||
// finish action mode when no items selected
|
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
// show total items selected on action mode title
|
|
||||||
actionMode?.title = selectedList.size.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
actionMode?.finish()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
|
val contactKey = arguments?.getString("contactKey").toString()
|
||||||
return binding.root
|
val message = arguments?.getString("message").toString()
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
return ComposeView(requireContext()).apply {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
super.onViewCreated(view, savedInstanceState)
|
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
||||||
|
setContent {
|
||||||
binding.toolbar.setNavigationOnClickListener {
|
AppTheme {
|
||||||
parentFragmentManager.popBackStack()
|
MessageScreen(
|
||||||
}
|
contactKey = contactKey,
|
||||||
|
message = message,
|
||||||
contactKey = arguments?.getString("contactKey").toString()
|
viewModel = model,
|
||||||
if (arguments?.getString("message") != null) {
|
) { parentFragmentManager.popBackStack() }
|
||||||
binding.messageInputText.setText(arguments?.getString("message").toString())
|
|
||||||
}
|
|
||||||
val channelIndex = contactKey[0].digitToIntOrNull()
|
|
||||||
val nodeId = contactKey.substring(1)
|
|
||||||
val channelName = channelIndex?.let { model.channels.value.getChannel(it)?.name }
|
|
||||||
?: "Unknown Channel"
|
|
||||||
|
|
||||||
binding.toolbar.title = when (nodeId) {
|
|
||||||
DataPacket.ID_BROADCAST -> channelName
|
|
||||||
else -> model.getUser(nodeId).longName
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channelIndex == DataPacket.PKC_CHANNEL_INDEX) {
|
|
||||||
binding.toolbar.title = "${binding.toolbar.title}🔒"
|
|
||||||
} else if (nodeId != DataPacket.ID_BROADCAST) {
|
|
||||||
binding.toolbar.subtitle = "(ch: $channelIndex - $channelName)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessageInputText() {
|
|
||||||
val str = binding.messageInputText.text.toString().trim()
|
|
||||||
if (str.isNotEmpty()) {
|
|
||||||
model.sendMessage(str, contactKey)
|
|
||||||
}
|
|
||||||
binding.messageInputText.setText("") // blow away the string the user just entered
|
|
||||||
// requireActivity().hideKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.sendButton.setOnClickListener {
|
|
||||||
debug("User clicked sendButton")
|
|
||||||
sendMessageInputText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// max payload length should be 237 bytes but anything over 200 becomes less reliable
|
|
||||||
binding.messageInputText.filters += Utf8ByteLengthFilter(200)
|
|
||||||
|
|
||||||
binding.messageListView.setContent {
|
|
||||||
val messages by model.getMessagesFrom(contactKey).collectAsStateWithLifecycle(listOf())
|
|
||||||
|
|
||||||
AppTheme {
|
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
MessageListView(
|
|
||||||
messages = messages,
|
|
||||||
selectedList = selectedList,
|
|
||||||
onClick = ::onClick,
|
|
||||||
onLongClick = ::onLongClick,
|
|
||||||
onChipClick = ::openNodeInfo,
|
|
||||||
onUnreadChanged = { model.clearUnreadCount(contactKey, it) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
}
|
||||||
model.connectionState.asLiveData().observe(viewLifecycleOwner) {
|
|
||||||
// If we don't know our node ID and we are offline don't let user try to send
|
sealed class MessageMenuAction {
|
||||||
val isConnected = model.isConnected()
|
data object ClipboardCopy : MessageMenuAction()
|
||||||
binding.textInputLayout.isEnabled = isConnected
|
data object Delete : MessageMenuAction()
|
||||||
binding.sendButton.isEnabled = isConnected
|
data object Dismiss : MessageMenuAction()
|
||||||
for (subView: View in binding.quickChatLayout.allViews) {
|
data object SelectAll : MessageMenuAction()
|
||||||
if (subView is Button) {
|
}
|
||||||
subView.isEnabled = isConnected
|
|
||||||
}
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
}
|
@Composable
|
||||||
}
|
internal fun MessageScreen(
|
||||||
|
contactKey: String,
|
||||||
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
|
message: String,
|
||||||
actions?.let {
|
viewModel: UIViewModel = hiltViewModel(),
|
||||||
// This seems kinda hacky it might be better to replace with a recycler view
|
onNavigateBack: () -> Unit
|
||||||
binding.quickChatLayout.removeAllViews()
|
) {
|
||||||
for (action in actions) {
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val button = Button(context)
|
val clipboardManager = LocalClipboardManager.current
|
||||||
button.text = action.name
|
|
||||||
button.isEnabled = model.isConnected()
|
val channelIndex = contactKey[0].digitToIntOrNull()
|
||||||
if (action.mode == QuickChatAction.Mode.Instant) {
|
val nodeId = contactKey.substring(1)
|
||||||
button.backgroundTintList =
|
val channelName = channelIndex?.let { viewModel.channels.value.getChannel(it)?.name }
|
||||||
ContextCompat.getColorStateList(requireActivity(), R.color.colorMyMsg)
|
?: "Unknown Channel"
|
||||||
}
|
|
||||||
button.setOnClickListener {
|
val title = when (nodeId) {
|
||||||
if (action.mode == QuickChatAction.Mode.Append) {
|
DataPacket.ID_BROADCAST -> channelName
|
||||||
val originalText = binding.messageInputText.text ?: ""
|
else -> viewModel.getUser(nodeId).longName
|
||||||
val needsSpace =
|
}
|
||||||
!originalText.endsWith(' ') && originalText.isNotEmpty()
|
|
||||||
val newText = buildString {
|
// if (channelIndex != DataPacket.PKC_CHANNEL_INDEX && nodeId != DataPacket.ID_BROADCAST) {
|
||||||
append(originalText)
|
// subtitle = "(ch: $channelIndex - $channelName)"
|
||||||
if (needsSpace) append(' ')
|
// }
|
||||||
append(action.message)
|
|
||||||
}
|
val selectedIds = rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||||
binding.messageInputText.setText(newText)
|
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
|
||||||
binding.messageInputText.setSelection(newText.length)
|
|
||||||
} else {
|
val connState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||||
model.sendMessage(action.message, contactKey)
|
val quickChat by viewModel.quickChatActions.collectAsStateWithLifecycle()
|
||||||
}
|
val messages by viewModel.getMessagesFrom(contactKey).collectAsStateWithLifecycle(listOf())
|
||||||
}
|
|
||||||
binding.quickChatLayout.addView(button)
|
val messageInput = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||||
}
|
mutableStateOf(TextFieldValue(message))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
|
if (showDeleteDialog) {
|
||||||
override fun onDestroyView() {
|
DeleteMessageDialog(
|
||||||
super.onDestroyView()
|
size = selectedIds.value.size,
|
||||||
actionMode?.finish()
|
onConfirm = {
|
||||||
actionMode = null
|
viewModel.deleteMessages(selectedIds.value.toList())
|
||||||
_binding = null
|
selectedIds.value = emptySet()
|
||||||
}
|
showDeleteDialog = false
|
||||||
|
},
|
||||||
private inner class ActionModeCallback : ActionMode.Callback {
|
onDismiss = { showDeleteDialog = false }
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
)
|
||||||
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
}
|
||||||
menu.findItem(R.id.muteButton).isVisible = false
|
|
||||||
mode.title = "1"
|
Scaffold(
|
||||||
return true
|
topBar = {
|
||||||
}
|
if (inSelectionMode) {
|
||||||
|
ActionModeTopBar(selectedIds.value) { action ->
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
when (action) {
|
||||||
return false
|
MessageMenuAction.ClipboardCopy -> coroutineScope.launch {
|
||||||
}
|
val copiedText = messages
|
||||||
|
.filter { it.uuid in selectedIds.value }
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
.joinToString("\n") { it.text }
|
||||||
when (item.itemId) {
|
|
||||||
R.id.deleteButton -> {
|
clipboardManager.setText(AnnotatedString(copiedText))
|
||||||
val deleteMessagesString = resources.getQuantityString(
|
selectedIds.value = emptySet()
|
||||||
R.plurals.delete_messages,
|
}
|
||||||
selectedList.size,
|
|
||||||
selectedList.size
|
MessageMenuAction.Delete -> {
|
||||||
)
|
showDeleteDialog = true
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
}
|
||||||
.setMessage(deleteMessagesString)
|
|
||||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
MessageMenuAction.Dismiss -> selectedIds.value = emptySet()
|
||||||
debug("User clicked deleteButton")
|
MessageMenuAction.SelectAll -> {
|
||||||
model.deleteMessages(selectedList.map { it.uuid })
|
if (selectedIds.value.size == messages.size) {
|
||||||
mode.finish()
|
selectedIds.value = emptySet()
|
||||||
}
|
} else {
|
||||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
selectedIds.value = messages.map { it.uuid }.toSet()
|
||||||
}
|
}
|
||||||
.show()
|
}
|
||||||
}
|
}
|
||||||
R.id.selectAllButton -> lifecycleScope.launch {
|
}
|
||||||
model.getMessagesFrom(contactKey).firstOrNull()?.let { messages ->
|
} else {
|
||||||
if (selectedList.size == messages.size) {
|
MessageTopBar(title, channelIndex, onNavigateBack)
|
||||||
// if all selected -> unselect all
|
}
|
||||||
selectedList.clear()
|
},
|
||||||
mode.finish()
|
bottomBar = {
|
||||||
} else {
|
val isConnected = connState.isConnected()
|
||||||
// else --> select all
|
Column(
|
||||||
selectedList.clear()
|
modifier = Modifier
|
||||||
selectedList.addAll(messages)
|
.background(MaterialTheme.colors.background)
|
||||||
}
|
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp),
|
||||||
actionMode?.title = selectedList.size.toString()
|
) {
|
||||||
}
|
QuickChatRow(isConnected, quickChat) { action ->
|
||||||
}
|
if (action.mode == QuickChatAction.Mode.Append) {
|
||||||
R.id.resendButton -> lifecycleScope.launch {
|
val originalText = messageInput.value.text
|
||||||
debug("User clicked resendButton")
|
val needsSpace = !originalText.endsWith(' ') && originalText.isNotEmpty()
|
||||||
val resendText = getSelectedMessagesText()
|
val newText = buildString {
|
||||||
binding.messageInputText.setText(resendText)
|
append(originalText)
|
||||||
mode.finish()
|
if (needsSpace) append(' ')
|
||||||
}
|
append(action.message)
|
||||||
R.id.copyButton -> lifecycleScope.launch {
|
}
|
||||||
val copyText = getSelectedMessagesText()
|
messageInput.value = TextFieldValue(newText, TextRange(newText.length))
|
||||||
val clipboardManager =
|
} else {
|
||||||
requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
viewModel.sendMessage(action.message, contactKey)
|
||||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("message text", copyText))
|
}
|
||||||
requireActivity().toast(getString(R.string.copied))
|
}
|
||||||
mode.finish()
|
TextInput(isConnected, messageInput) { viewModel.sendMessage(it, contactKey) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
) { innerPadding ->
|
||||||
}
|
if (messages.isNotEmpty()) {
|
||||||
|
MessageListView(
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
messages = messages,
|
||||||
selectedList.clear()
|
selectedIds = selectedIds,
|
||||||
actionMode = null
|
onUnreadChanged = { viewModel.clearUnreadCount(contactKey, it) },
|
||||||
}
|
contentPadding = innerPadding
|
||||||
}
|
) {
|
||||||
|
// TODO onCLick()
|
||||||
private fun openNodeInfo(msg: Message) = lifecycleScope.launch {
|
}
|
||||||
model.nodeList.firstOrNull()?.find { it.user.id == msg.user.id }?.let { node ->
|
}
|
||||||
parentFragmentManager.popBackStack()
|
}
|
||||||
model.focusUserNode(node)
|
}
|
||||||
}
|
|
||||||
}
|
@Composable
|
||||||
|
private fun DeleteMessageDialog(
|
||||||
private fun getSelectedMessagesText(): String {
|
size: Int,
|
||||||
var messageText = ""
|
onConfirm: () -> Unit = {},
|
||||||
selectedList.forEach {
|
onDismiss: () -> Unit = {},
|
||||||
messageText = messageText + it.text + System.lineSeparator()
|
) {
|
||||||
}
|
val deleteMessagesString = pluralStringResource(R.plurals.delete_messages, size, size)
|
||||||
if (messageText != "") {
|
|
||||||
messageText = messageText.substring(0, messageText.length - 1)
|
AlertDialog(
|
||||||
}
|
onDismissRequest = onDismiss,
|
||||||
return messageText
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = deleteMessagesString,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(R.string.delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionModeTopBar(
|
||||||
|
selectedList: Set<Long>,
|
||||||
|
onAction: (MessageMenuAction) -> Unit,
|
||||||
|
) = TopAppBar(
|
||||||
|
title = { Text(text = selectedList.size.toString()) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onAction(MessageMenuAction.Dismiss) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.clear),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ContentCopy,
|
||||||
|
contentDescription = stringResource(id = R.string.copy)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(id = R.string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SelectAll,
|
||||||
|
contentDescription = stringResource(id = R.string.select_all)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor = MaterialTheme.colors.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageTopBar(
|
||||||
|
title: String,
|
||||||
|
channelIndex: Int?,
|
||||||
|
onNavigateBack: () -> Unit
|
||||||
|
) = TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.navigate_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (channelIndex == DataPacket.PKC_CHANNEL_INDEX) {
|
||||||
|
NodeKeyStatusIcon(hasPKC = true, mismatchKey = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QuickChatRow(
|
||||||
|
enabled: Boolean,
|
||||||
|
actions: List<QuickChatAction>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (QuickChatAction) -> Unit
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
items(actions, key = { it.uuid }) { action ->
|
||||||
|
Button(
|
||||||
|
onClick = { onClick(action) },
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = colorResource(id = R.color.colorMyMsg),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = action.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TextInput(
|
||||||
|
enabled: Boolean,
|
||||||
|
message: MutableState<TextFieldValue>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
maxSize: Int = 200,
|
||||||
|
onClick: (String) -> Unit = {}
|
||||||
|
) = Column(modifier) {
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = message.value,
|
||||||
|
onValueChange = {
|
||||||
|
if (it.text.toByteArray().size <= maxSize) {
|
||||||
|
message.value = it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.onFocusEvent { isFocused = it.isFocused },
|
||||||
|
enabled = enabled,
|
||||||
|
placeholder = { Text(stringResource(id = R.string.send_text)) },
|
||||||
|
maxLines = 3,
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (message.value.text.isNotEmpty()) {
|
||||||
|
onClick(message.value.text)
|
||||||
|
message.value = TextFieldValue("")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
enabled = enabled,
|
||||||
|
shape = CircleShape,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.Send,
|
||||||
|
contentDescription = stringResource(id = R.string.send_text),
|
||||||
|
modifier = Modifier.scale(scale = 1.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isFocused) {
|
||||||
|
Text(
|
||||||
|
text = "${message.value.text.toByteArray().size}/$maxSize",
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(top = 4.dp, end = 72.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewLightDark
|
||||||
|
@Composable
|
||||||
|
private fun TextInputPreview() {
|
||||||
|
AppTheme {
|
||||||
|
TextInput(
|
||||||
|
enabled = true,
|
||||||
|
message = remember { mutableStateOf(TextFieldValue("")) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,9 @@ enum class AdminRoute(@StringRes val title: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Route {
|
sealed interface Route {
|
||||||
|
@Serializable
|
||||||
|
data class Messages(val contactKey: String, val message: String = "") : Route
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RadioConfig(val destNum: Int? = null) : Route
|
data class RadioConfig(val destNum: Int? = null) : Route
|
||||||
@Serializable data object User : Route
|
@Serializable data object User : Route
|
||||||
|
|
|
@ -17,12 +17,6 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.RepeatMode
|
|
||||||
import androidx.compose.animation.core.repeatable
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -87,7 +81,6 @@ fun NodeItem(
|
||||||
tempInFahrenheit: Boolean,
|
tempInFahrenheit: Boolean,
|
||||||
ignoreIncomingList: List<Int> = emptyList(),
|
ignoreIncomingList: List<Int> = emptyList(),
|
||||||
menuItemActionClicked: (MenuItemAction) -> Unit = {},
|
menuItemActionClicked: (MenuItemAction) -> Unit = {},
|
||||||
blinking: Boolean = false,
|
|
||||||
expanded: Boolean = false,
|
expanded: Boolean = false,
|
||||||
currentTimeMillis: Long,
|
currentTimeMillis: Long,
|
||||||
isConnected: Boolean = false,
|
isConnected: Boolean = false,
|
||||||
|
@ -112,16 +105,6 @@ fun NodeItem(
|
||||||
thatNode.user.role.name
|
thatNode.user.role.name
|
||||||
}
|
}
|
||||||
|
|
||||||
val bgColor by animateColorAsState(
|
|
||||||
targetValue = if (blinking) Color(color = 0x33FFFFFF) else Color.Transparent,
|
|
||||||
animationSpec = repeatable(
|
|
||||||
iterations = 6,
|
|
||||||
animation = tween(durationMillis = 250, easing = FastOutSlowInEasing),
|
|
||||||
repeatMode = RepeatMode.Reverse
|
|
||||||
),
|
|
||||||
label = "blinking node"
|
|
||||||
)
|
|
||||||
|
|
||||||
val style = if (thatNode.isUnknownUser) {
|
val style = if (thatNode.isUnknownUser) {
|
||||||
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
|
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,8 +125,7 @@ fun NodeItem(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp),
|
||||||
.background(bgColor),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ShareFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
private fun shareMessage(contact: Contact) {
|
private fun shareMessage(contact: Contact) {
|
||||||
debug("calling MessagesFragment filter:${contact.contactKey}")
|
debug("calling MessagesFragment filter:${contact.contactKey}")
|
||||||
parentFragmentManager.navigateToPreInitMessages(
|
parentFragmentManager.navigateToMessages(
|
||||||
contact.contactKey,
|
contact.contactKey,
|
||||||
arguments?.getString("message").toString()
|
arguments?.getString("message").toString()
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
@ -100,16 +99,6 @@ fun NodesScreen(
|
||||||
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
|
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val focusedNode by model.focusedNode.collectAsStateWithLifecycle()
|
|
||||||
LaunchedEffect(focusedNode) {
|
|
||||||
focusedNode?.let { node ->
|
|
||||||
val index = nodes.indexOfFirst { it.num == node.num }
|
|
||||||
if (index != -1) {
|
|
||||||
listState.animateScrollToItem(index)
|
|
||||||
}
|
|
||||||
model.focusUserNode(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentTimeMillis = rememberTimeTickWithLifecycle()
|
val currentTimeMillis = rememberTimeTickWithLifecycle()
|
||||||
val connectionState by model.connectionState.collectAsStateWithLifecycle()
|
val connectionState by model.connectionState.collectAsStateWithLifecycle()
|
||||||
|
@ -153,7 +142,6 @@ fun NodesScreen(
|
||||||
MenuItemAction.MoreDetails -> navigateToNodeDetails(node.num)
|
MenuItemAction.MoreDetails -> navigateToNodeDetails(node.num)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blinking = node == focusedNode,
|
|
||||||
expanded = state.showDetails,
|
expanded = state.showDetails,
|
||||||
currentTimeMillis = currentTimeMillis,
|
currentTimeMillis = currentTimeMillis,
|
||||||
isConnected = connectionState.isConnected(),
|
isConnected = connectionState.isConnected(),
|
||||||
|
|
|
@ -334,11 +334,11 @@ fun MapView(
|
||||||
position = nodePosition
|
position = nodePosition
|
||||||
icon = markerIcon
|
icon = markerIcon
|
||||||
|
|
||||||
setOnLongClickListener {
|
// setOnLongClickListener {
|
||||||
performHapticFeedback()
|
// performHapticFeedback()
|
||||||
model.focusUserNode(node)
|
// TODO NodeMenu?
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
setNodeColors(node.colors)
|
setNodeColors(node.colors)
|
||||||
setPrecisionBits(p.precisionBits)
|
setPrecisionBits(p.precisionBits)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.util
|
package com.geeksville.mesh.util
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -30,14 +29,6 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import androidx.core.os.ParcelCompat
|
import androidx.core.os.ParcelCompat
|
||||||
|
|
||||||
object PendingIntentCompat {
|
|
||||||
val FLAG_IMMUTABLE = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? =
|
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? =
|
||||||
ParcelCompat.readParcelable(this, loader, T::class.java)
|
ParcelCompat.readParcelable(this, loader, T::class.java)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<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:pathData="M8,7h11v14H8z"
|
|
||||||
android:strokeAlpha="0.3"
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:fillAlpha="0.3"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"
|
|
||||||
android:fillColor="@android:color/white"/>
|
|
||||||
</vector>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<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:pathData="M10,17c0,-3.31 2.69,-6 6,-6h3V5h-2v3H7V5H5v14h5v-2z"
|
|
||||||
android:strokeAlpha="0.3"
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:fillAlpha="0.3"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M10,19L5,19L5,5h2v3h10L17,5h2v6h2L21,5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1s-2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h5v-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1s0.45,-1 1,-1z"
|
|
||||||
android:fillColor="@android:color/white"/>
|
|
||||||
<path
|
|
||||||
android:pathData="m18.01,13l-1.42,1.41l1.58,1.58H12v2h6.17l-1.58,1.59l1.42,1.41l3.99,-4z"
|
|
||||||
android:fillColor="@android:color/white"/>
|
|
||||||
</vector>
|
|
|
@ -1,99 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright (c) 2024 Meshtastic LLC
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/colorAdvancedBackground"
|
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
style="@style/MyToolbar"
|
|
||||||
android:title="@string/channel_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
app:layout_constraintHeight_min="?attr/actionBarSize"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/toolbarBackground"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navigationIcon="?android:attr/homeAsUpIndicator"/>
|
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
|
||||||
android:id="@+id/messageListView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:contentDescription="@string/text_messages"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/quickChatView"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
|
||||||
|
|
||||||
</androidx.compose.ui.platform.ComposeView>
|
|
||||||
|
|
||||||
<HorizontalScrollView
|
|
||||||
android:id="@+id/quickChatView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/quickChatLayout"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal" />
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/textInputLayout"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
android:hint="@string/send_text"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/messageInputText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="textMultiLine|textCapSentences"
|
|
||||||
android:text="" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/sendButton"
|
|
||||||
android:layout_width="64dp"
|
|
||||||
android:layout_height="64dp"
|
|
||||||
android:contentDescription="@string/send_text"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout"
|
|
||||||
app:srcCompat="@drawable/ic_twotone_send_24" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -23,11 +23,6 @@
|
||||||
android:icon="@drawable/ic_twotone_volume_off_24"
|
android:icon="@drawable/ic_twotone_volume_off_24"
|
||||||
android:title="@string/mute"
|
android:title="@string/mute"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
|
||||||
android:id="@+id/resendButton"
|
|
||||||
android:icon="@drawable/ic_twotone_content_paste_go_24"
|
|
||||||
android:title="@string/resend"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/deleteButton"
|
android:id="@+id/deleteButton"
|
||||||
android:icon="@drawable/ic_twotone_delete_24"
|
android:icon="@drawable/ic_twotone_delete_24"
|
||||||
|
@ -38,9 +33,4 @@
|
||||||
android:icon="@drawable/ic_twotone_select_all_24"
|
android:icon="@drawable/ic_twotone_select_all_24"
|
||||||
android:title="@string/select_all"
|
android:title="@string/select_all"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
|
||||||
android:id="@+id/copyButton"
|
|
||||||
android:icon="@drawable/ic_twotone_content_copy_24"
|
|
||||||
android:title="@string/copy"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
</menu>
|
</menu>
|
|
@ -278,5 +278,4 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="traceroute_diff">Skoki do: %1$d. Skoki od: %2$d</string>
|
<string name="traceroute_diff">Skoki do: %1$d. Skoki od: %2$d</string>
|
||||||
<string name="copy">Kopiuj</string>
|
<string name="copy">Kopiuj</string>
|
||||||
<string name="copied">Skopiowano</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -311,5 +311,4 @@
|
||||||
<string name="not_selected">Not Selected</string>
|
<string name="not_selected">Not Selected</string>
|
||||||
<string name="unknown_age">Unknown Age</string>
|
<string name="unknown_age">Unknown Age</string>
|
||||||
<string name="copy">Copy</string>
|
<string name="copy">Copy</string>
|
||||||
<string name="copied">Copied</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue