Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt

278 lines
9.8 KiB
Kotlin
Raw Normal View History

2020-04-08 16:49:27 -07:00
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
2020-04-08 17:50:23 -07:00
import android.view.inputmethod.EditorInfo
import android.widget.EditText
2020-04-08 16:49:27 -07:00
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.TextMessage
import com.geeksville.mesh.model.UIViewModel
2020-04-08 17:12:39 -07:00
import kotlinx.android.synthetic.main.adapter_message_layout.view.*
2020-04-08 16:49:27 -07:00
import kotlinx.android.synthetic.main.messages_fragment.*
2020-04-08 17:50:23 -07:00
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
fun EditText.on(actionId: Int, func: () -> Unit) {
setOnEditorActionListener { _, receivedActionId, _ ->
if (actionId == receivedActionId) {
func()
}
true
}
}
2020-04-08 16:49:27 -07:00
class MessagesFragment : ScreenFragment("Messages"), Logging {
private val model: UIViewModel by activityViewModels()
// 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: View) : RecyclerView.ViewHolder(itemView) {
2020-04-08 17:12:39 -07:00
val username = itemView.username
val messageText = itemView.messageText
2020-04-08 16:49:27 -07:00
}
private val messagesAdapter = 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
// Inflate the custom layout
2020-04-08 17:12:39 -07:00
val contactView: View = inflater.inflate(R.layout.adapter_message_layout, parent, false)
2020-04-08 16:49:27 -07:00
// Return a new holder instance
return ViewHolder(contactView)
}
/**
* 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 = messages.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) {
2020-04-08 17:12:39 -07:00
val msg = messages[position]
val nodes = model.nodeDB.nodes.value!!
// If we can't find the sender, just use the ID
val node = nodes.get(msg.from)
val user = node?.user
holder.username.text = user?.shortName ?: msg.from
if (msg.errorMessage != null) {
// FIXME, set the style to show a red error message
holder.messageText.text = msg.errorMessage
} else {
holder.messageText.text = msg.text
}
2020-04-08 16:49:27 -07:00
}
private var messages = arrayOf<TextMessage>()
/// Called when our node DB changes
fun onMessagesChanged(nodesIn: Collection<TextMessage>) {
messages = nodesIn.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages
2020-04-08 17:50:23 -07:00
// scroll to the last line
messageListView.scrollToPosition(this.itemCount - 1)
2020-04-08 16:49:27 -07:00
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.messages_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2020-04-08 17:50:23 -07:00
messageInputText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
val str = messageInputText.text.toString()
model.messagesState.sendMessage(str)
messageInputText.setText("") // blow away the string the user just entered
}
2020-04-08 16:49:27 -07:00
messageListView.adapter = messagesAdapter
2020-04-08 17:50:23 -07:00
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true // We want the last rows to always be shown
messageListView.layoutManager = layoutManager
2020-04-08 16:49:27 -07:00
model.messagesState.messages.observe(viewLifecycleOwner, Observer { it ->
messagesAdapter.onMessagesChanged(it)
})
}
}
/*
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.Modifier
import androidx.ui.foundation.Text
import androidx.ui.foundation.VerticalScroller
import androidx.ui.graphics.Color
import androidx.ui.input.ImeAction
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutSize
import androidx.ui.layout.Row
import androidx.ui.material.Emphasis
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.MessagesState.messages
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.TextMessage
import java.text.SimpleDateFormat
private val dateFormat = SimpleDateFormat("h:mm a")
val TimestampEmphasis = object : Emphasis {
override fun emphasize(color: Color) = color.copy(alpha = 0.25f)
}
/// A pretty version the text, with user icon to the left, name and time of arrival (copy slack look and feel)
@Composable
fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
Row(modifier = modifier) {
UserIcon(NodeDB.nodes[msg.from])
Column(modifier = LayoutPadding(start = 12.dp)) {
Row {
val nodes = NodeDB.nodes
// If we can't find the sender, just use the ID
val node = nodes.get(msg.from)
val user = node?.user
val senderName = user?.longName ?: msg.from
Text(text = senderName)
ProvideEmphasis(emphasis = TimestampEmphasis) {
Text(
text = dateFormat.format(msg.date),
modifier = LayoutPadding(start = 8.dp),
style = MaterialTheme.typography.caption
)
}
}
if (msg.errorMessage != null)
Text(text = msg.errorMessage, style = TextStyle(color = palette.error))
else
Text(text = msg.text)
}
}
}
@Composable
fun MessagesContent() {
Column(modifier = LayoutSize.Fill) {
val sidePad = 8.dp
val topPad = 4.dp
VerticalScroller(
modifier = LayoutWeight(1f)
) {
Column {
messages.forEach { msg ->
MessageCard(
msg, modifier = LayoutPadding(
start = sidePad,
end = sidePad,
top = topPad,
bottom = topPad
)
)
}
}
}
// Spacer(LayoutFlexible(1f))
val message = state { "" }
StyledTextField(
value = message.value,
onValueChange = { message.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Send,
onImeActionPerformed = {
MessagesState.info("did IME action")
val str = message.value
MessagesState.sendMessage(str)
message.value = "" // blow away the string the user just entered
},
hintText = "Type your message here..."
)
}
}
*/