OXYGENHUB
# 29a375612f92 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / dashboard / DashboardViewModel.kt
5.0 KiB · last changed The Dashboard refreshes each time it comes back to the front (closes #35) 2 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
    /**
63
     * The dashboard is in front again (back from a proposal, or the app
64
     * brought back): fetch afresh, keeping what shows meanwhile, so a
65
     * proposal just landed no longer shows as open (#35). Not while the
66
     * first load is still on its way.
67
     */
68
    fun resumed() {
69
        if (_state.value !is Load.Loading) refresh()
70
    }
71
72
    private suspend fun fetch(): Dashboard = coroutineScope {
73
        // The account is a nicety (a display name); the repositories are the page.
74
        val user = async {
75
            try {
76
                api.user()
77
            } catch (e: CancellationException) {
78
                throw e
79
            } catch (_: Exception) {
80
                null
81
            }
82
        }
83
        // As are the proposals: a hub without the call shows none.
84
        val proposals = async {
85
            try {
86
                api.userProposals()
87
            } catch (e: CancellationException) {
88
                throw e
89
            } catch (_: Exception) {
90
                null
91
            }
92
        }
93
        // As are the issues assigned to it (a hub before oxygenhub#359 answers 404).
94
        val assigned = async {
95
            try {
96
                api.userIssues()
97
            } catch (e: CancellationException) {
98
                throw e
99
            } catch (_: Exception) {
100
                null
101
            }
102
        }
103
        // As is the sky behind it all.
104
        val sky = async {
105
            try {
106
                api.userSky()
107
            } catch (e: CancellationException) {
108
                throw e
109
            } catch (_: Exception) {
110
                null
111
            }
112
        }
113
        // As is the avatar: its failure leaves the last one, or the initial.
114
        val avatar = async { Avatars.load(api, session.username) }
115
        val repos = api.repositories()
116
        val me = user.await()
117
        val username = me?.username ?: session.username
118
        val mine = repos.filter { it.owner == username }
119
        val pool = mine.ifEmpty { repos }
120
        Dashboard(
121
            greeting = greeting(hourOfDay(), me?.shownName ?: session.username),
122
            user = me,
123
            avatar = avatar.await(),
124
            username = username,
125
            proposals = proposals.await(),
126
            assigned = assigned.await(),
127
            recent = pool.sortedByDescending { it.updatedMs }.take(RECENT),
128
            recentAreYours = mine.isNotEmpty(),
129
            repositoryCount = mine.size,
130
            sky = sky.await(),
131
        )
132
    }
133
134
    companion object {
135
        const val RECENT = 5
136
137
        fun greeting(hour: Int, name: String): String {
138
            val part = when (hour) {
139
                in 5..11 -> "Good morning"
140
                in 12..17 -> "Good afternoon"
141
                else -> "Good evening"
142
            }
143
            return "$part, $name"
144
        }
145
    }
146
}