| 1 |
package io.oxygenhub.android.ui.inbox |
| 2 |
|
| 3 |
import androidx.lifecycle.ViewModel |
| 4 |
import androidx.lifecycle.viewModelScope |
| 5 |
import io.oxygenhub.android.api.HubApi |
| 6 |
import io.oxygenhub.android.api.Inbox |
| 7 |
import io.oxygenhub.android.api.Notification |
| 8 |
import io.oxygenhub.android.api.userMessage |
| 9 |
import io.oxygenhub.android.ui.common.Load |
| 10 |
import io.oxygenhub.android.ui.common.load |
| 11 |
import kotlinx.coroutines.CancellationException |
| 12 |
import kotlinx.coroutines.Job |
| 13 |
import kotlinx.coroutines.flow.MutableStateFlow |
| 14 |
import kotlinx.coroutines.flow.StateFlow |
| 15 |
import kotlinx.coroutines.flow.asStateFlow |
| 16 |
import kotlinx.coroutines.flow.update |
| 17 |
import kotlinx.coroutines.launch |
| 18 |
|
| 19 |
/** |
| 20 |
* The inbox, shared by the inbox screen, the dashboard's unread count and |
| 21 |
* the navigation badge. Marking read shows at once and is put back if the |
| 22 |
* hub refuses it. |
| 23 |
*/ |
| 24 |
class InboxViewModel(private val api: HubApi) : ViewModel() { |
| 25 |
private val _state = MutableStateFlow<Load<Inbox>>(Load.Loading) |
| 26 |
val state: StateFlow<Load<Inbox>> = _state.asStateFlow() |
| 27 |
|
| 28 |
/** A one-line message for a snackbar (a failed mark-read). */ |
| 29 |
private val _message = MutableStateFlow<String?>(null) |
| 30 |
val message: StateFlow<String?> = _message.asStateFlow() |
| 31 |
|
| 32 |
private var job: Job? = null |
| 33 |
|
| 34 |
init { |
| 35 |
refresh() |
| 36 |
} |
| 37 |
|
| 38 |
fun refresh() { |
| 39 |
job = load(_state, job) { api.inbox(LIMIT) } |
| 40 |
} |
| 41 |
|
| 42 |
/** Marks [n] read (if it is not already); the caller then opens its page. */ |
| 43 |
fun markRead(n: Notification) { |
| 44 |
if (n.read) return |
| 45 |
val before = _state.value |
| 46 |
edit { inbox -> |
| 47 |
inbox.copy( |
| 48 |
unread = (inbox.unread - 1).coerceAtLeast(0), |
| 49 |
items = inbox.items.map { if (it.id == n.id) it.copy(read = true) else it }, |
| 50 |
) |
| 51 |
} |
| 52 |
viewModelScope.launch { |
| 53 |
try { |
| 54 |
api.markRead(n.id) |
| 55 |
} catch (e: CancellationException) { |
| 56 |
throw e |
| 57 |
} catch (e: Exception) { |
| 58 |
_state.value = before |
| 59 |
_message.value = "Couldn't mark it read: ${e.userMessage()}" |
| 60 |
} |
| 61 |
} |
| 62 |
} |
| 63 |
|
| 64 |
fun markAllRead() { |
| 65 |
val before = _state.value |
| 66 |
if ((before.valueOrNull?.unread ?: 0) == 0L) return |
| 67 |
edit { inbox -> inbox.copy(unread = 0, items = inbox.items.map { it.copy(read = true) }) } |
| 68 |
viewModelScope.launch { |
| 69 |
try { |
| 70 |
api.markAllRead() |
| 71 |
} catch (e: CancellationException) { |
| 72 |
throw e |
| 73 |
} catch (e: Exception) { |
| 74 |
_state.value = before |
| 75 |
_message.value = "Couldn't mark the inbox read: ${e.userMessage()}" |
| 76 |
} |
| 77 |
} |
| 78 |
} |
| 79 |
|
| 80 |
fun messageShown() { |
| 81 |
_message.value = null |
| 82 |
} |
| 83 |
|
| 84 |
private fun edit(change: (Inbox) -> Inbox) { |
| 85 |
_state.update { s -> if (s is Load.Ready) s.copy(value = change(s.value)) else s } |
| 86 |
} |
| 87 |
|
| 88 |
companion object { |
| 89 |
const val LIMIT = 100 |
| 90 |
} |
| 91 |
} |