Add detail view back
This commit is contained in:
parent
785a36e257
commit
2a64f44916
17 changed files with 416 additions and 88 deletions
|
@ -1,40 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.heckel.ntfy">
|
||||
|
||||
package="io.heckel.ntfy">
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Main app -->
|
||||
<application
|
||||
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: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">
|
||||
|
||||
<!-- Main activity -->
|
||||
<activity android:name="io.heckel.ntfy.ui.MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Detail activity -->
|
||||
<activity android:name=".ui.DetailActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<!-- Firebase messaging -->
|
||||
<service android:name="io.heckel.ntfy.msg.MessagingService"
|
||||
android:exported="false">
|
||||
<service
|
||||
android:name=".msg.MessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<meta-data android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_notification_icon" />
|
||||
android:resource="@drawable/ic_notification_icon"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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<List<Subscription>>
|
||||
|
||||
@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<List<Notification>>
|
||||
|
||||
@Insert
|
||||
fun add(notification: Notification)
|
||||
|
||||
@Delete
|
||||
fun remove(notification: Notification)
|
||||
|
||||
@Query("DELETE FROM notification WHERE subscriptionId = :subscriptionId")
|
||||
fun removeAll(subscriptionId: Long)
|
||||
}
|
||||
|
|
|
@ -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<List<Subscription>> {
|
||||
class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) {
|
||||
fun getAllSubscriptions(): LiveData<List<Subscription>> {
|
||||
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<List<Notification>> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
95
app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
Normal file
95
app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
Normal file
|
@ -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<DetailViewModel> {
|
||||
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<Notification>)
|
||||
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)
|
||||
}
|
||||
}
|
57
app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
Normal file
57
app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
Normal file
|
@ -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<Notification, DetailAdapter.DetailViewHolder>(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<Notification>() {
|
||||
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
40
app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt
Normal file
40
app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt
Normal file
|
@ -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<List<Notification>> {
|
||||
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 <T : ViewModel?> create(modelClass: Class<T>) =
|
||||
with(modelClass){
|
||||
when {
|
||||
isAssignableFrom(DetailViewModel::class.java) -> DetailViewModel(repository) as T
|
||||
else -> throw IllegalArgumentException("Unknown viewModel class $modelClass")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<SubscriptionsViewModel> {
|
||||
private val viewModel by viewModels<SubscriptionsViewModel> {
|
||||
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<Subscription>)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,15 +11,15 @@ import kotlin.collections.List
|
|||
|
||||
class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
|
||||
fun list(): LiveData<List<Subscription>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
29
app/src/main/res/layout/detail_activity.xml
Normal file
29
app/src/main/res/layout/detail_activity.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.DetailActivity">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/detail_notification_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detail_no_notifications_text"
|
||||
android:text="@string/detail_no_subscriptions_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
android:padding="50dp" app:layout_constraintBottom_toBottomOf="parent" android:gravity="center_horizontal"
|
||||
android:textStyle="italic"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
28
app/src/main/res/layout/detail_fragment_item.xml
Normal file
28
app/src/main/res/layout/detail_fragment_item.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?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"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="vertical" android:clickable="true" android:focusable="true">
|
||||
<TextView
|
||||
android:text="Sun, October 31, 2021, 10:43:12"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_date_text"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
<TextView
|
||||
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/detail_item_message_text"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<TextView
|
||||
android:text="ntfy.sh/example"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/topic_text"
|
||||
android:layout_height="wrap_content" android:id="@+id/main_item_text"
|
||||
android:layout_marginTop="16dp" android:layout_marginStart="12dp"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<TextView
|
||||
android:text="Subscribed, 0 notifications"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/topic_status"
|
||||
android:layout_height="wrap_content" android:id="@+id/main_item_status"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginStart="12dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
|
3
app/src/main/res/menu/detail_action_bar_menu.xml
Normal file
3
app/src/main/res/menu/detail_action_bar_menu.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:title="@string/detail_menu_delete" android:id="@+id/detail_menu_delete"/>
|
||||
</menu>
|
|
@ -1,5 +1,5 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item android:id="@+id/menu_action_source"
|
||||
android:title="@string/main_menu_source_title"/>
|
||||
<item android:title="@string/main_menu_website_title" android:id="@+id/menu_action_website"/>
|
||||
<item android:title="@string/main_menu_website_title" android:id="@+id/detail_menu_delete"/>
|
||||
</menu>
|
||||
|
|
|
@ -28,4 +28,13 @@
|
|||
<string name="add_dialog_use_another_server">Use another server</string>
|
||||
<string name="add_dialog_button_cancel">Cancel</string>
|
||||
<string name="add_dialog_button_subscribe">Subscribe</string>
|
||||
|
||||
<!-- Detail activity -->
|
||||
<string name="detail_no_subscriptions_text">You haven\'t received any notifications for this topic yet.</string>
|
||||
<string name="detail_delete_dialog_message">Do you really want to permanently delete this subscription and all its messages?</string>
|
||||
<string name="detail_delete_dialog_permanently_delete">Permanently delete</string>
|
||||
<string name="detail_delete_dialog_cancel">Cancel</string>
|
||||
|
||||
<!-- Detail activity: Action bar -->
|
||||
<string name="detail_menu_delete">Delete topic</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue