OXYGENHUB
◆ v0.1.4 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / dashboard / DashboardViewModel.kt
4.6 KiB · last changed The widget's foot: a white mark and title, counts of unread, reviews and assigned issues that open their screens, and the phone's runner job; Assigned to you on the dashboard (closes #31) 3 days ago source blame history raw
1
package io.oxygenhub.android.ui.dashboard
2
3
import androidx.lifecycle.ViewModel
4
import io.oxygenhub.android.api.Avatar
5
import io.oxygenhub.android.api.HubApi
6
import io.oxygenhub.android.api.IssueBrief
7
import io.oxygenhub.android.api.Repository
8
import io.oxygenhub.android.api.User
9
import io.oxygenhub.android.api.UserSky
10
import io.oxygenhub.android.api.UserProposals
11
import io.oxygenhub.android.auth.Session
12
import io.oxygenhub.android.ui.common.Avatars
13
import io.oxygenhub.android.ui.common.Load
14
import io.oxygenhub.android.ui.common.load
15
import kotlinx.coroutines.CancellationException
16
import kotlinx.coroutines.Job
17
import kotlinx.coroutines.async
18
import kotlinx.coroutines.coroutineScope
19
import kotlinx.coroutines.flow.MutableStateFlow
20
import kotlinx.coroutines.flow.StateFlow
21
import kotlinx.coroutines.flow.asStateFlow
22
import java.util.Calendar
23
24
data class Dashboard(
25
    val greeting: String,
26
    /** The account, or null when the token may not read it (no `user:read`). */
27
    val user: User?,
28
    /** The account's avatar (#13), or null while the hub has none this app can draw. */
29
    val avatar: Avatar? = null,
30
    /** Whose avatar: the signed-in account's name. */
31
    val username: String = "",
32
    /** Proposals to review and the account's own (#18); null when the hub cannot say. */
33
    val proposals: UserProposals? = null,
34
    /** Open issues assigned to the account (#31); null when the hub cannot say. */
35
    val assigned: List<IssueBrief>? = null,
36
    /** The newest-updated repositories: the user's own, or everyone's when they own none. */
37
    val recent: List<Repository>,
38
    val recentAreYours: Boolean,
39
    val repositoryCount: Int,
40
    /** Your sky (#23), the backdrop's data; null when the hub cannot give it. */
41
    val sky: UserSky? = null,
42
)
43
44
class DashboardViewModel(
45
    private val api: HubApi,
46
    private val session: Session,
47
    private val hourOfDay: () -> Int = { Calendar.getInstance().get(Calendar.HOUR_OF_DAY) },
48
) : ViewModel() {
49
    private val _state = MutableStateFlow<Load<Dashboard>>(Load.Loading)
50
    val state: StateFlow<Load<Dashboard>> = _state.asStateFlow()
51
52
    private var job: Job? = null
53
54
    init {
55
        refresh()
56
    }
57
58
    fun refresh() {
59
        job = load(_state, job) { fetch() }
60
    }
61
62
    private suspend fun fetch(): Dashboard = coroutineScope {
63
        // The account is a nicety (a display name); the repositories are the page.
64
        val user = async {
65
            try {
66
                api.user()
67
            } catch (e: CancellationException) {
68
                throw e
69
            } catch (_: Exception) {
70
                null
71
            }
72
        }
73
        // As are the proposals: a hub without the call shows none.
74
        val proposals = async {
75
            try {
76
                api.userProposals()
77
            } catch (e: CancellationException) {
78
                throw e
79
            } catch (_: Exception) {
80
                null
81
            }
82
        }
83
        // As are the issues assigned to it (a hub before oxygenhub#359 answers 404).
84
        val assigned = async {
85
            try {
86
                api.userIssues()
87
            } catch (e: CancellationException) {
88
                throw e
89
            } catch (_: Exception) {
90
                null
91
            }
92
        }
93
        // As is the sky behind it all.
94
        val sky = async {
95
            try {
96
                api.userSky()
97
            } catch (e: CancellationException) {
98
                throw e
99
            } catch (_: Exception) {
100
                null
101
            }
102
        }
103
        // As is the avatar: its failure leaves the last one, or the initial.
104
        val avatar = async { Avatars.load(api, session.username) }
105
        val repos = api.repositories()
106
        val me = user.await()
107
        val username = me?.username ?: session.username
108
        val mine = repos.filter { it.owner == username }
109
        val pool = mine.ifEmpty { repos }
110
        Dashboard(
111
            greeting = greeting(hourOfDay(), me?.shownName ?: session.username),
112
            user = me,
113
            avatar = avatar.await(),
114
            username = username,
115
            proposals = proposals.await(),
116
            assigned = assigned.await(),
117
            recent = pool.sortedByDescending { it.updatedMs }.take(RECENT),
118
            recentAreYours = mine.isNotEmpty(),
119
            repositoryCount = mine.size,
120
            sky = sky.await(),
121
        )
122
    }
123
124
    companion object {
125
        const val RECENT = 5
126
127
        fun greeting(hour: Int, name: String): String {
128
            val part = when (hour) {
129
                in 5..11 -> "Good morning"
130
                in 12..17 -> "Good afternoon"
131
                else -> "Good evening"
132
            }
133
            return "$part, $name"
134
        }
135
    }
136
}