diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt index 8b6744ddd4ff5e5e21e83852f9e5d1edcbba9d39..b3bce311e4aa1512c3d75115b1b5b5736054a54b 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt @@ -50,7 +50,7 @@ fun main() = preview( ContactCard( ContactItem( idWrapper = RealContactIdWrapper(ContactId(0)), - authorId = AuthorId(getRandomId()), + authorId = AuthorId(getRandomIdPersistent()), name = getStringParameter("name"), alias = getStringParameter("alias"), isConnected = getBooleanParameter("isConnected"), @@ -88,7 +88,7 @@ fun ContactCard( ProfileCircle(36.dp, contactItem) MessageCounter( unread = contactItem.unread, - modifier = Modifier.align(Alignment.TopEnd) + modifier = Modifier.align(Alignment.TopEnd).offset(6.dp, (-6).dp) ) } RealContactInfo( @@ -130,7 +130,6 @@ fun MessageCounter(unread: Int, modifier: Modifier = Modifier) { if (unread > 0) { Box( modifier = modifier - .offset(6.dp, (-6).dp) .height(20.dp) .widthIn(min = 20.dp, max = Dp.Infinity) .border(2.dp, outlineColor, CircleShape) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt index 6dbbd900320400dfe3a9a97f0a026e54201c5c90..09c0e01c93a34878f30941c132ad98414346154f 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt @@ -49,8 +49,8 @@ fun main() = preview { ConversationNoticeItem( notice = "Text of notice message.", text = "Let's test a received notice message.", - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = true, @@ -64,8 +64,8 @@ fun main() = preview { ConversationMessageItemView( ConversationMessageItem( text = "This is a medium-sized message that has been sent before receiving the request message.", - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = false, @@ -80,12 +80,12 @@ fun main() = preview { ConversationRequestItem( requestedGroupId = null, requestType = INTRODUCTION, - sessionId = SessionId(getRandomId()), + sessionId = SessionId(getRandomIdPersistent()), answered = false, notice = "Text of notice message.", text = "Short message.", - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = true, @@ -100,8 +100,8 @@ fun main() = preview { ConversationNoticeItem( notice = "Text of notice message.", text = "This is a long long long message that spans over several lines.\n\nIt ends here.", - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = false, @@ -115,8 +115,8 @@ fun main() = preview { ConversationMessageItemView( ConversationMessageItem( text = "Just also receiving a normal message.", - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = true, @@ -131,8 +131,8 @@ fun main() = preview { ConversationNoticeItem( notice = "Text of notice message.", text = null, - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = Instant.now().toEpochMilli(), autoDeleteTimer = 0, isIncoming = false, diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt new file mode 100644 index 0000000000000000000000000000000000000000..ddc487fe0bcad4614588440dd74a5f410b3ece1e --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt @@ -0,0 +1,213 @@ +package org.briarproject.briar.desktop.conversation + +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.briar.desktop.contact.MessageCounter +import org.briarproject.briar.desktop.theme.ChevronDown +import org.briarproject.briar.desktop.theme.ChevronUp +import org.briarproject.briar.desktop.theme.divider +import org.briarproject.briar.desktop.ui.HorizontalDivider +import org.briarproject.briar.desktop.ui.Loader +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.PreviewUtils.preview +import org.briarproject.briar.desktop.utils.replaceIfIndexed +import java.time.Instant + +fun main() = preview( + "num_messages" to 20, + "first_unread_index" to 5 +) { + val numMessages = getIntParameter("num_messages") + val initialFirstUnreadIndex = getIntParameter("first_unread_index") + + // re-create messages and currentUnreadMessageInfo as soon as numMessages or initialFirstUnreadIndex change + val loading by produceState(true, numMessages, initialFirstUnreadIndex) { value = true; delay(500); value = false } + + val messages = remember(loading) { + mutableStateListOf<ConversationItem>().apply { + addAll( + (0 until numMessages).map { idx -> + ConversationMessageItem( + text = "Example Text $idx", + id = MessageId(getRandomId()), + groupId = GroupId(getRandomId()), + time = Instant.now().minusSeconds((numMessages - idx).toLong() * 60).toEpochMilli(), + autoDeleteTimer = 0, + isIncoming = idx % 2 == 0, + isRead = idx % 2 == 1 || idx < initialFirstUnreadIndex, + isSent = false, + isSeen = false + ) + } + ) + } + } + + val currentUnreadMessagesInfo by remember(loading) { + derivedStateOf { + ConversationViewModel.UnreadMessagesInfo( + amount = messages.count { !it.isRead }, + firstIndex = messages.indexOfFirst { !it.isRead } + ) + } + } + + if (loading) { + Loader() + return@preview + } + + ConversationList( + padding = PaddingValues(0.dp), + messages = messages, + initialFirstUnreadMessageIndex = initialFirstUnreadIndex, + currentUnreadMessagesInfo = currentUnreadMessagesInfo, + markMessagesRead = { lst -> + messages.replaceIfIndexed( + { idx, it -> idx in lst && !it.isRead }, + { _, it -> it.markRead() } + ) + }, + respondToRequest = { _, _ -> }, + deleteMessage = {} + ) +} + +@Composable +fun ConversationList( + padding: PaddingValues, + messages: List<ConversationItem>, + initialFirstUnreadMessageIndex: Int, + currentUnreadMessagesInfo: ConversationViewModel.UnreadMessagesInfo, + markMessagesRead: (List<Int>) -> Unit, + respondToRequest: (ConversationRequestItem, Boolean) -> Unit, + deleteMessage: (MessageId) -> Unit, +) { + // we need to make sure the ConversationList is out of composition before showing new messages + // so that the list state and the coroutine scope is created anew + // this is currently assured by (briefly) showing a loader while loading the messages + val scope = rememberCoroutineScope() + val scrollState = rememberLazyListState( + if (initialFirstUnreadMessageIndex != -1) initialFirstUnreadMessageIndex + else if (messages.isNotEmpty()) messages.lastIndex + else 0 + ) + + Box(modifier = Modifier.padding(padding).fillMaxSize()) { + LazyColumn( + state = scrollState, + contentPadding = PaddingValues(top = 8.dp, bottom = 8.dp), + modifier = Modifier.fillMaxSize().padding(end = 12.dp) + ) { + itemsIndexed(messages) { idx, m -> + if (idx == initialFirstUnreadMessageIndex) { + UnreadMessagesMarker() + } + when (m) { + is ConversationMessageItem -> ConversationMessageItemView(m) + is ConversationNoticeItem -> ConversationNoticeItemView(m) + is ConversationRequestItem -> + ConversationRequestItemView( + m, + onResponse = { accept -> respondToRequest(m, accept) }, + ) + } + } + } + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState), + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() + ) + + if (currentUnreadMessagesInfo.amount > 0) { + val delayUntilMarkedAsRead = 500L + LaunchedEffect(currentUnreadMessagesInfo, scrollState.firstVisibleItemIndex) { + // mark all messages visible on the screen for more than [delayUntilMarkedAsRead] milliseconds as read + delay(delayUntilMarkedAsRead) + markMessagesRead(scrollState.layoutInfo.visibleItemsInfo.map { it.index }) + } + val showUnreadButton by produceState(false) { + // never show FAB before all messages currently visible are marked as read + delay(delayUntilMarkedAsRead + 100) + value = true + } + + if (showUnreadButton) { + UnreadMessagesFAB( + arrowUp = currentUnreadMessagesInfo.firstIndex < scrollState.firstVisibleItemIndex, + counter = currentUnreadMessagesInfo.amount, + onClick = { + scope.launch { + scrollState.animateScrollToItem(currentUnreadMessagesInfo.firstIndex, 0) + } + }, + modifier = Modifier.align(Alignment.BottomEnd).padding(32.dp) // todo: check padding + ) + } + } + } +} + +@Composable +fun UnreadMessagesMarker() = Box { + HorizontalDivider(Modifier.align(Alignment.Center)) + Text( + text = i18n("conversation.message.unread"), + modifier = Modifier + .align(Alignment.Center) + .padding(8.dp) + .border(1.dp, MaterialTheme.colors.divider, RoundedCornerShape(16.dp)) + .background(MaterialTheme.colors.background) + .padding(8.dp) + ) +} + +@Composable +fun UnreadMessagesFAB( + arrowUp: Boolean, + counter: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) = Box(modifier) { + FloatingActionButton(onClick) { + val arrow = if (arrowUp) + Icons.Filled.ChevronUp else Icons.Filled.ChevronDown + Icon(arrow, i18n("access.message.jump_to_unread")) + } + MessageCounter( + counter, + Modifier.align(Alignment.TopEnd).offset(3.dp, (-3).dp) + ) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt index 3619b174251fa5b64689355d0c0f85303463b015..43ba815cea392e61f038454e72e595651e903cb8 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt @@ -27,8 +27,8 @@ fun main() = preview( ConversationMessageItemView( ConversationMessageItem( text = getStringParameter("text"), - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = getLongParameter("time"), autoDeleteTimer = 0, isIncoming = getBooleanParameter("isIncoming"), diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt index 8a79f0b1f96accd13c7ca7b0efcb85c90e675d56..733c6e5bd3c7840180c2855640adb956d66821cb 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt @@ -38,8 +38,8 @@ fun main() = preview( ConversationNoticeItem( notice = getStringParameter("notice"), text = getStringParameter("text"), - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = getLongParameter("time"), autoDeleteTimer = 0, isIncoming = getBooleanParameter("isIncoming"), diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt index 865919e7a91b209c176339df9d9f97279f4f50ee..73db6fc889c5a218c18600a3f95f06c92ba028e3 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt @@ -47,14 +47,14 @@ fun main() = preview( ) { ConversationRequestItemView( ConversationRequestItem( - requestedGroupId = if (getBooleanParameter("canBeOpened")) GroupId(getRandomId()) else null, + requestedGroupId = if (getBooleanParameter("canBeOpened")) GroupId(getRandomIdPersistent()) else null, requestType = INTRODUCTION, - sessionId = SessionId(getRandomId()), + sessionId = SessionId(getRandomIdPersistent()), answered = getBooleanParameter("answered"), notice = getStringParameter("notice"), text = getStringParameter("text"), - id = MessageId(getRandomId()), - groupId = GroupId(getRandomId()), + id = MessageId(getRandomIdPersistent()), + groupId = GroupId(getRandomIdPersistent()), time = getLongParameter("time"), autoDeleteTimer = 0, isIncoming = getBooleanParameter("isIncoming"), diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt index 89b4a307476aadb896897cace783f53945087827..f7eff442c0f8830cbae81417243a7ac24bff9597 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -7,16 +7,11 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme @@ -58,7 +53,6 @@ fun ConversationScreen( val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } val (deleteAllMessagesDialogVisible, setDeleteAllMessagesDialog) = remember { mutableStateOf(false) } - val scrollState = rememberLazyListState() BoxWithConstraints(Modifier.fillMaxSize()) { val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp) @@ -79,27 +73,15 @@ fun ConversationScreen( Loader() return@Scaffold } - - LazyColumn( - state = scrollState, - // reverseLayout to display most recent message (index 0) at the bottom - reverseLayout = true, - contentPadding = PaddingValues(8.dp), - modifier = Modifier.padding(padding).fillMaxHeight() - ) { - items(viewModel.messages) { m -> - when (m) { - is ConversationMessageItem -> ConversationMessageItemView(m, viewModel::deleteMessage) - is ConversationNoticeItem -> ConversationNoticeItemView(m, viewModel::deleteMessage) - is ConversationRequestItem -> - ConversationRequestItemView( - m, - onResponse = { accept -> viewModel.respondToRequest(m, accept) }, - onDelete = viewModel::deleteMessage, - ) - } - } - } + ConversationList( + padding, + viewModel.messages, + viewModel.initialFirstUnreadMessageIndex.value, + viewModel.currentUnreadMessagesInfo.value, + viewModel::markMessagesRead, + viewModel::respondToRequest, + viewModel::deleteMessage, + ) }, bottomBar = { ConversationInput( @@ -110,13 +92,6 @@ fun ConversationScreen( }, ) - if (viewModel.hasUnreadMessages.value) { - LaunchedEffect(scrollState.firstVisibleItemIndex) { - // mark all messages older than the first visible item as read - viewModel.markMessagesRead(scrollState.firstVisibleItemIndex) - } - } - if (infoDrawer) { // TODO Find non-hacky way of setting scrim on entire app Box( diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt index 85da3d34147320b7a3d1cd65e8b3c736212362ac..969423ab5a62c6fcba468ca55aa675fa1d73c3af 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -133,7 +133,7 @@ constructor( ) val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn) val msg = h.accept(visitor)!! - txn.attach { _messages.add(0, msg) } + txn.attach { _messages.add(msg) } } catch (e: UnexpectedTimerException) { // todo: handle this properly LOG.warn(e) {} @@ -144,19 +144,35 @@ constructor( } } - val hasUnreadMessages = derivedStateOf { _messages.any { !it.isRead } } + // first unread message when first opening the list + // used to draw a horizontal divider on that position as long as list is opened + // we cannot use [derivedStateOf] here as it would move the line after first showing the list + private val _initialFirstUnreadMessageIndex = mutableStateOf(-1) + val initialFirstUnreadMessageIndex = _initialFirstUnreadMessageIndex.asState() + + val currentUnreadMessagesInfo = derivedStateOf { + UnreadMessagesInfo( + amount = _messages.count { !it.isRead }, + firstIndex = _messages.indexOfFirst { !it.isRead } + ) + } + + data class UnreadMessagesInfo( + val amount: Int, + val firstIndex: Int + ) - fun markMessagesRead(untilIndex: Int) { + fun markMessagesRead(indices: List<Int>) { val id = _contactId.value!! val messages = _messages.toList() runOnDbThreadWithTransaction(false) { txn -> var count = 0 - messages.filterIndexed { idx, it -> idx >= untilIndex && !it.isRead }.forEach { + messages.filterIndexed { idx, it -> idx in indices && !it.isRead }.forEach { conversationManager.setReadFlag(txn, it.groupId, it.id, true) count++ } txn.attach { - _messages.replaceIfIndexed({ idx, it -> idx >= untilIndex && !it.isRead }) { _, it -> + _messages.replaceIfIndexed({ idx, it -> idx in indices && !it.isRead }) { _, it -> it.markRead() } } @@ -209,12 +225,16 @@ constructor( val headers = conversationManager.getMessageHeaders(txn, contact.idWrapper.contactId) LOG.logDuration("Loading message headers", start) // Sort headers by timestamp in *descending* order - val sorted = headers.sortedByDescending { it.timestamp } + // val sorted = headers.sortedByDescending { it.timestamp } + val sorted = headers.sortedBy { it.timestamp } start = LogUtils.now() val visitor = ConversationVisitor(contact.name, messagingManager, txn) val messages = sorted.map { h -> h.accept(visitor)!! } LOG.logDuration("Loading messages", start) - txn.attach { _messages.clearAndAddAll(messages) } + txn.attach { + _messages.clearAndAddAll(messages) + _initialFirstUnreadMessageIndex.value = messages.indexOfFirst { !it.isRead } + } } catch (e: NoSuchContactException) { // todo: handle this properly LOG.warn(e) {} @@ -239,7 +259,7 @@ constructor( runOnDbThreadWithTransaction(true) { txn -> val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn) val msg = h.accept(visitor)!! - txn.attach { _messages.add(0, msg) } + txn.attach { _messages.add(msg) } } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ca7efe177905dc3823be101e7f4ddee91af7aff --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt @@ -0,0 +1,8 @@ +package org.briarproject.briar.desktop.theme + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore + +val Icons.Filled.ChevronUp get() = ExpandLess +val Icons.Filled.ChevronDown get() = ExpandMore diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt index 2af666641ee99b47d5684ad439af973ad828128e..967b0e4bb606c32e9f123642e1f3ef23abc84569 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt @@ -64,9 +64,11 @@ object PreviewUtils { fun setLongParameter(name: String, value: Long) = setDatatype(name, value) + fun getRandomId() = random.nextBytes(UniqueId.LENGTH) + @Composable - fun getRandomId() = - remember { random.nextBytes(UniqueId.LENGTH) } + fun getRandomIdPersistent() = + remember { getRandomId() } } @Composable diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties index 038922edf28accb623d93fa47885caad00ec8616..26999bf9f8ca9b1aa78ba393bf61495b494dff4b 100644 --- a/src/main/resources/strings/BriarDesktop.properties +++ b/src/main/resources/strings/BriarDesktop.properties @@ -7,6 +7,7 @@ access.contacts.dropdown.contacts.expand=Expand contacts menu access.contacts.search=Icon for searching contacts access.introduction.back.contact=Go back to contact screen of introduction process access.introduction.close=Close introduction screen +access.message.jump_to_unread=Jump to next unread message access.message.send=Send message access.message.sent=Message sent access.logo=Briar logo @@ -30,6 +31,7 @@ contacts.dropdown.introduction=Make Introduction contacts.search.title=Contacts # Conversation +conversation.message.unread=Unread messages conversation.message.new=New Message conversation.delete.all.dialog.title=Confirm Message Deletion conversation.delete.all.dialog.message=Are you sure that you want to delete all messages?