diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
index 72443f0..dd4f8a4 100644
--- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
+++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
@@ -171,7 +171,7 @@ internal class FeatureModules internal constructor(
             coroutineDispatchers
         )
     }
-    val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
+    val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room) }
     val notificationsModule by unsafeLazy {
         NotificationsModule(
             matrixModules.push,
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
index 89753c5..826694b 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
@@ -27,14 +27,13 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
     }
 
     Column {
-        Toolbar(
-            onNavigate = navigateAndPopStack,
-            title = currentPage.label
-        )
-
-        currentPage.parent?.let {
-            BackHandler(onBack = navigateAndPopStack)
+        if (currentPage.hasToolbar) {
+            Toolbar(
+                onNavigate = navigateAndPopStack,
+                title = currentPage.label
+            )
         }
+        BackHandler(onBack = navigateAndPopStack)
         computedWeb[currentPage.route]!!.invoke(currentPage.state)
     }
 }
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
index d76316e..6fdc9fa 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
@@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
 @Composable
-fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null) {
+fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) {
     val modifier = Modifier.padding(horizontal = 24.dp)
     Column(
         Modifier
@@ -31,6 +31,7 @@ fun TextRow(title: String, content: String? = null, includeDivider: Boolean = tr
                     Text(text = content, fontSize = 18.sp)
                 }
             }
+            body()
             Spacer(modifier = Modifier.height(24.dp))
         }
         if (includeDivider) {
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
index 7c6a9c6..b9bb5f8 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
@@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite
 import app.dapk.st.matrix.sync.RoomOverview
 import com.squareup.sqldelight.runtime.coroutines.asFlow
 import com.squareup.sqldelight.runtime.coroutines.mapToList
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.withContext
 import kotlinx.serialization.json.Json
 
 private val json = Json
@@ -31,11 +29,22 @@ internal class OverviewPersistence(
             .map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
     }
 
+    override suspend fun removeRooms(roomsToRemove: List<RoomId>) {
+        dispatchers.withIoContext {
+            database.transaction {
+                roomsToRemove.forEach {
+                    database.inviteStateQueries.remove(it.value)
+                    database.overviewStateQueries.remove(it.value)
+                }
+            }
+        }
+    }
+
     override suspend fun persistInvites(invites: List<RoomInvite>) {
         dispatchers.withIoContext {
             database.inviteStateQueries.transaction {
                 invites.forEach {
-                    database.inviteStateQueries.insert(it.roomId.value)
+                    database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
                 }
             }
         }
@@ -45,7 +54,15 @@ internal class OverviewPersistence(
         return database.inviteStateQueries.selectAll()
             .asFlow()
             .mapToList()
-            .map { it.map { RoomInvite(RoomId(it)) } }
+            .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
+    }
+
+    override suspend fun removeInvites(invites: List<RoomId>) {
+        dispatchers.withIoContext {
+            database.inviteStateQueries.transaction {
+                invites.forEach { database.inviteStateQueries.remove(it.value) }
+            }
+        }
     }
 
     override suspend fun persist(overviewState: OverviewState) {
@@ -59,7 +76,7 @@ internal class OverviewPersistence(
     }
 
     override suspend fun retrieve(): OverviewState {
-        return withContext(Dispatchers.IO) {
+        return dispatchers.withIoContext {
             val overviews = database.overviewStateQueries.selectAll().executeAsList()
             overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
         }
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
index 24f06d4..451effd 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
@@ -37,6 +37,13 @@ internal class RoomPersistence(
         }
     }
 
+    override suspend fun remove(rooms: List<RoomId>) {
+        coroutineDispatchers
+        database.roomEventQueries.transaction {
+            rooms.forEach { database.roomEventQueries.remove(it.value) }
+        }
+    }
+
     override fun latest(roomId: RoomId): Flow<RoomState> {
         val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
             json.decodeFromString(RoomOverview.serializer(), it)
diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq
index d29ad00..d30ddbe 100644
--- a/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq
+++ b/domains/store/src/main/sqldelight/app/dapk/db/model/InviteState.sq
@@ -1,12 +1,17 @@
 CREATE TABLE dbInviteState (
     room_id TEXT NOT NULL,
+    blob TEXT NOT NULL,
     PRIMARY KEY (room_id)
 );
 
 selectAll:
-SELECT room_id
+SELECT room_id, blob
 FROM dbInviteState;
 
 insert:
-INSERT OR REPLACE INTO dbInviteState(room_id)
-VALUES (?);
\ No newline at end of file
+INSERT OR REPLACE INTO dbInviteState(room_id, blob)
+VALUES (?, ?);
+
+remove:
+DELETE FROM dbInviteState
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
index afc9da1..100d397 100644
--- a/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
+++ b/domains/store/src/main/sqldelight/app/dapk/db/model/OverviewState.sq
@@ -18,4 +18,8 @@ WHERE room_id = ?;
 
 insert:
 INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
-VALUES (?, ?, ?, ?);
\ No newline at end of file
+VALUES (?, ?, ?, ?);
+
+remove:
+DELETE FROM dbOverviewState
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
index ce5a04b..46883cf 100644
--- a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
+++ b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomEvent.sq
@@ -30,4 +30,10 @@ WHERE event_id = ?;
 selectAllUnread:
 SELECT dbRoomEvent.blob, dbRoomEvent.room_id
 FROM dbUnreadEvent
-INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id;
+INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id
+ORDER BY dbRoomEvent.timestamp_utc DESC
+LIMIT 100;
+
+remove:
+DELETE FROM dbRoomEvent
+WHERE room_id = ?;
\ No newline at end of file
diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt
index ec85869..62b7605 100644
--- a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt
+++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt
@@ -1,6 +1,5 @@
 package app.dapk.st.home
 
-import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.layout.*
 import androidx.compose.material.*
 import androidx.compose.runtime.Composable
@@ -38,8 +37,9 @@ fun HomeScreen(homeViewModel: HomeViewModel) {
                                 when (state.page) {
                                     Directory -> DirectoryScreen(homeViewModel.directory())
                                     Profile -> {
-                                        BackHandler { homeViewModel.changePage(Directory) }
-                                        ProfileScreen(homeViewModel.profile())
+                                        ProfileScreen(homeViewModel.profile()) {
+                                            homeViewModel.changePage(Directory)
+                                        }
                                     }
                                 }
                             }
diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt
index f9fb12f..8ca8992 100644
--- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt
+++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt
@@ -15,7 +15,7 @@ class NotificationRenderer(
     private val notificationFactory: NotificationFactory,
 ) {
 
-    suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>) {
+    suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>, onlyContainsRemovals: Boolean) {
         removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
         val notifications = notificationFactory.createNotifications(result)
 
@@ -26,7 +26,11 @@ class NotificationRenderer(
         notifications.delegates.forEach {
             when (it) {
                 is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
-                is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
+                is NotificationDelegate.Room ->  {
+                    if (!onlyContainsRemovals) {
+                        notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
+                    }
+                }
             }
         }
 
diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
index 82b5219..87cd027 100644
--- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
+++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt
@@ -3,6 +3,7 @@ package app.dapk.st.notifications
 import app.dapk.st.core.AppLogTag.NOTIFICATION
 import app.dapk.st.core.log
 import app.dapk.st.matrix.common.RoomId
+import app.dapk.st.matrix.sync.RoomEvent
 import app.dapk.st.matrix.sync.RoomStore
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.drop
@@ -14,7 +15,7 @@ class NotificationsUseCase(
     notificationChannels: NotificationChannels,
 ) {
 
-    private val inferredCurrentNotifications = mutableSetOf<RoomId>()
+    private val inferredCurrentNotifications = mutableMapOf<RoomId, List<RoomEvent>>()
 
     init {
         notificationChannels.initChannels()
@@ -26,13 +27,17 @@ class NotificationsUseCase(
             .onEach { result ->
                 log(NOTIFICATION, "unread changed - render notifications")
 
-                val asRooms = result.keys.map { it.roomId }.toSet()
-                val removedRooms = inferredCurrentNotifications - asRooms
+                val changes = result.mapKeys { it.key.roomId }
 
+                val asRooms = changes.keys
+                val removedRooms = inferredCurrentNotifications.keys - asRooms
+
+                val onlyContainsRemovals =
+                    inferredCurrentNotifications.filterKeys { !removedRooms.contains(it) } == changes.filterKeys { !removedRooms.contains(it) }
                 inferredCurrentNotifications.clear()
-                inferredCurrentNotifications.addAll(asRooms)
+                inferredCurrentNotifications.putAll(changes)
 
-                notificationRenderer.render(result, removedRooms)
+                notificationRenderer.render(result, removedRooms, onlyContainsRemovals)
             }
             .collect()
     }
diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
index 8b845c7..5c6a0c9 100644
--- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
+++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/PushAndroidService.kt
@@ -28,8 +28,10 @@ class PushAndroidService : FirebaseMessagingService() {
     }
 
     override fun onNewToken(token: String) {
+        log(PUSH, "new push token received")
         GlobalScope.launch {
             module.pushUseCase().registerPush(token)
+            log(PUSH, "token registered")
         }
     }
 
diff --git a/features/profile/build.gradle b/features/profile/build.gradle
index efb3058..1e02634 100644
--- a/features/profile/build.gradle
+++ b/features/profile/build.gradle
@@ -2,6 +2,7 @@ applyAndroidLibraryModule(project)
 
 dependencies {
     implementation project(":matrix:services:sync")
+    implementation project(":matrix:services:room")
     implementation project(":matrix:services:profile")
     implementation project(":features:settings")
     implementation project(':domains:store')
diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt
index cc83b7b..0fb5111 100644
--- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt
+++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt
@@ -2,15 +2,17 @@ package app.dapk.st.profile
 
 import app.dapk.st.core.ProvidableModule
 import app.dapk.st.matrix.room.ProfileService
+import app.dapk.st.matrix.room.RoomService
 import app.dapk.st.matrix.sync.SyncService
 
 class ProfileModule(
     private val profileService: ProfileService,
     private val syncService: SyncService,
+    private val roomService: RoomService,
 ) : ProvidableModule {
 
     fun profileViewModel(): ProfileViewModel {
-        return ProfileViewModel(profileService, syncService)
+        return ProfileViewModel(profileService, syncService, roomService)
     }
 
 }
\ No newline at end of file
diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt
index 5587329..e3a0f0d 100644
--- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt
+++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt
@@ -1,12 +1,13 @@
 package app.dapk.st.profile
 
+import android.content.Context
 import android.content.Intent
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
+import androidx.compose.material.*
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.CameraAlt
 import androidx.compose.material.icons.filled.Settings
@@ -16,25 +17,44 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
+import app.dapk.st.core.Lce
 import app.dapk.st.core.LifecycleEffect
 import app.dapk.st.core.StartObserving
 import app.dapk.st.core.components.CenteredLoading
-import app.dapk.st.design.components.CircleishAvatar
-import app.dapk.st.design.components.Spider
-import app.dapk.st.design.components.TextRow
-import app.dapk.st.design.components.percentOfHeight
+import app.dapk.st.design.components.*
+import app.dapk.st.matrix.sync.InviteMeta
+import app.dapk.st.matrix.sync.RoomInvite
 import app.dapk.st.settings.SettingsActivity
 
 @Composable
-fun ProfileScreen(viewModel: ProfileViewModel) {
+fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
     viewModel.ObserveEvents()
 
-    LifecycleEffect(onStart = {
-        viewModel.start()
-    })
+    LifecycleEffect(
+        onStart = { viewModel.start() },
+        onStop = { viewModel.stop() }
+    )
 
     val context = LocalContext.current
 
+    val onNavigate: (SpiderPage<out Page>?) -> Unit = {
+        when (it) {
+            null -> onTopLevelBack()
+            else -> viewModel.goTo(it)
+        }
+    }
+    Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
+        item(Page.Routes.profile) {
+            ProfilePage(context, viewModel, it)
+        }
+        item(Page.Routes.invitation) {
+            Invitations(viewModel, it)
+        }
+    }
+}
+
+@Composable
+private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: Page.Profile) {
     Box(
         modifier = Modifier
             .fillMaxWidth()
@@ -45,20 +65,20 @@ fun ProfileScreen(viewModel: ProfileViewModel) {
         }
     }
 
-    when (val state = viewModel.state) {
-        ProfileScreenState.Loading -> CenteredLoading()
-        is ProfileScreenState.Content -> {
+    when (val state = profile.content) {
+        is Lce.Loading -> CenteredLoading()
+        is Lce.Content -> {
             val configuration = LocalConfiguration.current
-
+            val content = state.value
             Column {
                 Spacer(modifier = Modifier.fillMaxHeight(0.05f))
                 Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
-                    val fallbackLabel = state.me.displayName ?: state.me.userId.value
+                    val fallbackLabel = content.me.displayName ?: content.me.userId.value
                     val avatarSize = configuration.percentOfHeight(0.2f)
                     Box {
-                        CircleishAvatar(state.me.avatarUrl?.value, fallbackLabel, avatarSize)
+                        CircleishAvatar(content.me.avatarUrl?.value, fallbackLabel, avatarSize)
 
-                        // TODO enable once edit support it added
+                        // TODO enable once edit support is added
                         if (false) {
                             IconButton(modifier = Modifier
                                 .size(avatarSize * 0.314f)
@@ -76,26 +96,61 @@ fun ProfileScreen(viewModel: ProfileViewModel) {
 
                 TextRow(
                     title = "Display name",
-                    content = state.me.displayName ?: "Not set",
+                    content = content.me.displayName ?: "Not set",
                 )
                 TextRow(
                     title = "User id",
-                    content = state.me.userId.value,
+                    content = content.me.userId.value,
                 )
                 TextRow(
                     title = "Homeserver",
-                    content = state.me.homeServerUrl.value,
+                    content = content.me.homeServerUrl.value,
                 )
 
                 TextRow(
                     title = "Invitations",
-                    content = "${state.invitationsCount} pending",
+                    content = "${content.invitationsCount} pending",
+                    onClick = { viewModel.goToInvitations() }
                 )
             }
         }
     }
 }
 
+@Composable
+private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
+    when (val state = invitations.content) {
+        is Lce.Loading -> CenteredLoading()
+        is Lce.Content -> {
+            LazyColumn {
+                items(state.value) {
+                    val text = when (val meta = it.inviteMeta) {
+                        InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat"
+                        is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}"
+                    }
+
+                    TextRow(title = text, includeDivider = false) {
+                        Spacer(modifier = Modifier.height(4.dp))
+                        Row {
+                            Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) {
+                                Text("Reject".uppercase())
+                            }
+                            Spacer(modifier = Modifier.fillMaxWidth(0.1f))
+                            Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) {
+                                Text("Accept".uppercase())
+                            }
+                        }
+                    }
+
+                }
+            }
+        }
+        is Lce.Error -> TODO()
+    }
+}
+
+private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
+
 @Composable
 private fun ProfileViewModel.ObserveEvents() {
     val context = LocalContext.current
diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt
index ab5acac..5e0b6e2 100644
--- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt
+++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt
@@ -1,14 +1,30 @@
 package app.dapk.st.profile
 
+import app.dapk.st.core.Lce
+import app.dapk.st.design.components.Route
+import app.dapk.st.design.components.SpiderPage
+import app.dapk.st.matrix.common.RoomId
 import app.dapk.st.matrix.room.ProfileService
+import app.dapk.st.matrix.sync.RoomInvite
 
-sealed interface ProfileScreenState {
-    object Loading : ProfileScreenState
-    data class Content(
-        val me: ProfileService.Me,
-        val invitationsCount: Int,
-    ) : ProfileScreenState
+data class ProfileScreenState(
+    val page: SpiderPage<out Page>,
+)
 
+sealed interface Page {
+    data class Profile(val content: Lce<Content>) : Page {
+        data class Content(
+            val me: ProfileService.Me,
+            val invitationsCount: Int,
+        )
+    }
+
+    data class Invitations(val content: Lce<List<RoomInvite>>): Page
+
+    object Routes {
+        val profile = Route<Profile>("Profile")
+        val invitation = Route<Invitations>("Invitations")
+    }
 }
 
 sealed interface ProfileEvent {
diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt
index 3bb17b2..484cdcf 100644
--- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt
+++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt
@@ -1,27 +1,74 @@
 package app.dapk.st.profile
 
 import androidx.lifecycle.viewModelScope
+import app.dapk.st.core.Lce
+import app.dapk.st.design.components.SpiderPage
+import app.dapk.st.matrix.common.RoomId
 import app.dapk.st.matrix.room.ProfileService
+import app.dapk.st.matrix.room.RoomService
 import app.dapk.st.matrix.sync.SyncService
 import app.dapk.st.viewmodel.DapkViewModel
-import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
 
 class ProfileViewModel(
     private val profileService: ProfileService,
     private val syncService: SyncService,
+    private val roomService: RoomService,
 ) : DapkViewModel<ProfileScreenState, ProfileEvent>(
-    initialState = ProfileScreenState.Loading
+    ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
 ) {
 
+    private var syncingJob: Job? = null
+    private var currentPageJob: Job? = null
+
     fun start() {
-        viewModelScope.launch {
-            val invitationsCount = syncService.invites().firstOrNull()?.size ?: 0
-            val me = profileService.me(forceRefresh = true)
-            state = ProfileScreenState.Content(me, invitationsCount = invitationsCount)
+        goToProfile()
+    }
+
+    private fun goToProfile() {
+        syncingJob = syncService.startSyncing().launchIn(viewModelScope)
+
+        combine(
+            flow { emit(profileService.me(forceRefresh = true)) },
+            syncService.invites(),
+            transform = { me, invites -> me to invites }
+        )
+            .onEach { (me, invites) ->
+                updatePageState<Page.Profile> {
+                    copy(content = Lce.Content(Page.Profile.Content(me, invites.size)))
+                }
+            }
+            .launchPageJob()
+    }
+
+
+    fun goToInvitations() {
+        updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
+
+        syncService.invites()
+            .onEach {
+                updatePageState<Page.Invitations> {
+                    copy(content = Lce.Content(it))
+                }
+            }
+            .launchPageJob()
+    }
+
+    fun goTo(page: SpiderPage<out Page>) {
+        currentPageJob?.cancel()
+        updateState { copy(page = page) }
+        when (page.state) {
+            is Page.Invitations -> goToInvitations()
+            is Page.Profile -> goToProfile()
         }
     }
 
+    private fun <T> Flow<T>.launchPageJob() {
+        currentPageJob?.cancel()
+        currentPageJob = this.launchIn(viewModelScope)
+    }
 
     fun updateDisplayName() {
         // TODO
@@ -31,4 +78,40 @@ class ProfileViewModel(
         // TODO
     }
 
+    fun acceptRoomInvite(roomId: RoomId) {
+        launchCatching { roomService.joinRoom(roomId) }.fold(
+            onError = {}
+        )
+    }
+
+    fun rejectRoomInvite(roomId: RoomId) {
+        launchCatching { roomService.rejectJoinRoom(roomId) }.fold(
+            onError = {}
+        )
+    }
+
+    fun stop() {
+        syncingJob?.cancel()
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private inline fun <reified S : Page> updatePageState(crossinline block: S.() -> S) {
+        val page = state.page
+        val currentState = page.state
+        require(currentState is S)
+        updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
+    }
+
 }
+
+fun <S, VE, T> DapkViewModel<S, VE>.launchCatching(block: suspend () -> T): LaunchCatching<T> {
+    return object : LaunchCatching<T> {
+        override fun fold(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
+            viewModelScope.launch { runCatching { block() }.fold(onSuccess, onError) }
+        }
+    }
+}
+
+interface LaunchCatching<T> {
+    fun fold(onSuccess: (T) -> Unit = {}, onError: (Throwable) -> Unit = {})
+}
\ No newline at end of file
diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt
index 6495835..9b3b793 100644
--- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt
+++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt
@@ -199,9 +199,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
 @Composable
 private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
     Column {
-        TextRow("Import room keys", includeDivider = false) {
-            viewModel.goToImportRoom()
-        }
+        TextRow("Import room keys", includeDivider = false, onClick = { viewModel.goToImportRoom() })
     }
 }
 
diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt
index 0f4164b..b5eb838 100644
--- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt
+++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt
@@ -25,6 +25,7 @@ interface RoomService : MatrixService {
     suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId
 
     suspend fun joinRoom(roomId: RoomId)
+    suspend fun rejectJoinRoom(roomId: RoomId)
 
     data class JoinedMember(
         val userId: UserId,
diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt
index 686d9b2..977a233 100644
--- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt
+++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt
@@ -62,6 +62,10 @@ class DefaultRoomService(
     override suspend fun joinRoom(roomId: RoomId) {
         httpClient.execute(joinRoomRequest(roomId))
     }
+
+    override suspend fun rejectJoinRoom(roomId: RoomId) {
+        httpClient.execute(rejectJoinRoomRequest(roomId))
+    }
 }
 
 internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersResponse>(
@@ -87,6 +91,13 @@ internal fun joinRoomRequest(roomId: RoomId) = httpRequest<Unit>(
     body = emptyJsonBody()
 )
 
+internal fun rejectJoinRoomRequest(roomId: RoomId) = httpRequest<Unit>(
+    path = "_matrix/client/r0/rooms/${roomId.value}/leave",
+    method = MatrixHttpClient.Method.POST,
+    body = emptyJsonBody()
+)
+
+
 @Suppress("EnumEntryName")
 @Serializable
 enum class RoomVisibility {
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt
index b914799..75bf8ad 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/OverviewState.kt
@@ -31,5 +31,18 @@ data class LastMessage(
 
 @Serializable
 data class RoomInvite(
+    @SerialName("from") val from: RoomMember,
     @SerialName("room_id") val roomId: RoomId,
+    @SerialName("meta") val inviteMeta: InviteMeta,
 )
+
+@Serializable
+sealed class InviteMeta {
+    @Serializable
+    @SerialName("direct_message")
+    object DirectMessage : InviteMeta()
+
+    @Serializable
+    @SerialName("room")
+    data class Room(val roomName: String? = null) : InviteMeta()
+}
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
index 97ddf63..de59886 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.Flow
 interface RoomStore {
 
     suspend fun persist(roomId: RoomId, state: RoomState)
+    suspend fun remove(rooms: List<RoomId>)
     suspend fun retrieve(roomId: RoomId): RoomState?
     fun latest(roomId: RoomId): Flow<RoomState>
     suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
@@ -28,6 +29,7 @@ interface FilterStore {
 
 interface OverviewStore {
 
+    suspend fun removeRooms(roomsToRemove: List<RoomId>)
     suspend fun persistInvites(invite: List<RoomInvite>)
     suspend fun persist(overviewState: OverviewState)
 
@@ -35,6 +37,7 @@ interface OverviewStore {
 
     fun latest(): Flow<OverviewState>
     fun latestInvites(): Flow<List<RoomInvite>>
+    suspend fun removeInvites(map: List<RoomId>)
 }
 
 interface SyncStore {
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt
index 725d13a..7fc345c 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt
@@ -18,7 +18,7 @@ private val SERVICE_KEY = SyncService::class
 
 interface SyncService : MatrixService {
 
-    suspend fun invites(): Flow<InviteState>
+    fun invites(): Flow<InviteState>
     fun overview(): Flow<OverviewState>
     fun room(roomId: RoomId): Flow<RoomState>
     fun startSyncing(): Flow<Unit>
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt
index eff24ac..8a12cef 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt
@@ -63,6 +63,7 @@ internal class DefaultSyncService(
                 EphemeralEventsUseCase(roomMembersService, syncEventsFlow),
             ),
             roomRefresher,
+            roomDataSource,
             logger,
             coroutineDispatchers,
         )
@@ -100,7 +101,7 @@ internal class DefaultSyncService(
     }
 
     override fun startSyncing() = syncFlow
-    override suspend fun invites() = overviewStore.latestInvites()
+    override fun invites() = overviewStore.latestInvites()
     override fun overview() = overviewStore.latest()
     override fun room(roomId: RoomId) = roomStore.latest(roomId)
     override fun events() = syncEventsFlow
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt
index f068675..b125924 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt
@@ -214,6 +214,7 @@ sealed class ApiToDeviceEvent {
 internal data class ApiSyncRooms(
     @SerialName("join") val join: Map<RoomId, ApiSyncRoom>? = null,
     @SerialName("invite") val invite: Map<RoomId, ApiSyncRoomInvite>? = null,
+    @SerialName("leave") val leave: Map<RoomId, ApiSyncRoom>? = null,
 )
 
 @Serializable
@@ -230,14 +231,30 @@ internal data class ApiInviteEvents(
 sealed class ApiStrippedEvent {
 
     @Serializable
-    @SerialName("m.room.create")
-    internal data class RoomCreate(
+    @SerialName("m.room.member")
+    internal data class RoomMember(
+        @SerialName("content") val content: Content,
+        @SerialName("sender") val sender: UserId,
+    ) : ApiStrippedEvent() {
+
+        @Serializable
+        internal data class Content(
+            @SerialName("displayname") val displayName: String? = null,
+            @SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null,
+            @SerialName("is_direct") val isDirect: Boolean? = null,
+            @SerialName("avatar_url") val avatarUrl: MxUrl? = null,
+        )
+    }
+
+    @Serializable
+    @SerialName("m.room.name")
+    internal data class RoomName(
         @SerialName("content") val content: Content,
     ) : ApiStrippedEvent() {
 
         @Serializable
         internal data class Content(
-            @SerialName("type") val type: String? = null
+            @SerialName("name") val name: String? = null
         )
     }
 
@@ -407,6 +424,7 @@ internal sealed class ApiTimelineEvent {
             value class Membership(val value: String) {
                 fun isJoin() = value == "join"
                 fun isInvite() = value == "invite"
+                fun isLeave() = value == "leave"
             }
 
         }
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt
index d58026b..df4bb80 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt
@@ -14,6 +14,8 @@ class RoomDataSource(
 
     private val roomCache = mutableMapOf<RoomId, RoomState>()
 
+    fun contains(roomId: RoomId) = roomCache.containsKey(roomId)
+
     suspend fun read(roomId: RoomId) = when (val cached = roomCache[roomId]) {
         null -> roomStore.retrieve(roomId)?.also { roomCache[roomId] = it }
         else -> cached
@@ -27,4 +29,9 @@ class RoomDataSource(
             roomStore.persist(roomId, newState)
         }
     }
+
+    suspend fun remove(roomsLeft: List<RoomId>) {
+        roomsLeft.forEach { roomCache.remove(it) }
+        roomStore.remove(roomsLeft)
+    }
 }
\ No newline at end of file
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt
index 24eebc9..aa422ce 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncReducer.kt
@@ -4,30 +4,35 @@ import app.dapk.st.core.CoroutineDispatchers
 import app.dapk.st.core.withIoContextAsync
 import app.dapk.st.matrix.common.*
 import app.dapk.st.matrix.common.MatrixLogTag.SYNC
+import app.dapk.st.matrix.sync.InviteMeta
 import app.dapk.st.matrix.sync.RoomInvite
 import app.dapk.st.matrix.sync.RoomState
-import app.dapk.st.matrix.sync.internal.request.ApiAccountEvent
-import app.dapk.st.matrix.sync.internal.request.ApiSyncResponse
-import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom
+import app.dapk.st.matrix.sync.internal.request.*
 import app.dapk.st.matrix.sync.internal.room.SideEffectResult
 import kotlinx.coroutines.awaitAll
 
 internal class SyncReducer(
     private val roomProcessor: RoomProcessor,
     private val roomRefresher: RoomRefresher,
+    private val roomDataSource: RoomDataSource,
     private val logger: MatrixLogger,
     private val coroutineDispatchers: CoroutineDispatchers,
 ) {
 
     data class ReducerResult(
+        val newRoomsJoined: List<RoomId>,
         val roomState: List<RoomState>,
-        val invites: List<RoomInvite>
+        val invites: List<RoomInvite>,
+        val roomsLeft: List<RoomId>
     )
 
     suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
         val directMessages = response.directMessages()
 
-        val invites = response.rooms?.invite?.keys?.map { RoomInvite(it) } ?: emptyList()
+        val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList()
+        val roomsLeft = findRoomsLeft(response, userCredentials)
+        val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList()
+
         val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
         val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
             logger.matrixLog(SYNC, "reducing: $roomId")
@@ -50,7 +55,34 @@ internal class SyncReducer(
             }
         }
 
-        return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites)
+        roomDataSource.remove(roomsLeft)
+
+        return ReducerResult(
+            newRooms,
+            (apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(),
+            invites,
+            roomsLeft
+        )
+    }
+
+    private fun findRoomsLeft(response: ApiSyncResponse, userCredentials: UserCredentials) = response.rooms?.leave?.filter {
+        it.value.state.stateEvents.filterIsInstance<ApiTimelineEvent.RoomMember>().any {
+            it.content.membership.isLeave() && it.senderId == userCredentials.userId
+        }
+    }?.map { it.key } ?: emptyList()
+
+    private fun roomInvite(entry: Map.Entry<RoomId, ApiSyncRoomInvite>, userCredentials: UserCredentials): RoomInvite {
+        val memberEvents = entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomMember>()
+        val invitee = memberEvents.first { it.content.membership?.isInvite() ?: false }
+        val from = memberEvents.first { it.sender == invitee.sender }
+        return RoomInvite(
+            RoomMember(from.sender, from.content.displayName, from.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }),
+            roomId = entry.key,
+            inviteMeta = when (invitee.content.isDirect) {
+                true -> InviteMeta.DirectMessage
+                null, false -> InviteMeta.Room(entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomName>().firstOrNull()?.content?.name)
+            },
+        )
     }
 }
 
diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
index d56e8a8..5e840f4 100644
--- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
+++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/SyncUseCase.kt
@@ -46,9 +46,15 @@ internal class SyncUseCase(
                         val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
                         val overview = nextState.roomState.map { it.roomOverview }
 
+                        if (nextState.roomsLeft.isNotEmpty()) {
+                            persistence.removeRooms(nextState.roomsLeft)
+                        }
                         if (nextState.invites.isNotEmpty()) {
                             persistence.persistInvites(nextState.invites)
                         }
+                        if (nextState.newRoomsJoined.isNotEmpty()) {
+                            persistence.removeInvites(nextState.newRoomsJoined)
+                        }
 
                         when {
                             previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }