From 4c7bad0f3ba3c3b2b6bf450b632e4a24791890ea Mon Sep 17 00:00:00 2001 From: ialokim <ialokim@mailbox.org> Date: Sun, 27 Nov 2022 13:21:02 +0100 Subject: [PATCH] show forum sharing status in newly added InfoDrawer --- .../forums/ForumSharingDrawerContent.kt | 110 +++++++++++ .../desktop/forums/ForumSharingViewModel.kt | 123 ++++++++++++ .../desktop/forums/GroupConversationScreen.kt | 18 ++ .../briar/desktop/ui/InfoDrawer.kt | 187 ++++++++++++++++++ .../briar/desktop/ui/MainScreen.kt | 81 ++++++-- .../desktop/viewmodel/ViewModelModule.kt | 6 + .../resources/strings/BriarDesktop.properties | 5 + 7 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt create mode 100644 briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/InfoDrawer.kt diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt new file mode 100644 index 0000000000..b06a03988c --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt @@ -0,0 +1,110 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.forums + +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.briar.desktop.contact.ContactCard +import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE +import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.viewmodel.viewModel + +@Composable +fun ForumSharingDrawerContent( + groupId: GroupId, + close: () -> Unit, + viewModel: ForumSharingViewModel = viewModel(), +) = Column { + LaunchedEffect(groupId) { + viewModel.setGroupId(groupId) + } + Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) { + IconButton( + icon = Icons.Filled.Close, + contentDescription = i18n("access.forum.sharing.status.close"), + onClick = close, + modifier = Modifier.padding(start = 24.dp).size(24.dp).align(Alignment.CenterVertically) + ) + Text( + text = i18n("forum.sharing.status.title"), + modifier = Modifier.align(Alignment.CenterVertically).padding(start = 16.dp), + style = MaterialTheme.typography.h3, + ) + } + HorizontalDivider() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + modifier = Modifier.padding(8.dp), + ) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Text( + text = i18n("forum.sharing.status.info"), + style = MaterialTheme.typography.body2, + ) + } + HorizontalDivider() + Box(Modifier.fillMaxSize()) { + if (viewModel.currentlySharedWith.isEmpty()) { + Text( + text = i18n("forum.sharing.status.nobody"), + style = MaterialTheme.typography.body1, + modifier = Modifier.align(Alignment.Center), + ) + } else { + LazyColumn { + items(viewModel.currentlySharedWith) { contactItem -> + ContactCard( + contactItem, + onSel = {}, + selected = false, + onRemovePending = {}, + ) + } + } + } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt new file mode 100644 index 0000000000..3516d975d1 --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt @@ -0,0 +1,123 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.forums + +import androidx.compose.runtime.mutableStateListOf +import mu.KotlinLogging +import org.briarproject.bramble.api.connection.ConnectionRegistry +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.db.TransactionManager +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventBus +import org.briarproject.bramble.api.lifecycle.LifecycleManager +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.briar.api.attachment.AttachmentReader +import org.briarproject.briar.api.conversation.ConversationManager +import org.briarproject.briar.api.forum.ForumSharingManager +import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent +import org.briarproject.briar.api.identity.AuthorManager +import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent +import org.briarproject.briar.desktop.contact.ContactItem +import org.briarproject.briar.desktop.threading.BriarExecutors +import org.briarproject.briar.desktop.threading.UiExecutor +import org.briarproject.briar.desktop.utils.ImageUtils.loadImage +import org.briarproject.briar.desktop.utils.clearAndAddAll +import org.briarproject.briar.desktop.utils.removeFirst +import org.briarproject.briar.desktop.utils.replaceFirst +import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel +import javax.inject.Inject + +class ForumSharingViewModel @Inject constructor( + private val forumSharingManager: ForumSharingManager, + private val contactManager: ContactManager, + private val authorManager: AuthorManager, + private val conversationManager: ConversationManager, + private val connectionRegistry: ConnectionRegistry, + private val attachmentReader: AttachmentReader, + briarExecutors: BriarExecutors, + lifecycleManager: LifecycleManager, + db: TransactionManager, + eventBus: EventBus, +) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + private lateinit var groupId: GroupId + + private val _currentlySharedWith = mutableStateListOf<ContactItem>() + val currentlySharedWith: List<ContactItem> = _currentlySharedWith + + @UiExecutor + fun setGroupId(groupId: GroupId) { + this.groupId = groupId + loadSharedWith() + } + + @UiExecutor + override fun eventOccurred(e: Event) { + when { + e is ForumInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId && e.messageHeader.wasAccepted() -> + runOnDbThreadWithTransaction(false) { txn -> + val contact = contactManager.getContact(txn, e.contactId) + val authorInfo = authorManager.getAuthorInfo(txn, contact) + val item = ContactItem( + contact, + authorInfo, + connectionRegistry.isConnected(contact.id), + conversationManager.getGroupCount(txn, contact.id), // todo: not necessary to be shown here + authorInfo.avatarHeader?.let { loadImage(txn, attachmentReader, it) }, + ) + txn.attach { _currentlySharedWith.add(item) } + } + + e is ContactLeftShareableEvent && e.groupId == groupId -> + _currentlySharedWith.removeFirst { it.idWrapper.contactId == e.contactId } + + e is ContactConnectedEvent -> + _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { it.updateIsConnected(true) } + + e is ContactDisconnectedEvent -> + _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) { + it.updateIsConnected( + false + ) + } + } + } + + private fun loadSharedWith() = runOnDbThreadWithTransaction(true) { txn -> + val list = forumSharingManager.getSharedWith(txn, groupId).map { contact -> + val authorInfo = authorManager.getAuthorInfo(txn, contact) + ContactItem( + contact, + authorInfo, + connectionRegistry.isConnected(contact.id), + conversationManager.getGroupCount(txn, contact.id), // todo: not necessary to be shown here + authorInfo.avatarHeader?.let { loadImage(txn, attachmentReader, it) }, + ) + } + txn.attach { + _currentlySharedWith.clearAndAddAll(list) + } + } +} diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt index 7e75ef5ac5..c6aa4e1239 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt @@ -53,6 +53,7 @@ import org.briarproject.briar.desktop.contact.ContactDropDown.State.CLOSED import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.getInfoDrawerHandler import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable @@ -91,6 +92,7 @@ private fun GroupConversationHeader( val deleteGroupDialogVisible = remember { mutableStateOf(false) } val menuState = remember { mutableStateOf(CLOSED) } val close = { menuState.value = CLOSED } + val infoDrawerHandler = getInfoDrawerHandler() Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) { Row( horizontalArrangement = SpaceBetween, @@ -127,6 +129,22 @@ private fun GroupConversationHeader( expanded = menuState.value == MAIN, onDismissRequest = close, ) { + DropdownMenuItem( + onClick = { + close() + infoDrawerHandler.open { + ForumSharingDrawerContent( + groupId = groupItem.id, + close = infoDrawerHandler::close, + ) + } + } + ) { + Text( + i18n("forum.sharing.status.title"), + style = MaterialTheme.typography.body2, + ) + } DropdownMenuItem( onClick = { close() diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/InfoDrawer.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/InfoDrawer.kt new file mode 100644 index 0000000000..e18b9d169a --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/InfoDrawer.kt @@ -0,0 +1,187 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.ui + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.material.DrawerDefaults +import androidx.compose.material.DrawerValue +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalDrawer +import androidx.compose.material.Surface +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH +import kotlin.math.roundToInt + +/** + * State of the [InfoDrawer] composable. + * + * @param initialValue The initial value of the state. + */ +@Stable +class InfoDrawerState( + initialValue: DrawerValue, +) { + /** + * Whether the drawer is open. + */ + val isOpen: Boolean + get() = currentValue == DrawerValue.Open + + /** + * Whether the drawer is closed. + */ + val isClosed: Boolean + get() = currentValue == DrawerValue.Closed + + /** + * The current value of the state. + */ + var currentValue: DrawerValue by mutableStateOf(initialValue) + private set + + /** + * Open the drawer. + */ + fun open() { + currentValue = DrawerValue.Open + } + + /** + * Close the drawer. + */ + fun close() { + currentValue = DrawerValue.Closed + } +} + +/** + * Create and [remember] an [InfoDrawerState]. + * + * @param initialValue The initial value of the state. + */ +@Composable +fun rememberInfoDrawerState(initialValue: DrawerValue): InfoDrawerState { + return remember { + InfoDrawerState(initialValue) + } +} + +/** + * Material Design modal info drawer. + * + * Modal drawers block interaction with the rest of an app’s content with a scrim. + * They are elevated above most of the app’s UI and don’t affect the screen’s layout grid. + * + * This Composable is heavily inspired by [ModalDrawer], but simplified and adapted to our use-case + * of a modal drawer opening from the end of the screen. + * + * @param drawerContent composable that represents content inside the drawer + * @param modifier optional modifier for the drawer + * @param drawerState state of the drawer + * @param drawerShape shape of the drawer sheet + * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the + * drawer sheet + * @param drawerBackgroundColor background color to be used for the drawer sheet + * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to + * either the matching content color for [drawerBackgroundColor], or, if it is not a color from + * the theme, this will keep the same value set above this Surface. + * @param scrimColor color of the scrim that obscures content when the drawer is open + * @param content content of the rest of the UI + * + * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] width + */ +@Composable +fun InfoDrawer( + drawerContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + drawerState: InfoDrawerState = rememberInfoDrawerState(DrawerValue.Closed), + drawerShape: Shape = MaterialTheme.shapes.large, + drawerElevation: Dp = DrawerDefaults.Elevation, + drawerBackgroundColor: Color = MaterialTheme.colors.surface, + drawerContentColor: Color = contentColorFor(drawerBackgroundColor), + scrimColor: Color = DrawerDefaults.scrimColor, + content: @Composable () -> Unit, +) { + val animatedOffset by animateDpAsState(if (drawerState.isClosed) COLUMN_WIDTH else 0.dp) + BoxWithConstraints(modifier.fillMaxSize()) { + Box { content() } + Scrim( + open = drawerState.isOpen, + onClose = { drawerState.close() }, + color = scrimColor + ) + Surface( + modifier = Modifier + .fillMaxHeight() + .requiredWidth(COLUMN_WIDTH) + .align(Alignment.CenterEnd) + .offset { IntOffset(animatedOffset.value.roundToInt(), 0) }, + shape = drawerShape, + color = drawerBackgroundColor, + contentColor = drawerContentColor, + elevation = drawerElevation, + content = drawerContent + ) + } +} + +@Composable +private fun Scrim( + open: Boolean, + onClose: () -> Unit, + color: Color, +) { + val alpha by animateFloatAsState(if (open) 1f else 0f) + val dismissDrawer = if (open) { + Modifier.pointerInput(onClose) { detectTapGestures { onClose() } } + } else { + Modifier + } + + Canvas( + Modifier + .fillMaxSize() + .then(dismissDrawer) + ) { + drawRect(color, alpha = alpha) + } +} 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 0e2dfd36d7..4dcdf9c52c 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 @@ -19,7 +19,14 @@ package org.briarproject.briar.desktop.ui import androidx.compose.foundation.layout.Row +import androidx.compose.material.DrawerValue import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import org.briarproject.briar.desktop.conversation.PrivateMessageScreen import org.briarproject.briar.desktop.forums.ForumScreen import org.briarproject.briar.desktop.navigation.BriarSidebar @@ -35,20 +42,66 @@ import org.briarproject.briar.desktop.viewmodel.viewModel */ @Composable fun MainScreen(viewModel: SidebarViewModel = viewModel()) { - Row { - BriarSidebar( - viewModel.account.value, - viewModel.uiMode.value, - viewModel::setUiMode, - ) - VerticalDivider() - when (viewModel.uiMode.value) { - UiMode.CONTACTS -> PrivateMessageScreen() - UiMode.GROUPS -> PrivateGroupScreen() - UiMode.FORUMS -> ForumScreen() - UiMode.SETTINGS -> SettingsScreen() - UiMode.ABOUT -> AboutScreen() - else -> UiPlaceholder() + val drawerHandler = remember { InfoDrawerHandler() } + InfoDrawer( + drawerState = drawerHandler.state, + drawerContent = { + drawerHandler.content() } + ) { + CompositionLocalProvider(LocalInfoDrawerHandler provides drawerHandler) { + Row { + BriarSidebar( + viewModel.account.value, + viewModel.uiMode.value, + viewModel::setUiMode, + ) + VerticalDivider() + when (viewModel.uiMode.value) { + UiMode.CONTACTS -> PrivateMessageScreen() + UiMode.GROUPS -> PrivateGroupScreen() + UiMode.FORUMS -> ForumScreen() + UiMode.SETTINGS -> SettingsScreen() + UiMode.ABOUT -> AboutScreen() + else -> UiPlaceholder() + } + } + } + } +} + +val LocalInfoDrawerHandler = staticCompositionLocalOf<InfoDrawerHandler?> { null } + +@Composable +fun getInfoDrawerHandler() = checkNotNull(LocalInfoDrawerHandler.current) { + "No InfoDrawerHandler was provided via LocalInfoDrawerHandler" // NON-NLS +} + +/** + * Handler to interact with the current [InfoDrawer]. + * Should be provided via [LocalInfoDrawerHandler] and retrieved using [getInfoDrawerHandler]. + */ +class InfoDrawerHandler( + val state: InfoDrawerState = InfoDrawerState(DrawerValue.Closed), + initialContent: @Composable () -> Unit = {}, +) { + + var content by mutableStateOf(initialContent) + private set + + /** + * Open the associated [InfoDrawer] with the given [content]. + * + * @param content Composable content to be shown in the [InfoDrawer]. + * May be null, in which case the last content will be shown again. + */ + fun open(content: (@Composable () -> Unit)? = null) { + if (content != null) this.content = content + state.open() } + + /** + * Close the associated [InfoDrawer]. + */ + fun close() = state.close() } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt index bcb4353996..6cccf37d38 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt @@ -25,6 +25,7 @@ import dagger.multibindings.IntoMap import org.briarproject.briar.desktop.contact.ContactListViewModel import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel import org.briarproject.briar.desktop.conversation.ConversationViewModel +import org.briarproject.briar.desktop.forums.ForumSharingViewModel import org.briarproject.briar.desktop.forums.ForumViewModel import org.briarproject.briar.desktop.introduction.IntroductionViewModel import org.briarproject.briar.desktop.login.StartupViewModel @@ -84,6 +85,11 @@ abstract class ViewModelModule { @ViewModelKey(ThreadedConversationViewModel::class) abstract fun bindThreadedConversationViewModel(threadedConversationViewModel: ThreadedConversationViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(ForumSharingViewModel::class) + abstract fun bindForumSharingViewModel(forumSharingViewModel: ForumSharingViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(SettingsViewModel::class) diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index c0888d808d..27063d7799 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -67,6 +67,7 @@ access.forums.unread_count={0, plural, one {one unread posts} other {{0} unread access.forums.last_post_timestamp=last post: {0} access.forums.jump_to_prev_unread=Jump to previous unread post access.forums.jump_to_next_unread=Jump to next unread post +access.forum.sharing.status.close=Close sharing status # Contacts contacts.none_selected.title=No contact selected @@ -127,6 +128,10 @@ forum.message.reply.intro=Reply to: forum.message.new=Unread Post group.card.no_posts=No posts group.card.posts={0, plural, one {{0} post} other {{0} posts}} +forum.sharing.status.title=Sharing Status +forum.sharing.status.info=Any member of a forum can share it with their contacts. You are sharing this forum with the following contacts. There may also be other members who you can't see in this list, although you can see their posts in the forum. +forum.sharing.status.with=Shared with {0} ({1} online) +forum.sharing.status.nobody=Nobody # Private Groups groups.card.created=Created by {0} -- GitLab