From bf720298fec917d7bc9652134b39793ecb40aca3 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Fri, 1 Dec 2023 00:03:16 +0100
Subject: [PATCH] wip: pre-navigation rewrite state

---
 .../de/chaosdorf/meteroid/MainActivity.kt     |   2 +-
 .../meteroid/storage/AccountPreferences.kt    |   2 +-
 .../storage/AccountPreferencesImpl.kt         |   8 +-
 .../de/chaosdorf/meteroid/ui/AppRouter.kt     | 209 +++++++++++-------
 .../de/chaosdorf/meteroid/ui/AppViewModel.kt  |  41 ++--
 .../meteroid/ui/NavigationViewModel.kt        |  13 +-
 .../meteroid/ui/drinks/DrinkListFilterChip.kt |   6 +-
 .../meteroid/ui/drinks/DrinkListScreen.kt     |  55 ++---
 .../chaosdorf/meteroid/ui/drinks/DrinkTile.kt |   7 +-
 .../meteroid/ui/money/MoneyListScreen.kt      |  57 +----
 .../chaosdorf/meteroid/ui/money/MoneyTile.kt  |   6 +-
 .../meteroid/ui/navigation/HomeSections.kt    |   4 +-
 .../ui/navigation/MeteroidBottomBar.kt        |  71 +++---
 .../meteroid/ui/navigation/MeteroidTopBar.kt  | 158 +++++++++----
 .../meteroid/ui/servers/AddServerScreen.kt    | 126 ++++-------
 .../meteroid/ui/servers/ServerListScreen.kt   |  56 +++--
 .../de/chaosdorf/meteroid/ui/theme/Color.kt   |  80 ++++++-
 .../de/chaosdorf/meteroid/ui/theme/Theme.kt   |  87 ++++++--
 .../meteroid/ui/theme/ThemeGradient.kt        |  58 +++++
 .../ui/transactions/PurchaseListItem.kt       |   9 +-
 .../ui/transactions/PurchaseListScreen.kt     |  39 +---
 .../meteroid/ui/users/UserListItem.kt         |  84 +++++++
 .../meteroid/ui/users/UserListScreen.kt       |  66 +++---
 .../meteroid/ui/users/UserListViewModel.kt    |   6 +-
 .../meteroid/ui/wrapped/WrappedScreen.kt      | 197 +++++++----------
 .../de/chaosdorf/meteroid/util/PopUpToRoot.kt |  33 +++
 .../meteroid/util/RememberAvatarPainter.kt    |  51 +++++
 .../de/chaosdorf/meteroid/model/PinnedUser.kt |   3 +
 28 files changed, 941 insertions(+), 593 deletions(-)
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt

diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
index 81481f3..9f09bde 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
@@ -44,7 +44,7 @@ class MainActivity : ComponentActivity() {
     val viewModel = viewModelProvider.get<AppViewModel>()
 
     installSplashScreen().setKeepOnScreenCondition {
-      viewModel.initialRoute.value == null
+      viewModel.initialBackStack.value == null
     }
 
     setContent {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
index 3d0751f..ebec013 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
@@ -37,5 +37,5 @@ interface AccountPreferences {
   val state: Flow<State>
 
   suspend fun setServer(server: ServerId?)
-  suspend fun setUser(user: UserId?)
+  suspend fun setUser(server: ServerId, user: UserId?)
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
index 62f4b80..e696e02 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
@@ -24,6 +24,7 @@
 
 package de.chaosdorf.meteroid.storage
 
+import android.util.Log
 import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
@@ -42,6 +43,7 @@ class AccountPreferencesImpl @Inject constructor(
     dataStore.data.mapLatest {
       val serverId = it[SERVER_KEY] ?: -1L
       val userId = it[USER_KEY] ?: -1L
+      Log.i("AccountPreferences", "Restored account data: $userId@$serverId")
 
       AccountPreferences.State(
         if (serverId >= 0) ServerId(serverId) else null,
@@ -50,13 +52,17 @@ class AccountPreferencesImpl @Inject constructor(
     }
 
   override suspend fun setServer(server: ServerId?) {
+    Log.i("AccountPreferences", "Setting account to -1@${server?.value ?: -1L}")
     dataStore.edit {
       it[SERVER_KEY] = server?.value ?: -1L
+      it[USER_KEY] = -1L
     }
   }
 
-  override suspend fun setUser(user: UserId?) {
+  override suspend fun setUser(server: ServerId, user: UserId?) {
+    Log.i("AccountPreferences", "Setting account to ${user?.value ?: -1L}@${server.value}")
     dataStore.edit {
+      it[SERVER_KEY] = server.value
       it[USER_KEY] = user?.value ?: -1L
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
index b00553b..d10008b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
@@ -24,16 +24,28 @@
 
 package de.chaosdorf.meteroid.ui
 
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.NavType
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.navArgument
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
 import de.chaosdorf.meteroid.ui.money.MoneyListScreen
@@ -51,99 +63,130 @@ import de.chaosdorf.meteroid.ui.users.UserListScreen
 import de.chaosdorf.meteroid.ui.users.UserListViewModel
 import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen
 import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel
+import de.chaosdorf.meteroid.util.popUpToRoot
 
 @Composable
 fun AppRouter(viewModel: AppViewModel) {
   val navController = rememberNavController()
-  val initialRoute by viewModel.initialRoute.collectAsState()
+  val snackbarHostState = remember { SnackbarHostState() }
+  val navigationViewModel = hiltViewModel<NavigationViewModel>()
 
-  LaunchedEffect(initialRoute) {
-    initialRoute?.let {
-      navController.navigate(it)
-    }
+  val initialBackStack by viewModel.initialBackStack.collectAsState()
+  val offline by viewModel.offline.collectAsState()
+
+  navController.addOnDestinationChangedListener { _, _, arguments ->
+    val serverId = arguments?.getLong("server")?.let(::ServerId)
+    val userId = arguments?.getLong("user")?.let(::UserId)
+
+    navigationViewModel.serverId.value = serverId
+    navigationViewModel.userId.value = userId
   }
 
-  NavHost(navController, startDestination = Routes.Servers.List) {
-    composable(Routes.Servers.Add) { _ ->
-      val hiltViewModel = hiltViewModel<AddServerViewModel>()
-      AddServerScreen(navController, hiltViewModel)
+  LaunchedEffect(initialBackStack) {
+    initialBackStack?.let { initialBackStack ->
+      navController.popUpToRoot()
+      for (entry in initialBackStack) {
+        navController.navigate(entry)
+      }
     }
-    composable(Routes.Servers.List) { _ ->
-      val hiltViewModel = hiltViewModel<ServerListViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      ServerListScreen(navController, hiltViewModel, topBar)
-    }
-    /*
-    composable(Routes.Users.Add) { _ ->
-      AddUserScreen(
-        hiltViewModel(),
-        onAdd = { navController.navigate(Routes.Users.List) }
-      )
-    }
-     */
-    composable(
-      Routes.Users.List,
-      arguments = listOf(
-        navArgument("server") { type = NavType.LongType }
-      )
-    ) { _ ->
-      val hiltViewModel = hiltViewModel<UserListViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      UserListScreen(navController, hiltViewModel, topBar)
-    }
-    composable(
-      Routes.Home.Purchase,
-      arguments = listOf(
-        navArgument("server") { type = NavType.LongType },
-        navArgument("user") { type = NavType.LongType },
-      )
-    ) { _ ->
-      val hiltViewModel = hiltViewModel<DrinkListViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) }
-      DrinkListScreen(navController, hiltViewModel, topBar, bottomBar)
-    }
-    composable(
-      Routes.Home.Deposit,
-      arguments = listOf(
-        navArgument("server") { type = NavType.LongType },
-        navArgument("user") { type = NavType.LongType },
-      )
-    ) { _ ->
-      val hiltViewModel = hiltViewModel<MoneyListViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) }
-      MoneyListScreen(navController, hiltViewModel, topBar, bottomBar)
-    }
-    composable(
-      Routes.Home.History,
-      arguments = listOf(
-        navArgument("server") { type = NavType.LongType },
-        navArgument("user") { type = NavType.LongType },
+  }
+
+  LaunchedEffect(offline) {
+    snackbarHostState.currentSnackbarData?.dismiss()
+    if (offline) {
+      snackbarHostState.showSnackbar(
+        message = "Unable to connect to server",
+        duration = SnackbarDuration.Indefinite
       )
-    ) { _ ->
-      val hiltViewModel = hiltViewModel<TransactionViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) }
-      TransactionListScreen(hiltViewModel, topBar, bottomBar)
     }
-    composable(
-      Routes.Home.Wrapped,
-      arguments = listOf(
-        navArgument("server") { type = NavType.LongType },
-        navArgument("user") { type = NavType.LongType },
-      )
-    ) { _ ->
-      val hiltViewModel = hiltViewModel<WrappedViewModel>()
-      val navigationViewModel = hiltViewModel<NavigationViewModel>()
-      val topBar = @Composable { MeteroidTopBar(navController, navigationViewModel) }
-      val bottomBar = @Composable { MeteroidBottomBar(navController, navigationViewModel) }
-      WrappedScreen(hiltViewModel, topBar, bottomBar)
+  }
+
+  Box {
+    Scaffold(
+      topBar = {
+        MeteroidTopBar(navController, navigationViewModel, Modifier.align(Alignment.TopCenter))
+      },
+      bottomBar = {
+        MeteroidBottomBar(navController, navigationViewModel)
+      },
+      snackbarHost = {
+        SnackbarHost(hostState = snackbarHostState)
+      }
+    ) { paddingValues ->
+      NavHost(
+        navController,
+        startDestination = Routes.Servers.List,
+        enterTransition = { EnterTransition.None },
+        exitTransition = { ExitTransition.None },
+        popEnterTransition = { EnterTransition.None },
+        popExitTransition = { ExitTransition.None }
+      ) {
+        composable(Routes.Servers.Add) { _ ->
+          val hiltViewModel = hiltViewModel<AddServerViewModel>()
+          AddServerScreen(navController, hiltViewModel, paddingValues)
+        }
+        composable(Routes.Servers.List) { _ ->
+          val hiltViewModel = hiltViewModel<ServerListViewModel>()
+          ServerListScreen(navController, hiltViewModel, paddingValues)
+        }
+        /*
+      composable(Routes.Users.Add) { _ ->
+        AddUserScreen(
+          hiltViewModel(),
+          onAdd = { navController.navigate(Routes.Users.List) }
+        )
+      }
+       */
+        composable(
+          Routes.Users.List,
+          arguments = listOf(
+            navArgument("server") { type = NavType.LongType }
+          )
+        ) { _ ->
+          val hiltViewModel = hiltViewModel<UserListViewModel>()
+          UserListScreen(navController, hiltViewModel, paddingValues)
+        }
+        composable(
+          Routes.Home.Purchase,
+          arguments = listOf(
+            navArgument("server") { type = NavType.LongType },
+            navArgument("user") { type = NavType.LongType },
+          )
+        ) { _ ->
+          val hiltViewModel = hiltViewModel<DrinkListViewModel>()
+          DrinkListScreen(navController, hiltViewModel, paddingValues)
+        }
+        composable(
+          Routes.Home.Deposit,
+          arguments = listOf(
+            navArgument("server") { type = NavType.LongType },
+            navArgument("user") { type = NavType.LongType },
+          )
+        ) { _ ->
+          val hiltViewModel = hiltViewModel<MoneyListViewModel>()
+          MoneyListScreen(navController, hiltViewModel, paddingValues)
+        }
+        composable(
+          Routes.Home.History,
+          arguments = listOf(
+            navArgument("server") { type = NavType.LongType },
+            navArgument("user") { type = NavType.LongType },
+          )
+        ) { _ ->
+          val hiltViewModel = hiltViewModel<TransactionViewModel>()
+          TransactionListScreen(hiltViewModel, paddingValues)
+        }
+        composable(
+          Routes.Home.Wrapped,
+          arguments = listOf(
+            navArgument("server") { type = NavType.LongType },
+            navArgument("user") { type = NavType.LongType },
+          )
+        ) { _ ->
+          val hiltViewModel = hiltViewModel<WrappedViewModel>()
+          WrappedScreen(hiltViewModel, paddingValues)
+        }
+      }
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
index b465366..c940440 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
@@ -27,26 +27,27 @@ package de.chaosdorf.meteroid.ui
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.model.UserId
-import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.storage.AccountPreferences
+import de.chaosdorf.meteroid.sync.SyncManager
 import de.chaosdorf.meteroid.ui.navigation.Routes
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
 import javax.inject.Inject
 
 @HiltViewModel
 class AppViewModel @Inject constructor(
-  private val accountPreferences: AccountPreferences,
+  accountPreferences: AccountPreferences,
   private val serverRepository: ServerRepository,
-  private val userRepository: UserRepository
+  private val userRepository: UserRepository,
+  private val syncManager: SyncManager
 ) : ViewModel() {
-  val initialRoute = accountPreferences.state.flatMapLatest {
+  val initialBackStack = accountPreferences.state.flatMapLatest {
     combine(
       serverRepository.countFlow(),
       if (it.server == null) flowOf(null)
@@ -55,23 +56,29 @@ class AppViewModel @Inject constructor(
       else userRepository.getFlow(it.server, it.user)
     ) { serverCount, server, user ->
       if (user != null) {
-        Routes.Home.purchase(user.serverId, user.userId)
+        listOf(
+          Routes.Servers.List,
+          Routes.Users.list(user.serverId),
+          Routes.Home.purchase(user.serverId, user.userId)
+        )
       } else if (server != null) {
-        Routes.Users.list(server.serverId)
+        listOf(
+          Routes.Servers.List,
+          Routes.Users.list(server.serverId)
+        )
       } else if (serverCount > 0) {
-        Routes.Servers.List
+        listOf(Routes.Servers.List)
       } else {
-        Routes.Servers.Add
+        listOf(Routes.Servers.Add)
       }
     }
   }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
-  suspend fun selectServer(server: ServerId) {
-    accountPreferences.setServer(server)
-    accountPreferences.setUser(null)
-  }
-
-  suspend fun selectUser(user: UserId) {
-    accountPreferences.setUser(user)
-  }
+  val offline = accountPreferences.state.flatMapLatest { state ->
+    state.server?.let { serverId ->
+      serverRepository.getFlow(serverId)
+    } ?: flowOf(null)
+  }.mapLatest { server ->
+    server?.let { syncManager.checkOffline(it) } ?: false
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt
index 77537ef..6346fe5 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt
@@ -24,7 +24,6 @@
 
 package de.chaosdorf.meteroid.ui
 
-import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
@@ -34,30 +33,24 @@ import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
 class NavigationViewModel @Inject constructor(
-  savedStateHandle: SavedStateHandle,
   serverRepository: ServerRepository,
   userRepository: UserRepository,
   pinnedUserRepository: PinnedUserRepository,
   private val accountProvider: AccountProvider
 ) : ViewModel() {
-  private val serverId = savedStateHandle.getStateFlow<Long?>("server", null)
-    .mapLatest { it?.let(::ServerId) }
-    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
-
-  private val userId = savedStateHandle.getStateFlow<Long?>("user", null)
-    .mapLatest { it?.let(::UserId) }
-    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
+  val serverId = MutableStateFlow<ServerId?>(null)
+  val userId = MutableStateFlow<UserId?>(null)
 
   val server = serverId.flatMapLatest {
     if (it == null) flowOf(null)
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
index cdd66c3..cba1aa8 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
@@ -55,6 +56,9 @@ fun DrinkListFilterChip(
         )
       }
     },
-    onClick = onClick
+    onClick = onClick,
+    colors = FilterChipDefaults.filterChipColors(
+      selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer
+    )
   )
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
index d454baf..3e7db6e 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
@@ -25,23 +25,17 @@
 package de.chaosdorf.meteroid.ui.drinks
 
 import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
@@ -51,33 +45,17 @@ import androidx.navigation.NavController
 fun DrinkListScreen(
   navController: NavController,
   viewModel: DrinkListViewModel,
-  topBar: @Composable () -> Unit,
-  bottomBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
-  val account by viewModel.account.collectAsState()
   val drinks by viewModel.drinks.collectAsState()
   val filters by viewModel.filters.collectAsState()
-  val snackbarHostState = remember { SnackbarHostState() }
 
-  LaunchedEffect(account) {
-    val offline = viewModel.checkOffline(account?.server)
-    snackbarHostState.currentSnackbarData?.dismiss()
-    if (offline) {
-      snackbarHostState.showSnackbar(
-        message = "Unable to connect to server",
-        duration = SnackbarDuration.Indefinite
-      )
-    }
-  }
-
-  Scaffold(
-    topBar = topBar,
-    bottomBar = bottomBar,
-    snackbarHost = {
-      SnackbarHost(hostState = snackbarHostState)
-    }
-  ) { paddingValues: PaddingValues ->
-    Column(Modifier.padding(paddingValues)) {
+  LazyVerticalGrid(
+    GridCells.Adaptive(104.dp),
+    contentPadding = contentPadding,
+    modifier = Modifier.padding(horizontal = 8.dp),
+  ) {
+    item("filter", span = { GridItemSpan(maxLineSpan) }) {
       FlowRow(
         modifier = Modifier.padding(horizontal = 16.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -93,15 +71,14 @@ fun DrinkListScreen(
           onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) }
         )
       }
-      LazyVerticalGrid(
-        GridCells.Adaptive(104.dp),
-        modifier = Modifier.padding(horizontal = 8.dp)
-      ) {
-        items(drinks) { drink ->
-          DrinkTile(drink) {
-            viewModel.purchase(it, navController::navigateUp)
-          }
-        }
+    }
+
+    items(
+      drinks,
+      key = { "${it.serverId}-${it.drinkId}" },
+    ) { drink ->
+      DrinkTile(drink) {
+        viewModel.purchase(it, navController::navigateUp)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
index f079ffd..9a6d972 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
@@ -33,7 +33,6 @@ import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -57,12 +56,14 @@ import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.sample.SampleDrinkProvider
 import de.chaosdorf.meteroid.ui.PriceBadge
+import de.chaosdorf.meteroid.ui.theme.secondaryGradient
 import java.math.BigDecimal
 
 @Preview(widthDp = 120, showBackground = true)
 @Composable
 fun DrinkTile(
   @PreviewParameter(SampleDrinkProvider::class) item: Drink,
+  modifier: Modifier = Modifier,
   onPurchase: (Drink) -> Unit = {}
 ) {
   val thumbPainter = rememberAsyncImagePainter(
@@ -74,7 +75,7 @@ fun DrinkTile(
   )
 
   Column(
-    modifier = Modifier
+    modifier = modifier
       .height(IntrinsicSize.Max)
       .alpha(if (item.active) 1.0f else 0.67f)
       .clip(RoundedCornerShape(8.dp))
@@ -89,7 +90,7 @@ fun DrinkTile(
         modifier = Modifier
           .aspectRatio(1.0f)
           .clip(CircleShape)
-          .background(MaterialTheme.colorScheme.primaryContainer)
+          .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient())
       )
       PriceBadge(
         item.price,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
index b7df727..ee78739 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
@@ -24,22 +24,12 @@
 
 package de.chaosdorf.meteroid.ui.money
 
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
@@ -48,42 +38,19 @@ import androidx.navigation.NavController
 fun MoneyListScreen(
   navController: NavController,
   viewModel: MoneyListViewModel,
-  topBar: @Composable () -> Unit,
-  bottomBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
-  val account by viewModel.account.collectAsState()
-  val snackbarHostState = remember { SnackbarHostState() }
-
-  LaunchedEffect(account) {
-    val offline = viewModel.checkOffline(account?.server)
-    snackbarHostState.currentSnackbarData?.dismiss()
-    if (offline) {
-      snackbarHostState.showSnackbar(
-        message = "Unable to connect to server",
-        duration = SnackbarDuration.Indefinite
-      )
-    }
-  }
-
-  Scaffold(
-    topBar = topBar,
-    bottomBar = bottomBar,
-    snackbarHost = {
-      SnackbarHost(hostState = snackbarHostState)
-    }
-  ) { paddingValues: PaddingValues ->
-    Column {
-      LazyVerticalGrid(
-        GridCells.Adaptive(120.dp),
-        modifier = Modifier.padding(paddingValues),
-        contentPadding = PaddingValues(12.dp),
-        horizontalArrangement = Arrangement.SpaceBetween,
-      ) {
-        items(viewModel.money) { monetaryAmount ->
-          MoneyTile(monetaryAmount) {
-            viewModel.deposit(it, navController::navigateUp)
-          }
-        }
+  LazyVerticalGrid(
+    GridCells.Adaptive(104.dp),
+    contentPadding = contentPadding,
+    modifier = Modifier.padding(horizontal = 8.dp),
+  ) {
+    items(
+      viewModel.money,
+      key = { "${it.ordinal}" },
+    ) { monetaryAmount ->
+      MoneyTile(monetaryAmount) {
+        viewModel.deposit(it, navController::navigateUp)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
index 5254c6f..c503e5e 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
@@ -25,16 +25,13 @@
 package de.chaosdorf.meteroid.ui.money
 
 import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -47,10 +44,11 @@ import de.chaosdorf.meteroid.ui.PriceBadge
 @Composable
 fun MoneyTile(
   item: MonetaryAmount,
+  modifier: Modifier = Modifier,
   onDeposit: (MonetaryAmount) -> Unit = {}
 ) {
   Box(
-    modifier = Modifier
+    modifier = modifier
       .height(IntrinsicSize.Max)
       .clip(RoundedCornerShape(8.dp))
       .clickable { onDeposit(item) }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt
index 08f21cc..2ff9789 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt
@@ -42,8 +42,8 @@ enum class HomeSections(
   override val iconActive: ImageVector,
   override val route: String
 ) : MeteroidNavSection {
-  PURCHASE("Drinks", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase),
-  DEPOSIT("Money", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit),
+  PURCHASE("Purchase", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase),
+  DEPOSIT("Deposit", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit),
   HISTORY("History", Icons.Outlined.History, Icons.TwoTone.History, Routes.Home.History),
   WRAPPED("Wrapped", Icons.Outlined.Celebration, Icons.TwoTone.Celebration, Routes.Home.Wrapped);
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt
index 9710b66..a8c0369 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt
@@ -25,6 +25,7 @@
 package de.chaosdorf.meteroid.ui.navigation
 
 import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.NavigationBar
 import androidx.compose.material3.NavigationBarItem
 import androidx.compose.material3.Text
@@ -32,9 +33,10 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.navigation.NavController
-import androidx.navigation.NavGraph.Companion.findStartDestination
 import androidx.navigation.compose.currentBackStackEntryAsState
 import de.chaosdorf.meteroid.ui.NavigationViewModel
+import de.chaosdorf.meteroid.ui.theme.onPrimaryContainerTinted
+import de.chaosdorf.meteroid.util.popUpToRoot
 import kotlinx.datetime.Clock
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toLocalDateTime
@@ -52,42 +54,47 @@ fun MeteroidBottomBar(
     .month.let { it == Month.NOVEMBER || it == Month.DECEMBER }
   val navBackStackEntry by navController.currentBackStackEntryAsState()
   val currentDestination = navBackStackEntry?.destination
-  val activeRoute = if (user == null) HomeSections.PURCHASE
+  val activeRoute = if (user == null) null
   else HomeSections.entries.find {
-    it.withArguments(
-      "server" to user!!.serverId.value.toString(),
-      "user" to user!!.userId.value.toString()
-    ) == currentDestination?.route
+    it.route == currentDestination?.route
   }
 
-  NavigationBar {
-    for (route in HomeSections.entries) {
-      if (wrappedEnabled || route != HomeSections.WRAPPED) {
-        NavigationBarItem(
-          icon = {
-            Icon(
-              if (route == activeRoute) route.iconActive else route.icon,
-              contentDescription = route.title
-            )
-          },
-          label = { Text(route.title) },
-          selected = route == activeRoute,
-          onClick = {
-            navController.navigate(
-              route.withArguments(
-                "server" to user!!.serverId.value.toString(),
-                "user" to user!!.userId.value.toString()
+  if (activeRoute != null) {
+    NavigationBar(
+      contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted
+    ) {
+      for (route in HomeSections.entries) {
+        if (wrappedEnabled || route != HomeSections.WRAPPED) {
+          NavigationBarItem(
+            icon = {
+              Icon(
+                if (route == activeRoute) route.iconActive else route.icon,
+                contentDescription = route.title,
+                tint = MaterialTheme.colorScheme.onPrimaryContainerTinted
               )
-            ) {
-              popUpTo(navController.graph.findStartDestination().id) {
-                saveState = true
-              }
-              launchSingleTop = true
-              restoreState = true
+            },
+            label = { Text(route.title, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) },
+            selected = route == activeRoute,
+            onClick = {
+              navController.popUpToRoot()
+              navController.navigate(Routes.Servers.List)
+              navController.navigate(Routes.Users.list(user!!.serverId))
+              navController.navigate(
+                route.withArguments(
+                  "server" to user!!.serverId.value.toString(),
+                  "user" to user!!.userId.value.toString()
+                )
+              )
+            },
+            enabled = when (route) {
+              HomeSections.PURCHASE,
+              HomeSections.DEPOSIT -> true
+
+              HomeSections.HISTORY,
+              HomeSections.WRAPPED -> historyEnabled
             }
-          },
-          enabled = route != HomeSections.HISTORY || historyEnabled
-        )
+          )
+        }
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
index 41abc77..a018e1b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
@@ -24,16 +24,21 @@
 
 package de.chaosdorf.meteroid.ui.navigation
 
+import android.util.Log
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.filled.PushPin
 import androidx.compose.material.icons.outlined.PushPin
 import androidx.compose.material3.Icon
@@ -42,32 +47,57 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
-import coil.compose.AsyncImage
-import de.chaosdorf.meteroid.ui.PriceBadge
+import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.ui.NavigationViewModel
+import de.chaosdorf.meteroid.ui.PriceBadge
+import de.chaosdorf.meteroid.util.rememberAvatarPainter
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
 @Composable
 fun MeteroidTopBar(
   navController: NavController,
-  viewModel: NavigationViewModel
+  viewModel: NavigationViewModel,
+  modifier: Modifier = Modifier
 ) {
   val server by viewModel.server.collectAsState()
   val user by viewModel.user.collectAsState()
   val pinned by viewModel.pinned.collectAsState()
 
+  val backstack by navController.currentBackStack.collectAsState()
+  val canNavigateUp = backstack.size > 2
+  LaunchedEffect(backstack) {
+    val backstackEntries = backstack.map {
+      it.destination.route
+        ?.replace("{server}", it.arguments?.getLong("server")?.toString() ?: "{server}")
+        ?.replace("{user}", it.arguments?.getLong("user")?.toString() ?: "{user}")
+    }
+    Log.e("Navigation", "BACKSTACK: [${backstackEntries.joinToString(" › ")}]")
+  }
+
+  val avatarPainter = rememberAvatarPainter(
+    user?.gravatarUrl,
+    32.dp, 32.dp,
+    MaterialTheme.colorScheme.primary
+  )
+
+  val iconPainter = painterResource(R.drawable.ic_launcher)
+
   Surface(
-    modifier = Modifier.padding(8.dp),
+    modifier = modifier
+      .padding(8.dp)
+      .height(64.dp),
     color = MaterialTheme.colorScheme.surface,
     shadowElevation = 6.dp,
     tonalElevation = 6.dp,
@@ -75,49 +105,95 @@ fun MeteroidTopBar(
     onClick = { navController.navigateUp() }
   ) {
     Row(modifier = Modifier.padding(8.dp)) {
-      AsyncImage(
-        user?.gravatarUrl,
-        contentDescription = "User List",
-        contentScale = ContentScale.Crop,
-        modifier = Modifier
-          .size(48.dp)
-          .clip(CircleShape)
-          .background(MaterialTheme.colorScheme.tertiary)
-      )
+      if (user != null) {
+        Box(
+          modifier = Modifier
+            .size(48.dp)
+            .clip(CircleShape)
+            .background(MaterialTheme.colorScheme.primaryContainer)
+        ) {
+          Image(
+            avatarPainter,
+            contentDescription = null,
+            contentScale = ContentScale.Crop,
+            modifier = Modifier.align(Alignment.Center)
+          )
+        }
+      } else if (canNavigateUp) {
+        Icon(
+          Icons.AutoMirrored.Default.ArrowBack,
+          contentDescription = null,
+          tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+          modifier = Modifier
+            .padding(8.dp)
+            .size(32.dp)
+        )
+      } else {
+        Image(
+          iconPainter,
+          contentDescription = null,
+          contentScale = ContentScale.Crop,
+          modifier = Modifier
+            .size(48.dp)
+            .clip(CircleShape)
+        )
+      }
       Spacer(Modifier.width(16.dp))
       Column(
         modifier = Modifier
           .align(Alignment.CenterVertically)
           .weight(1.0f, fill = true)
       ) {
-        if (server == null) {
-          Text(
-            "Meteroid",
-            fontWeight = FontWeight.SemiBold,
-            overflow = TextOverflow.Ellipsis,
-            softWrap = false
-          )
-        } else if (user == null) {
-          Text(
-            server!!.url.toHttpUrl().host,
-            fontWeight = FontWeight.SemiBold,
-            overflow = TextOverflow.Ellipsis,
-            softWrap = false
-          )
-        } else {
-          Text(
-            user!!.name,
-            fontWeight = FontWeight.SemiBold,
-            overflow = TextOverflow.Ellipsis,
-            softWrap = false
-          )
-          Text(
-            server!!.url.toHttpUrl().host,
-            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
-            fontWeight = FontWeight.Medium,
-            overflow = TextOverflow.Ellipsis,
-            softWrap = false
-          )
+        when {
+          user != null && server != null -> {
+            Text(
+              user!!.name,
+              fontWeight = FontWeight.SemiBold,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+            Text(
+              server!!.url.toHttpUrl().host,
+              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+              fontWeight = FontWeight.Normal,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+          }
+
+          server != null && server!!.name != null -> {
+            Text(
+              server!!.name!!,
+              fontWeight = FontWeight.SemiBold,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+            Text(
+              server!!.url.toHttpUrl().host,
+              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+              fontWeight = FontWeight.Normal,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+          }
+
+          server != null -> {
+            Text(
+              server!!.url.toHttpUrl().host,
+              fontWeight = FontWeight.SemiBold,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+          }
+
+          else -> {
+            Text(
+              "Meteroid",
+              fontWeight = FontWeight.SemiBold,
+              overflow = TextOverflow.Ellipsis,
+              softWrap = false
+            )
+          }
         }
       }
       Spacer(Modifier.width(16.dp))
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
index a801dff..f5ea039 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt
@@ -24,8 +24,8 @@
 
 package de.chaosdorf.meteroid.ui.servers
 
-import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -38,24 +38,19 @@ import androidx.compose.material3.Card
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextField
-import androidx.compose.material3.TopAppBar
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
 import coil.compose.AsyncImage
-import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.ui.navigation.Routes
 import kotlinx.coroutines.launch
 import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -64,88 +59,63 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
 fun AddServerScreen(
   navController: NavController,
   viewModel: AddServerViewModel,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
   val scope = rememberCoroutineScope()
   val url by viewModel.url.collectAsState()
   val server by viewModel.server.collectAsState()
 
-  Scaffold(
-    topBar = {
-      TopAppBar(
-        title = {
-          Row {
-            Image(
-              painterResource(R.drawable.ic_launcher),
-              contentDescription = null,
-              contentScale = ContentScale.Crop,
-              modifier = Modifier.size(48.dp)
-            )
-            Spacer(Modifier.width(16.dp))
-            Column(modifier = Modifier.align(Alignment.CenterVertically)) {
-              Text(
-                "Meteroid",
-                fontWeight = FontWeight.SemiBold,
-                style = MaterialTheme.typography.bodyMedium
-              )
-            }
-          }
-        },
-        modifier = Modifier.shadow(4.dp)
-      )
-    },
-  ) { paddingValues ->
-    Column(
-      Modifier
-        .padding(paddingValues)
-        .padding(16.dp, 8.dp)
-    ) {
-      TextField(
-        label = { Text("Server URL") },
-        value = url,
-        onValueChange = { viewModel.url.value = it },
-        modifier = Modifier.fillMaxWidth()
-      )
+  Column(
+    Modifier
+      .padding(contentPadding)
+      .padding(16.dp, 8.dp)
+  ) {
+    TextField(
+      label = { Text("Server URL") },
+      value = url,
+      onValueChange = { viewModel.url.value = it },
+      modifier = Modifier.fillMaxWidth()
+    )
 
-      server?.let { server ->
-        Card(
-          modifier = Modifier.padding(vertical = 8.dp)
-        ) {
-          Row(modifier = Modifier.padding(16.dp, 8.dp)) {
-            AsyncImage(
-              server.logoUrl,
-              contentDescription = null,
-              contentScale = ContentScale.Crop,
-              modifier = Modifier.size(48.dp)
+    server?.let { server ->
+      Card(
+        modifier = Modifier.padding(vertical = 8.dp)
+      ) {
+        Row(modifier = Modifier.padding(16.dp, 8.dp)) {
+          AsyncImage(
+            server.logoUrl,
+            contentDescription = null,
+            contentScale = ContentScale.Crop,
+            modifier = Modifier.size(48.dp)
+          )
+          Spacer(Modifier.width(16.dp))
+          Column(modifier = Modifier.align(Alignment.CenterVertically)) {
+            Text(
+              server.name!!,
+              fontWeight = FontWeight.SemiBold,
+              style = MaterialTheme.typography.bodyMedium
             )
-            Spacer(Modifier.width(16.dp))
-            Column(modifier = Modifier.align(Alignment.CenterVertically)) {
-              Text(
-                server.name!!,
-                fontWeight = FontWeight.SemiBold,
-                style = MaterialTheme.typography.bodyMedium
-              )
-              Text(
-                server.url.toHttpUrl().host,
-                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
-                fontWeight = FontWeight.Medium,
-                style = MaterialTheme.typography.bodyMedium
-              )
-            }
-
-            Spacer(
-              Modifier
-                .width(16.dp)
-                .weight(1.0f)
+            Text(
+              server.url.toHttpUrl().host,
+              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+              fontWeight = FontWeight.Medium,
+              style = MaterialTheme.typography.bodyMedium
             )
+          }
+
+          Spacer(
+            Modifier
+              .width(16.dp)
+              .weight(1.0f)
+          )
 
-            IconButton(onClick = {
-              scope.launch {
-                viewModel.addServer()
-                navController.navigate(Routes.Servers.List)
-              }
-            }) {
-              Icon(Icons.Default.Add, contentDescription = "Add Server")
+          IconButton(onClick = {
+            scope.launch {
+              viewModel.addServer()
+              navController.navigate(Routes.Servers.List)
             }
+          }) {
+            Icon(Icons.Default.Add, contentDescription = "Add Server")
           }
         }
       }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt
index 5625e6f..19242ea 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt
@@ -25,13 +25,11 @@
 package de.chaosdorf.meteroid.ui.servers
 
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material3.ListItem
-import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -48,40 +46,36 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
 fun ServerListScreen(
   navController: NavController,
   viewModel: ServerListViewModel,
-  topBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
   val servers by viewModel.servers.collectAsState()
 
-  Scaffold(topBar = topBar) { paddingValues ->
-    Column {
-      LazyColumn(modifier = Modifier.padding(paddingValues)) {
-        items(servers) { server ->
-          ListItem(
-            headlineContent = { Text(server.name ?: server.url) },
-            supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) },
-            leadingContent = {
-              AsyncImage(
-                server.logoUrl,
-                contentDescription = null,
-                contentScale = ContentScale.Crop,
-                modifier = Modifier.size(48.dp)
-              )
-            },
-            modifier = Modifier.clickable {
-              navController.navigate(Routes.Users.list(server.serverId))
-              viewModel.selectServer(server.serverId)
-            }
+  LazyColumn(contentPadding = contentPadding) {
+    items(servers) { server ->
+      ListItem(
+        headlineContent = { Text(server.name ?: server.url) },
+        supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) },
+        leadingContent = {
+          AsyncImage(
+            server.logoUrl,
+            contentDescription = null,
+            contentScale = ContentScale.Crop,
+            modifier = Modifier.size(48.dp)
           )
+        },
+        modifier = Modifier.clickable {
+          navController.navigate(Routes.Users.list(server.serverId))
+          viewModel.selectServer(server.serverId)
         }
-        item {
-          ListItem(
-            headlineContent = { Text("Add Server") },
-            modifier = Modifier.clickable {
-              navController.navigate(Routes.Servers.Add)
-            }
-          )
+      )
+    }
+    item {
+      ListItem(
+        headlineContent = { Text("Add Server") },
+        modifier = Modifier.clickable {
+          navController.navigate(Routes.Servers.Add)
         }
-      }
+      )
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt
index 33ce00f..e5687bd 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt
@@ -1,11 +1,79 @@
 package de.chaosdorf.meteroid.ui.theme
 
+import androidx.compose.material3.ColorScheme
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
 
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+val md_theme_light_primary = Color(0xFF345CA8)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_primaryContainer = Color(0xFFD9E2FF)
+val md_theme_light_onPrimaryContainer = Color(0xFF001A43)
+val md_theme_light_secondary = Color(0xFF8B5000)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFFFDCBE)
+val md_theme_light_onSecondaryContainer = Color(0xFF2C1600)
+val md_theme_light_tertiary = Color(0xFFC0000A)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFFFDAD5)
+val md_theme_light_onTertiaryContainer = Color(0xFF410001)
+val md_theme_light_error = Color(0xFFC0000A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD5)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410001)
+val md_theme_light_background = Color(0xFFFEFBFF)
+val md_theme_light_onBackground = Color(0xFF1B1B1F)
+val md_theme_light_surface = Color(0xFFFEFBFF)
+val md_theme_light_onSurface = Color(0xFF1B1B1F)
+val md_theme_light_surfaceVariant = Color(0xFFE1E2EC)
+val md_theme_light_onSurfaceVariant = Color(0xFF44474F)
+val md_theme_light_outline = Color(0xFF757780)
+val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4)
+val md_theme_light_inverseSurface = Color(0xFF303034)
+val md_theme_light_inversePrimary = Color(0xFFAFC6FF)
+val md_theme_light_shadow = Color(0xFF000000)
+val md_theme_light_surfaceTint = Color(0xFF345CA8)
+val md_theme_light_outlineVariant = Color(0xFFC5C6D0)
+val md_theme_light_scrim = Color(0xFF000000)
 
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val md_theme_dark_primary = Color(0xFFAFC6FF)
+val md_theme_dark_onPrimary = Color(0xFF002D6C)
+val md_theme_dark_primaryContainer = Color(0xFF15448F)
+val md_theme_dark_onPrimaryContainer = Color(0xFFD9E2FF)
+val md_theme_dark_secondary = Color(0xFFFFB870)
+val md_theme_dark_onSecondary = Color(0xFF4A2800)
+val md_theme_dark_secondaryContainer = Color(0xFF693C00)
+val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCBE)
+val md_theme_dark_tertiary = Color(0xFFFFB4AA)
+val md_theme_dark_onTertiary = Color(0xFF690003)
+val md_theme_dark_tertiaryContainer = Color(0xFF930006)
+val md_theme_dark_onTertiaryContainer = Color(0xFFFFDAD5)
+val md_theme_dark_error = Color(0xFFFFB4AA)
+val md_theme_dark_errorContainer = Color(0xFF930006)
+val md_theme_dark_onError = Color(0xFF690003)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD5)
+val md_theme_dark_background = Color(0xFF1B1B1F)
+val md_theme_dark_onBackground = Color(0xFFE3E2E6)
+val md_theme_dark_surface = Color(0xFF1B1B1F)
+val md_theme_dark_onSurface = Color(0xFFE3E2E6)
+val md_theme_dark_surfaceVariant = Color(0xFF44474F)
+val md_theme_dark_onSurfaceVariant = Color(0xFFC5C6D0)
+val md_theme_dark_outline = Color(0xFF8F9099)
+val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
+val md_theme_dark_inverseSurface = Color(0xFFE3E2E6)
+val md_theme_dark_inversePrimary = Color(0xFF345CA8)
+val md_theme_dark_shadow = Color(0xFF000000)
+val md_theme_dark_surfaceTint = Color(0xFFAFC6FF)
+val md_theme_dark_outlineVariant = Color(0xFF44474F)
+val md_theme_dark_scrim = Color(0xFF000000)
+
+
+val ColorScheme.secondaryGradient
+  get() = ThemeGradient(
+    listOf(
+      secondaryContainer.copy(0.2f).compositeOver(surface),
+      secondaryContainer,
+    )
+  )
+
+val ColorScheme.onPrimaryContainerTinted
+  get() = primary.copy(alpha = 0.4f).compositeOver(onPrimaryContainer)
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt
index db521b5..9affa43 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt
@@ -32,33 +32,78 @@ import androidx.compose.material3.darkColorScheme
 import androidx.compose.material3.dynamicDarkColorScheme
 import androidx.compose.material3.dynamicLightColorScheme
 import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.surfaceColorAtElevation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.dp
 import androidx.core.view.WindowCompat
 
-private val DarkColorScheme = darkColorScheme(
-  primary = Purple80,
-  secondary = PurpleGrey80,
-  tertiary = Pink80
+private val LightColors = lightColorScheme(
+  primary = md_theme_light_primary,
+  onPrimary = md_theme_light_onPrimary,
+  primaryContainer = md_theme_light_primaryContainer,
+  onPrimaryContainer = md_theme_light_onPrimaryContainer,
+  secondary = md_theme_light_secondary,
+  onSecondary = md_theme_light_onSecondary,
+  secondaryContainer = md_theme_light_secondaryContainer,
+  onSecondaryContainer = md_theme_light_onSecondaryContainer,
+  tertiary = md_theme_light_tertiary,
+  onTertiary = md_theme_light_onTertiary,
+  tertiaryContainer = md_theme_light_tertiaryContainer,
+  onTertiaryContainer = md_theme_light_onTertiaryContainer,
+  error = md_theme_light_error,
+  errorContainer = md_theme_light_errorContainer,
+  onError = md_theme_light_onError,
+  onErrorContainer = md_theme_light_onErrorContainer,
+  background = md_theme_light_background,
+  onBackground = md_theme_light_onBackground,
+  surface = md_theme_light_surface,
+  onSurface = md_theme_light_onSurface,
+  surfaceVariant = md_theme_light_surfaceVariant,
+  onSurfaceVariant = md_theme_light_onSurfaceVariant,
+  outline = md_theme_light_outline,
+  inverseOnSurface = md_theme_light_inverseOnSurface,
+  inverseSurface = md_theme_light_inverseSurface,
+  inversePrimary = md_theme_light_inversePrimary,
+  surfaceTint = md_theme_light_surfaceTint,
+  outlineVariant = md_theme_light_outlineVariant,
+  scrim = md_theme_light_scrim,
 )
 
-private val LightColorScheme = lightColorScheme(
-  primary = Purple40,
-  secondary = PurpleGrey40,
-  tertiary = Pink40
 
-  /* Other default colors to override
-  background = Color(0xFFFFFBFE),
-  surface = Color(0xFFFFFBFE),
-  onPrimary = Color.White,
-  onSecondary = Color.White,
-  onTertiary = Color.White,
-  onBackground = Color(0xFF1C1B1F),
-  onSurface = Color(0xFF1C1B1F),
-  */
+private val DarkColors = darkColorScheme(
+  primary = md_theme_dark_primary,
+  onPrimary = md_theme_dark_onPrimary,
+  primaryContainer = md_theme_dark_primaryContainer,
+  onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+  secondary = md_theme_dark_secondary,
+  onSecondary = md_theme_dark_onSecondary,
+  secondaryContainer = md_theme_dark_secondaryContainer,
+  onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+  tertiary = md_theme_dark_tertiary,
+  onTertiary = md_theme_dark_onTertiary,
+  tertiaryContainer = md_theme_dark_tertiaryContainer,
+  onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+  error = md_theme_dark_error,
+  errorContainer = md_theme_dark_errorContainer,
+  onError = md_theme_dark_onError,
+  onErrorContainer = md_theme_dark_onErrorContainer,
+  background = md_theme_dark_background,
+  onBackground = md_theme_dark_onBackground,
+  surface = md_theme_dark_surface,
+  onSurface = md_theme_dark_onSurface,
+  surfaceVariant = md_theme_dark_surfaceVariant,
+  onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+  outline = md_theme_dark_outline,
+  inverseOnSurface = md_theme_dark_inverseOnSurface,
+  inverseSurface = md_theme_dark_inverseSurface,
+  inversePrimary = md_theme_dark_inversePrimary,
+  surfaceTint = md_theme_dark_surfaceTint,
+  outlineVariant = md_theme_dark_outlineVariant,
+  scrim = md_theme_dark_scrim,
 )
 
 @Composable
@@ -74,14 +119,18 @@ fun MeteroidTheme(
       if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
     }
 
-    darkTheme -> DarkColorScheme
-    else -> LightColorScheme
+    darkTheme -> DarkColors
+    else -> LightColors
   }
   val view = LocalView.current
   if (!view.isInEditMode) {
     SideEffect {
       val window = (view.context as Activity).window
       window.statusBarColor = colorScheme.primary.toArgb()
+      window.navigationBarColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb()
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+        window.navigationBarDividerColor = colorScheme.surfaceColorAtElevation(3.dp).toArgb()
+      }
       WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt
new file mode 100644
index 0000000..3de9104
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt
@@ -0,0 +1,58 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.theme
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
+
+class ThemeGradient(val colors: List<Color>) {
+  fun linearGradient(
+      start: Offset = Offset.Zero,
+      end: Offset = Offset.Infinite,
+      tileMode: TileMode = TileMode.Clamp
+  ) = Brush.linearGradient(colors, start, end, tileMode)
+
+  fun verticalGradient(
+    startY: Float = 0.0f,
+    endY: Float = Float.POSITIVE_INFINITY,
+    tileMode: TileMode = TileMode.Clamp
+  ) = linearGradient(
+    start = Offset(0.0f, startY),
+    end = Offset(0.0f, endY),
+    tileMode = tileMode,
+  )
+
+  fun horizontalGradient(
+    startX: Float = 0.0f,
+    endX: Float = Float.POSITIVE_INFINITY,
+    tileMode: TileMode = TileMode.Clamp
+  ) = linearGradient(
+    start = Offset(startX, 0.0f),
+    end = Offset(endX, 0.0f),
+    tileMode = tileMode,
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
index 7376c6d..7b5eaad 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
@@ -49,6 +49,7 @@ import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.Transaction
 import de.chaosdorf.meteroid.ui.PriceBadge
+import de.chaosdorf.meteroid.ui.theme.secondaryGradient
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toJavaLocalDateTime
 import kotlinx.datetime.toLocalDateTime
@@ -59,7 +60,8 @@ import java.time.format.FormatStyle
 @Composable
 fun TransactionListItem(
   transaction: Transaction,
-  drink: Drink?
+  drink: Drink?,
+  modifier: Modifier = Modifier
 ) {
   val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault())
   val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
@@ -81,8 +83,8 @@ fun TransactionListItem(
         modifier = Modifier
           .size(48.dp)
           .clip(CircleShape)
-          .background(MaterialTheme.colorScheme.primaryContainer)
           .aspectRatio(1.0f)
+          .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient())
       ) {
         if (drink != null) {
           val thumbPainter = rememberAsyncImagePainter(
@@ -121,6 +123,7 @@ fun TransactionListItem(
         transaction.difference,
         modifier = Modifier.padding(horizontal = 8.dp)
       )
-    }
+    },
+    modifier = modifier,
   )
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
index 30620f8..7d208d9 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
@@ -27,48 +27,23 @@ package de.chaosdorf.meteroid.ui.transactions
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 
 @Composable
 fun TransactionListScreen(
   viewModel: TransactionViewModel,
-  topBar: @Composable () -> Unit,
-  bottomBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
-  val account by viewModel.account.collectAsState()
   val transactions by viewModel.transactions.collectAsState()
-  val snackbarHostState = remember { SnackbarHostState() }
 
-  LaunchedEffect(account) {
-    val offline = viewModel.checkOffline(account?.server)
-    snackbarHostState.currentSnackbarData?.dismiss()
-    if (offline) {
-      snackbarHostState.showSnackbar(
-        message = "Unable to connect to server",
-        duration = SnackbarDuration.Indefinite
-      )
-    }
-  }
-
-  Scaffold(
-    topBar = topBar,
-    bottomBar = bottomBar,
-    snackbarHost = {
-      SnackbarHost(hostState = snackbarHostState)
-    }
-  ) { paddingValues: PaddingValues ->
-    LazyColumn(contentPadding = paddingValues) {
-      items(transactions) { (transaction, drink) ->
-        TransactionListItem(transaction, drink)
-      }
+  LazyColumn(contentPadding = contentPadding) {
+    items(
+      transactions,
+      key = { "${it.transaction.serverId}-${it.transaction.transactionId}" },
+    ) { (transaction, drink) ->
+      TransactionListItem(transaction, drink)
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt
new file mode 100644
index 0000000..ba11f91
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt
@@ -0,0 +1,84 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.users
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.util.rememberAvatarPainter
+
+@Composable
+fun UserListItem(
+  item: User,
+  onSelect: (ServerId, UserId) -> Unit = { _, _ -> }
+) {
+  val avatarPainter = rememberAvatarPainter(
+    item.gravatarUrl,
+    32.dp, 32.dp,
+    MaterialTheme.colorScheme.secondary
+  )
+
+  ListItem(
+    headlineContent = { Text(item.name) },
+    supportingContent = {
+      item.email?.let { email ->
+        Text(email)
+      }
+    },
+    leadingContent = {
+      Box(
+        modifier = Modifier
+          .size(48.dp)
+          .clip(CircleShape)
+          .background(MaterialTheme.colorScheme.secondaryContainer)
+      ) {
+        Image(
+          avatarPainter,
+          contentDescription = null,
+          contentScale = ContentScale.Crop,
+          modifier = Modifier.align(Alignment.Center)
+        )
+      }
+    },
+    modifier = Modifier.clickable {
+      onSelect(item.serverId, item.userId)
+    }
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
index 38218bd..ef1c017 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
@@ -24,13 +24,13 @@
 
 package de.chaosdorf.meteroid.ui.users
 
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ListItem
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -40,55 +40,59 @@ import androidx.compose.ui.unit.dp
 import androidx.navigation.NavController
 import de.chaosdorf.meteroid.ui.navigation.Routes
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun UserListScreen(
   navController: NavController,
   viewModel: UserListViewModel,
-  topBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
   val users by viewModel.users.collectAsState()
   val pinnedUsers by viewModel.pinnedUsers.collectAsState()
 
-  Scaffold(
-    topBar = topBar,
-  ) { paddingValues ->
-    LazyVerticalGrid(
-      GridCells.Adaptive(80.dp),
-      modifier = Modifier.padding(horizontal = 8.dp),
-      contentPadding = paddingValues
-    ) {
-      if (pinnedUsers.isNotEmpty()) {
-        item("pinned", span = { GridItemSpan(maxLineSpan) }) {
+  LazyColumn(contentPadding = contentPadding) {
+    if (pinnedUsers.isNotEmpty()) {
+      stickyHeader("pinned") {
+        ListItem(headlineContent = {
           Text(
             "Pinned",
             style = MaterialTheme.typography.labelMedium,
             modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp)
           )
-        }
+        })
+      }
 
-        items(pinnedUsers) { user ->
-          UserTile(user) {
-            navController.navigate(Routes.Home.purchase(user.serverId, user.userId))
-          }
+      items(
+        pinnedUsers,
+        key = { "pinned-${it.userId}" },
+      ) { user ->
+        UserListItem(user) { serverId, userId ->
+          navController.navigate(Routes.Home.purchase(serverId, userId))
+          viewModel.selectUser(serverId, userId)
         }
       }
+    }
 
-      for (character in 'A'..'Z') {
-        val group = users.filter { it.name.startsWith(character, ignoreCase = true) }
-        if (group.isNotEmpty()) {
-          item(character.toString(), span = { GridItemSpan(maxLineSpan) }) {
+    for (character in 'A'..'Z') {
+      val group = users.filter { it.name.startsWith(character, ignoreCase = true) }
+      if (group.isNotEmpty()) {
+        stickyHeader(character.toString()) {
+          ListItem(headlineContent = {
             Text(
               "$character",
               style = MaterialTheme.typography.labelMedium,
               modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp)
             )
-          }
+          })
+        }
 
-          items(group) { user ->
-            UserTile(user) {
-              navController.navigate(Routes.Home.purchase(user.serverId, user.userId))
-              viewModel.selectUser(user.serverId, user.userId)
-            }
+        items(
+          group,
+          key = { "${it.serverId}-${it.userId}" },
+        ) { user ->
+          UserListItem(user) { serverId, userId ->
+            navController.navigate(Routes.Home.purchase(serverId, userId))
+            viewModel.selectUser(serverId, userId)
           }
         }
       }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
index 468071c..0823feb 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
@@ -24,6 +24,7 @@
 
 package de.chaosdorf.meteroid.ui.users
 
+import android.util.Log
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
@@ -61,9 +62,10 @@ class UserListViewModel @Inject constructor(
   }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
 
   fun selectUser(serverId: ServerId, userId: UserId) {
+    Log.i("UserListViewModel", "Updating AccountPreferences: $serverId $userId")
     viewModelScope.launch {
-      preferences.setServer(serverId)
-      preferences.setUser(userId)
+
+      preferences.setUser(serverId, userId)
     }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt
index 73848b6..6d8e2cc 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt
@@ -30,16 +30,10 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material3.ListItem
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalConfiguration
@@ -52,121 +46,102 @@ import java.time.format.TextStyle
 @Composable
 fun WrappedScreen(
   viewModel: WrappedViewModel,
-  topBar: @Composable () -> Unit,
-  bottomBar: @Composable () -> Unit,
+  contentPadding: PaddingValues = PaddingValues(),
 ) {
-  val account by viewModel.account.collectAsState()
   val slides by viewModel.slides.collectAsState()
-  val snackbarHostState = remember { SnackbarHostState() }
 
-  LaunchedEffect(account) {
-    val offline = viewModel.checkOffline(account?.server)
-    snackbarHostState.currentSnackbarData?.dismiss()
-    if (offline) {
-      snackbarHostState.showSnackbar(
-        message = "Unable to connect to server",
-        duration = SnackbarDuration.Indefinite
-      )
-    }
-  }
+  LazyColumn(contentPadding = contentPadding) {
+    items(
+      slides,
+      key = { "${it::class.qualifiedName}" },
+    ) { slide ->
+      when (slide) {
+        is WrappedSlide.MostBoughtDrink ->
+          ListItem(
+            headlineContent = {
+              Text("Your favorite drink is ${slide.drink.name}")
+            },
+            supportingContent = {
+              Text("At least you enjoyed it ${slide.count} times this year.")
+            },
+            leadingContent = {
+              val thumbPainter = rememberAsyncImagePainter(
+                slide.drink.logoUrl
+              )
+              val drinkPainter = rememberAsyncImagePainter(
+                slide.drink.originalLogoUrl,
+                error = thumbPainter
+              )
 
-  Scaffold(
-    topBar = topBar,
-    bottomBar = bottomBar,
-    snackbarHost = {
-      SnackbarHost(hostState = snackbarHostState)
-    }
-  ) { paddingValues: PaddingValues ->
-    LazyColumn(contentPadding = paddingValues) {
-      items(slides) { slide ->
-        when (slide) {
-          is WrappedSlide.MostBoughtDrink ->
-            ListItem(
-              headlineContent = {
-                Text("Your favorite drink is ${slide.drink.name}")
-              },
-              supportingContent = {
-                Text("At least you enjoyed it ${slide.count} times this year.")
-              },
-              leadingContent = {
-                val thumbPainter = rememberAsyncImagePainter(
-                  slide.drink.logoUrl
-                )
-                val drinkPainter = rememberAsyncImagePainter(
-                  slide.drink.originalLogoUrl,
-                  error = thumbPainter
-                )
+              Image(
+                drinkPainter,
+                contentDescription = null,
+                contentScale = ContentScale.Fit,
+                modifier = Modifier.size(72.dp)
+              )
+            }
+          )
 
-                Image(
-                  drinkPainter,
-                  contentDescription = null,
-                  contentScale = ContentScale.Fit,
-                  modifier = Modifier.size(72.dp)
-                )
+        is WrappedSlide.Caffeine ->
+          ListItem(
+            headlineContent = {
+              Text("You consumed ${slide.total} mg of caffeine this year.")
+            },
+            supportingContent = {
+              slide.wouldKill?.let { animal ->
+                Text("This could kill a medium-weight ${animal.name}. Glad you're still here.")
               }
-            )
-
-          is WrappedSlide.Caffeine ->
-            ListItem(
-              headlineContent = {
-                Text("You consumed ${slide.total} mg of caffeine this year.")
-              },
-              supportingContent = {
-                slide.wouldKill?.let { animal ->
-                  Text("This could kill a medium-weight ${animal.name}. Glad you're still here.")
+            },
+            leadingContent = {
+              val painter = painterResource(
+                when (slide.wouldKill) {
+                  WrappedSlide.Caffeine.Animal.Squirrel -> R.drawable.wrapped_squirrel
+                  WrappedSlide.Caffeine.Animal.Rat -> R.drawable.wrapped_rat
+                  WrappedSlide.Caffeine.Animal.Cat -> R.drawable.wrapped_cat
+                  WrappedSlide.Caffeine.Animal.Koala -> R.drawable.wrapped_koala
+                  WrappedSlide.Caffeine.Animal.Lynx -> R.drawable.wrapped_lynx
+                  WrappedSlide.Caffeine.Animal.Jaguar -> R.drawable.wrapped_jaguar
+                  WrappedSlide.Caffeine.Animal.Reindeer -> R.drawable.wrapped_reindeer
+                  WrappedSlide.Caffeine.Animal.Gorilla -> R.drawable.wrapped_gorilla
+                  WrappedSlide.Caffeine.Animal.Lion -> R.drawable.wrapped_lion
+                  WrappedSlide.Caffeine.Animal.Bear -> R.drawable.wrapped_bear
+                  WrappedSlide.Caffeine.Animal.Moose -> R.drawable.wrapped_moose
+                  else -> R.drawable.wrapped_coffee_beans
                 }
-              },
-              leadingContent = {
-                val painter = painterResource(
-                  when (slide.wouldKill) {
-                    WrappedSlide.Caffeine.Animal.Squirrel -> R.drawable.wrapped_squirrel
-                    WrappedSlide.Caffeine.Animal.Rat -> R.drawable.wrapped_rat
-                    WrappedSlide.Caffeine.Animal.Cat -> R.drawable.wrapped_cat
-                    WrappedSlide.Caffeine.Animal.Koala -> R.drawable.wrapped_koala
-                    WrappedSlide.Caffeine.Animal.Lynx -> R.drawable.wrapped_lynx
-                    WrappedSlide.Caffeine.Animal.Jaguar -> R.drawable.wrapped_jaguar
-                    WrappedSlide.Caffeine.Animal.Reindeer -> R.drawable.wrapped_reindeer
-                    WrappedSlide.Caffeine.Animal.Gorilla -> R.drawable.wrapped_gorilla
-                    WrappedSlide.Caffeine.Animal.Lion -> R.drawable.wrapped_lion
-                    WrappedSlide.Caffeine.Animal.Bear -> R.drawable.wrapped_bear
-                    WrappedSlide.Caffeine.Animal.Moose -> R.drawable.wrapped_moose
-                    else -> R.drawable.wrapped_coffee_beans
-                  }
-                )
+              )
 
-                Image(
-                  painter,
-                  contentDescription = null,
-                  contentScale = ContentScale.Fit,
-                  modifier = Modifier.size(72.dp)
-                )
-              }
-            )
+              Image(
+                painter,
+                contentDescription = null,
+                contentScale = ContentScale.Fit,
+                modifier = Modifier.size(72.dp)
+              )
+            }
+          )
 
-          is WrappedSlide.MostActive ->
-            ListItem(
-              headlineContent = {
-                Text(
-                  "You were most active on ${
-                    slide.weekday.getDisplayName(
-                      TextStyle.FULL,
-                      LocalConfiguration.current.locale
-                    )
-                  }s at ${slide.hour} o'clock."
-                )
-              },
-              leadingContent = {
-                val painter = painterResource(R.drawable.wrapped_clock)
+        is WrappedSlide.MostActive ->
+          ListItem(
+            headlineContent = {
+              Text(
+                "You were most active on ${
+                  slide.weekday.getDisplayName(
+                    TextStyle.FULL,
+                    LocalConfiguration.current.locale
+                  )
+                }s at ${slide.hour} o'clock."
+              )
+            },
+            leadingContent = {
+              val painter = painterResource(R.drawable.wrapped_clock)
 
-                Image(
-                  painter,
-                  contentDescription = null,
-                  contentScale = ContentScale.Fit,
-                  modifier = Modifier.size(72.dp)
-                )
-              }
-            )
-        }
+              Image(
+                painter,
+                contentDescription = null,
+                contentScale = ContentScale.Fit,
+                modifier = Modifier.size(72.dp)
+              )
+            }
+          )
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt
new file mode 100644
index 0000000..f350dde
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PopUpToRoot.kt
@@ -0,0 +1,33 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.navigation.NavController
+
+fun NavController.popUpToRoot() {
+  while (popBackStack()) {
+    // repeat
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt
new file mode 100644
index 0000000..d1f83fa
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt
@@ -0,0 +1,51 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.RenderVectorGroup
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.unit.Dp
+import coil.compose.AsyncImagePainter
+import coil.compose.rememberAsyncImagePainter
+
+@Composable
+fun rememberAvatarPainter(url: String?, iconWidth: Dp, iconHeight: Dp, iconTint: Color): AsyncImagePainter {
+  val personPainter = rememberVectorPainter(
+      defaultHeight = iconHeight,
+      defaultWidth = iconWidth,
+      viewportWidth = Icons.Filled.Person.viewportWidth,
+      viewportHeight = Icons.Filled.Person.viewportHeight,
+      name = Icons.Filled.Person.name,
+      tintColor = iconTint,
+      tintBlendMode = Icons.Filled.Person.tintBlendMode,
+      autoMirror = Icons.Filled.Person.autoMirror,
+      content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) }
+  )
+  return rememberAsyncImagePainter(url, fallback = personPainter)
+}
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
index dae71d6..455a90c 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt
@@ -59,6 +59,9 @@ interface PinnedUserRepository {
   @Query("SELECT userId FROM PinnedUser WHERE serverId = :serverId")
   fun getAllFlow(serverId: ServerId): Flow<List<UserId>>
 
+  @Query("SELECT User.* FROM PinnedUser JOIN User on PinnedUser.userId = User.userId AND PinnedUser.serverId = User.serverId")
+  fun getPinnedUsersFlow(): Flow<List<User>>
+
   @Insert(onConflict = OnConflictStrategy.REPLACE)
   suspend fun save(user: PinnedUser)
 
-- 
GitLab