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()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
private val _focusedNode = MutableStateFlow<NodeEntity?>(null)
|
||||
val focusedNode: StateFlow<NodeEntity?> = _focusedNode
|
||||
|
||||
private val nodeFilterText = MutableStateFlow("")
|
||||
private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD)
|
||||
private val includeUnknown = MutableStateFlow(preferences.getBoolean("include-unknown", false))
|
||||
|
@ -729,11 +726,6 @@ class UIViewModel @Inject constructor(
|
|||
_currentTab.value = tab
|
||||
}
|
||||
|
||||
fun focusUserNode(node: NodeEntity?) {
|
||||
_currentTab.value = 1
|
||||
_focusedNode.value = node
|
||||
}
|
||||
|
||||
fun setNodeFilterText(text: String) {
|
||||
nodeFilterText.value = text
|
||||
}
|
||||
|
|
|
@ -126,8 +126,6 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
|
|||
private inner class ActionModeCallback : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
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"
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -41,12 +43,12 @@ import kotlinx.coroutines.flow.debounce
|
|||
@Composable
|
||||
internal fun MessageListView(
|
||||
messages: List<Message>,
|
||||
selectedList: List<Message>,
|
||||
onClick: (Message) -> Unit,
|
||||
onLongClick: (Message) -> Unit,
|
||||
onChipClick: (Message) -> Unit,
|
||||
selectedIds: MutableState<Set<Long>>,
|
||||
onUnreadChanged: (Long) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
onClick: (Message) -> Unit = {}
|
||||
) {
|
||||
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = messages.indexOfLast { !it.read }.coerceAtLeast(0)
|
||||
)
|
||||
|
@ -60,14 +62,20 @@ internal fun MessageListView(
|
|||
SimpleAlertDialog(title = title, text = text) { showStatusDialog = null }
|
||||
}
|
||||
|
||||
fun toggle(uuid: Long) = if (selectedIds.value.contains(uuid)) {
|
||||
selectedIds.value -= uuid
|
||||
} else {
|
||||
selectedIds.value += uuid
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
// contentPadding = PaddingValues(8.dp)
|
||||
contentPadding = contentPadding
|
||||
) {
|
||||
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(
|
||||
shortName = msg.user.shortName.takeIf { msg.user.id != DataPacket.ID_LOCAL },
|
||||
|
@ -75,9 +83,9 @@ internal fun MessageListView(
|
|||
messageTime = msg.time,
|
||||
messageStatus = msg.status,
|
||||
selected = selected,
|
||||
onClick = { onClick(msg) },
|
||||
onLongClick = { onLongClick(msg) },
|
||||
onChipClick = { onChipClick(msg) },
|
||||
onClick = { if (inSelectionMode) toggle(msg.uuid) },
|
||||
onLongClick = { toggle(msg.uuid) },
|
||||
onChipClick = { onClick(msg) },
|
||||
onStatusClick = { showStatusDialog = msg }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,55 +17,85 @@
|
|||
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.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.os.bundleOf
|
||||
import androidx.core.view.allViews
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.toast
|
||||
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.getChannel
|
||||
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||
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 kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal fun FragmentManager.navigateToMessages(contactKey: 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) {
|
||||
internal fun FragmentManager.navigateToMessages(contactKey: String, message: String = "") {
|
||||
val messagesFragment = MessagesFragment().apply {
|
||||
arguments = bundleOf("contactKey" to contactKey, "message" to message)
|
||||
}
|
||||
|
@ -77,256 +107,345 @@ internal fun FragmentManager.navigateToPreInitMessages(contactKey: String, messa
|
|||
|
||||
@AndroidEntryPoint
|
||||
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 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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
val contactKey = arguments?.getString("contactKey").toString()
|
||||
val message = arguments?.getString("message").toString()
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
contactKey = arguments?.getString("contactKey").toString()
|
||||
if (arguments?.getString("message") != null) {
|
||||
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) },
|
||||
)
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
||||
setContent {
|
||||
AppTheme {
|
||||
MessageScreen(
|
||||
contactKey = contactKey,
|
||||
message = message,
|
||||
viewModel = model,
|
||||
) { parentFragmentManager.popBackStack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
val isConnected = model.isConnected()
|
||||
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.text = action.name
|
||||
button.isEnabled = model.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.sendMessage(action.message, contactKey)
|
||||
}
|
||||
}
|
||||
binding.quickChatLayout.addView(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class ActionModeCallback : ActionMode.Callback {
|
||||
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"
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.deleteButton -> {
|
||||
val deleteMessagesString = resources.getQuantityString(
|
||||
R.plurals.delete_messages,
|
||||
selectedList.size,
|
||||
selectedList.size
|
||||
)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(deleteMessagesString)
|
||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||
debug("User clicked deleteButton")
|
||||
model.deleteMessages(selectedList.map { it.uuid })
|
||||
mode.finish()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
R.id.selectAllButton -> lifecycleScope.launch {
|
||||
model.getMessagesFrom(contactKey).firstOrNull()?.let { messages ->
|
||||
if (selectedList.size == messages.size) {
|
||||
// if all selected -> unselect all
|
||||
selectedList.clear()
|
||||
mode.finish()
|
||||
} else {
|
||||
// else --> select all
|
||||
selectedList.clear()
|
||||
selectedList.addAll(messages)
|
||||
}
|
||||
actionMode?.title = selectedList.size.toString()
|
||||
}
|
||||
}
|
||||
R.id.resendButton -> lifecycleScope.launch {
|
||||
debug("User clicked resendButton")
|
||||
val resendText = getSelectedMessagesText()
|
||||
binding.messageInputText.setText(resendText)
|
||||
mode.finish()
|
||||
}
|
||||
R.id.copyButton -> lifecycleScope.launch {
|
||||
val copyText = getSelectedMessagesText()
|
||||
val clipboardManager =
|
||||
requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("message text", copyText))
|
||||
requireActivity().toast(getString(R.string.copied))
|
||||
mode.finish()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
selectedList.clear()
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun openNodeInfo(msg: Message) = lifecycleScope.launch {
|
||||
model.nodeList.firstOrNull()?.find { it.user.id == msg.user.id }?.let { node ->
|
||||
parentFragmentManager.popBackStack()
|
||||
model.focusUserNode(node)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedMessagesText(): String {
|
||||
var messageText = ""
|
||||
selectedList.forEach {
|
||||
messageText = messageText + it.text + System.lineSeparator()
|
||||
}
|
||||
if (messageText != "") {
|
||||
messageText = messageText.substring(0, messageText.length - 1)
|
||||
}
|
||||
return messageText
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MessageMenuAction {
|
||||
data object ClipboardCopy : MessageMenuAction()
|
||||
data object Delete : MessageMenuAction()
|
||||
data object Dismiss : MessageMenuAction()
|
||||
data object SelectAll : MessageMenuAction()
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
internal fun MessageScreen(
|
||||
contactKey: String,
|
||||
message: String,
|
||||
viewModel: UIViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
val channelIndex = contactKey[0].digitToIntOrNull()
|
||||
val nodeId = contactKey.substring(1)
|
||||
val channelName = channelIndex?.let { viewModel.channels.value.getChannel(it)?.name }
|
||||
?: "Unknown Channel"
|
||||
|
||||
val title = when (nodeId) {
|
||||
DataPacket.ID_BROADCAST -> channelName
|
||||
else -> viewModel.getUser(nodeId).longName
|
||||
}
|
||||
|
||||
// if (channelIndex != DataPacket.PKC_CHANNEL_INDEX && nodeId != DataPacket.ID_BROADCAST) {
|
||||
// subtitle = "(ch: $channelIndex - $channelName)"
|
||||
// }
|
||||
|
||||
val selectedIds = rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
|
||||
|
||||
val connState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val quickChat by viewModel.quickChatActions.collectAsStateWithLifecycle()
|
||||
val messages by viewModel.getMessagesFrom(contactKey).collectAsStateWithLifecycle(listOf())
|
||||
|
||||
val messageInput = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(message))
|
||||
}
|
||||
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
if (showDeleteDialog) {
|
||||
DeleteMessageDialog(
|
||||
size = selectedIds.value.size,
|
||||
onConfirm = {
|
||||
viewModel.deleteMessages(selectedIds.value.toList())
|
||||
selectedIds.value = emptySet()
|
||||
showDeleteDialog = false
|
||||
},
|
||||
onDismiss = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (inSelectionMode) {
|
||||
ActionModeTopBar(selectedIds.value) { action ->
|
||||
when (action) {
|
||||
MessageMenuAction.ClipboardCopy -> coroutineScope.launch {
|
||||
val copiedText = messages
|
||||
.filter { it.uuid in selectedIds.value }
|
||||
.joinToString("\n") { it.text }
|
||||
|
||||
clipboardManager.setText(AnnotatedString(copiedText))
|
||||
selectedIds.value = emptySet()
|
||||
}
|
||||
|
||||
MessageMenuAction.Delete -> {
|
||||
showDeleteDialog = true
|
||||
}
|
||||
|
||||
MessageMenuAction.Dismiss -> selectedIds.value = emptySet()
|
||||
MessageMenuAction.SelectAll -> {
|
||||
if (selectedIds.value.size == messages.size) {
|
||||
selectedIds.value = emptySet()
|
||||
} else {
|
||||
selectedIds.value = messages.map { it.uuid }.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MessageTopBar(title, channelIndex, onNavigateBack)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
val isConnected = connState.isConnected()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp),
|
||||
) {
|
||||
QuickChatRow(isConnected, quickChat) { action ->
|
||||
if (action.mode == QuickChatAction.Mode.Append) {
|
||||
val originalText = messageInput.value.text
|
||||
val needsSpace = !originalText.endsWith(' ') && originalText.isNotEmpty()
|
||||
val newText = buildString {
|
||||
append(originalText)
|
||||
if (needsSpace) append(' ')
|
||||
append(action.message)
|
||||
}
|
||||
messageInput.value = TextFieldValue(newText, TextRange(newText.length))
|
||||
} else {
|
||||
viewModel.sendMessage(action.message, contactKey)
|
||||
}
|
||||
}
|
||||
TextInput(isConnected, messageInput) { viewModel.sendMessage(it, contactKey) }
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
if (messages.isNotEmpty()) {
|
||||
MessageListView(
|
||||
messages = messages,
|
||||
selectedIds = selectedIds,
|
||||
onUnreadChanged = { viewModel.clearUnreadCount(contactKey, it) },
|
||||
contentPadding = innerPadding
|
||||
) {
|
||||
// TODO onCLick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteMessageDialog(
|
||||
size: Int,
|
||||
onConfirm: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
val deleteMessagesString = pluralStringResource(R.plurals.delete_messages, size, size)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
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 {
|
||||
@Serializable
|
||||
data class Messages(val contactKey: String, val message: String = "") : Route
|
||||
|
||||
@Serializable
|
||||
data class RadioConfig(val destNum: Int? = null) : Route
|
||||
@Serializable data object User : Route
|
||||
|
|
|
@ -17,12 +17,6 @@
|
|||
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -87,7 +81,6 @@ fun NodeItem(
|
|||
tempInFahrenheit: Boolean,
|
||||
ignoreIncomingList: List<Int> = emptyList(),
|
||||
menuItemActionClicked: (MenuItemAction) -> Unit = {},
|
||||
blinking: Boolean = false,
|
||||
expanded: Boolean = false,
|
||||
currentTimeMillis: Long,
|
||||
isConnected: Boolean = false,
|
||||
|
@ -112,16 +105,6 @@ fun NodeItem(
|
|||
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) {
|
||||
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
|
||||
} else {
|
||||
|
@ -142,8 +125,7 @@ fun NodeItem(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.background(bgColor),
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -49,7 +49,7 @@ class ShareFragment : ScreenFragment("Messages"), Logging {
|
|||
|
||||
private fun shareMessage(contact: Contact) {
|
||||
debug("calling MessagesFragment filter:${contact.contactKey}")
|
||||
parentFragmentManager.navigateToPreInitMessages(
|
||||
parentFragmentManager.navigateToMessages(
|
||||
contact.contactKey,
|
||||
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.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
|
@ -100,16 +99,6 @@ fun NodesScreen(
|
|||
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
|
||||
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 connectionState by model.connectionState.collectAsStateWithLifecycle()
|
||||
|
@ -153,7 +142,6 @@ fun NodesScreen(
|
|||
MenuItemAction.MoreDetails -> navigateToNodeDetails(node.num)
|
||||
}
|
||||
},
|
||||
blinking = node == focusedNode,
|
||||
expanded = state.showDetails,
|
||||
currentTimeMillis = currentTimeMillis,
|
||||
isConnected = connectionState.isConnected(),
|
||||
|
|
|
@ -334,11 +334,11 @@ fun MapView(
|
|||
position = nodePosition
|
||||
icon = markerIcon
|
||||
|
||||
setOnLongClickListener {
|
||||
performHapticFeedback()
|
||||
model.focusUserNode(node)
|
||||
true
|
||||
}
|
||||
// setOnLongClickListener {
|
||||
// performHapticFeedback()
|
||||
// TODO NodeMenu?
|
||||
// true
|
||||
// }
|
||||
setNodeColors(node.colors)
|
||||
setPrecisionBits(p.precisionBits)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -30,14 +29,6 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.content.IntentCompat
|
||||
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? =
|
||||
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:title="@string/mute"
|
||||
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
|
||||
android:id="@+id/deleteButton"
|
||||
android:icon="@drawable/ic_twotone_delete_24"
|
||||
|
@ -38,9 +33,4 @@
|
|||
android:icon="@drawable/ic_twotone_select_all_24"
|
||||
android:title="@string/select_all"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/copyButton"
|
||||
android:icon="@drawable/ic_twotone_content_copy_24"
|
||||
android:title="@string/copy"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -278,5 +278,4 @@
|
|||
</plurals>
|
||||
<string name="traceroute_diff">Skoki do: %1$d. Skoki od: %2$d</string>
|
||||
<string name="copy">Kopiuj</string>
|
||||
<string name="copied">Skopiowano</string>
|
||||
</resources>
|
||||
|
|
|
@ -311,5 +311,4 @@
|
|||
<string name="not_selected">Not Selected</string>
|
||||
<string name="unknown_age">Unknown Age</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="copied">Copied</string>
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue