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