Skip to content

Commit 1675300

Browse files
authored
Merge pull request #1736 from DimensionDev/feature/notification_ux
feat: enhance notification screen with multi-account notification sor…
2 parents c2dc546 + ab62c22 commit 1675300

File tree

5 files changed

+96
-139
lines changed

5 files changed

+96
-139
lines changed

app/src/main/java/dev/dimension/flare/ui/screen/home/NotificationScreen.kt

Lines changed: 60 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
package dev.dimension.flare.ui.screen.home
22

3-
import androidx.compose.animation.AnimatedVisibility
4-
import androidx.compose.foundation.background
5-
import androidx.compose.foundation.clickable
6-
import androidx.compose.foundation.horizontalScroll
73
import androidx.compose.foundation.layout.Arrangement
84
import androidx.compose.foundation.layout.Box
95
import androidx.compose.foundation.layout.Row
106
import androidx.compose.foundation.layout.fillMaxWidth
117
import androidx.compose.foundation.layout.padding
128
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
139
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
14-
import androidx.compose.foundation.rememberScrollState
15-
import androidx.compose.foundation.shape.RoundedCornerShape
10+
import androidx.compose.foundation.shape.CircleShape
1611
import androidx.compose.material3.Badge
17-
import androidx.compose.material3.ButtonGroup
1812
import androidx.compose.material3.ExperimentalMaterial3Api
1913
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
20-
import androidx.compose.material3.LocalTextStyle
14+
import androidx.compose.material3.FilterChip
15+
import androidx.compose.material3.LeadingIconTab
16+
import androidx.compose.material3.LocalContentColor
2117
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.SecondaryScrollableTabRow
2219
import androidx.compose.material3.Text
2320
import androidx.compose.material3.TopAppBarDefaults
2421
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
2522
import androidx.compose.runtime.Composable
26-
import androidx.compose.runtime.CompositionLocalProvider
2723
import androidx.compose.runtime.getValue
2824
import androidx.compose.runtime.remember
2925
import androidx.compose.runtime.rememberCoroutineScope
3026
import androidx.compose.ui.Alignment
3127
import androidx.compose.ui.Modifier
3228
import androidx.compose.ui.draw.clip
29+
import androidx.compose.ui.graphics.Color
3330
import androidx.compose.ui.input.nestedscroll.nestedScroll
3431
import androidx.compose.ui.res.stringResource
3532
import androidx.compose.ui.unit.dp
@@ -43,6 +40,7 @@ import dev.dimension.flare.ui.component.AvatarComponent
4340
import dev.dimension.flare.ui.component.FlareScaffold
4441
import dev.dimension.flare.ui.component.FlareTopAppBar
4542
import dev.dimension.flare.ui.component.RefreshContainer
43+
import dev.dimension.flare.ui.component.TabRowIndicator
4644
import dev.dimension.flare.ui.component.status.LazyStatusVerticalStaggeredGrid
4745
import dev.dimension.flare.ui.component.status.status
4846
import dev.dimension.flare.ui.model.onSuccess
@@ -80,108 +78,58 @@ internal fun NotificationScreen() {
8078
FlareTopAppBar(
8179
title = {
8280
if (state.notifications.size > 1) {
83-
Row(
81+
SecondaryScrollableTabRow(
82+
containerColor = Color.Transparent,
8483
modifier =
8584
Modifier
86-
.fillMaxWidth()
87-
.horizontalScroll(rememberScrollState()),
88-
verticalAlignment = Alignment.CenterVertically,
89-
horizontalArrangement = Arrangement.spacedBy(8.dp),
85+
.fillMaxWidth(),
86+
selectedTabIndex = state.selectedAccountIndex,
87+
edgePadding = 0.dp,
88+
divider = {},
89+
indicator = {
90+
TabRowIndicator(
91+
selectedIndex = state.selectedAccountIndex,
92+
)
93+
},
94+
minTabWidth = 48.dp,
9095
) {
9196
state.notifications.forEach { (account, badge) ->
92-
Row(
93-
verticalAlignment = Alignment.CenterVertically,
94-
modifier =
95-
Modifier
96-
.clip(RoundedCornerShape(100))
97-
.background(
98-
if (state.selectedAccount?.key == account.key) {
99-
MaterialTheme.colorScheme.primaryContainer
100-
} else {
101-
MaterialTheme.colorScheme.surfaceVariant
102-
},
103-
).clickable {
104-
state.setAccount(account)
105-
}.padding(8.dp),
106-
) {
107-
AvatarComponent(
108-
account.avatar,
109-
size = 24.dp,
110-
)
111-
CompositionLocalProvider(
112-
LocalTextStyle provides MaterialTheme.typography.titleMedium,
113-
) {
114-
AnimatedVisibility(state.selectedAccount?.key == account.key) {
115-
Text(
116-
text = account.handle,
117-
modifier = Modifier.padding(horizontal = 4.dp),
118-
maxLines = 1,
97+
LeadingIconTab(
98+
modifier = Modifier.clip(CircleShape),
99+
selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
100+
unselectedContentColor = LocalContentColor.current,
101+
selected = state.selectedAccount?.key == account.key,
102+
onClick = {
103+
state.setAccount(account)
104+
},
105+
text = {
106+
Text(
107+
text = account.handle,
108+
maxLines = 1,
109+
)
110+
},
111+
icon = {
112+
Box(
113+
contentAlignment = Alignment.BottomEnd,
114+
) {
115+
AvatarComponent(
116+
account.avatar,
117+
size = 24.dp,
119118
)
120-
}
121-
AnimatedVisibility(badge > 0) {
122-
Badge {
123-
Text(
124-
text = badge.toString(),
125-
modifier = Modifier.padding(4.dp),
126-
maxLines = 1,
127-
)
119+
if (badge > 0) {
120+
Badge {
121+
Text(
122+
text = badge.toString(),
123+
maxLines = 1,
124+
style = MaterialTheme.typography.labelSmall,
125+
)
126+
}
128127
}
129128
}
130-
}
131-
}
129+
},
130+
)
132131
}
133132
}
134-
// SecondaryScrollableTabRow(
135-
// containerColor = Color.Transparent,
136-
// modifier =
137-
// Modifier
138-
// .fillMaxWidth(),
139-
// edgePadding = 0.dp,
140-
// divider = {},
141-
// indicator = {
142-
// TabRowIndicator(
143-
// selectedIndex =
144-
// state.notifications.keys
145-
// .indexOf(state.selectedAccount)
146-
// .coerceIn(0, state.notifications.size - 1),
147-
// )
148-
// },
149-
// minTabWidth = 48.dp,
150-
// selectedTabIndex =
151-
// state.notifications.keys
152-
// .indexOf(state.selectedAccount)
153-
// .coerceIn(0, state.notifications.size - 1),
154-
// ) {
155-
// state.notifications.forEach { (account, badge) ->
156-
// LeadingIconTab(
157-
// selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
158-
// unselectedContentColor = LocalContentColor.current,
159-
// selected = state.selectedAccount == account,
160-
// onClick = {
161-
// state.setAccount(account)
162-
// },
163-
// text = {
164-
// if (state.selectedAccount == account) {
165-
// Text(text = account.handle)
166-
// }
167-
// if (badge > 0) {
168-
// if (state.selectedAccount == account) {
169-
// Spacer(modifier = Modifier.width(4.dp))
170-
// }
171-
// Badge {
172-
// Text(text = badge.toString())
173-
// }
174-
// }
175-
// },
176-
// icon = {
177-
// AvatarComponent(
178-
// data = account.avatar,
179-
// size = AvatarComponentDefaults.compatSize,
180-
// )
181-
// },
182-
// )
183-
// }
184-
// }
185133
} else {
186134
Text(text = stringResource(id = R.string.home_tab_notifications_title))
187135
}
@@ -258,18 +206,19 @@ private fun NotificationFilterSelector(
258206
onFilterChanged: (NotificationFilter) -> Unit,
259207
modifier: Modifier = Modifier,
260208
) {
261-
val titles = filters.map { stringResource(id = it.title) }
262-
ButtonGroup(
209+
Row(
263210
modifier = modifier,
264-
overflowIndicator = {},
211+
horizontalArrangement = Arrangement.spacedBy(8.dp),
265212
) {
266-
filters.forEachIndexed { index, notificationType ->
267-
toggleableItem(
268-
checked = selectedFilter == notificationType,
269-
onCheckedChange = {
270-
onFilterChanged(notificationType)
213+
filters.forEach { filter ->
214+
FilterChip(
215+
selected = filter == selectedFilter,
216+
onClick = {
217+
onFilterChanged(filter)
218+
},
219+
label = {
220+
Text(stringResource(id = filter.title))
271221
},
272-
label = titles[index],
273222
)
274223
}
275224
}

desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/home/NotificationScreen.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,11 @@ internal fun NotificationScreen() {
7373
data = profile.avatar,
7474
size = AvatarComponentDefaults.compatSize,
7575
)
76-
AnimatedVisibility(state.selectedAccount?.key == profile.key) {
77-
Text(
78-
profile.handle,
79-
maxLines = 1,
80-
modifier = Modifier.padding(start = 8.dp),
81-
)
82-
}
76+
Text(
77+
profile.handle,
78+
maxLines = 1,
79+
modifier = Modifier.padding(start = 8.dp),
80+
)
8381
AnimatedVisibility(badge > 0) {
8482
Badge(
8583
status = BadgeStatus.Informational,

iosApp/flare/UI/Screen/NotificationScreen.swift

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,27 +110,21 @@ struct NotificationAccountsBar: View {
110110
ForEach(Array(items.keys), id: \.handle) { key in
111111
let value = items[key]?.intValue
112112
HStack {
113-
if selectedAccount?.key == key.key {
114-
Label {
115-
Text(key.handle)
116-
} icon: {
117-
AvatarView(data: key.avatar)
118-
.frame(width: 20, height: 20)
119-
}
120-
} else {
113+
ZStack(alignment: .bottomTrailing) {
121114
AvatarView(data: key.avatar)
122-
.frame(width: 20, height: 20)
123-
}
124-
if let badge = value, badge > 0 {
125-
Text("\(badge)")
126-
.font(.caption2)
127-
.padding(4)
128-
.background(
129-
Circle()
130-
.fill(Color.accentColor)
131-
)
132-
.foregroundStyle(.white)
115+
if let badge = value, badge > 0 {
116+
Text("\(badge)")
117+
.font(.caption2)
118+
.padding(2)
119+
.background(
120+
Circle()
121+
.fill(Color.red)
122+
)
123+
.foregroundStyle(.white)
124+
.frame(width: 12, height: 12)
125+
}
133126
}
127+
Text(key.handle)
134128
}
135129
.onTapGesture {
136130
onSelect(key)

shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ private fun Status.renderStatus(
491491
},
492492
),
493493
)
494-
} else {
494+
} else if (accountKey == null) {
495495
add(
496496
ActionMenu.Item(
497497
icon = if (reblogged == true) ActionMenu.Item.Icon.Unretweet else ActionMenu.Item.Icon.Retweet,

shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/AllNotificationPresenter.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.dimension.flare.ui.presenter.home
33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.LaunchedEffect
55
import androidx.compose.runtime.collectAsState
6+
import androidx.compose.runtime.derivedStateOf
67
import androidx.compose.runtime.getValue
78
import androidx.compose.runtime.mutableStateOf
89
import androidx.compose.runtime.remember
@@ -52,6 +53,7 @@ public class AllNotificationPresenter :
5253
public val timeline: PagingState<UiTimeline>
5354
public val selectedFilter: NotificationFilter?
5455
public val selectedAccount: UiProfile?
56+
public val selectedAccountIndex: Int
5557

5658
public fun setAccount(profile: UiProfile)
5759

@@ -79,7 +81,12 @@ public class AllNotificationPresenter :
7981
}
8082
}.combineLatestFlowLists()
8183
.map {
82-
it.filterNotNull().toMap().toImmutableMap()
84+
it
85+
.filterNotNull()
86+
.sortedByDescending {
87+
it.second
88+
}.toMap()
89+
.toImmutableMap()
8390
}
8491
}
8592

@@ -91,6 +98,14 @@ public class AllNotificationPresenter :
9198
var selectedAccount by remember {
9299
mutableStateOf<UiProfile?>(null)
93100
}
101+
val selectedAccountIndex by remember {
102+
derivedStateOf {
103+
val maxIndex = (notifications.size - 1).coerceAtLeast(0)
104+
selectedAccount?.let { profile ->
105+
notifications.keys.indexOf(profile).coerceIn(0, maxIndex)
106+
} ?: 0
107+
}
108+
}
94109
var selectedNotificationFilter by remember {
95110
mutableStateOf<NotificationFilter?>(null)
96111
}
@@ -138,6 +153,7 @@ public class AllNotificationPresenter :
138153
override val timeline = listState
139154
override val selectedFilter = selectedNotificationFilter
140155
override val selectedAccount = selectedAccount
156+
override val selectedAccountIndex = selectedAccountIndex
141157

142158
override fun setAccount(profile: UiProfile) {
143159
selectedAccount = profile

0 commit comments

Comments
 (0)