From 38c82679675d44ac5b6d170e1d2d48ef7fc4be12 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Tue, 26 Oct 2021 22:41:19 -0400
Subject: [PATCH] Status bar, WIP

---
 .../main/java/io/heckel/ntfy/MainActivity.kt  |  7 ++--
 .../main/java/io/heckel/ntfy/TopicsAdapter.kt | 32 +++++++++++------
 .../java/io/heckel/ntfy/TopicsViewModel.kt    |  2 +-
 .../java/io/heckel/ntfy/data/Repository.kt    | 36 +++++++++++++++----
 .../main/java/io/heckel/ntfy/data/Topic.kt    | 11 +++++-
 app/src/main/res/values/strings.xml           |  5 ++-
 6 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/app/src/main/java/io/heckel/ntfy/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/MainActivity.kt
index 746b815..2d3c6a1 100644
--- a/app/src/main/java/io/heckel/ntfy/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/MainActivity.kt
@@ -14,7 +14,10 @@ import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import androidx.recyclerview.widget.RecyclerView
 import io.heckel.ntfy.add.AddTopicActivity
+import io.heckel.ntfy.data.Status
 import io.heckel.ntfy.data.Topic
+import io.heckel.ntfy.data.topicShortUrl
+import io.heckel.ntfy.data.topicUrl
 import io.heckel.ntfy.detail.DetailActivity
 import kotlin.random.Random
 
@@ -74,7 +77,7 @@ class MainActivity : AppCompatActivity() {
             intentData?.let { data ->
                 val name = data.getStringExtra(TOPIC_NAME) ?: return
                 val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return
-                val topic = Topic(Random.nextLong(), name, baseUrl)
+                val topic = Topic(Random.nextLong(), name, baseUrl, Status.CONNECTING, 0)
 
                 topicsViewModel.add(topic)
             }
@@ -85,7 +88,7 @@ class MainActivity : AppCompatActivity() {
         val channelId = getString(R.string.notification_channel_id)
         val notification = NotificationCompat.Builder(this, channelId)
             .setSmallIcon(R.drawable.ntfy)
-            .setContentTitle(n.topic)
+            .setContentTitle(topicShortUrl(n.topic))
             .setContentText(n.message)
             .setPriority(NotificationCompat.PRIORITY_DEFAULT)
             .build()
diff --git a/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt b/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt
index f97080f..dced274 100644
--- a/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt
@@ -1,5 +1,6 @@
 package io.heckel.ntfy
 
+import android.content.Context
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -7,7 +8,9 @@ import android.widget.TextView
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.ListAdapter
 import androidx.recyclerview.widget.RecyclerView
+import io.heckel.ntfy.data.Status
 import io.heckel.ntfy.data.Topic
+import io.heckel.ntfy.data.topicUrl
 
 class TopicsAdapter(private val onClick: (Topic) -> Unit) :
     ListAdapter<Topic, TopicsAdapter.TopicViewHolder>(TopicDiffCallback) {
@@ -15,22 +18,32 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) :
     /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
     class TopicViewHolder(itemView: View, val onClick: (Topic) -> Unit) :
         RecyclerView.ViewHolder(itemView) {
-        private val topicTextView: TextView = itemView.findViewById(R.id.topic_text)
-        private var currentTopic: Topic? = null
+        private var topic: Topic? = null
+        private val context: Context = itemView.context
+        private val nameView: TextView = itemView.findViewById(R.id.topic_text)
+        private val statusView: TextView = itemView.findViewById(R.id.topic_status)
 
         init {
             itemView.setOnClickListener {
-                currentTopic?.let {
+                topic?.let {
                     onClick(it)
                 }
             }
         }
 
         fun bind(topic: Topic) {
-            currentTopic = topic
-            val shortBaseUrl = topic.baseUrl.replace("https://", "") // Leave http:// untouched
-            val shortName = itemView.context.getString(R.string.topic_short_name_format, shortBaseUrl, topic.name)
-            topicTextView.text = shortName
+            this.topic = topic
+            val statusText = when (topic.status) {
+                Status.CONNECTING -> context.getString(R.string.status_connecting)
+                else -> context.getString(R.string.status_subscribed)
+            }
+            val statusMessage = if (topic.messages == 1) {
+                context.getString(R.string.status_text_one, statusText, topic.messages)
+            } else {
+                context.getString(R.string.status_text_not_one, statusText, topic.messages)
+            }
+            nameView.text = topicUrl(topic)
+            statusView.text = statusMessage
         }
     }
 
@@ -45,16 +58,15 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) :
     override fun onBindViewHolder(holder: TopicViewHolder, position: Int) {
         val topic = getItem(position)
         holder.bind(topic)
-
     }
 }
 
 object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() {
     override fun areItemsTheSame(oldItem: Topic, newItem: Topic): Boolean {
-        return oldItem == newItem
+        return oldItem.id == newItem.id
     }
 
     override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean {
-        return oldItem.name == newItem.name
+        return oldItem == newItem
     }
 }
diff --git a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt
index 7cf58ba..9faf507 100644
--- a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt
+++ b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt
@@ -8,7 +8,7 @@ import io.heckel.ntfy.data.Repository
 import io.heckel.ntfy.data.Topic
 import kotlin.collections.List
 
-data class Notification(val topic: String, val message: String)
+data class Notification(val topic: Topic, val message: String)
 typealias NotificationListener = (notification: Notification) -> Unit
 
 class TopicsViewModel(private val repository: Repository) : ViewModel() {
diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt
index 99b34aa..87bc8e2 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt
@@ -12,14 +12,14 @@ import java.io.IOException
 import java.net.HttpURLConnection
 import java.net.URL
 
+const val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed
+
 class Repository {
-    private val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed
     private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
     private val jobs = mutableMapOf<Long, Job>()
     private val gson = GsonBuilder().create()
     private var notificationListener: NotificationListener? = null;
 
-    /* Adds topic to liveData and posts value. */
     fun add(topic: Topic, scope: CoroutineScope) {
         val currentList = topics.value
         if (currentList == null) {
@@ -32,7 +32,24 @@ class Repository {
         jobs[topic.id] = subscribeTopic(topic, scope)
     }
 
-    /* Removes topic from liveData and posts value. */
+    fun update(topic: Topic) {
+        val currentList = topics.value
+        if (currentList == null) {
+            topics.postValue(listOf(topic))
+        } else {
+            val index = currentList.indexOfFirst { it.id == topic.id } // Find index by Topic ID
+            if (index == -1) {
+                return // TODO race?
+            } else {
+                val updatedList = currentList.toMutableList()
+                updatedList[index] = topic
+                println("PHIL updated list:")
+                println(updatedList)
+                topics.postValue(updatedList)
+            }
+        }
+    }
+
     fun remove(topic: Topic) {
         val currentList = topics.value
         if (currentList != null) {
@@ -40,10 +57,9 @@ class Repository {
             updatedList.remove(topic)
             topics.postValue(updatedList)
         }
-        jobs.remove(topic.id)?.cancel() // Cancel and remove
+        jobs.remove(topic.id)?.cancel() // Cancel coroutine and remove
     }
 
-    /* Returns topic given an ID. */
     fun get(id: Long): Topic? {
         topics.value?.let { topics ->
             return topics.firstOrNull{ it.id == id}
@@ -75,6 +91,7 @@ class Repository {
             it.doInput = true
             it.readTimeout = READ_TIMEOUT
         }
+        update(topic.copy(status = Status.SUBSCRIBED))
         try {
             val input = conn.inputStream.bufferedReader()
             while (scope.isActive) {
@@ -86,7 +103,13 @@ class Repository {
                     val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line
                     if (!json.isJsonNull && json.has("message")) {
                         val message = json.get("message").asString
-                        notificationListener?.let { it(Notification(topic.name, message)) }
+                        notificationListener?.let { it(Notification(topic, message)) }
+
+                        // TODO ugly
+                        val currentTopic = get(topic.id)
+                        if (currentTopic != null) {
+                            update(currentTopic.copy(messages = currentTopic.messages+1))
+                        }
                     }
                 } catch (e: JsonSyntaxException) {
                     break // Break on unexpected line
@@ -97,6 +120,7 @@ class Repository {
         } finally {
             conn.disconnect()
         }
+        update(topic.copy(status = Status.CONNECTING))
         println("Connection terminated: $url")
     }
 
diff --git a/app/src/main/java/io/heckel/ntfy/data/Topic.kt b/app/src/main/java/io/heckel/ntfy/data/Topic.kt
index 390c408..558cdfb 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Topic.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Topic.kt
@@ -1,7 +1,16 @@
 package io.heckel.ntfy.data
 
+enum class Status {
+    SUBSCRIBED, CONNECTING
+}
+
 data class Topic(
-    val id: Long, // Internal to Repository only
+    val id: Long, // Internal ID, only used in Repository and activities
     val name: String,
     val baseUrl: String,
+    val status: Status,
+    val messages: Int
 )
+
+fun topicUrl(t: Topic) = "${t.baseUrl}/${t.name}"
+fun topicShortUrl(t: Topic) = topicUrl(t).replace("http://", "").replace("https://", "")
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bc8db6a..05b351d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,8 +6,11 @@
     <string name="topic_name_edit_text">Topic Name</string>
     <string name="topic_base_url_edit_text">Service URL</string>
     <string name="topic_base_url_default_value">https://ntfy.sh</string>
-    <string name="topic_short_name_format">%1$s/%2$s</string>
     <string name="subscribe_button_text">Subscribe</string>
+    <string name="status_subscribed">Subscribed</string>
+    <string name="status_connecting">Connecting</string>
+    <string name="status_text_one">%1$s, %2$d notification</string>
+    <string name="status_text_not_one">%1$s, %2$d notifications</string>
     <string name="fab_content_description">fab</string>
     <string name="remove_topic">Unsubscribe</string>