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