diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
index 501c155c41de3e98b0d898839c156cf1ea14636a..d803d29109dd732dc53fe3317feb896980ae88a4 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
@@ -78,11 +78,10 @@ class PrivateGroupConversationViewModel @Inject constructor(
 
     override fun loadThreadItems(txn: Transaction, groupId: GroupId) =
         privateGroupManager.getHeaders(txn, groupId).map { header ->
-            PrivateGroupMessageItem(
-                header,
-                if (header !is JoinMessageHeader) privateGroupManager.getMessageText(txn, header.id)
-                else "" // todo
-            )
+            if (header is JoinMessageHeader)
+                PrivateGroupJoinItem(header)
+            else
+                PrivateGroupMessageItem(header, privateGroupManager.getMessageText(txn, header.id))
         }
 
     @UiExecutor
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt
index 546ff08f3441e4074a3433c877665e4a0aabb93b..aef62faf056b40d5109b75fb363f3560610deee5 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupMessageItem.kt
@@ -18,12 +18,16 @@
 
 package org.briarproject.briar.desktop.privategroup.conversation
 
+import org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES
 import org.briarproject.briar.api.privategroup.GroupMessageHeader
+import org.briarproject.briar.api.privategroup.JoinMessageHeader
 import org.briarproject.briar.desktop.threadedgroup.conversation.ThreadItem
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
 import javax.annotation.concurrent.NotThreadSafe
 
 @NotThreadSafe
-class PrivateGroupMessageItem(h: GroupMessageHeader, text: String) : ThreadItem(
+open class PrivateGroupMessageItem(h: GroupMessageHeader, text: String) : ThreadItem(
     messageId = h.id,
     parentId = h.parentId,
     text = text,
@@ -32,3 +36,21 @@ class PrivateGroupMessageItem(h: GroupMessageHeader, text: String) : ThreadItem(
     authorInfo = h.authorInfo,
     isRead = h.isRead
 )
+
+class PrivateGroupJoinItem(h: JoinMessageHeader) : PrivateGroupMessageItem(h, "") {
+    val isInitial = h.isInitial
+}
+
+val ThreadItem.isMeta: Boolean get() = this is PrivateGroupJoinItem
+
+val ThreadItem.metaText: String
+    get() {
+        if (this !is PrivateGroupJoinItem) throw IllegalArgumentException()
+        return if (isInitial) {
+            if (authorInfo.status == OURSELVES) i18n("group.meta.created.you")
+            else i18nF("group.meta.created.other", authorName)
+        } else {
+            if (authorInfo.status == OURSELVES) i18n("group.meta.joined.you")
+            else i18nF("group.meta.joined.other", authorName)
+        }
+    }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt
index 64cc1ec225e0b2ee8ca12e0b909308266c98cd94..2a0b46e0828199dbe3d4d91c3d12469728ef74f1 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadItemView.kt
@@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment.Companion.Center
 import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight.Companion.Bold
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
@@ -54,6 +55,8 @@ import androidx.compose.ui.unit.dp
 import org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES
 import org.briarproject.briar.desktop.contact.ProfileCircle
 import org.briarproject.briar.desktop.forum.conversation.ForumPostItem
+import org.briarproject.briar.desktop.privategroup.conversation.isMeta
+import org.briarproject.briar.desktop.privategroup.conversation.metaText
 import org.briarproject.briar.desktop.theme.Blue500
 import org.briarproject.briar.desktop.theme.divider
 import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
@@ -65,6 +68,7 @@ import org.briarproject.briar.desktop.ui.VerticalDivider
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.UiUtils.modifyIf
 import org.briarproject.briar.desktop.utils.getRandomForumPostHeader
 import org.briarproject.briar.desktop.utils.getRandomString
 import kotlin.math.min
@@ -110,24 +114,24 @@ fun ThreadItemView(
         Column(
             modifier = Modifier
                 .fillMaxWidth()
-                .then(
-                    if (isSelected) {
-                        Modifier.border(3.dp, Blue500)
-                    } else Modifier
-                ).selectable(
-                    selected = isSelected,
-                    onClick = { onPostSelected(item) }
+                // only make normal thread items selectable
+                .modifyIf(
+                    !item.isMeta,
+                    Modifier.selectable(
+                        selected = isSelected,
+                        onClick = { onPostSelected(item) }
+                    ).modifyIf(isSelected, Modifier.border(3.dp, Blue500))
                 ),
         ) {
             HorizontalDivider()
-            ThreadItemContentComposable(item)
+            ThreadItemContent(item)
         }
     }
 }
 
 @Composable
 @OptIn(ExperimentalFoundationApi::class)
-fun ThreadItemContentComposable(
+fun ThreadItemContent(
     item: ThreadItem,
     modifier: Modifier = Modifier,
     isPreview: Boolean = false,
@@ -167,7 +171,15 @@ fun ThreadItemContentComposable(
             // should be changed back to verticalArrangement = spacedBy(8.dp) on the containing Column
             // when https://github.com/JetBrains/compose-jb/issues/2729 is fixed
             Spacer(Modifier.height(8.dp))
-            if (!isPreview) {
+            if (item.isMeta) {
+                // italicised text for special thread items
+                Text(
+                    modifier = Modifier.fillMaxWidth(),
+                    text = item.metaText,
+                    fontStyle = FontStyle.Italic,
+                )
+            } else if (!isPreview) {
+                // selectable text for normal thread items which are not previewed for reply
                 SelectionContainer {
                     Text(
                         modifier = Modifier.fillMaxWidth(),
@@ -175,6 +187,7 @@ fun ThreadItemContentComposable(
                     )
                 }
             } else {
+                // shorten normal thread items when previewed for reply
                 Text(
                     modifier = Modifier.fillMaxWidth(),
                     text = item.text,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationInput.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationInput.kt
index 89b77665cb5f6c0d40571853fca21bf753ca2a0c..f61e6445c377bf209bba054afe4aecd4085d3ff8 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationInput.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationInput.kt
@@ -83,7 +83,7 @@ fun ThreadedGroupConversationInput(
                         text = strings.messageReplyIntro,
                         modifier = Modifier.padding(bottom = 8.dp),
                     )
-                    ThreadItemContentComposable(
+                    ThreadItemContent(
                         item = selectedThreadItem,
                         isPreview = true,
                         modifier = Modifier
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt
index cd88bf24c6910b3bf1d2f0d0db5c7f1e9c60ebd5..2e5be7248705468649ee08ab90ad4323fec9289c 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * Copyright (C) 2021-2023 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
@@ -19,6 +19,7 @@
 package org.briarproject.briar.desktop.utils
 
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
@@ -78,4 +79,9 @@ object UiUtils {
             return Dimension(width.dp.roundToPx(), height.dp.roundToPx())
         }
     }
+
+    fun Modifier.modifyIf(
+        condition: Boolean,
+        modifier: Modifier,
+    ) = if (condition) then(modifier) else this
 }
diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
index a088ec95c26ab4a51d8932a38c3e500a22622bca..a16d623948c096b66b647693bb2c109d1cb6902f 100644
--- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties
+++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
@@ -187,6 +187,10 @@ group.sharing.status.with=Shared with {0} ({1} online)
 group.message.hint=New Message
 group.message.reply.hint=New Reply
 group.message.reply.intro=Reply to:
+group.meta.created.you=You created the group
+group.meta.created.other={0} created the group
+group.meta.joined.you=You joined the group
+group.meta.joined.other={0} joined the group
 group.leave.title=Leave Group
 group.leave.dialog.title=Confirm Leaving Group
 group.leave.dialog.message=Are you sure that you want to leave this private group?