diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt index bc7b2959e2427b8dcd169ad2ce2b4a926ed2eec8..fac09ce5d9c2b9dc2aa83ee380378259826de1b2 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt @@ -4,6 +4,7 @@ import org.briarproject.bramble.api.sync.GroupId import org.briarproject.bramble.api.sync.MessageId sealed class ConversationItem { + abstract var text: String? abstract val id: MessageId abstract val groupId: GroupId abstract val time: Long diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItem.kt index 2d48f76bb69eae1a3b8a75ab7b8683008c927436..24eb5f486fe4b248dbb6374d4612f39308c7e073 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItem.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItem.kt @@ -5,7 +5,7 @@ import org.briarproject.bramble.api.sync.MessageId import org.briarproject.briar.api.conversation.ConversationMessageHeader data class ConversationMessageItem( - var text: String? = null, + override var text: String? = null, override val id: MessageId, override val groupId: GroupId, override val time: Long, @@ -19,23 +19,20 @@ data class ConversationMessageItem( // val attachments: List<AttachmentItem> ) : ConversationItem() { - constructor(h: ConversationMessageHeader) : - this( - id = h.id, - groupId = h.groupId, - time = h.timestamp, - autoDeleteTimer = h.autoDeleteTimer, - isRead = h.isRead, - isSent = h.isSent, - isSeen = h.isSeen, - isIncoming = !h.isLocal, - ) + constructor(h: ConversationMessageHeader) : this( + id = h.id, + groupId = h.groupId, + time = h.timestamp, + autoDeleteTimer = h.autoDeleteTimer, + isRead = h.isRead, + isSent = h.isSent, + isSeen = h.isSeen, + isIncoming = !h.isLocal, + ) - override fun mark(sent: Boolean, seen: Boolean): ConversationItem { - return copy(isSent = sent, isSeen = seen) - } + override fun mark(sent: Boolean, seen: Boolean): ConversationItem = + copy(isSent = sent, isSeen = seen) - override fun markRead(): ConversationItem { - return copy(isRead = true) - } + override fun markRead(): ConversationItem = + copy(isRead = true) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3858be9257e88dd27f9abf4ed04c2d6e9faa3c7 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItem.kt @@ -0,0 +1,52 @@ +package org.briarproject.briar.desktop.conversation + +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.briar.api.conversation.ConversationRequest +import org.briarproject.briar.api.conversation.ConversationResponse + +data class ConversationNoticeItem( + val msgText: String, + override var text: String?, + override val id: MessageId, + override val groupId: GroupId, + override val time: Long, + override val autoDeleteTimer: Long, + override val isIncoming: Boolean, + override var isRead: Boolean, + override var isSent: Boolean, + override var isSeen: Boolean, +) : ConversationItem() { + + constructor(msgText: String, r: ConversationRequest<*>) : this( + msgText = msgText, + text = r.text, + id = r.id, + groupId = r.groupId, + time = r.timestamp, + autoDeleteTimer = r.autoDeleteTimer, + isRead = r.isRead, + isSent = r.isSent, + isSeen = r.isSeen, + isIncoming = !r.isLocal, + ) + + constructor(msgText: String, r: ConversationResponse) : this( + msgText = msgText, + text = null, + id = r.id, + groupId = r.groupId, + time = r.timestamp, + autoDeleteTimer = r.autoDeleteTimer, + isRead = r.isRead, + isSent = r.isSent, + isSeen = r.isSeen, + isIncoming = !r.isLocal, + ) + + override fun mark(sent: Boolean, seen: Boolean): ConversationItem = + copy(isSent = sent, isSeen = seen) + + override fun markRead(): ConversationItem = + copy(isRead = true) +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..a960f2988fbf58486d35157c5ce61b8410f5c3e9 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItem.kt @@ -0,0 +1,58 @@ +package org.briarproject.briar.desktop.conversation + +import org.briarproject.bramble.api.sync.GroupId +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.briar.api.client.SessionId +import org.briarproject.briar.api.conversation.ConversationRequest +import org.briarproject.briar.api.sharing.InvitationRequest +import org.briarproject.briar.api.sharing.Shareable + +data class ConversationRequestItem( + val requestedGroupId: GroupId?, + val requestType: RequestType, + val sessionId: SessionId, + val answered: Boolean, + val msgText: String, + override var text: String?, + override val id: MessageId, + override val groupId: GroupId, + override val time: Long, + override val autoDeleteTimer: Long, + override val isIncoming: Boolean, + override var isRead: Boolean, + override var isSent: Boolean, + override var isSeen: Boolean, +) : ConversationItem() { + + enum class RequestType { + INTRODUCTION, FORUM, BLOG, GROUP + } + + constructor(msgText: String, type: RequestType, r: ConversationRequest<*>) : this( + requestedGroupId = if (r is InvitationRequest) (r.nameable as Shareable).id else null, + requestType = type, + sessionId = r.sessionId, + answered = r.wasAnswered(), + msgText = msgText, + text = r.text, + id = r.id, + groupId = r.groupId, + time = r.timestamp, + autoDeleteTimer = r.autoDeleteTimer, + isRead = r.isRead, + isSent = r.isSent, + isSeen = r.isSeen, + isIncoming = !r.isLocal, + ) + + val canBeOpened = requestedGroupId != null + + override fun mark(sent: Boolean, seen: Boolean): ConversationItem = + copy(isSent = sent, isSeen = seen) + + override fun markRead(): ConversationItem = + copy(isRead = true) + + fun markAnswered(): ConversationItem = + copy(answered = true) +} 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 0b3d1898ba2b9dda9929667a4fc7b245ac1cfe73..bde1041e197380018711a1741127dbb4ef920319 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -79,8 +80,10 @@ fun ConversationScreen( modifier = Modifier.padding(padding).fillMaxHeight() ) { items(viewModel.messages) { m -> - if (m is ConversationMessageItem) - TextBubble(m) + when (m) { + is ConversationMessageItem -> TextBubble(m) + else -> Text("Placeholder for something else.") + } } } }, 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 b4356dc1cac2ec71607efd126e3618d0591fab00..d5da4f1587abe48fff4a075dc4dc1c31adc1febc 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -61,6 +61,14 @@ constructor( private val LOG = KotlinLogging.logger {} } + private val conversationVisitor = derivedStateOf { + val c = _contactItem.value + if (c != null) + ConversationVisitor(c.name, ::loadMessageText) + else + null + } + private val _contactId = mutableStateOf<ContactId?>(null) private val _contactItem = mutableStateOf<ContactItem?>(null) private val _messages = mutableStateListOf<ConversationItem>() @@ -114,9 +122,8 @@ constructor( m.hasText(), m.attachmentHeaders, m.autoDeleteTimer ) - val msg = messageHeaderToItem(txn, h) txn.attach { - _messages.add(0, msg) + _messages.add(0, h.accept(conversationVisitor.value)!!) } } catch (e: UnexpectedTimerException) { // todo: handle this properly @@ -185,7 +192,7 @@ constructor( val sorted = headers.sortedByDescending { it.timestamp } // todo: use ConversationVisitor to also display Request and Notice Messages start = LogUtils.now() - val messages = sorted.filterIsInstance<PrivateMessageHeader>().map { messageHeaderToItem(txn, it) } + val messages = sorted.map { h -> h.accept(conversationVisitor.value)!! } LOG.logDuration("Loading messages", start) txn.attach { _messages.clearAndAddAll(messages) } } catch (e: NoSuchContactException) { @@ -194,20 +201,9 @@ constructor( } } - private fun messageHeaderToItem(txn: Transaction, h: PrivateMessageHeader): ConversationMessageItem { - // todo: use ConversationVisitor instead and support other MessageHeader - val item = ConversationMessageItem(h) - if (h.hasText()) { - item.text = loadMessageText(txn, h.id) - } else { - LOG.warn { "private message without text" } - } - return item - } - - private fun loadMessageText(txn: Transaction, m: MessageId): String? { + private fun loadMessageText(m: MessageId): String? { try { - return messagingManager.getMessageText(txn, m) + return messagingManager.getMessageText(m) // todo: use transactional API call somehow } catch (e: DbException) { LOG.warn(e) {} } @@ -226,13 +222,8 @@ constructor( if (e.contactId == _contactId.value) { LOG.info("Message received, adding") val h = e.messageHeader - if (h is PrivateMessageHeader) { - // insert at start of list according to descending sort order - runOnDbThreadWithTransaction(true) { txn -> - val msg = messageHeaderToItem(txn, h) - txn.attach { _messages.add(0, msg) } - } - } + // insert at start of list according to descending sort order + _messages.add(0, h.accept(conversationVisitor.value)!!) } } is MessagesSentEvent -> { diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt new file mode 100644 index 0000000000000000000000000000000000000000..a17f736652d198c8c25d08e1eaf0747ba07498b0 --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt @@ -0,0 +1,198 @@ +package org.briarproject.briar.desktop.conversation + +import mu.KotlinLogging +import org.briarproject.bramble.api.db.DatabaseExecutor +import org.briarproject.bramble.api.db.Transaction +import org.briarproject.bramble.api.sync.MessageId +import org.briarproject.briar.api.blog.BlogInvitationRequest +import org.briarproject.briar.api.blog.BlogInvitationResponse +import org.briarproject.briar.api.conversation.ConversationMessageVisitor +import org.briarproject.briar.api.forum.ForumInvitationRequest +import org.briarproject.briar.api.forum.ForumInvitationResponse +import org.briarproject.briar.api.introduction.IntroductionRequest +import org.briarproject.briar.api.introduction.IntroductionResponse +import org.briarproject.briar.api.messaging.PrivateMessageHeader +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF + +internal class ConversationVisitor( + private val contactName: String, + private val loadMessageText: (MessageId) -> String? +) : ConversationMessageVisitor<ConversationItem?> { + + companion object { + private val LOG = KotlinLogging.logger {} + } + + @DatabaseExecutor + override fun visitPrivateMessageHeader(h: PrivateMessageHeader): ConversationItem { + val item = ConversationMessageItem(h) + if (h.hasText()) { + item.text = loadMessageText(h.id) + } else { + LOG.warn { "private message without text" } + } + return item + } + + override fun visitBlogInvitationRequest(r: BlogInvitationRequest): ConversationItem { + + return if (r.isLocal) + ConversationNoticeItem( + i18nF("blogs_sharing_invitation_sent", r.name, contactName), + r + ) + else + ConversationRequestItem( + i18nF("blogs_sharing_invitation_received", contactName, r.name), + ConversationRequestItem.RequestType.BLOG, r + ) + } + + override fun visitBlogInvitationResponse(r: BlogInvitationResponse): ConversationItem { + return if (r.isLocal) { + val text = when { + r.wasAccepted() -> + i18nF("blogs_sharing_response_accepted_sent", contactName) + r.isAutoDecline -> + i18nF("blogs_sharing_response_declined_auto", contactName) + else -> + i18nF("blogs_sharing_response_declined_sent", contactName) + } + ConversationNoticeItem(text, r) + } else { + val text = when { + r.wasAccepted() -> + i18nF("blogs_sharing_response_accepted_received", contactName) + else -> + i18nF("blogs_sharing_response_declined_received", contactName) + } + ConversationNoticeItem(text, r) + } + } + + override fun visitForumInvitationRequest(r: ForumInvitationRequest): ConversationItem { + return if (r.isLocal) + ConversationNoticeItem( + i18nF("forum_invitation_sent", r.name, contactName), + r + ) + else + ConversationRequestItem( + i18nF("forum_invitation_received", contactName, r.name), + ConversationRequestItem.RequestType.FORUM, r + ) + } + + override fun visitForumInvitationResponse(r: ForumInvitationResponse): ConversationItem { + return if (r.isLocal) { + val text = when { + r.wasAccepted() -> + i18nF("forum_invitation_response_accepted_sent", contactName) + r.isAutoDecline -> + i18nF("forum_invitation_response_declined_auto", contactName) + else -> + i18nF("forum_invitation_response_declined_sent", contactName) + } + ConversationNoticeItem(text, r) + } else { + val text = when { + r.wasAccepted() -> + i18nF("forum_invitation_response_accepted_received", contactName) + else -> + i18nF("forum_invitation_response_declined_received", contactName) + } + ConversationNoticeItem(text, r) + } + } + + override fun visitGroupInvitationRequest(r: GroupInvitationRequest): ConversationItem { + return if (r.isLocal) + ConversationNoticeItem( + i18nF("groups_invitations_invitation_sent", contactName, r.name), + r + ) + else + ConversationRequestItem( + i18nF("groups_invitations_invitation_received", contactName, r.name), + ConversationRequestItem.RequestType.GROUP, r + ) + } + + override fun visitGroupInvitationResponse(r: GroupInvitationResponse): ConversationItem { + return if (r.isLocal) { + val text = when { + r.wasAccepted() -> + i18nF("groups_invitations_response_accepted_sent", contactName) + r.isAutoDecline -> + i18nF("groups_invitations_response_declined_auto", contactName) + else -> + i18nF("groups_invitations_response_declined_sent", contactName) + } + ConversationNoticeItem(text, r) + } else { + val text = when { + r.wasAccepted() -> + i18nF("groups_invitations_response_accepted_received", contactName) + else -> + i18nF("groups_invitations_response_declined_received", contactName) + } + ConversationNoticeItem(text, r) + } + } + + override fun visitIntroductionRequest(r: IntroductionRequest): ConversationItem { + // todo: use displayName logic somehow? + val name = r.nameable.name + return if (r.isLocal) + ConversationNoticeItem( + i18nF("introduction_request_sent", contactName, name), + r + ) + else { + val text = when { + r.wasAnswered() -> + i18nF("introduction_request_answered_received", contactName, name) + r.isContact -> + i18nF("introduction_request_exists_received", contactName, name) + else -> + i18nF("introduction_request_received", contactName, name) + } + ConversationRequestItem( + text, + ConversationRequestItem.RequestType.INTRODUCTION, r + ) + } + } + + override fun visitIntroductionResponse(r: IntroductionResponse): ConversationItem { + // todo: use displayName logic somehow? + val name = r.introducedAuthor.name + return if (r.isLocal) { + val text = when { + r.wasAccepted() -> { + val suffix = if (r.canSucceed()) + "\n\n" + i18nF("introduction_response_accepted_sent_info", name) + else "" + i18nF("introduction_request_answered_received", name) + suffix + } + r.isAutoDecline -> + i18nF("introduction_response_declined_auto", name) + else -> + i18nF("introduction_response_declined_sent", name) + } + ConversationNoticeItem(text, r) + } else { + val text = when { + r.wasAccepted() -> + i18nF("introduction_response_accepted_received", contactName, name) + r.isIntroducer -> + i18nF("introduction_response_declined_received", contactName, name) + else -> + i18nF("introduction_response_declined_received_by_introducee", contactName, name) + } + ConversationNoticeItem(text, r) + } + } +}