OXYGENHUB
# 0ca942a9a1e4 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / inbox / InboxScreen.kt
6.3 KiB · last changed The inbox opens what it is about in the app, not the website (refs #5) 10 days ago source blame history raw
1
package io.oxygenhub.android.ui.inbox
2
3
import androidx.compose.foundation.clickable
4
import androidx.compose.foundation.layout.Box
5
import androidx.compose.foundation.layout.fillMaxSize
6
import androidx.compose.foundation.layout.size
7
import androidx.compose.foundation.lazy.LazyColumn
8
import androidx.compose.foundation.lazy.items
9
import androidx.compose.foundation.shape.CircleShape
10
import androidx.compose.material.icons.Icons
11
import androidx.compose.material.icons.outlined.AlternateEmail
12
import androidx.compose.material.icons.outlined.BugReport
13
import androidx.compose.material.icons.automirrored.outlined.CallMerge
14
import androidx.compose.material.icons.outlined.Backup
15
import androidx.compose.material.icons.outlined.ChatBubbleOutline
16
import androidx.compose.material.icons.outlined.DoneAll
17
import androidx.compose.material.icons.outlined.Inbox
18
import androidx.compose.material.icons.outlined.Notifications
19
import androidx.compose.material.icons.outlined.PlayCircle
20
import androidx.compose.material.icons.outlined.RateReview
21
import androidx.compose.material3.HorizontalDivider
22
import androidx.compose.material3.Icon
23
import androidx.compose.material3.IconButton
24
import androidx.compose.material3.ListItem
25
import androidx.compose.material3.ListItemDefaults
26
import androidx.compose.material3.MaterialTheme
27
import androidx.compose.material3.SnackbarHostState
28
import androidx.compose.material3.Surface
29
import androidx.compose.material3.Text
30
import androidx.compose.runtime.Composable
31
import androidx.compose.runtime.LaunchedEffect
32
import androidx.compose.runtime.getValue
33
import androidx.compose.runtime.remember
34
import androidx.compose.ui.Alignment
35
import androidx.compose.ui.Modifier
36
import androidx.compose.ui.graphics.vector.ImageVector
37
import androidx.compose.ui.text.font.FontWeight
38
import androidx.compose.ui.text.style.TextOverflow
39
import androidx.compose.ui.unit.dp
40
import androidx.lifecycle.compose.collectAsStateWithLifecycle
41
import io.oxygenhub.android.api.Notification
42
import io.oxygenhub.android.ui.common.AppScreen
43
import io.oxygenhub.android.ui.common.EmptyState
44
import io.oxygenhub.android.ui.common.LoadContent
45
import io.oxygenhub.android.ui.common.relativeTime
46
47
/**
48
 * The inbox. A notification opens what it is about on its own screen in
49
 * the app ([onOpen]): an issue, proposal, run, change, file or board; the
50
 * browser only for a page the app does not show.
51
 */
52
@Composable
53
fun InboxScreen(vm: InboxViewModel, onOpen: (url: String) -> Unit) {
54
    val state by vm.state.collectAsStateWithLifecycle()
55
    val message by vm.message.collectAsStateWithLifecycle()
56
    val snackbar = remember { SnackbarHostState() }
57
    LaunchedEffect(message) {
58
        message?.let {
59
            snackbar.showSnackbar(it)
60
            vm.messageShown()
61
        }
62
    }
63
    val unread = state.valueOrNull?.unread ?: 0
64
    AppScreen(
65
        title = "Inbox",
66
        subtitle = if (unread > 0) "$unread unread" else null,
67
        snackbar = snackbar,
68
        actions = {
69
            if (unread > 0) {
70
                IconButton(onClick = vm::markAllRead) { Icon(Icons.Outlined.DoneAll, contentDescription = "Mark all read") }
71
            }
72
        },
73
    ) {
74
        LoadContent(state, onRefresh = vm::refresh) { inbox ->
75
            if (inbox.items.isEmpty()) {
76
                EmptyState(Icons.Outlined.Inbox, "Your inbox is empty", "Mentions, reviews and activity on what you watch arrive here.")
77
            } else {
78
                LazyColumn(Modifier.fillMaxSize()) {
79
                    items(inbox.items, key = { it.id }) { n ->
80
                        NotificationRow(n, onClick = {
81
                            vm.markRead(n)
82
                            if (n.url.isNotBlank()) onOpen(n.url)
83
                        })
84
                        HorizontalDivider()
85
                    }
86
                }
87
            }
88
        }
89
    }
90
}
91
92
@Composable
93
fun NotificationRow(n: Notification, onClick: () -> Unit) {
94
    ListItem(
95
        modifier = Modifier.clickable(onClick = onClick),
96
        colors = if (n.read) ListItemDefaults.colors() else ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
97
        leadingContent = {
98
            Box(contentAlignment = Alignment.TopEnd) {
99
                Icon(iconFor(n.kind), contentDescription = null)
100
                if (!n.read) {
101
                    Surface(Modifier.size(8.dp), shape = CircleShape, color = MaterialTheme.colorScheme.primary) {}
102
                }
103
            }
104
        },
105
        headlineContent = {
106
            Text(n.title, maxLines = 2, overflow = TextOverflow.Ellipsis, fontWeight = if (n.read) FontWeight.Normal else FontWeight.SemiBold)
107
        },
108
        supportingContent = {
109
            val who = if (n.actor.isNotBlank()) "${n.actor} · " else ""
110
            Text("$who${describe(n.kind)} · ${relativeTime(n.atMs)}", maxLines = 1, overflow = TextOverflow.Ellipsis)
111
        },
112
    )
113
}
114
115
private fun iconFor(kind: String): ImageVector = when {
116
    kind == "mention" -> Icons.Outlined.AlternateEmail
117
    kind.startsWith("proposal.review") -> Icons.Outlined.RateReview
118
    kind.endsWith(".comment") -> Icons.Outlined.ChatBubbleOutline
119
    kind.startsWith("proposal") -> Icons.AutoMirrored.Outlined.CallMerge
120
    kind.startsWith("issue") -> Icons.Outlined.BugReport
121
    kind.startsWith("flow") -> Icons.Outlined.PlayCircle
122
    kind.startsWith("backup") -> Icons.Outlined.Backup
123
    else -> Icons.Outlined.Notifications
124
}
125
126
/**
127
 * What a kind of notification is called, as the hub's emails name them
128
 * (oxygenhub crates/hub/src/notify/mod.rs, `label`), without the emoji.
129
 */
130
internal fun describe(kind: String): String = when (kind) {
131
    "flow.failed" -> "Build broken"
132
    "flow.fixed" -> "Build fixed"
133
    "flow.runner_drained" -> "Runner drained"
134
    "backup" -> "Backup failed"
135
    "backup.recovered" -> "Backup recovered"
136
    "backup.summary" -> "Daily backup summary"
137
    "proposal.opened" -> "New proposal"
138
    "proposal.review_requested" -> "Review requested"
139
    "proposal.review" -> "New review"
140
    "proposal.landed" -> "Proposal landed"
141
    "proposal.comment", "issue.comment", "drop.comment" -> "New comment"
142
    "issue.opened" -> "New issue"
143
    "issue.assigned" -> "Issue assigned"
144
    "issue.closed" -> "Issue closed"
145
    "account.registered" -> "New sign-up"
146
    "mention" -> "Mentioned you"
147
    else -> kind.replace('.', ' ').replace('_', ' ').replaceFirstChar { it.uppercase() }
148
}