OXYGENHUB
# 1b55d7f47d9f oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / issues / IssueViewModel.kt
4.9 KiB · last changed Assign issues from the phone: the Assignees action, with Assign me and any username, saved as the whole set (refs #4) 8 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
    /** Who the issue is assigned to, as the whole set (#4): names trimmed, `@` dropped, each once. */
81
    fun setAssignees(names: List<String>) =
82
        act({ swap(api.setIssueAssignees(owner, name, number, names.map { it.trim().removePrefix("@") }.filter { it.isNotEmpty() }.distinct())) })
83
84
    fun dismissError() {
85
        _error.value = null
86
    }
87
88
    private fun current(): Thread? = (_state.value as? Load.Ready)?.value
89
90
    private fun swap(issue: Issue) {
91
        current()?.let { _state.value = Load.Ready(it.copy(issue = issue)) }
92
    }
93
94
    private fun act(step: suspend () -> Unit, then: (Boolean) -> Unit = {}) {
95
        if (_working.value) return
96
        _working.value = true
97
        _error.value = null
98
        viewModelScope.launch {
99
            val ok = try {
100
                step()
101
                true
102
            } catch (e: CancellationException) {
103
                throw e
104
            } catch (e: Exception) {
105
                _error.value = e.userMessage()
106
                false
107
            } finally {
108
                _working.value = false
109
            }
110
            then(ok)
111
        }
112
    }
113
}
114
115
/** A new issue: its title and body, opened on the hub. */
116
class NewIssueViewModel(
117
    private val api: HubApi,
118
    val owner: String,
119
    val name: String,
120
) : ViewModel() {
121
    private val _working = MutableStateFlow(false)
122
    val working: StateFlow<Boolean> = _working.asStateFlow()
123
124
    private val _error = MutableStateFlow<String?>(null)
125
    val error: StateFlow<String?> = _error.asStateFlow()
126
127
    fun open(title: String, body: String, opened: (Issue) -> Unit) {
128
        if (_working.value || title.isBlank()) return
129
        _working.value = true
130
        _error.value = null
131
        viewModelScope.launch {
132
            try {
133
                opened(api.createIssue(owner, name, title.trim(), body.trim()))
134
            } catch (e: CancellationException) {
135
                throw e
136
            } catch (e: Exception) {
137
                _error.value = e.userMessage()
138
            } finally {
139
                _working.value = false
140
            }
141
        }
142
    }
143
}