mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-16 12:59:00 -04:00
@@ -29,7 +29,9 @@ data class DataPacket(
|
||||
var time: Long = System.currentTimeMillis(), // msecs since 1970
|
||||
var id: Int = 0, // 0 means unassigned
|
||||
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
||||
var hopLimit: Int = 0
|
||||
var hopLimit: Int = 0,
|
||||
var channel: Int = 0, // channel index
|
||||
var delayed: Int = 0 // S&F MeshProtos.MeshPacket.Delayed.(...)_VALUE
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@@ -64,6 +66,8 @@ data class DataPacket(
|
||||
parcel.readLong(),
|
||||
parcel.readInt(),
|
||||
parcel.readParcelable(MessageStatus::class.java.classLoader),
|
||||
parcel.readInt(),
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
)
|
||||
|
||||
@@ -75,6 +79,7 @@ data class DataPacket(
|
||||
|
||||
if (from != other.from) return false
|
||||
if (to != other.to) return false
|
||||
if (channel != other.channel) return false
|
||||
if (time != other.time) return false
|
||||
if (id != other.id) return false
|
||||
if (dataType != other.dataType) return false
|
||||
@@ -94,6 +99,8 @@ data class DataPacket(
|
||||
result = 31 * result + bytes!!.contentHashCode()
|
||||
result = 31 * result + status.hashCode()
|
||||
result = 31 * result + hopLimit
|
||||
result = 31 * result + channel
|
||||
result = 31 * result + delayed
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -106,6 +113,8 @@ data class DataPacket(
|
||||
parcel.writeInt(id)
|
||||
parcel.writeParcelable(status, flags)
|
||||
parcel.writeInt(hopLimit)
|
||||
parcel.writeInt(channel)
|
||||
parcel.writeInt(delayed)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
@@ -122,6 +131,8 @@ data class DataPacket(
|
||||
id = parcel.readInt()
|
||||
status = parcel.readParcelable(MessageStatus::class.java.classLoader)
|
||||
hopLimit = parcel.readInt()
|
||||
channel = parcel.readInt()
|
||||
delayed = parcel.readInt()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
||||
@@ -145,7 +156,7 @@ data class DataPacket(
|
||||
override fun newArray(size: Int): Array<DataPacket?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
val utf8 = Charset.forName("UTF-8")
|
||||
val utf8: Charset = Charset.forName("UTF-8")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.RemoteException
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Menu
|
||||
@@ -144,7 +145,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
TabInfo(
|
||||
"Messages",
|
||||
R.drawable.ic_twotone_message_24,
|
||||
MessagesFragment()
|
||||
ContactsFragment()
|
||||
),
|
||||
TabInfo(
|
||||
"Users",
|
||||
@@ -1010,7 +1011,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
|
||||
val handler: Handler by lazy {
|
||||
Handler(mainLooper)
|
||||
Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
|
||||
@@ -30,10 +30,40 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||
|
||||
}
|
||||
|
||||
private var contactsList = emptyMap<String?, DataPacket>().toMutableMap()
|
||||
val contacts = object : MutableLiveData<MutableMap<String?, DataPacket>>() {
|
||||
|
||||
}
|
||||
|
||||
private fun emptyDataPacket(to: String? = DataPacket.ID_BROADCAST): DataPacket {
|
||||
return DataPacket(to, null, 1, DataPacket.ID_LOCAL, 0L)
|
||||
}
|
||||
|
||||
// Map each contactId to last DataPacket message sent or received
|
||||
// Broadcast: it.to == DataPacket.ID_BROADCAST; Direct Messages: it.to != DataPacket.ID_BROADCAST
|
||||
private fun buildContacts() {
|
||||
contactsList = messagesList.associateBy {
|
||||
if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST)
|
||||
it.to else it.from
|
||||
}.toMutableMap()
|
||||
|
||||
val all = DataPacket.ID_BROADCAST // always show contacts, even when empty
|
||||
if (contactsList[all] == null)
|
||||
contactsList[all] = emptyDataPacket()
|
||||
|
||||
val nodes = ui.nodeDB.nodes.value!!
|
||||
nodes.keys.forEachIndexed { index, node ->
|
||||
if (index != 0 && contactsList[node] == null)
|
||||
contactsList[node] = emptyDataPacket(node)
|
||||
}
|
||||
contacts.value = contactsList
|
||||
}
|
||||
|
||||
fun setMessages(m: List<DataPacket>) {
|
||||
messagesList.clear()
|
||||
messagesList.addAll(m)
|
||||
messages.value = messagesList
|
||||
buildContacts()
|
||||
}
|
||||
|
||||
/// add a message our GUI list of past msgs
|
||||
@@ -44,6 +74,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||
messagesList.add(m)
|
||||
|
||||
messages.value = messagesList
|
||||
buildContacts()
|
||||
}
|
||||
|
||||
fun removeMessage(m: DataPacket) {
|
||||
@@ -51,6 +82,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||
|
||||
messagesList.remove(m)
|
||||
messages.value = messagesList
|
||||
buildContacts()
|
||||
}
|
||||
|
||||
private fun removeAllMessages() {
|
||||
@@ -58,6 +90,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||
|
||||
messagesList.clear()
|
||||
messages.value = messagesList
|
||||
buildContacts()
|
||||
}
|
||||
|
||||
fun updateStatus(id: Int, status: MessageStatus) {
|
||||
|
||||
@@ -696,7 +696,9 @@ class MeshService : Service(), Logging {
|
||||
id = packet.id,
|
||||
dataType = data.portnumValue,
|
||||
bytes = bytes,
|
||||
hopLimit = hopLimit
|
||||
hopLimit = hopLimit,
|
||||
channel = packet.channel,
|
||||
delayed = packet.delayedValue
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -719,7 +721,7 @@ class MeshService : Service(), Logging {
|
||||
// we only care about old text messages, we just store those...
|
||||
if (dataPacket.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
|
||||
// discard old messages if needed then add the new one
|
||||
while (recentDataPackets.size > 50)
|
||||
while (recentDataPackets.size > 100)
|
||||
recentDataPackets.removeAt(0)
|
||||
|
||||
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
|
||||
|
||||
354
app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt
Normal file
354
app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt
Normal file
@@ -0,0 +1,354 @@
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.AdapterContactLayoutBinding
|
||||
import com.geeksville.mesh.databinding.FragmentContactsBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ContactsFragment : ScreenFragment("Messages"), Logging {
|
||||
|
||||
private var actionMode: ActionMode? = null
|
||||
private var _binding: FragmentContactsBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private val dateTimeFormat: DateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||
private val timeFormat: DateFormat =
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
|
||||
private fun getShortDateTime(time: Date): String {
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
val oneDayMsec = 60 * 60 * 24 * 1000L
|
||||
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
|
||||
dateTimeFormat.format(time)
|
||||
} else timeFormat.format(time)
|
||||
}
|
||||
|
||||
// Provide a direct reference to each of the views within a data item
|
||||
// Used to cache the views within the item layout for fast access
|
||||
class ViewHolder(itemView: AdapterContactLayoutBinding) :
|
||||
RecyclerView.ViewHolder(itemView.root) {
|
||||
val shortName = itemView.shortName
|
||||
val longName = itemView.longName
|
||||
val lastMessageTime = itemView.lastMessageTime
|
||||
val lastMessageText = itemView.lastMessageText
|
||||
}
|
||||
|
||||
private val contactsAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
||||
|
||||
/**
|
||||
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
|
||||
* an item.
|
||||
*
|
||||
*
|
||||
* This new ViewHolder should be constructed with a new View that can represent the items
|
||||
* of the given type. You can either create a new View manually or inflate it from an XML
|
||||
* layout file.
|
||||
*
|
||||
*
|
||||
* The new ViewHolder will be used to display items of the adapter using
|
||||
* [.onBindViewHolder]. Since it will be re-used to display
|
||||
* different items in the data set, it is a good idea to cache references to sub views of
|
||||
* the View to avoid unnecessary [View.findViewById] calls.
|
||||
*
|
||||
* @param parent The ViewGroup into which the new View will be added after it is bound to
|
||||
* an adapter position.
|
||||
* @param viewType The view type of the new View.
|
||||
*
|
||||
* @return A new ViewHolder that holds a View of the given view type.
|
||||
* @see .getItemViewType
|
||||
* @see .onBindViewHolder
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactsView = AdapterContactLayoutBinding.inflate(inflater, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactsView)
|
||||
}
|
||||
|
||||
private var messages = arrayOf<DataPacket>()
|
||||
private var contacts = arrayOf<DataPacket>()
|
||||
private var selectedList = ArrayList<String>()
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the data set held by the adapter.
|
||||
*
|
||||
* @return The total number of items in this adapter.
|
||||
*/
|
||||
override fun getItemCount(): Int = contacts.size
|
||||
|
||||
/**
|
||||
* Called by RecyclerView to display the data at the specified position. This method should
|
||||
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
|
||||
* position.
|
||||
*
|
||||
*
|
||||
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
|
||||
* again if the position of the item changes in the data set unless the item itself is
|
||||
* invalidated or the new position cannot be determined. For this reason, you should only
|
||||
* use the `position` parameter while acquiring the related data item inside
|
||||
* this method and should not keep a copy of it. If you need the position of an item later
|
||||
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
|
||||
* have the updated adapter position.
|
||||
*
|
||||
* Override [.onBindViewHolder] instead if Adapter can
|
||||
* handle efficient partial bind.
|
||||
*
|
||||
* @param holder The ViewHolder which should be updated to represent the contents of the
|
||||
* item at the given position in the data set.
|
||||
* @param position The position of the item within the adapter's data set.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val contact = contacts[position]
|
||||
|
||||
// Determine if this is my message (originated on this device)
|
||||
val isLocal = contact.from == DataPacket.ID_LOCAL
|
||||
val isBroadcast = contact.to == DataPacket.ID_BROADCAST
|
||||
val contactId = if (isLocal || isBroadcast) contact.to else contact.from
|
||||
|
||||
// grab usernames from NodeInfo
|
||||
val nodes = model.nodeDB.nodes.value!!
|
||||
val node = nodes[if (isLocal) contact.to else contact.from]
|
||||
|
||||
//grab channel names from RadioConfig
|
||||
val channels = model.channels.value
|
||||
val primaryChannel = channels?.primaryChannel
|
||||
|
||||
val shortName = node?.user?.shortName ?: "???"
|
||||
val longName =
|
||||
if (isBroadcast) primaryChannel?.name ?: getString(R.string.channel_name)
|
||||
else node?.user?.longName ?: getString(R.string.unknown_username)
|
||||
|
||||
holder.shortName.text = if (isBroadcast) "All" else shortName
|
||||
holder.longName.text = longName
|
||||
|
||||
val text = if (isLocal) contact.text else "$shortName: ${contact.text}"
|
||||
holder.lastMessageText.text = text
|
||||
|
||||
if (contact.time != 0L) {
|
||||
holder.lastMessageTime.visibility = View.VISIBLE
|
||||
holder.lastMessageTime.text = getShortDateTime(Date(contact.time))
|
||||
} else holder.lastMessageTime.visibility = View.INVISIBLE
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
if (actionMode == null) {
|
||||
actionMode =
|
||||
(activity as MainActivity).startActionMode(object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
||||
mode.title = "1"
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
clickItem(holder, contactId)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.deleteButton -> {
|
||||
val messagesByContactId = ArrayList<DataPacket>()
|
||||
selectedList.forEach { contactId ->
|
||||
messagesByContactId += messages.filter {
|
||||
if (contactId == DataPacket.ID_BROADCAST)
|
||||
it.to == DataPacket.ID_BROADCAST
|
||||
else
|
||||
it.from == contactId && it.to != DataPacket.ID_BROADCAST
|
||||
|| it.from == DataPacket.ID_LOCAL && it.to == contactId
|
||||
}
|
||||
}
|
||||
val deleteMessagesString = resources.getQuantityString(
|
||||
R.plurals.delete_messages,
|
||||
messagesByContactId.size,
|
||||
messagesByContactId.size
|
||||
)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(deleteMessagesString)
|
||||
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||
debug("User clicked deleteButton")
|
||||
// all items selected --> deleteAllMessages()
|
||||
if (messagesByContactId.size == messages.size) {
|
||||
model.messagesState.deleteAllMessages()
|
||||
} else {
|
||||
messagesByContactId.forEach {
|
||||
model.messagesState.deleteMessage(it)
|
||||
}
|
||||
}
|
||||
mode.finish()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
R.id.selectAllButton -> {
|
||||
// if all selected -> unselect all
|
||||
if (selectedList.size == contacts.size) {
|
||||
selectedList.clear()
|
||||
mode.finish()
|
||||
} else {
|
||||
// else --> select all
|
||||
selectedList.clear()
|
||||
|
||||
contacts.forEach {
|
||||
if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST)
|
||||
selectedList.add(it.to!!) else selectedList.add(it.from!!)
|
||||
}
|
||||
}
|
||||
actionMode?.title = selectedList.size.toString()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
selectedList.clear()
|
||||
notifyDataSetChanged()
|
||||
actionMode = null
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// when action mode is enabled
|
||||
clickItem(holder, contactId)
|
||||
}
|
||||
true
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
if (actionMode != null) clickItem(holder, contactId)
|
||||
else {
|
||||
debug("calling MessagesFragment filter:$contactId")
|
||||
setFragmentResult(
|
||||
"requestKey",
|
||||
bundleOf("contactId" to contactId, "contactName" to longName)
|
||||
)
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(R.id.mainActivityLayout, MessagesFragment())
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedList.contains(contactId)) {
|
||||
holder.itemView.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 32f
|
||||
setColor(Color.rgb(127, 127, 127))
|
||||
}
|
||||
} else {
|
||||
holder.itemView.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 32f
|
||||
setColor(
|
||||
ContextCompat.getColor(
|
||||
holder.itemView.context,
|
||||
R.color.colorAdvancedBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clickItem(
|
||||
holder: ViewHolder,
|
||||
contactId: String? = DataPacket.ID_BROADCAST
|
||||
) {
|
||||
val position = holder.bindingAdapterPosition
|
||||
if (contactId != null && !selectedList.contains(contactId)) {
|
||||
selectedList.add(contactId)
|
||||
} else {
|
||||
selectedList.remove(contactId)
|
||||
}
|
||||
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()
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
/// Called when our contacts DB changes
|
||||
fun onContactsChanged(contactsIn: Collection<DataPacket>) {
|
||||
contacts = contactsIn.sortedByDescending { it.time }.toTypedArray()
|
||||
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
|
||||
}
|
||||
|
||||
/// Called when our message DB changes
|
||||
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
|
||||
messages = msgIn.toTypedArray()
|
||||
}
|
||||
|
||||
fun onChannelsChanged() {
|
||||
val oldBroadcast = contacts.find { it.to == DataPacket.ID_BROADCAST }
|
||||
if (oldBroadcast != null) {
|
||||
notifyItemChanged(contacts.indexOf(oldBroadcast))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
actionMode?.finish()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.contactsView.adapter = contactsAdapter
|
||||
binding.contactsView.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
model.channels.observe(viewLifecycleOwner) {
|
||||
contactsAdapter.onChannelsChanged()
|
||||
}
|
||||
|
||||
model.messagesState.contacts.observe(viewLifecycleOwner) {
|
||||
debug("New contacts received: ${it.size}")
|
||||
contactsAdapter.onContactsChanged(it.values)
|
||||
}
|
||||
|
||||
model.messagesState.messages.observe(viewLifecycleOwner) {
|
||||
contactsAdapter.onMessagesChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
@@ -41,13 +43,15 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
class MessagesFragment : Fragment(), Logging {
|
||||
|
||||
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 var contactId: String = DataPacket.ID_BROADCAST
|
||||
private var contactName: String = DataPacket.ID_BROADCAST
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
@@ -158,14 +162,27 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
val msg = messages[position]
|
||||
val nodes = model.nodeDB.nodes.value!!
|
||||
val node = nodes.get(msg.from)
|
||||
// Determine if this is my message (originated on this device).
|
||||
// val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
|
||||
val isMe = msg.from == "^local"
|
||||
// Determine if this is my message (originated on this device)
|
||||
val isLocal = msg.from == DataPacket.ID_LOCAL
|
||||
val isBroadcast = (msg.to == DataPacket.ID_BROADCAST
|
||||
|| msg.delayed == 1) // MeshProtos.MeshPacket.Delayed.DELAYED_BROADCAST_VALUE == 1
|
||||
|
||||
// Filter messages by contactId
|
||||
if (contactId == DataPacket.ID_BROADCAST) {
|
||||
if (isBroadcast) {
|
||||
holder.card.visibility = View.VISIBLE
|
||||
} else holder.card.visibility = View.GONE
|
||||
} else {
|
||||
if (msg.from == contactId && msg.to != DataPacket.ID_BROADCAST
|
||||
|| msg.from == DataPacket.ID_LOCAL && msg.to == contactId) {
|
||||
holder.card.visibility = View.VISIBLE
|
||||
} else holder.card.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Set cardview offset and color.
|
||||
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
|
||||
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
|
||||
if (isMe) {
|
||||
if (isLocal) {
|
||||
marginParams.leftMargin = messageOffset
|
||||
marginParams.rightMargin = 0
|
||||
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
|
||||
@@ -191,7 +208,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
}
|
||||
}
|
||||
// Hide the username chip for my messages
|
||||
if (isMe) {
|
||||
if (isLocal) {
|
||||
holder.username.visibility = View.GONE
|
||||
} else {
|
||||
holder.username.visibility = View.VISIBLE
|
||||
@@ -259,22 +276,30 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
selectedList.forEach {
|
||||
model.messagesState.deleteMessage(it)
|
||||
}
|
||||
mode.finish()
|
||||
}
|
||||
mode.finish()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
R.id.selectAllButton -> {
|
||||
// filter messages by ContactId
|
||||
val messagesByContactId = messages.filter {
|
||||
if (contactId == DataPacket.ID_BROADCAST)
|
||||
it.to == DataPacket.ID_BROADCAST
|
||||
else
|
||||
it.from == contactId && it.to != DataPacket.ID_BROADCAST
|
||||
|| it.from == DataPacket.ID_LOCAL && it.to == contactId
|
||||
}
|
||||
// if all selected -> unselect all
|
||||
if (selectedList.size == messages.size) {
|
||||
if (selectedList.size == messagesByContactId.size) {
|
||||
selectedList.clear()
|
||||
mode.finish()
|
||||
} else {
|
||||
// else --> select all
|
||||
selectedList.clear()
|
||||
selectedList.addAll(messages)
|
||||
selectedList.addAll(messagesByContactId)
|
||||
}
|
||||
actionMode?.title = selectedList.size.toString()
|
||||
notifyDataSetChanged()
|
||||
@@ -326,7 +351,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
// show total items selected on action mode title
|
||||
actionMode?.title = "${selectedList.size}"
|
||||
actionMode?.title = selectedList.size.toString()
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
@@ -357,12 +382,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setFragmentResultListener("requestKey") { _, bundle->
|
||||
// get the result from bundle
|
||||
contactId = bundle.getString("contactId").toString()
|
||||
contactName = bundle.getString("contactName").toString()
|
||||
binding.messageTitle.text = contactName
|
||||
}
|
||||
|
||||
binding.sendButton.setOnClickListener {
|
||||
debug("User clicked sendButton")
|
||||
|
||||
val str = binding.messageInputText.text.toString().trim()
|
||||
if (str.isNotEmpty())
|
||||
model.messagesState.sendMessage(str)
|
||||
model.messagesState.sendMessage(str, contactId)
|
||||
binding.messageInputText.setText("") // blow away the string the user just entered
|
||||
|
||||
// requireActivity().hideKeyboard()
|
||||
|
||||
@@ -62,11 +62,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabIconTint="@color/tab_color_selector"
|
||||
app:tabIndicatorColor="@color/selectedColor"
|
||||
>
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
app:tabIndicatorColor="@color/selectedColor" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
@@ -74,10 +70,5 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
62
app/src/main/res/layout/adapter_contact_layout.xml
Normal file
62
app/src/main/res/layout/adapter_contact_layout.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.App.CardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/shortName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/some_username"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/longName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/unknown_username"
|
||||
app:layout_constraintStart_toEndOf="@+id/shortName"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lastMessageText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:autoLink="all"
|
||||
android:maxLines="2"
|
||||
android:text="@string/sample_message"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/shortName"
|
||||
app:layout_constraintTop_toBottomOf="@id/longName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lastMessageTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/message_reception_time"
|
||||
android:text="3 minutes ago"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
@@ -61,9 +61,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/message_reception_time"
|
||||
android:text="3 minutes ago"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/messageStatusIcon"
|
||||
app:layout_constraintTop_toBottomOf="@id/messageText" />
|
||||
|
||||
19
app/src/main/res/layout/fragment_contacts.xml
Normal file
19
app/src/main/res/layout/fragment_contacts.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contactsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -2,21 +2,45 @@
|
||||
<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:layout_height="match_parent"
|
||||
android:background="@color/colorAdvancedBackground">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/MyToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/colorPrimary"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/channel_name"
|
||||
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/messageListView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/text_messages"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout"
|
||||
|
||||
Reference in New Issue
Block a user