diff --git a/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt b/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt
index 2deeab1cd8be2a92dfae507872081dfeed8ea7b8..5871785d32033032013fd64d9b5d3ad5d346242f 100644
--- a/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt
+++ b/briar-desktop/src/automatedScreenshots/kotlin/org/briarproject/briar/desktop/ScreenshotTest.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
@@ -42,9 +42,11 @@ import org.briarproject.bramble.api.plugin.LanTcpConstants
 import org.briarproject.briar.BriarCoreEagerSingletons
 import org.briarproject.briar.desktop.TestUtils.getDataDir
 import org.briarproject.briar.desktop.contact.ContactListViewModel
-import org.briarproject.briar.desktop.ui.LocalWindowFocusState
-import org.briarproject.briar.desktop.ui.LocalWindowScope
-import org.briarproject.briar.desktop.ui.WindowFocusState
+import org.briarproject.briar.desktop.forums.ForumViewModel
+import org.briarproject.briar.desktop.navigation.SidebarViewModel
+import org.briarproject.briar.desktop.ui.LocalWindowInfo
+import org.briarproject.briar.desktop.ui.UiMode
+import org.briarproject.briar.desktop.ui.WindowInfo
 import org.jetbrains.annotations.NonNls
 import org.jetbrains.skia.Image
 import org.junit.Test
@@ -53,7 +55,7 @@ import java.io.FileOutputStream
 @OptIn(ExperimentalTestApi::class)
 class ScreenshotTest {
 
-    private fun DesktopComposeUiTest.start(): BriarDesktopTestApp {
+    private fun DesktopComposeUiTest.start(windowWidth: Int): BriarDesktopTestApp {
         // TODO: unify with interactive tests
         val dataDir = getDataDir()
         val app =
@@ -70,16 +72,19 @@ class ScreenshotTest {
         BrambleCoreEagerSingletons.Helper.injectEagerSingletons(app)
         BriarCoreEagerSingletons.Helper.injectEagerSingletons(app)
 
-        val windowScope = object : FrameWindowScope {
-            // would be needed for launching dialogs like the image picker
-            override val window: ComposeWindow get() = TODO()
+        val windowInfo = object : WindowInfo {
+            override val windowScope = object : FrameWindowScope {
+                override val window: ComposeWindow
+                    // would be needed for launching dialogs like the image picker
+                    get() = error("no ComposeWindow available in automatedScreenshots")
+            }
+            override val windowWidth: Int = windowWidth
+            override var focused: Boolean = true
         }
-        val windowFocusState = WindowFocusState().apply { focused = true }
 
         setContent {
             CompositionLocalProvider(
-                LocalWindowScope provides windowScope,
-                LocalWindowFocusState provides windowFocusState,
+                LocalWindowInfo provides windowInfo,
             ) {
                 app.getBriarUi().content()
             }
@@ -105,7 +110,7 @@ class ScreenshotTest {
     @Test
     fun makeScreenshot() = runDesktopComposeUiTest(700, 700) {
         runBlocking {
-            val app = start()
+            val app = start(windowWidth = 700)
 
             // close expiration banner and move mouse outside of window (to avoid hover effect)
             // TODO: *sometimes* leads to Compose crash (somehow connected to Tooltip?)
@@ -130,22 +135,38 @@ class ScreenshotTest {
                     "Faythe"
                 )
 
-                val viewModel = getViewModelProvider().get(ContactListViewModel::class)
+                val sidebarViewModel = getViewModelProvider().get(SidebarViewModel::class)
+                val contactListViewModel = getViewModelProvider().get(ContactListViewModel::class)
+                val forumViewModel = getViewModelProvider().get(ForumViewModel::class)
 
                 // give IO executor some time to add contacts and messages
                 delay(10_000)
-                waitUntil(60_000) { viewModel.combinedContactList.value.size > 2 }
+                waitUntil(60_000) { contactListViewModel.combinedContactList.value.size >= 2 }
 
                 // select Bob in list of contacts and wait for the chat history to load
-                viewModel.selectContact(viewModel.combinedContactList.value[1])
+                contactListViewModel.selectContact(contactListViewModel.combinedContactList.value[1])
+                awaitIdle()
+
+                // contact list screenshot
+                captureToImage().saveAsScreenshot("contact-list")
+
+                sidebarViewModel.setUiMode(UiMode.FORUMS)
+                waitUntil(60_000) { forumViewModel.forumList.value.size >= 2 }
+
+                // select the second forum in the list and wait for the forum posts to load
+                forumViewModel.selectGroup(forumViewModel.forumList.value[1])
                 awaitIdle()
 
-                captureToImage().save("contact-list.png")
+                // forum list screenshot
+                captureToImage().saveAsScreenshot("forum-list")
             }
         }
     }
 }
 
+private fun Image.saveAsScreenshot(name: String) =
+    save("../utils/screenshots/$name.png")
+
 private fun Image.save(file: String) {
     encodeToData()?.bytes?.let { bytes ->
         FileOutputStream(file).use { out ->
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt
index 6c11e914aafe51f517ae0c29db65720adb910a59..3fe54c14d46c0e09c0d26a63f7799d2e3b232455 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.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
@@ -53,7 +53,7 @@ import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.theme.sendButton
 import org.briarproject.briar.desktop.ui.ColoredIconButton
 import org.briarproject.briar.desktop.ui.HorizontalDivider
-import org.briarproject.briar.desktop.ui.LocalWindowScope
+import org.briarproject.briar.desktop.ui.getWindowScope
 import org.briarproject.briar.desktop.utils.ImagePicker.pickImageUsingDialog
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
@@ -112,7 +112,7 @@ fun ConversationInput(
                 unfocusedIndicatorColor = MaterialTheme.colors.background
             ),
             leadingIcon = {
-                val windowScope = LocalWindowScope.current!!
+                val windowScope = getWindowScope()
                 ColoredIconButton(
                     icon = if (image == null) Icons.Filled.Add else Icons.Filled.Close,
                     contentDescription = if (image == null) i18n("access.attachment_add") else i18n("access.attachment_remove"),
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..6b143789d309dc413dbb1eaebd9d258274a21244 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
@@ -58,10 +58,10 @@ import org.briarproject.briar.desktop.theme.Blue500
 import org.briarproject.briar.desktop.theme.divider
 import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
 import org.briarproject.briar.desktop.ui.HorizontalDivider
-import org.briarproject.briar.desktop.ui.LocalWindowScope
 import org.briarproject.briar.desktop.ui.Tooltip
 import org.briarproject.briar.desktop.ui.TrustIndicatorShort
 import org.briarproject.briar.desktop.ui.VerticalDivider
+import org.briarproject.briar.desktop.ui.getWindowWidth
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
@@ -221,7 +221,7 @@ fun NestingLevelView(
 }
 
 @Composable
-fun getMaxNestingLevel(): Int = when (LocalWindowScope.current!!.window.width.dp) {
+fun getMaxNestingLevel(): Int = when (getWindowWidth()) {
     in (0.dp..COLUMN_WIDTH * 2) -> 5
     in (COLUMN_WIDTH * 2..COLUMN_WIDTH * 3) -> 10
     in (COLUMN_WIDTH * 2..COLUMN_WIDTH * 4) -> 25
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 9824f142a92feedcaa3bc8910976018501774fc5..f8d2af51b3634d04b186ed861c15d5da8900914c 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -28,7 +28,6 @@ import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
 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 androidx.compose.ui.Modifier
@@ -37,7 +36,6 @@ import androidx.compose.ui.graphics.toAwtImage
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.window.FrameWindowScope
 import androidx.compose.ui.window.Window
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.event.EventListener
@@ -88,7 +86,6 @@ interface BriarUi {
     fun content()
 }
 
-val LocalWindowScope = staticCompositionLocalOf<FrameWindowScope?> { null }
 val LocalViewModelProvider = staticCompositionLocalOf<ViewModelProvider?> { null }
 val LocalAvatarManager = staticCompositionLocalOf<AvatarManager?> { null }
 val LocalConfiguration = staticCompositionLocalOf<Configuration?> { null }
@@ -122,12 +119,12 @@ constructor(
 
     @Composable
     override fun start(onClose: () -> Unit) {
-        val focusState = remember { WindowFocusState() }
-
         Window(
             title = Strings.APP_NAME,
             onCloseRequest = onClose,
         ) {
+            val windowInfo = rememberWindowInfo(this)
+
             // changing the icon in the Composable itself automatically brings the window to front
             // see https://github.com/JetBrains/compose-jb/issues/1861
             // therefore the icon is set here on the AWT Window
@@ -144,19 +141,19 @@ constructor(
 
                 val focusListener = object : WindowFocusListener {
                     override fun windowGainedFocus(e: WindowEvent?) {
-                        focusState.focused = true
+                        windowInfo.focused = true
                         window.iconImage = iconNormal
                     }
 
                     override fun windowLostFocus(e: WindowEvent?) {
-                        focusState.focused = false
+                        windowInfo.focused = false
                         // reset notification cool-down
                         lastNotificationPrivateMessage = 0
                         lastNotificationForum = 0
                     }
                 }
                 val messageCounterListener: MessageCounterListener = { (type, total, groups, inc) ->
-                    if (inc && total > 0 && !focusState.focused) {
+                    if (inc && total > 0 && !windowInfo.focused) {
                         val callback: NotificationProvider.() -> Unit = when (type) {
                             PrivateMessage -> {
                                 { notifyPrivateMessages(total, groups) }
@@ -200,8 +197,7 @@ constructor(
             }
 
             CompositionLocalProvider(
-                LocalWindowScope provides this,
-                LocalWindowFocusState provides focusState,
+                LocalWindowInfo provides windowInfo,
             ) {
                 // invalidate whole application window in case the theme, language or UI scale
                 // setting is changed
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt
deleted file mode 100644
index b16c3694adbae82f0d490f580f4caba3f61503aa..0000000000000000000000000000000000000000
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowFocusState.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.staticCompositionLocalOf
-
-class WindowFocusState {
-    var focused by mutableStateOf(false)
-}
-
-val LocalWindowFocusState = staticCompositionLocalOf<WindowFocusState?> { null }
-
-@Composable
-fun isWindowFocused() = checkNotNull(LocalWindowFocusState.current) {
-    "No WindowFocusState was provided via LocalWindowFocusState" // NON-NLS
-}.focused
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e0ed05f2e8255b9d6a4d1938905ab5dfaadb7a44
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/WindowInfo.kt
@@ -0,0 +1,61 @@
+/*
+ * Briar Desktop
+ * 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
+ * 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.runtime.Composable
+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 androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.window.FrameWindowScope
+
+interface WindowInfo {
+    val windowScope: FrameWindowScope
+    val windowWidth: Int
+    var focused: Boolean
+}
+
+@Composable
+fun rememberWindowInfo(windowScope: FrameWindowScope): WindowInfo =
+    remember { WindowInfoImpl(windowScope) }
+
+private class WindowInfoImpl(
+    override val windowScope: FrameWindowScope,
+    override val windowWidth: Int = windowScope.window.width,
+) : WindowInfo {
+    override var focused by mutableStateOf(false)
+}
+
+val LocalWindowInfo = staticCompositionLocalOf<WindowInfo?> { null }
+
+@Composable
+private fun getWindowInfo() = checkNotNull(LocalWindowInfo.current) {
+    "No WindowInfo was provided via LocalWindowFocusState" // NON-NLS
+}
+
+@Composable
+fun isWindowFocused() = getWindowInfo().focused
+
+@Composable
+fun getWindowScope() = getWindowInfo().windowScope
+
+@Composable
+fun getWindowWidth() = with(LocalDensity.current) { getWindowInfo().windowWidth.toDp() }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
index 60c4ffed5d9e9ebc9c5f27bd5fc3b890504ae123..bc1a00fc8c4b04ede810ed9ad5cf0481c30dfc84 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
@@ -54,9 +54,8 @@ import androidx.compose.ui.window.singleWindowApplication
 import org.briarproject.bramble.api.UniqueId
 import org.briarproject.briar.desktop.settings.UnencryptedSettings
 import org.briarproject.briar.desktop.theme.BriarTheme
-import org.briarproject.briar.desktop.ui.LocalWindowFocusState
-import org.briarproject.briar.desktop.ui.LocalWindowScope
-import org.briarproject.briar.desktop.ui.WindowFocusState
+import org.briarproject.briar.desktop.ui.LocalWindowInfo
+import org.briarproject.briar.desktop.ui.rememberWindowInfo
 import org.briarproject.briar.desktop.utils.UiUtils.DensityDimension
 import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
 import org.jetbrains.annotations.NonNls
@@ -217,10 +216,9 @@ object PreviewUtils {
         val settingsDensity: Float? = prefs.get("previewsUiScale", null)?.toFloat()
 
         singleWindowApplication(title = "Interactive Preview") {
-            val focusState = remember { WindowFocusState() }
+            val windowInfo = rememberWindowInfo(this)
             CompositionLocalProvider(
-                LocalWindowScope provides this,
-                LocalWindowFocusState provides focusState
+                LocalWindowInfo provides windowInfo
             ) {
                 Column {
                     val density = settingsDensity ?: LocalDensity.current.density