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 6a9614840ae79b811dca85015f5a83d0a47a93f5..aab9ac91388deca657d2415ac98cba035822ba87 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
@@ -4,7 +4,9 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.MaterialTheme
@@ -33,6 +35,8 @@ import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import java.time.Instant
 
 fun main() = preview(
+    "canBeOpened" to false,
+    "answered" to false,
     "notice" to "Text of notice message.",
     "text" to "Short message",
     "time" to Instant.now().toEpochMilli(),
@@ -43,10 +47,10 @@ fun main() = preview(
 ) {
     ConversationRequestItemView(
         ConversationRequestItem(
-            requestedGroupId = null,
+            requestedGroupId = if (getBooleanParameter("canBeOpened")) GroupId(getRandomId()) else null,
             requestType = INTRODUCTION,
             sessionId = SessionId(getRandomId()),
-            answered = false,
+            answered = getBooleanParameter("answered"),
             notice = getStringParameter("notice"),
             text = getStringParameter("text"),
             id = MessageId(getRandomId()),
@@ -64,8 +68,8 @@ fun main() = preview(
 @Composable
 fun ConversationRequestItemView(
     m: ConversationRequestItem,
-    onAccept: () -> Unit = {},
-    onDecline: () -> Unit = {}
+    onResponse: (Boolean) -> Unit = {},
+    onOpenRequestedShareable: () -> Unit = {},
 ) {
     val statusAlignment = if (m.isIncoming) Alignment.End else Alignment.Start
     val textColor = if (m.isIncoming) MaterialTheme.colors.textPrimary else Color.White
@@ -89,20 +93,33 @@ fun ConversationRequestItemView(
                     color = noticeColor,
                     modifier = Modifier.align(Alignment.Start),
                 )
+
                 Row(modifier = Modifier.align(statusAlignment)) {
-                    TextButton(onDecline) {
-                        Text(
-                            i18n("decline").uppercase(),
-                            fontSize = 16.sp,
-                            color = MaterialTheme.colors.buttonTextNegative
-                        )
-                    }
-                    TextButton(onAccept) {
-                        Text(
-                            i18n("accept").uppercase(),
-                            fontSize = 16.sp,
-                            color = MaterialTheme.colors.buttonTextPositive
-                        )
+                    if (!m.answered) {
+                        TextButton({ onResponse(false) }) {
+                            Text(
+                                i18n("decline").uppercase(),
+                                fontSize = 16.sp,
+                                color = MaterialTheme.colors.buttonTextNegative
+                            )
+                        }
+                        TextButton({ onResponse(true) }) {
+                            Text(
+                                i18n("accept").uppercase(),
+                                fontSize = 16.sp,
+                                color = MaterialTheme.colors.buttonTextPositive
+                            )
+                        }
+                    } else if (m.canBeOpened) {
+                        TextButton(onOpenRequestedShareable) {
+                            Text(
+                                i18n("open").uppercase(),
+                                fontSize = 16.sp,
+                                color = MaterialTheme.colors.buttonTextPositive
+                            )
+                        }
+                    } else {
+                        Spacer(Modifier.height(8.dp))
                     }
                 }
                 ConversationItemStatusView(m)
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 3d2864931a30d35b4a76db2d38d2e0e1bd53f2fa..a842320ce8d40e3427ceeb7910a5600203fc2ed0 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
@@ -80,7 +80,11 @@ fun ConversationScreen(
                         when (m) {
                             is ConversationMessageItem -> ConversationMessageItemView(m)
                             is ConversationNoticeItem -> ConversationNoticeItemView(m)
-                            is ConversationRequestItem -> ConversationRequestItemView(m)
+                            is ConversationRequestItem ->
+                                ConversationRequestItemView(
+                                    m,
+                                    onResponse = { accept -> viewModel.respondToRequest(m, accept) },
+                                )
                         }
                     }
                 }
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 d5da4f1587abe48fff4a075dc4dc1c31adc1febc..857f736f317ee56fdf61c63820b5930490cd0965 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -28,11 +28,13 @@ import org.briarproject.briar.api.autodelete.UnexpectedTimerException
 import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
+import org.briarproject.briar.api.introduction.IntroductionManager
 import org.briarproject.briar.api.messaging.MessagingManager
 import org.briarproject.briar.api.messaging.PrivateMessage
 import org.briarproject.briar.api.messaging.PrivateMessageFactory
 import org.briarproject.briar.api.messaging.PrivateMessageHeader
 import org.briarproject.briar.desktop.contact.ContactItem
+import org.briarproject.briar.desktop.conversation.ConversationRequestItem.RequestType.INTRODUCTION
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration
 import org.briarproject.briar.desktop.utils.clearAndAddAll
@@ -49,6 +51,7 @@ constructor(
     private val connectionRegistry: ConnectionRegistry,
     private val contactManager: ContactManager,
     private val conversationManager: ConversationManager,
+    private val introductionManager: IntroductionManager,
     private val messagingManager: MessagingManager,
     private val privateMessageFactory: PrivateMessageFactory,
     briarExecutors: BriarExecutors,
@@ -190,7 +193,6 @@ constructor(
             LOG.logDuration("Loading message headers", start)
             // Sort headers by timestamp in *descending* order
             val sorted = headers.sortedByDescending { it.timestamp }
-            // todo: use ConversationVisitor to also display Request and Notice Messages
             start = LogUtils.now()
             val messages = sorted.map { h -> h.accept(conversationVisitor.value)!! }
             LOG.logDuration("Loading messages", start)
@@ -275,4 +277,18 @@ constructor(
             it.mark(sent, seen)
         }
     }
+
+    fun respondToRequest(item: ConversationRequestItem, accept: Boolean) {
+        _messages.replaceIf({ item == it }) {
+            item.markAnswered()
+        }
+        runOnDbThreadWithTransaction(false) { txn ->
+            when (item.requestType) {
+                INTRODUCTION ->
+                    introductionManager.respondToIntroduction(txn, _contactId.value!!, item.sessionId, accept)
+                else ->
+                    throw IllegalArgumentException("Only introduction requests are supported for the time being.")
+            }
+        }
+    }
 }
diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties
index 9321ee2aff7de8490544e073922e9a63a5d42cf6..472217b34f4387b39ad27de257eb3a50e7171156 100644
--- a/src/main/resources/strings/BriarDesktop.properties
+++ b/src/main/resources/strings/BriarDesktop.properties
@@ -91,6 +91,7 @@ main.help.tor.port.control=Tor Control Port
 password=Password
 accept=Accept
 decline=Decline
+open=Open
 unsupported_feature=Unfortunately, this feature is not yet supported by Briar Desktop.
 
 # Registration screen