diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt index 44232a46fb81f1d7991b32e33a244ff0f68a7a0e..51ca936d49d9ee8bc66f59f9c80f6cb614fafb1f 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt @@ -18,11 +18,14 @@ package org.briarproject.briar.desktop.navigation +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material.IconButton @@ -55,20 +58,26 @@ fun BriarSidebar( account: LocalAuthor?, uiMode: UiMode, setUiMode: (UiMode) -> Unit, + messageCount: SidebarViewModel.MessageCount, ) { - val displayButton = @Composable { selectedMode: UiMode, mode: UiMode, icon: ImageVector -> - BriarSidebarButton( - selectedMode == mode, - { setUiMode(mode) }, - icon, - i18n(mode.contentDescriptionKey) - ) - } + @Composable + fun BriarSidebarButton( + mode: UiMode, + icon: ImageVector, + messageCount: Int = 0, + ) = BriarSidebarButton( + uiMode == mode, + { setUiMode(mode) }, + icon, + i18n(mode.contentDescriptionKey), + messageCount + ) Surface( modifier = Modifier.width(SIDEBAR_WIDTH).fillMaxHeight().selectableGroup(), color = MaterialTheme.colors.sidebarSurface ) { + val configuration = getConfiguration() Column(verticalArrangement = Arrangement.Top) { // profile button Box( @@ -76,52 +85,28 @@ fun BriarSidebar( ) { account?.let { ProfileCircle(size = 45.dp, it.id.bytes) } } - val modes = buildList { - add(Pair(UiMode.CONTACTS, Icons.Filled.Contacts)) - val configuration = getConfiguration() - if (configuration.shouldEnablePrivateGroups()) add(Pair(UiMode.GROUPS, Icons.Filled.Group)) - if (configuration.shouldEnableForums()) add(Pair(UiMode.FORUMS, Icons.Filled.Forum)) - if (configuration.shouldEnableBlogs()) add(Pair(UiMode.BLOGS, Icons.Filled.ChromeReaderMode)) - } - modes.forEach { (mode, icon) -> - displayButton(uiMode, mode, icon) - } + + BriarSidebarButton(UiMode.CONTACTS, Icons.Filled.Contacts, messageCount.privateMessages) + if (configuration.shouldEnablePrivateGroups()) BriarSidebarButton(UiMode.GROUPS, Icons.Filled.Group) + if (configuration.shouldEnableForums()) BriarSidebarButton(UiMode.FORUMS, Icons.Filled.Forum, messageCount.forumPosts) + if (configuration.shouldEnableBlogs()) BriarSidebarButton(UiMode.BLOGS, Icons.Filled.ChromeReaderMode) } Column(verticalArrangement = Arrangement.Bottom) { - val modes = buildList { - val configuration = getConfiguration() - if (configuration.shouldEnableTransportSettings()) add( - Pair(UiMode.TRANSPORTS, Icons.Filled.WifiTethering) - ) - add(Pair(UiMode.SETTINGS, Icons.Filled.Settings)) - add(Pair(UiMode.ABOUT, Icons.Filled.Info)) - } - modes.forEach { (mode, icon) -> - displayButton(uiMode, mode, icon) - } + if (configuration.shouldEnableTransportSettings()) BriarSidebarButton(UiMode.TRANSPORTS, Icons.Filled.WifiTethering) + BriarSidebarButton(UiMode.SETTINGS, Icons.Filled.Settings) + BriarSidebarButton(UiMode.ABOUT, Icons.Filled.Info) } } } -@Composable -fun BriarSidebarButton( - mode: UiMode, - currentMode: UiMode, - setUiMode: (UiMode) -> Unit, -) = BriarSidebarButton( - currentMode == mode, - { setUiMode(mode) }, - mode.icon, - i18n(mode.contentDescriptionKey) -) - @Composable fun BriarSidebarButton( selected: Boolean, onClick: () -> Unit, icon: ImageVector, contentDescription: String, -) { + messageCount: Int, +) = Box { val tint = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface IconButton( icon = icon, @@ -131,4 +116,10 @@ fun BriarSidebarButton( onClick = onClick, modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) + if (messageCount > 0) { + val color = MaterialTheme.colors.secondary + Canvas(modifier = Modifier.align(Alignment.TopEnd).requiredSize(12.dp).offset((-12).dp, 12.dp)) { + drawCircle(color) + } + } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt index 223e98e9ab737db652fe8dd3a5f615267411fa33..539da08caa51d7fa3b762d116a70713046a27f2a 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt @@ -18,29 +18,53 @@ package org.briarproject.briar.desktop.navigation -import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import org.briarproject.bramble.api.identity.IdentityManager import org.briarproject.bramble.api.identity.LocalAuthor +import org.briarproject.briar.desktop.ui.MessageCounter +import org.briarproject.briar.desktop.ui.MessageCounterData +import org.briarproject.briar.desktop.ui.MessageCounterDataType.Forum +import org.briarproject.briar.desktop.ui.MessageCounterDataType.PrivateMessage import org.briarproject.briar.desktop.ui.UiMode import org.briarproject.briar.desktop.viewmodel.ViewModel +import org.briarproject.briar.desktop.viewmodel.asState +import org.briarproject.briar.desktop.viewmodel.update import javax.inject.Inject class SidebarViewModel @Inject constructor( private val identityManager: IdentityManager, + private val messageCounter: MessageCounter, ) : ViewModel() { - override fun onInit() { + super.onInit() loadAccountInfo() + messageCounter.addListener(this::onMessageCounterUpdated) + } + + override fun onCleared() { + super.onCleared() + messageCounter.removeListener(this::onMessageCounterUpdated) + } + + private fun onMessageCounterUpdated(data: MessageCounterData) { + val (type, count) = data + when (type) { + PrivateMessage -> _messageCount.update { copy(privateMessages = count) } + Forum -> _messageCount.update { copy(forumPosts = count) } + } } private var _uiMode = mutableStateOf(UiMode.CONTACTS) private var _account = mutableStateOf<LocalAuthor?>(null) - val uiMode: State<UiMode> = _uiMode - val account: State<LocalAuthor?> = _account + private var _messageCount = mutableStateOf(MessageCount()) + + val uiMode = _uiMode.asState() + val account = _account.asState() + + val messageCount = _messageCount.asState() fun setUiMode(uiMode: UiMode) { _uiMode.value = uiMode @@ -49,4 +73,9 @@ constructor( fun loadAccountInfo() { _account.value = identityManager.localAuthor } + + data class MessageCount( + val privateMessages: Int = 0, + val forumPosts: Int = 0, + ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index 8badde07f6e3c2d1d46eb23793b84e131c741d76..9060f6b89e33b50dedca9f3e57f9512a0e4e5113 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -161,8 +161,8 @@ constructor( lastNotificationForum = 0 } } - val messageCounterListener: MessageCounterListener = { (type, total, groups) -> - if (total > 0 && !focusState.focused) { + val messageCounterListener: MessageCounterListener = { (type, total, groups, inc) -> + if (inc && total > 0 && !focusState.focused) { val callback: NotificationProvider.() -> Unit = when (type) { PrivateMessage -> { { notifyPrivateMessages(total, groups) } } Forum -> { { notifyForumPosts(total, groups) } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt index 4dcdf9c52c98192b165f3c78641c0b546f2fb592..d80e2e12750a7f77f666d772679813287f4b932f 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt @@ -55,6 +55,7 @@ fun MainScreen(viewModel: SidebarViewModel = viewModel()) { viewModel.account.value, viewModel.uiMode.value, viewModel::setUiMode, + viewModel.messageCount.value, ) VerticalDivider() when (viewModel.uiMode.value) { diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt index b5a07dfac71c34726bd80fc5c86a768f73b70dab..31cc4f6360bfd306df5e270c490d72997c1b9ebe 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt @@ -27,10 +27,26 @@ interface MessageCounter { enum class MessageCounterDataType { PrivateMessage, Forum } +/** + * Data holder for MessageCounter updates. + */ data class MessageCounterData( + /** + * Type of unread messages. + */ val type: MessageCounterDataType, + /** + * Sum of all unread messages of the given [type]. + */ val total: Int, + /** + * Amount of different private chats/groups/forums (depending on [type]) with unread messages. + */ val groups: Int, + /** + * If `true`, [total] has increased since the last time the listeners were informed. + */ + val increment: Boolean, ) typealias MessageCounterListener = (MessageCounterData) -> Unit diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt index 906a40ebfb6ec22c3cae739e681f4879a21f1fb3..85edf544570f728f39b20096c64e8b735706291f 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt @@ -68,28 +68,30 @@ constructor( countForumPosts.addCount(f.id, unreadMessages) } txn.attach { - informListeners(PrivateMessage) - informListeners(Forum) + informListeners(PrivateMessage, true) + informListeners(Forum, true) } } } is ConversationMessageReceivedEvent<*> -> { countPrivateMessages.add(e.contactId) - informListeners(PrivateMessage) + informListeners(PrivateMessage, true) } is ConversationMessagesReadEvent -> { countPrivateMessages.removeCount(e.contactId, e.count) + informListeners(PrivateMessage, false) } is ForumPostReceivedEvent -> { countForumPosts.add(e.groupId) - informListeners(Forum) + informListeners(Forum, true) } is ForumPostReadEvent -> { countForumPosts.removeCount(e.groupId, e.numMarkedRead) + informListeners(Forum, false) } } } @@ -99,12 +101,12 @@ constructor( override fun removeListener(listener: MessageCounterListener) = listeners.remove(listener) - private fun informListeners(type: MessageCounterDataType) = listeners.forEach { l -> + private fun informListeners(type: MessageCounterDataType, increment: Boolean) = listeners.forEach { l -> val groupCount = when (type) { PrivateMessage -> countPrivateMessages Forum -> countForumPosts } - l.invoke(MessageCounterData(type, groupCount.total, groupCount.unique)) + l.invoke(MessageCounterData(type, groupCount.total, groupCount.unique, increment)) } private fun <T> Multiset<T>.removeCount(t: T, count: Int) =