OXYGENHUB
# e833eb0a9e7f oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / issues / IssueViewModel.kt
4.6 KiB · last changed Issues on the phone: read, comment, close and reopen, labels, and new issues (refs #4) 10 days ago source blame history raw
1
package io.oxygenhub.android.ui.issues
2
3
import androidx.lifecycle.ViewModel
4
import androidx.lifecycle.viewModelScope
5
import io.oxygenhub.android.api.Comment
6
import io.oxygenhub.android.api.HubApi
7
import io.oxygenhub.android.api.Issue
8
import io.oxygenhub.android.api.Label
9
import io.oxygenhub.android.api.userMessage
10
import io.oxygenhub.android.ui.common.Load
11
import io.oxygenhub.android.ui.common.load
12
import kotlinx.coroutines.CancellationException
13
import kotlinx.coroutines.Job
14
import kotlinx.coroutines.async
15
import kotlinx.coroutines.coroutineScope
16
import kotlinx.coroutines.flow.MutableStateFlow
17
import kotlinx.coroutines.flow.StateFlow
18
import kotlinx.coroutines.flow.asStateFlow
19
import kotlinx.coroutines.launch
20
21
/**
22
 * One issue (#4): it and its comments; comment, close or reopen, and set
23
 * its labels. What the hub refuses is said in [error], and the page stays.
24
 */
25
class IssueViewModel(
26
    private val api: HubApi,
27
    val owner: String,
28
    val name: String,
29
    val number: Long,
30
) : ViewModel() {
31
    data class Thread(val issue: Issue, val comments: List<Comment>)
32
33
    private val _state = MutableStateFlow<Load<Thread>>(Load.Loading)
34
    val state: StateFlow<Load<Thread>> = _state.asStateFlow()
35
36
    private val _working = MutableStateFlow(false)
37
    val working: StateFlow<Boolean> = _working.asStateFlow()
38
39
    private val _error = MutableStateFlow<String?>(null)
40
    val error: StateFlow<String?> = _error.asStateFlow()
41
42
    /** The repository's labels, once the label picker asks. */
43
    private val _labels = MutableStateFlow<Load<List<Label>>>(Load.Loading)
44
    val labels: StateFlow<Load<List<Label>>> = _labels.asStateFlow()
45
46
    private var job: Job? = null
47
    private var labelsJob: Job? = null
48
49
    init {
50
        refresh()
51
    }
52
53
    fun refresh() {
54
        job = load(_state, job) {
55
            coroutineScope {
56
                val issue = async { api.issue(owner, name, number) }
57
                val comments = async { api.issueComments(owner, name, number) }
58
                Thread(issue.await(), comments.await())
59
            }
60
        }
61
    }
62
63
    fun loadLabels() {
64
        if (_labels.value is Load.Ready) return
65
        labelsJob = load(_labels, labelsJob) { api.labels(owner, name) }
66
    }
67
68
    /** Post [body]; true once the hub has it (the box empties then). */
69
    fun comment(body: String, then: (Boolean) -> Unit = {}) = act({
70
        val c = api.comment(owner, name, number, body.trim())
71
        current()?.let { t -> _state.value = Load.Ready(t.copy(comments = t.comments + c, issue = t.issue.copy(comments = t.issue.comments + 1))) }
72
    }, then)
73
74
    fun close(reason: String = "completed") = act({ swap(api.setIssueState(owner, name, number, open = false, reason = reason)) })
75
76
    fun reopen() = act({ swap(api.setIssueState(owner, name, number, open = true)) })
77
78
    fun setLabels(names: List<String>) = act({ swap(api.setIssueLabels(owner, name, number, names)) })
79
80
    fun dismissError() {
81
        _error.value = null
82
    }
83
84
    private fun current(): Thread? = (_state.value as? Load.Ready)?.value
85
86
    private fun swap(issue: Issue) {
87
        current()?.let { _state.value = Load.Ready(it.copy(issue = issue)) }
88
    }
89
90
    private fun act(step: suspend () -> Unit, then: (Boolean) -> Unit = {}) {
91
        if (_working.value) return
92
        _working.value = true
93
        _error.value = null
94
        viewModelScope.launch {
95
            val ok = try {
96
                step()
97
                true
98
            } catch (e: CancellationException) {
99
                throw e
100
            } catch (e: Exception) {
101
                _error.value = e.userMessage()
102
                false
103
            } finally {
104
                _working.value = false
105
            }
106
            then(ok)
107
        }
108
    }
109
}
110
111
/** A new issue: its title and body, opened on the hub. */
112
class NewIssueViewModel(
113
    private val api: HubApi,
114
    val owner: String,
115
    val name: String,
116
) : ViewModel() {
117
    private val _working = MutableStateFlow(false)
118
    val working: StateFlow<Boolean> = _working.asStateFlow()
119
120
    private val _error = MutableStateFlow<String?>(null)
121
    val error: StateFlow<String?> = _error.asStateFlow()
122
123
    fun open(title: String, body: String, opened: (Issue) -> Unit) {
124
        if (_working.value || title.isBlank()) return
125
        _working.value = true
126
        _error.value = null
127
        viewModelScope.launch {
128
            try {
129
                opened(api.createIssue(owner, name, title.trim(), body.trim()))
130
            } catch (e: CancellationException) {
131
                throw e
132
            } catch (e: Exception) {
133
                _error.value = e.userMessage()
134
            } finally {
135
                _working.value = false
136
            }
137
        }
138
    }
139
}