diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 65b9b85cd..aa6c9b9c8 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -30,6 +30,7 @@ import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.* import com.geeksville.mesh.ui.ChannelFragment import com.geeksville.mesh.ui.MapFragment +import com.geeksville.mesh.ui.MessagesFragment import com.geeksville.mesh.ui.UsersFragment import com.geeksville.util.Exceptions import com.geeksville.util.exceptionReporter @@ -113,6 +114,11 @@ class MainActivity : AppCompatActivity(), Logging, // private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings private val tabInfos = arrayOf( + TabInfo( + "Messages", + R.drawable.ic_twotone_message_24, + MessagesFragment() + ), TabInfo( "Users", R.drawable.ic_twotone_people_24, @@ -129,10 +135,7 @@ class MainActivity : AppCompatActivity(), Logging, MapFragment() ) /* - TabInfo( - "Messages", - R.drawable.ic_twotone_message_24, - ComposeFragment("Messages", 1) { MessagesContent() }), + TabInfo( @@ -297,7 +300,7 @@ class MainActivity : AppCompatActivity(), Logging, MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24 MeshService.ConnectionState.DISCONNECTED -> R.drawable.cloud_off } - + connectStatusImage.setImageDrawable(getDrawable(image)) }) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt b/app/src/main/java/com/geeksville/mesh/ui/Messages.kt deleted file mode 100644 index bcd254af0..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/Messages.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.geeksville.mesh.ui - -/* -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..." - ) - } -} - - -*/ \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt new file mode 100644 index 000000000..68df6af20 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -0,0 +1,235 @@ +package com.geeksville.mesh.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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 +import kotlinx.android.synthetic.main.messages_fragment.* + + +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) { + } + + private val messagesAdapter = object : RecyclerView.Adapter() { + + /** + * 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 + val contactView: View = inflater.inflate(R.layout.adapter_node_layout, parent, false) + + // 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) { + val n = messages[position] + } + + private var messages = arrayOf() + + /// Called when our node DB changes + fun onMessagesChanged(nodesIn: Collection) { + messages = nodesIn.toTypedArray() + notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages + } + } + + 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) + + messageListView.adapter = messagesAdapter + messageListView.layoutManager = LinearLayoutManager(requireContext()) + + 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..." + ) + } +} + + +*/ \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/Users.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt similarity index 94% rename from app/src/main/java/com/geeksville/mesh/ui/Users.kt rename to app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 073b5e70f..91c9e0e81 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Users.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -25,6 +25,7 @@ class UsersFragment : ScreenFragment("Users"), Logging { // Used to cache the views within the item layout for fast access class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val nodeNameView = itemView.nodeNameView + val distance_view = itemView.distance_view } private val nodesAdapter = object : RecyclerView.Adapter() { @@ -96,6 +97,15 @@ class UsersFragment : ScreenFragment("Users"), Logging { val n = nodes[position] holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node" + + val ourNodeInfo = model.nodeDB.ourNodeInfo + val distance = ourNodeInfo?.distanceStr(n) + if (distance != null) { + holder.distance_view.text = distance + holder.distance_view.visibility = View.VISIBLE + } else { + holder.distance_view.visibility = View.INVISIBLE + } } private var nodes = arrayOf() diff --git a/app/src/main/res/layout/adapter_node_layout.xml b/app/src/main/res/layout/adapter_node_layout.xml index 6de92db2d..60a894d15 100644 --- a/app/src/main/res/layout/adapter_node_layout.xml +++ b/app/src/main/res/layout/adapter_node_layout.xml @@ -9,25 +9,46 @@ style="@style/Widget.App.CardView" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" - app:cardCornerRadius="8dp" - app:contentPadding="5dp"> + android:layout_margin="8dp"> + + + + + app:layout_constraintTop_toBottomOf="@+id/imageView" /> \ No newline at end of file diff --git a/app/src/main/res/layout/messages_fragment.xml b/app/src/main/res/layout/messages_fragment.xml new file mode 100644 index 000000000..cd2569869 --- /dev/null +++ b/app/src/main/res/layout/messages_fragment.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb4cd27aa..45e84bb85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,6 @@ Connection status application icon Unknown Username + User avatar + 2.13 km