From 2a64f4491644b1c5e12812cd834ae14fbb0dbb3d Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 31 Oct 2021 15:19:25 -0400 Subject: [PATCH] Add detail view back --- app/src/main/AndroidManifest.xml | 51 ++++++---- .../java/io/heckel/ntfy/app/Application.kt | 2 +- .../main/java/io/heckel/ntfy/data/Database.kt | 36 ++++++- .../java/io/heckel/ntfy/data/Repository.kt | 41 ++++++-- .../io/heckel/ntfy/msg/MessagingService.kt | 17 +++- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 95 +++++++++++++++++++ .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 57 +++++++++++ .../java/io/heckel/ntfy/ui/DetailViewModel.kt | 40 ++++++++ .../java/io/heckel/ntfy/ui/MainActivity.kt | 48 +++++++--- .../io/heckel/ntfy/ui/SubscriptionsAdapter.kt | 32 ++----- .../heckel/ntfy/ui/SubscriptionsViewModel.kt | 10 +- app/src/main/res/layout/detail_activity.xml | 29 ++++++ .../main/res/layout/detail_fragment_item.xml | 28 ++++++ .../main/res/layout/main_fragment_item.xml | 4 +- .../main/res/menu/detail_action_bar_menu.xml | 3 + .../main/res/menu/main_action_bar_menu.xml | 2 +- app/src/main/res/values/strings.xml | 9 ++ 17 files changed, 416 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt create mode 100644 app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt create mode 100644 app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt create mode 100644 app/src/main/res/layout/detail_activity.xml create mode 100644 app/src/main/res/layout/detail_fragment_item.xml create mode 100644 app/src/main/res/menu/detail_action_bar_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b35b673..5c2c03d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,40 +1,51 @@ - + package="io.heckel.ntfy"> - + android:name=".app.Application" + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme"> - + - - + + + + + + + + - + - + - + + + android:resource="@drawable/ic_notification_icon"/> + diff --git a/app/src/main/java/io/heckel/ntfy/app/Application.kt b/app/src/main/java/io/heckel/ntfy/app/Application.kt index 317cdfa..d54ec31 100644 --- a/app/src/main/java/io/heckel/ntfy/app/Application.kt +++ b/app/src/main/java/io/heckel/ntfy/app/Application.kt @@ -6,5 +6,5 @@ import io.heckel.ntfy.data.Repository class Application : Application() { private val database by lazy { Database.getInstance(this) } - val repository by lazy { Repository.getInstance(database.subscriptionDao()) } + val repository by lazy { Repository.getInstance(database.subscriptionDao(), database.notificationDao()) } } diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt index 9c8856c..d364253 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -1,6 +1,7 @@ package io.heckel.ntfy.data import android.content.Context +import androidx.lifecycle.LiveData import androidx.room.* import kotlinx.coroutines.flow.Flow @@ -9,12 +10,22 @@ data class Subscription( @PrimaryKey val id: Long, // Internal ID, only used in Repository and activities @ColumnInfo(name = "baseUrl") val baseUrl: String, @ColumnInfo(name = "topic") val topic: String, - @ColumnInfo(name = "messages") val messages: Int + @ColumnInfo(name = "notifications") val notifications: Int, + @ColumnInfo(name = "lastActive") val lastActive: Long // Unix timestamp ) -@androidx.room.Database(entities = [Subscription::class], version = 1) +@Entity +data class Notification( + @PrimaryKey val id: Long, // Internal ID, only used in Repository and activities + @ColumnInfo(name = "subscriptionId") val subscriptionId: Long, + @ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp + @ColumnInfo(name = "message") val message: String +) + +@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 1) abstract class Database : RoomDatabase() { abstract fun subscriptionDao(): SubscriptionDao + abstract fun notificationDao(): NotificationDao companion object { @Volatile @@ -35,7 +46,7 @@ abstract class Database : RoomDatabase() { @Dao interface SubscriptionDao { - @Query("SELECT * FROM subscription") + @Query("SELECT * FROM subscription ORDER BY lastActive DESC") fun list(): Flow> @Query("SELECT * FROM subscription WHERE baseUrl = :baseUrl AND topic = :topic") @@ -47,6 +58,21 @@ interface SubscriptionDao { @Update fun update(subscription: Subscription) - @Delete - fun remove(subscription: Subscription) + @Query("DELETE FROM subscription WHERE id = :subscriptionId") + fun remove(subscriptionId: Long) +} + +@Dao +interface NotificationDao { + @Query("SELECT * FROM notification WHERE subscriptionId = :subscriptionId ORDER BY timestamp DESC") + fun list(subscriptionId: Long): Flow> + + @Insert + fun add(notification: Notification) + + @Delete + fun remove(notification: Notification) + + @Query("DELETE FROM notification WHERE subscriptionId = :subscriptionId") + fun removeAll(subscriptionId: Long) } 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 5fcda49..1d390bb 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -4,41 +4,64 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData -class Repository(private val subscriptionDao: SubscriptionDao) { - fun list(): LiveData> { +class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) { + fun getAllSubscriptions(): LiveData> { return subscriptionDao.list().asLiveData() } @Suppress("RedundantSuspendModifier") @WorkerThread - suspend fun get(baseUrl: String, topic: String): Subscription? { + suspend fun getSubscription(baseUrl: String, topic: String): Subscription? { return subscriptionDao.get(baseUrl, topic) } @Suppress("RedundantSuspendModifier") @WorkerThread - suspend fun add(subscription: Subscription) { + suspend fun addSubscription(subscription: Subscription) { subscriptionDao.add(subscription) } @Suppress("RedundantSuspendModifier") @WorkerThread - suspend fun update(subscription: Subscription) { + suspend fun updateSubscription(subscription: Subscription) { subscriptionDao.update(subscription) } @Suppress("RedundantSuspendModifier") @WorkerThread - suspend fun remove(subscription: Subscription) { - subscriptionDao.remove(subscription) + suspend fun removeSubscription(subscriptionId: Long) { + subscriptionDao.remove(subscriptionId) + } + + fun getAllNotifications(subscriptionId: Long): LiveData> { + return notificationDao.list(subscriptionId).asLiveData() + } + + + @Suppress("RedundantSuspendModifier") + @WorkerThread + suspend fun addNotification(notification: Notification) { + notificationDao.add(notification) + } + + @Suppress("RedundantSuspendModifier") + @WorkerThread + suspend fun removeNotification(notification: Notification) { + notificationDao.remove(notification) + } + + @Suppress("RedundantSuspendModifier") + @WorkerThread + fun removeAllNotifications(subscriptionId: Long) { + notificationDao.removeAll(subscriptionId) } companion object { private var instance: Repository? = null - fun getInstance(subscriptionDao: SubscriptionDao): Repository { + fun getInstance(subscriptionDao: SubscriptionDao, notificationDao: NotificationDao): Repository { return synchronized(Repository::class) { - val newInstance = instance ?: Repository(subscriptionDao) + val newInstance = instance ?: Repository(subscriptionDao, notificationDao) instance = newInstance newInstance } diff --git a/app/src/main/java/io/heckel/ntfy/msg/MessagingService.kt b/app/src/main/java/io/heckel/ntfy/msg/MessagingService.kt index f3adbb6..7c54940 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/MessagingService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/MessagingService.kt @@ -11,16 +11,18 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import io.heckel.ntfy.R import io.heckel.ntfy.data.Database +import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.topicShortUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import java.util.* import kotlin.random.Random class MessagingService : FirebaseMessagingService() { private val database by lazy { Database.getInstance(this) } - private val repository by lazy { Repository.getInstance(database.subscriptionDao()) } + private val repository by lazy { Repository.getInstance(database.subscriptionDao(), database.notificationDao()) } private val job = SupervisorJob() override fun onMessageReceived(remoteMessage: RemoteMessage) { @@ -32,9 +34,10 @@ class MessagingService : FirebaseMessagingService() { // Check if valid data, and send notification val data = remoteMessage.data + val timestamp = data["time"]?.toLongOrNull() val topic = data["topic"] val message = data["message"] - if (topic == null || message == null) { + if (topic == null || message == null || timestamp == null) { Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, data=${data}") return } @@ -43,9 +46,13 @@ class MessagingService : FirebaseMessagingService() { val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL! // Update message counter - val subscription = repository.get(baseUrl, topic) ?: return@launch - val newSubscription = subscription.copy(messages = subscription.messages + 1) - repository.update(newSubscription) + val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch + val newSubscription = subscription.copy(notifications = subscription.notifications + 1, lastActive = Date().time/1000) + repository.updateSubscription(newSubscription) + + // Add notification + val notification = Notification(id = Random.nextLong(), subscriptionId = subscription.id, timestamp = timestamp, message = message) + repository.addNotification(notification) // Send notification Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}") diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt new file mode 100644 index 0000000..c25adca --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -0,0 +1,95 @@ +package io.heckel.ntfy.ui + +import android.app.AlertDialog +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import io.heckel.ntfy.R +import io.heckel.ntfy.app.Application +import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.data.topicShortUrl + +class DetailActivity : AppCompatActivity() { + private val viewModel by viewModels { + DetailViewModelFactory((application as Application).repository) + } + private var subscriptionId: Long = 0L // Set in onCreate() + private var subscriptionTopic: String = "" // Set in onCreate() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.detail_activity) + supportActionBar?.setDisplayHomeAsUpEnabled(true) // Show 'Back' button + + // Get extras required for the return to the main activity + subscriptionId = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, 0) + subscriptionTopic = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return + + // Set title + val subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return + title = topicShortUrl(subscriptionBaseUrl, subscriptionTopic) + + // Update main list based on viewModel (& its datasource/livedata) + val noEntriesText: View = findViewById(R.id.detail_no_notifications_text) + val adapter = DetailAdapter { notification -> onNotificationClick(notification) } + val mainList: RecyclerView = findViewById(R.id.detail_notification_list) + mainList.adapter = adapter + + viewModel.list(subscriptionId).observe(this) { + it?.let { + adapter.submitList(it as MutableList) + if (it.isEmpty()) { + mainList.visibility = View.GONE + noEntriesText.visibility = View.VISIBLE + } else { + mainList.visibility = View.VISIBLE + noEntriesText.visibility = View.GONE + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.detail_action_bar_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.detail_menu_delete -> { + onDeleteClick() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun onDeleteClick() { + val builder = AlertDialog.Builder(this) + builder + .setMessage(R.string.detail_delete_dialog_message) + .setPositiveButton(R.string.detail_delete_dialog_permanently_delete) { _, _ -> + // Return to main activity + val result = Intent() + .putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscriptionId) + .putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscriptionTopic) + setResult(RESULT_OK, result) + finish() + + // Delete notifications + viewModel.removeAll(subscriptionId) + } + .setNegativeButton(R.string.detail_delete_dialog_cancel) { _, _ -> /* Do nothing */ } + .create() + .show() + } + + private fun onNotificationClick(notification: Notification) { + println("clicked " + notification.id) + } +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt new file mode 100644 index 0000000..ec7fe5f --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -0,0 +1,57 @@ +package io.heckel.ntfy.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.heckel.ntfy.R +import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.data.Subscription +import io.heckel.ntfy.data.topicShortUrl +import java.time.Instant +import java.util.* + +class DetailAdapter(private val onClick: (Notification) -> Unit) : + ListAdapter(TopicDiffCallback) { + + /* Creates and inflates view and return TopicViewHolder. */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.detail_fragment_item, parent, false) + return DetailViewHolder(view, onClick) + } + + /* Gets current topic and uses it to bind view. */ + override fun onBindViewHolder(holder: DetailViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ + class DetailViewHolder(itemView: View, val onClick: (Notification) -> Unit) : + RecyclerView.ViewHolder(itemView) { + private var notification: Notification? = null + private val dateView: TextView = itemView.findViewById(R.id.detail_item_date_text) + private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text) + + fun bind(notification: Notification) { + this.notification = notification + dateView.text = Date(notification.timestamp * 1000).toString() + messageView.text = notification.message + itemView.setOnClickListener { onClick(notification) } + } + } + + object TopicDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt new file mode 100644 index 0000000..332ca1c --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt @@ -0,0 +1,40 @@ +package io.heckel.ntfy.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.data.Repository +import io.heckel.ntfy.data.Subscription +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class DetailViewModel(private val repository: Repository) : ViewModel() { + fun list(subscriptionId: Long): LiveData> { + return repository.getAllNotifications(subscriptionId) + } + + fun add(notification: Notification) = viewModelScope.launch(Dispatchers.IO) { + repository.addNotification(notification) + } + + fun remove(notification: Notification) = viewModelScope.launch(Dispatchers.IO) { + repository.removeNotification(notification) + } + + fun removeAll(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { + repository.removeAllNotifications(subscriptionId) + } +} + +class DetailViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class) = + with(modelClass){ + when { + isAssignableFrom(DetailViewModel::class.java) -> DetailViewModel(repository) as T + else -> throw IllegalArgumentException("Unknown viewModel class $modelClass") + } + } +} diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 6e97651..ee08f18 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -3,22 +3,22 @@ package io.heckel.ntfy.ui import android.content.Intent import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView -import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.data.Subscription +import io.heckel.ntfy.data.topicShortUrl +import java.util.* import kotlin.random.Random class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { - private val subscriptionsViewModel by viewModels { + private val viewModel by viewModels { SubscriptionsViewModelFactory((application as Application).repository) } @@ -35,21 +35,21 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { onSubscribeButtonClick() } - // Update main list based on topicsViewModel (& its datasource/livedata) - val noSubscriptionsText: View = findViewById(R.id.main_no_subscriptions_text) - val adapter = SubscriptionsAdapter { subscription -> onUnsubscribe(subscription) } + // Update main list based on viewModel (& its datasource/livedata) + val noEntriesText: View = findViewById(R.id.main_no_subscriptions_text) + val adapter = SubscriptionsAdapter { subscription -> onSubscriptionItemClick(subscription) } val mainList: RecyclerView = findViewById(R.id.main_subscriptions_list) mainList.adapter = adapter - subscriptionsViewModel.list().observe(this) { + viewModel.list().observe(this) { it?.let { adapter.submitList(it as MutableList) if (it.isEmpty()) { mainList.visibility = View.GONE - noSubscriptionsText.visibility = View.VISIBLE + noEntriesText.visibility = View.VISIBLE } else { mainList.visibility = View.VISIBLE - noSubscriptionsText.visibility = View.GONE + noEntriesText.visibility = View.GONE } } } @@ -66,7 +66,7 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_source_url)))) true } - R.id.menu_action_website -> { + R.id.detail_menu_delete -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.app_base_url)))) true } @@ -80,17 +80,35 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener { } override fun onSubscribe(topic: String, baseUrl: String) { - val subscription = Subscription(id = Random.nextLong(), baseUrl = baseUrl, topic = topic, messages = 0) - subscriptionsViewModel.add(subscription) + val subscription = Subscription(id = Random.nextLong(), baseUrl = baseUrl, topic = topic, notifications = 0, lastActive = Date().time/1000) + viewModel.add(subscription) FirebaseMessaging.getInstance().subscribeToTopic(topic) // FIXME ignores baseUrl } - private fun onUnsubscribe(subscription: Subscription) { - subscriptionsViewModel.remove(subscription) - FirebaseMessaging.getInstance().unsubscribeFromTopic(subscription.topic) + private fun onSubscriptionItemClick(subscription: Subscription) { + val intent = Intent(this, DetailActivity::class.java) + intent.putExtra(EXTRA_SUBSCRIPTION_ID, subscription.id) + intent.putExtra(EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) + intent.putExtra(EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) + startActivityForResult(intent, REQUEST_CODE_DELETE_SUBSCRIPTION) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_DELETE_SUBSCRIPTION && resultCode == RESULT_OK) { + val subscriptionId = data?.getLongExtra(EXTRA_SUBSCRIPTION_ID, 0) + val subscriptionTopic = data?.getStringExtra(EXTRA_SUBSCRIPTION_TOPIC) + subscriptionId?.let { id -> viewModel.remove(id) } + subscriptionTopic?.let { topic -> FirebaseMessaging.getInstance().unsubscribeFromTopic(topic) } // FIXME This only works for ntfy.sh + } else { + super.onActivityResult(requestCode, resultCode, data) + } } companion object { const val TAG = "NtfyMainActivity" + const val EXTRA_SUBSCRIPTION_ID = "subscriptionId" + const val EXTRA_SUBSCRIPTION_BASE_URL = "subscriptionBaseUrl" + const val EXTRA_SUBSCRIPTION_TOPIC = "subscriptionTopic" + const val REQUEST_CODE_DELETE_SUBSCRIPTION = 1; } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsAdapter.kt index 455c7a2..b85bba7 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsAdapter.kt @@ -4,7 +4,6 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.PopupMenu import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -30,40 +29,23 @@ class SubscriptionsAdapter(private val onClick: (Subscription) -> Unit) : } /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ - class SubscriptionViewHolder(itemView: View, val onUnsubscribe: (Subscription) -> Unit) : + class SubscriptionViewHolder(itemView: View, val onClick: (Subscription) -> Unit) : RecyclerView.ViewHolder(itemView) { private var subscription: Subscription? = 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 { - val popup = PopupMenu(context, itemView) - popup.inflate(R.menu.main_item_popup_menu) - popup.setOnMenuItemClickListener { item -> - when (item.itemId) { - R.id.main_item_popup_unsubscribe -> { - subscription?.let { s -> onUnsubscribe(s) } - true - } - else -> false - } - } - itemView.setOnLongClickListener { - subscription?.let { popup.show() } - true - } - } + private val nameView: TextView = itemView.findViewById(R.id.main_item_text) + private val statusView: TextView = itemView.findViewById(R.id.main_item_status) fun bind(subscription: Subscription) { this.subscription = subscription - val statusMessage = if (subscription.messages == 1) { - context.getString(R.string.main_item_status_text_one, subscription.messages) + val statusMessage = if (subscription.notifications == 1) { + context.getString(R.string.main_item_status_text_one, subscription.notifications) } else { - context.getString(R.string.main_item_status_text_not_one, subscription.messages) + context.getString(R.string.main_item_status_text_not_one, subscription.notifications) } nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic) statusView.text = statusMessage + itemView.setOnClickListener { onClick(subscription) } } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsViewModel.kt b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsViewModel.kt index 538ee27..9c0d1d7 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsViewModel.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionsViewModel.kt @@ -11,15 +11,15 @@ import kotlin.collections.List class SubscriptionsViewModel(private val repository: Repository) : ViewModel() { fun list(): LiveData> { - return repository.list() + return repository.getAllSubscriptions() } - fun add(topic: Subscription) = viewModelScope.launch(Dispatchers.IO) { - repository.add(topic) + fun add(subscription: Subscription) = viewModelScope.launch(Dispatchers.IO) { + repository.addSubscription(subscription) } - fun remove(topic: Subscription) = viewModelScope.launch(Dispatchers.IO) { - repository.remove(topic) + fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) { + repository.removeSubscription(subscriptionId) } } diff --git a/app/src/main/res/layout/detail_activity.xml b/app/src/main/res/layout/detail_activity.xml new file mode 100644 index 0000000..92b493d --- /dev/null +++ b/app/src/main/res/layout/detail_activity.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/detail_fragment_item.xml b/app/src/main/res/layout/detail_fragment_item.xml new file mode 100644 index 0000000..0994681 --- /dev/null +++ b/app/src/main/res/layout/detail_fragment_item.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/layout/main_fragment_item.xml b/app/src/main/res/layout/main_fragment_item.xml index ee245fb..e8715e7 100644 --- a/app/src/main/res/layout/main_fragment_item.xml +++ b/app/src/main/res/layout/main_fragment_item.xml @@ -15,7 +15,7 @@ @@ -23,7 +23,7 @@ diff --git a/app/src/main/res/menu/detail_action_bar_menu.xml b/app/src/main/res/menu/detail_action_bar_menu.xml new file mode 100644 index 0000000..9b62583 --- /dev/null +++ b/app/src/main/res/menu/detail_action_bar_menu.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/menu/main_action_bar_menu.xml b/app/src/main/res/menu/main_action_bar_menu.xml index 3a66530..4a5c360 100644 --- a/app/src/main/res/menu/main_action_bar_menu.xml +++ b/app/src/main/res/menu/main_action_bar_menu.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f632c5..43c27ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,13 @@ Use another server Cancel Subscribe + + + You haven\'t received any notifications for this topic yet. + Do you really want to permanently delete this subscription and all its messages? + Permanently delete + Cancel + + + Delete topic