diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..d9f5985b38a1a7bed21654eb1178b3412c73106f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,30 @@
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+[{*.mod, *.dtd, *.ent, *.elt}]
+indent_style = space
+indent_size = 2
+
+[{*.jhm, *.rng, *.wsdl, *.fxml, *.xslt, *.jrxml, *.ant, *.xul, *.xsl, *.xsd, *.tld, *.jnlp, *.xml}]
+indent_style = space
+indent_size = 2
+
+[*.json]
+indent_style = space
+indent_size = 2
+
+[*.java]
+indent_style = space
+indent_size = 2
+
+[{*.kts, *.kt}]
+indent_style = space
+indent_size = 2
+
+[{*.yml, *.yaml}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..49ca4c228ed79f9d56056083a5bfcf559e1cd597
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/signing.properties
+/.idea/*
+!/.idea/copyright/
+.DS_Store
+/captures
+build/
+/reports/
+/persistence/schemas/
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..baf04cf77c8afb388ff9a9a780ab13debda7b312
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,102 @@
+@file:Suppress("UnstableApiUsage")
+
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Mareike Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+plugins {
+  id("justjanne.android.app")
+  alias(libs.plugins.kotlin.serialization)
+  alias(libs.plugins.kotlin.ksp)
+  alias(libs.plugins.dagger.hilt)
+}
+
+android {
+  namespace = "de.justjanne.chatconcept"
+
+  buildTypes {
+    getByName("release") {
+
+      proguardFiles(
+        getDefaultProguardFile("proguard-android.txt"),
+        "proguard-rules.pro"
+      )
+      signingConfig = signingConfigs.getByName("debug")
+    }
+
+    getByName("debug") {
+      applicationIdSuffix = ".debug"
+    }
+  }
+
+  buildFeatures {
+    compose = true
+  }
+
+  composeOptions {
+    kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
+  }
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.kotlinx.datetime)
+  implementation(libs.kotlinx.serialization.json)
+  coreLibraryDesugaring(libs.desugar.jdk)
+
+  implementation(libs.kotlinx.coroutines.android)
+  testImplementation(libs.kotlinx.coroutines.test)
+
+  testImplementation(libs.kotlin.test)
+  testImplementation(libs.junit.api)
+  testImplementation(libs.junit.params)
+  testRuntimeOnly(libs.junit.engine)
+
+  implementation(libs.androidx.appcompat)
+  implementation(libs.androidx.appcompat.resources)
+
+  implementation(libs.androidx.activity)
+  implementation(libs.androidx.activity.compose)
+
+  implementation(libs.androidx.compose.animation)
+  implementation(libs.androidx.compose.compiler)
+  implementation(libs.androidx.compose.foundation)
+  implementation(libs.androidx.compose.material)
+  implementation(libs.androidx.compose.material.icons)
+  implementation(libs.androidx.compose.runtime)
+  implementation(libs.androidx.compose.ui.tooling)
+
+  implementation(libs.androidx.room.runtime)
+  implementation(libs.androidx.room.ktx)
+  implementation(libs.androidx.room.paging)
+
+  implementation(libs.androidx.navigation.compose)
+
+  implementation(libs.okhttp)
+  implementation(libs.coil.compose)
+
+  implementation(libs.hilt.navigation)
+  implementation(libs.hilt.android)
+  ksp(libs.hilt.compiler)
+
+  implementation(libs.androidx.datastore.preferences)
+
+  debugImplementation(libs.androidx.compose.ui.tooling)
+  implementation(libs.androidx.compose.ui.preview)
+  testImplementation(libs.androidx.compose.ui.test)
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000000000000000000000000000000000000..690a4be68db285dea8841626be81fa215a589a84
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,54 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/lib/android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.kts.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# The project is open source anyway, obfuscation is useless.
+-dontobfuscate
+
+# remove unnecessary warnings
+# Android HTTP Libs
+-dontnote android.net.http.**
+-dontnote org.apache.http.**
+# Kotlin stuff
+-dontnote kotlin.**
+# Gson
+-dontnote com.google.gson.**
+# Dagger
+-dontwarn com.google.errorprone.annotations.*
+# Retrofit
+-dontwarn retrofit2.**
+# Annotation used by Retrofit on Java 8 VMs
+-dontwarn javax.annotation.Nullable
+-dontwarn javax.annotation.ParametersAreNonnullByDefault
+-dontwarn javax.annotation.concurrent.GuardedBy
+# Retain generic type information for use by reflection by converters and adapters.
+-keepattributes Signature
+# Retain declared checked exceptions for use by a Proxy instance.
+-keepattributes Exceptions
+# Okio
+-dontwarn okio.**
+-dontwarn org.conscrypt.**
+# OkHttp3
+-dontwarn okhttp3.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000000000000000000000000000000000..14ec190e23a483ba93c5726220241d9dfe0c6c72
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <uses-permission android:name="android.permission.INTERNET" />
+
+  <application
+    android:name=".ChatConceptApplication"
+    android:allowBackup="true"
+    android:icon="@mipmap/ic_launcher"
+    android:label="@string/application_name"
+    android:supportsRtl="true"
+    android:theme="@style/Theme.ChatConcept">
+    <activity
+      android:name=".MainActivity"
+      android:exported="true"
+      android:theme="@style/Theme.ChatConcept"
+      android:windowSoftInputMode="adjustResize">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <action android:name="android.intent.action.VIEW" />
+
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+
+</manifest>
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/ChatConceptApplication.kt b/app/src/main/kotlin/de/justjanne/chatconcept/ChatConceptApplication.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b26f8c340cfde0019ecea53cfdd07dad749497a8
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/ChatConceptApplication.kt
@@ -0,0 +1,7 @@
+package de.justjanne.chatconcept
+
+import android.app.Application
+
+class ChatConceptApplication : Application() {
+
+}
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/InputArea.kt b/app/src/main/kotlin/de/justjanne/chatconcept/InputArea.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f9a555f0d3363226f204dbed132d8c3cbb945d0e
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/InputArea.kt
@@ -0,0 +1,86 @@
+package de.justjanne.chatconcept
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+@Preview
+fun InputArea(modifier: Modifier = Modifier) {
+  var input by remember { mutableStateOf("") }
+  val interactionSource = remember { MutableInteractionSource() }
+
+  Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
+    BasicTextField(
+      value = input,
+      onValueChange = { input = it },
+      modifier = Modifier
+        .weight(1.0f, true)
+        .fillMaxHeight(),
+      singleLine = false,
+      maxLines = 5,
+      minLines = 1,
+      enabled = true,
+      interactionSource = interactionSource,
+    ) {
+      OutlinedTextFieldDefaults.DecorationBox(
+        value = input,
+        visualTransformation = VisualTransformation.None,
+        innerTextField = it,
+        singleLine = false,
+        enabled = true,
+        placeholder = { Text("Send a meow message…") },
+        interactionSource = interactionSource,
+        container = {
+          OutlinedTextFieldDefaults.ContainerBox(
+            enabled = true,
+            isError = false,
+            colors = OutlinedTextFieldDefaults.colors(
+              focusedBorderColor = Color.Transparent,
+              unfocusedBorderColor = Color.Transparent,
+              errorBorderColor = Color.Transparent,
+              disabledBorderColor = Color.Transparent,
+            ),
+            interactionSource = interactionSource,
+            shape = RectangleShape,
+            unfocusedBorderThickness = 0.dp,
+            focusedBorderThickness = 0.dp
+          )
+        }
+      )
+    }
+    IconButton(
+      onClick = { },
+      Modifier
+        .padding(4.dp)
+        .align(Alignment.Top)
+    ) {
+      Icon(
+        Icons.AutoMirrored.Default.Send,
+        contentDescription = null,
+        tint = MaterialTheme.colorScheme.secondary
+      )
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/MainActivity.kt b/app/src/main/kotlin/de/justjanne/chatconcept/MainActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..575ce058aca2e0f05de83797232b0ab5bea677a2
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/MainActivity.kt
@@ -0,0 +1,266 @@
+package de.justjanne.chatconcept
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+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.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+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.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberAsyncImagePainter
+import de.justjanne.chatconcept.theme.ChatConceptTheme
+import kotlinx.datetime.Instant
+
+data class User(
+  val id: String,
+  val displayName: String,
+  val avatarUrl: String,
+)
+
+sealed class FileAttachment {
+  data class Image(val url: String) : FileAttachment()
+  data class Video(val url: String) : FileAttachment()
+  data class Audio(val url: String) : FileAttachment()
+  data class Generic(val url: String) : FileAttachment()
+}
+
+sealed class Message {
+  data class TextMessage(val timestamp: Instant, val sender: User, val content: String) : Message()
+  data class Join(val timestamp: Instant, val sender: User) : Message()
+  data class Leave(val timestamp: Instant, val sender: User) : Message()
+  data class Kick(val timestamp: Instant, val sender: User, val subject: User) : Message()
+  data class AttachmentMessage(
+    val timestamp: Instant,
+    val sender: User,
+    val content: FileAttachment
+  ) : Message()
+}
+
+data class RoomMember(
+  val user: User,
+  val powerLevel: Int,
+  val membership: Membership
+)
+
+enum class Membership {
+  Joined,
+  Invited,
+  Banned
+}
+
+sealed class Room {
+  data class GroupRoom(
+    val displayName: String,
+    val topic: String,
+    val avatarUrl: String,
+    val members: List<RoomMember>,
+    val timeline: Timeline,
+  ) : Room()
+
+  data class DmRoom(
+    val user: User,
+    val members: List<RoomMember>,
+    val timeline: Timeline,
+  ) : Room()
+}
+
+data class Timeline(
+  val messages: List<Message>,
+  val outbox: List<Message>,
+)
+
+object SampleClient {
+  private val evelyn = User(
+    id = "@evelyn:eve.li",
+    displayName = "Evelyn",
+    avatarUrl = "https://avatars.githubusercontent.com/u/36997087?v=4"
+  )
+
+  private val janne = User(
+    id = "@justjanne:decentralised.chat",
+    displayName = "justJanne",
+    avatarUrl = "https://avatars.githubusercontent.com/u/3933349?v=4"
+  )
+
+  val rooms: List<Room> = listOf(
+    Room.DmRoom(
+      user = evelyn,
+      members = listOf(
+        RoomMember(evelyn, 100, Membership.Joined),
+        RoomMember(janne, 100, Membership.Joined),
+      ),
+      timeline = Timeline(
+        messages = listOf(
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:06:54Z"),
+            sender = evelyn,
+            content = "meow!"
+          ),
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:07:08Z"),
+            sender = evelyn,
+            content = "lust zu telefonieren? \uD83E\uDD7A"
+          ),
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:07:12Z"),
+            sender = janne,
+            content = "miaaaauuu"
+          ),
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:07:22Z"),
+            sender = evelyn,
+            content = "mag ui basteln \uD83E\uDD7A"
+          ),
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:07:32Z"),
+            sender = janne,
+            content = "gerne, wie ist's in 2min?"
+          ),
+          Message.TextMessage(
+            timestamp = Instant.parse("2023-11-16T17:07:41Z"),
+            sender = evelyn,
+            content = "ja"
+          ),
+        ),
+        outbox = emptyList()
+      )
+    )
+  )
+}
+
+class SampleMessageProvider : PreviewParameterProvider<Message> {
+  override val values = (SampleClient.rooms[0] as Room.DmRoom).timeline.messages.asSequence()
+}
+
+@Composable
+@Preview(widthDp = 120, showBackground = true)
+fun MessageView(
+  @PreviewParameter(SampleMessageProvider::class)
+  message: Message
+) {
+  Row {
+
+    Text("Hallo")
+  }
+}
+
+@Composable
+fun Avatar(name: String, imageUrl: String, modifier: Modifier = Modifier) {
+  val avatarPainter = rememberAsyncImagePainter(imageUrl)
+
+  Box(modifier) {
+    Text(
+      name.first().toString(),
+      modifier = Modifier.align(Alignment.Center)
+    )
+    Image(
+      avatarPainter,
+      contentDescription = null,
+      contentScale = ContentScale.Fit,
+      modifier = Modifier
+        .aspectRatio(1.0f)
+        .clip(CircleShape)
+        .background(MaterialTheme.colorScheme.primaryContainer)
+    )
+  }
+}
+
+@Composable
+@Preview(showSystemUi = true)
+fun ChatConceptApp() {
+  val room = SampleClient.rooms[0] as Room.DmRoom
+
+  Scaffold(
+    topBar = {
+      TopAppBar(
+        title = { Text(room.user.displayName) },
+        navigationIcon = {
+          Avatar(
+            room.user.displayName,
+            room.user.avatarUrl,
+            modifier = Modifier.size(48.dp)
+          )
+        },
+        modifier = Modifier.shadow(8.dp)
+      )
+    }
+  ) { padding ->
+    Box {
+      Column {
+        LazyColumn(
+          contentPadding = padding,
+          modifier = Modifier
+            .background(Color.Blue)
+            .fillMaxSize()
+        ) {
+          items(room.timeline.messages) { message ->
+            MessageView(message)
+          }
+        }
+        Spacer(Modifier.height(64.dp))
+      }
+      Surface(
+        modifier = Modifier
+          .height(240.dp)
+          .fillMaxWidth()
+          .align(Alignment.BottomCenter)
+          .offset(0.dp, 240.dp - 64.dp),
+        tonalElevation = 16.dp,
+        shadowElevation = 16.dp
+      ) {
+        Column(Modifier.fillMaxWidth()) {
+
+          Surface(
+            color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f),
+            shape = MaterialTheme.shapes.extraLarge,
+            modifier = Modifier
+              .padding(top = 4.dp)
+              .align(Alignment.CenterHorizontally)
+          ) {
+            Box(Modifier.size(width = 32.dp, height = 4.dp))
+          }
+          InputArea(Modifier.fillMaxSize())
+        }
+      }
+    }
+  }
+}
+
+class MainActivity : ComponentActivity() {
+  override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+    setContent {
+      ChatConceptTheme {
+        ChatConceptApp()
+      }
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/theme/Color.kt b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Color.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5b4b98c0f7dee7042f0d5fcf6a0455332127298e
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Color.kt
@@ -0,0 +1,11 @@
+package de.justjanne.chatconcept.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/theme/Theme.kt b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Theme.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6398668eebccf4b95171eab55bf141a379c4a147
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Theme.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.justjanne.chatconcept.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+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.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+  primary = Purple80,
+  secondary = PurpleGrey80,
+  tertiary = Pink80
+)
+
+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),
+  */
+)
+
+@Composable
+fun ChatConceptTheme(
+  darkTheme: Boolean = isSystemInDarkTheme(),
+  // Dynamic color is available on Android 12+
+  dynamicColor: Boolean = true,
+  content: @Composable () -> Unit
+) {
+  val colorScheme = when {
+    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+      val context = LocalContext.current
+      if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+    }
+
+    darkTheme -> DarkColorScheme
+    else -> LightColorScheme
+  }
+  val view = LocalView.current
+  if (!view.isInEditMode) {
+    SideEffect {
+      val window = (view.context as Activity).window
+      window.statusBarColor = colorScheme.primary.toArgb()
+      WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+    }
+  }
+
+  MaterialTheme(
+    colorScheme = colorScheme,
+    typography = Typography,
+    content = content
+  )
+}
diff --git a/app/src/main/kotlin/de/justjanne/chatconcept/theme/Type.kt b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Type.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be1fa7a4aefbe7c079acbd77f2deb3926ad7b4f6
--- /dev/null
+++ b/app/src/main/kotlin/de/justjanne/chatconcept/theme/Type.kt
@@ -0,0 +1,34 @@
+package de.justjanne.chatconcept.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+  bodyLarge = TextStyle(
+    fontFamily = FontFamily.Default,
+    fontWeight = FontWeight.Normal,
+    fontSize = 16.sp,
+    lineHeight = 24.sp,
+    letterSpacing = 0.5.sp
+  )
+  /* Other default text styles to override
+  titleLarge = TextStyle(
+      fontFamily = FontFamily.Default,
+      fontWeight = FontWeight.Normal,
+      fontSize = 22.sp,
+      lineHeight = 28.sp,
+      letterSpacing = 0.sp
+  ),
+  labelSmall = TextStyle(
+      fontFamily = FontFamily.Default,
+      fontWeight = FontWeight.Medium,
+      fontSize = 11.sp,
+      lineHeight = 16.sp,
+      letterSpacing = 0.5.sp
+  )
+  */
+)
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b6c4512da56438e2b5145882b4e74cf572649f3
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3212f661f259dbcb980dadfcdb05946da5e88940
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<resources>
+    <string name="application_name">ChatConcept</string>
+</resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000000000000000000000000000000000000..895101c2a5379c9ab1bed01ae952ec707a284407
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <style name="Theme.ChatConcept" parent="Theme.Material.DayNight.NoActionBar">
+    <item name="android:colorPrimary">#ff9800</item>
+    <item name="android:colorAccent">#3a3f44</item>
+    <item name="android:statusBarColor">@android:color/transparent</item>
+    <item name="android:navigationBarColor">@android:color/transparent</item>
+    <item name="android:dialogTheme">@style/Theme.DialogFullScreen</item>
+  </style>
+
+  <style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar" />
+
+  <style name="Theme.DialogFullScreen" parent="Theme.Material.DayNight.NoActionBar">
+    <item name="android:windowMinWidthMajor">100%</item>
+    <item name="android:windowMinWidthMinor">100%</item>
+  </style>
+</resources>
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..c2d0836b0754bedeed89b7086bb684a4bb9c94dd
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,16 @@
+group = "de.justjanne"
+
+buildscript {
+  repositories {
+    google()
+    mavenCentral()
+  }
+}
+
+plugins {
+  alias(libs.plugins.android.application) apply false
+  alias(libs.plugins.kotlin.jvm) apply false
+  alias(libs.plugins.kotlin.serialization) apply false
+  alias(libs.plugins.kotlin.ksp) apply false
+  alias(libs.plugins.dagger.hilt) apply false
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..de22634d32d2c6b737ec4a7481c65d653f5e0053
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,33 @@
+# Quasseldroid - Quassel client for Android
+#
+# Copyright (c) 2019 Janne Mareike Koschinski
+# Copyright (c) 2019 The Quassel Project
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3 as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+#org.gradle.parallel=true
+# Enable gradle build cache
+#org.gradle.caching=true
+# Enable AndroidX
+android.useAndroidX=true
diff --git a/gradle/convention/build.gradle.kts b/gradle/convention/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..5039b124e68adbd03fc09bf01043bef7cbe3e458
--- /dev/null
+++ b/gradle/convention/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+  `kotlin-dsl`
+}
+
+repositories {
+  gradlePluginPortal()
+  mavenCentral()
+  google()
+}
+
+dependencies {
+  compileOnly(libs.android.gradlePlugin)
+  compileOnly(libs.kotlin.gradlePlugin)
+}
+
+gradlePlugin {
+  plugins {
+    register("androidApplication") {
+      id = "justjanne.android.app"
+      implementationClass = "AndroidApplicationConvention"
+    }
+    register("androidLibrary") {
+      id = "justjanne.android.library"
+      implementationClass = "AndroidLibraryConvention"
+    }
+    register("kotlinAndroid") {
+      id = "justjanne.kotlin.android"
+      implementationClass = "KotlinAndroidConvention"
+    }
+    register("kotlin") {
+      id = "justjanne.kotlin"
+      implementationClass = "KotlinConvention"
+    }
+  }
+}
+
+configure<JavaPluginExtension> {
+  toolchain {
+    languageVersion.set(JavaLanguageVersion.of(17))
+  }
+}
diff --git a/gradle/convention/gradle.properties b/gradle/convention/gradle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..c6cd2a7e2f146d9ed89db0cb1f4de21ac673e9fd
--- /dev/null
+++ b/gradle/convention/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
diff --git a/gradle/convention/gradle/wrapper/gradle-wrapper.jar b/gradle/convention/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa
Binary files /dev/null and b/gradle/convention/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/convention/gradle/wrapper/gradle-wrapper.properties b/gradle/convention/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..fb6d3d63b99d27ecd955b3e04b14c08a0c03ab39
--- /dev/null
+++ b/gradle/convention/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionSha256Szm=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradle/convention/settings.gradle.kts b/gradle/convention/settings.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..1475431930d7052a7235f9527a9c4926f9013683
--- /dev/null
+++ b/gradle/convention/settings.gradle.kts
@@ -0,0 +1,13 @@
+rootProject.name = "convention"
+
+dependencyResolutionManagement {
+  repositories {
+    google()
+    mavenCentral()
+  }
+  versionCatalogs {
+    create("libs") {
+      from(files("../libs.versions.toml"))
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a6a58c475dcc2b736963bf649aad2bbd940a4f26
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt
@@ -0,0 +1,66 @@
+import com.android.build.api.dsl.ApplicationExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import util.cmd
+import util.properties
+import java.util.Locale
+
+class AndroidApplicationConvention : Plugin<Project> {
+  override fun apply(target: Project) {
+    with(target) {
+      with(pluginManager) {
+        apply("com.android.application")
+        apply("justjanne.kotlin.android")
+      }
+
+      extensions.configure<ApplicationExtension> {
+        compileSdk = 34
+
+        defaultConfig {
+          minSdk = 21
+          targetSdk = 34
+
+          applicationId = "${rootProject.group}.${rootProject.name.lowercase(Locale.ROOT)}"
+          versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1
+          versionName = cmd("git", "describe", "--always", "--tags", "HEAD") ?: "1.0.0"
+
+          signingConfig = signingConfigs.findByName("default")
+
+          setProperty("archivesBaseName", "${rootProject.name}-$versionName")
+
+          // Disable test runner analytics
+          testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+        }
+
+        signingConfigs {
+          SigningData.of(project.rootProject.properties("signing.properties"))?.let {
+            create("default") {
+              storeFile = file(it.storeFile)
+              storePassword = it.storePassword
+              keyAlias = it.keyAlias
+              keyPassword = it.keyPassword
+            }
+          }
+        }
+
+        compileOptions {
+          sourceCompatibility = JavaVersion.VERSION_17
+          targetCompatibility = JavaVersion.VERSION_17
+
+          isCoreLibraryDesugaringEnabled = true
+        }
+
+        testOptions {
+          unitTests.isIncludeAndroidResources = true
+        }
+
+        lint {
+          warningsAsErrors = true
+          lintConfig = file("../lint.xml")
+        }
+      }
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ead4e60c0cec6ebd386f3b559a5210b89e8a51d2
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/AndroidLibraryConvention.kt
@@ -0,0 +1,37 @@
+import com.android.build.api.dsl.LibraryExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+
+class AndroidLibraryConvention : Plugin<Project> {
+  override fun apply(target: Project) {
+    with(target) {
+      with(pluginManager) {
+        apply("com.android.library")
+        apply("justjanne.kotlin.android")
+      }
+
+      extensions.configure<LibraryExtension> {
+        compileSdk = 34
+
+        defaultConfig {
+          minSdk = 21
+
+          consumerProguardFiles("proguard-rules.pro")
+
+          // Disable test runner analytics
+          testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+        }
+
+        compileOptions {
+          isCoreLibraryDesugaringEnabled = true
+        }
+
+        lint {
+          warningsAsErrors = true
+          lintConfig = file("../lint.xml")
+        }
+      }
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/KotlinAndroidConvention.kt b/gradle/convention/src/main/kotlin/KotlinAndroidConvention.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dbbe81b8fa8dbe87369a75269b16f5c87be27a60
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/KotlinAndroidConvention.kt
@@ -0,0 +1,46 @@
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.tasks.testing.Test
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.withType
+import util.kotlinOptions
+
+@Suppress("UnstableApiUsage")
+class KotlinAndroidConvention : Plugin<Project> {
+  override fun apply(target: Project) {
+    with(target) {
+      with(pluginManager) {
+        apply("org.jetbrains.kotlin.android")
+      }
+
+      extensions.configure<BaseExtension> {
+        kotlinOptions {
+          freeCompilerArgs = freeCompilerArgs + listOf(
+            "-opt-in=kotlin.RequiresOptIn",
+            // Enable experimental coroutines APIs, including Flow
+            "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+            "-opt-in=kotlinx.coroutines.FlowPreview",
+            "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
+            "-opt-in=kotlin.ExperimentalUnsignedTypes",
+          )
+
+          jvmTarget = JavaVersion.VERSION_17.toString()
+        }
+      }
+
+      tasks.withType<Test> {
+        useJUnitPlatform()
+      }
+
+      configure<JavaPluginExtension> {
+        toolchain {
+          languageVersion.set(JavaLanguageVersion.of(17))
+        }
+      }
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/KotlinConvention.kt b/gradle/convention/src/main/kotlin/KotlinConvention.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d99f88171db54cdf468239e7c182cf8221b3d722
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/KotlinConvention.kt
@@ -0,0 +1,45 @@
+import org.gradle.api.JavaVersion
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.tasks.testing.Test
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
+
+class KotlinConvention : Plugin<Project> {
+  override fun apply(target: Project) {
+    with(target) {
+      with(pluginManager) {
+        apply("org.jetbrains.kotlin.jvm")
+      }
+
+      extensions.configure<KotlinJvmProjectExtension> {
+        compilerOptions.freeCompilerArgs.addAll(
+          "-opt-in=kotlin.RequiresOptIn",
+          // Enable experimental coroutines APIs, including Flow
+          "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+          "-opt-in=kotlinx.coroutines.FlowPreview",
+          "-opt-in=kotlin.Experimental",
+          "-opt-in=kotlin.ExperimentalUnsignedTypes",
+        )
+
+        compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
+      }
+
+      tasks.withType<Test> {
+        useJUnitPlatform()
+      }
+
+      configure<JavaPluginExtension> {
+        toolchain {
+          languageVersion.set(JavaLanguageVersion.of(17))
+        }
+      }
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/SigningData.kt b/gradle/convention/src/main/kotlin/SigningData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..606738eecbb111455e19e9b5d03d2eb1bcba617e
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/SigningData.kt
@@ -0,0 +1,21 @@
+import java.util.*
+
+data class SigningData(
+  val storeFile: String,
+  val storePassword: String,
+  val keyAlias: String,
+  val keyPassword: String
+) {
+  companion object {
+    fun of(properties: Properties?): SigningData? {
+      if (properties == null) return null
+
+      val storeFile = properties.getProperty("storeFile") ?: return null
+      val storePassword = properties.getProperty("storePassword") ?: return null
+      val keyAlias = properties.getProperty("keyAlias") ?: return null
+      val keyPassword = properties.getProperty("keyPassword") ?: return null
+
+      return SigningData(storeFile, storePassword, keyAlias, keyPassword)
+    }
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/util/BaseExtensionExtensions.kt b/gradle/convention/src/main/kotlin/util/BaseExtensionExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..667f8dac0c5f3e58641f887081bd7af00ca9b388
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/util/BaseExtensionExtensions.kt
@@ -0,0 +1,8 @@
+package util
+
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.plugins.ExtensionAware
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
+
+fun BaseExtension.kotlinOptions(configure: KotlinJvmOptions.() -> Unit): Unit =
+  (this as ExtensionAware).extensions.configure("kotlinOptions", configure)
diff --git a/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9ab40febbebc72d5631e5771626939ccef16e4a9
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt
@@ -0,0 +1,26 @@
+package util
+
+import org.gradle.api.Project
+import java.io.ByteArrayOutputStream
+import java.util.Properties
+
+fun Project.cmd(vararg command: String) = try {
+  val stdOut = ByteArrayOutputStream()
+  exec {
+    commandLine(*command)
+    standardOutput = stdOut
+  }
+  stdOut.toString(Charsets.UTF_8.name()).trim()
+} catch (e: Throwable) {
+  e.printStackTrace()
+  null
+}
+
+fun Project.properties(fileName: String): Properties? {
+  val file = file(fileName)
+  if (!file.exists())
+    return null
+  val props = Properties()
+  props.load(file.inputStream())
+  return props
+}
diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..7f161bac4f5349cc190dad7a4fa818f30d51da63
--- /dev/null
+++ b/gradle/init.gradle.kts
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+val ktlintVersion = "0.48.1"
+
+initscript {
+  val spotlessVersion = "6.13.0"
+
+  repositories {
+    mavenCentral()
+  }
+
+  dependencies {
+    classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
+  }
+}
+
+rootProject {
+  subprojects {
+    apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
+    extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
+      kotlin {
+        target("**/*.kt")
+        targetExclude("**/build/**/*.kt")
+        ktlint(ktlintVersion).userData(mapOf("android" to "true"))
+        licenseHeaderFile(rootProject.file("gradle/spotless/copyright.kt"))
+      }
+      format("kts") {
+        target("**/*.kts")
+        targetExclude("**/build/**/*.kts")
+        // Look for the first line that doesn't have a block comment (assumed to be the license)
+        licenseHeaderFile(rootProject.file("gradle/spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
+      }
+      format("xml") {
+        target("**/*.xml")
+        targetExclude("**/build/**/*.xml")
+        // Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
+        licenseHeaderFile(rootProject.file("gradle/spotless/copyright.xml"), "(<[^!?])")
+      }
+    }
+  }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000000000000000000000000000000000000..7d17d98c36153e3781bde380b0437745f8e56180
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,92 @@
+[versions]
+androidGradlePlugin = "8.1.3"
+androidx-activity = "1.8.0"
+androidx-appcompat = "1.6.1"
+androidx-compose-bom = "2023.10.01"
+androidx-compose-compiler = "1.5.4"
+androidx-compose-material = "1.5.0-alpha04"
+androidx-compose-material3 = "1.2.0-alpha10"
+androidx-compose-runtimetracing = "1.0.0-alpha04"
+androidx-compose-tooling = "1.6.0-alpha08"
+androidx-datastore = "1.0.0"
+androidx-hilt = "1.1.0"
+androidx-navigation = "2.7.5"
+androidx-room = "2.6.0"
+coil = "2.4.0"
+dagger-hilt = "2.48.1"
+desugar-jdk = "2.0.4"
+kotlin = "1.9.20"
+kotlin-ksp = "1.9.20-1.0.14"
+kotlinxCoroutines = "1.7.1"
+kotlinxDatetime = "0.4.0"
+kotlinxSerializationJson = "1.5.1"
+junit = "5.10.0"
+
+[libraries]
+androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
+
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
+androidx-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" }
+
+androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "androidx-compose-compiler" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
+androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
+androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
+androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
+androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.6.0-alpha08" }
+androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
+androidx-compose-material-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" }
+androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
+androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
+androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidx-compose-runtimetracing" }
+androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test" }
+androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-tooling" }
+androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose-tooling" }
+androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
+
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
+
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" }
+androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" }
+
+coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
+
+hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger-hilt" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger-hilt" }
+hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
+okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" }
+retrofit-core = { module = "com.squareup.retrofit2:retrofit", version = "2.9.0" }
+retrofit-converter-kotlinx = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
+
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
+
+junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
+junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
+junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine" }
+
+kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
+kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "kotlin" }
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
+kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+
+desugar-jdk = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk" }
+
+# Dependencies of the included build-logic
+android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
+kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
+dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin.ksp" }
diff --git a/gradle/spotless/copyright.kt b/gradle/spotless/copyright.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cc357e57ccfd48837c7436bd410605601f2200c4
--- /dev/null
+++ b/gradle/spotless/copyright.kt
@@ -0,0 +1,23 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-$YEAR 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.
+ */
diff --git a/gradle/spotless/copyright.kts b/gradle/spotless/copyright.kts
new file mode 100644
index 0000000000000000000000000000000000000000..cc357e57ccfd48837c7436bd410605601f2200c4
--- /dev/null
+++ b/gradle/spotless/copyright.kts
@@ -0,0 +1,23 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-$YEAR 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.
+ */
diff --git a/gradle/spotless/copyright.xml b/gradle/spotless/copyright.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6c59cbaeeab079c0254e5cc4df972b333efa8204
--- /dev/null
+++ b/gradle/spotless/copyright.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    The MIT License (MIT)
+
+    Copyright (c) 2013-$YEAR 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.
+-->
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..3fa8f862f753336d4fabfd607678a7a2317e8a06
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000000000000000000000000000000000..1aa94a4269074199e6ed2c37e8db3e0826030965
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000000000000000000000000000000000..6689b85beecde676054c39c2408085f41e6be6dc
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lint.xml b/lint.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eab67bd5e6f29d10e42082945d8a9195c76e4777
--- /dev/null
+++ b/lint.xml
@@ -0,0 +1,53 @@
+<!--
+  Quasseldroid - Quassel client for Android
+
+  Copyright (c) 2019 Janne Mareike Koschinski
+  Copyright (c) 2019 The Quassel Project
+
+  This program is free software: you can redistribute it and/or modify it
+  under the terms of the GNU General Public License version 3 as published
+  by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program. If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<lint>
+  <issue id="NewerVersionAvailable" severity="error" />
+
+  <!-- Because of course paging and room have incompatible versions -->
+  <issue id="GradleCompatible" severity="ignore" />
+
+  <!-- Because these are entirely broken -->
+  <issue id="ResourceType" severity="ignore" />
+  <issue id="UnusedResources" severity="ignore" />
+  <issue id="ObsoleteLintCustomCheck" severity="ignore" />
+  <issue id="UnusedAttribute" severity="informational" />
+
+  <!-- Because this doesn’t work when using splash themes -->
+  <issue id="Overdraw" severity="ignore" />
+
+  <!-- Can’t request a translation without a release, can’t release without translation -->
+  <issue id="MissingTranslation" severity="informational" />
+  <!-- Because we don’t use app bundles and never will use them -->
+  <issue id="AppBundleLocaleChanges" severity="informational" />
+  <!-- Because this tries to apply english orthography to other locales -->
+  <issue id="Typos" severity="ignore" />
+
+  <!-- Because Autofill isn’t a priority at the moment -->
+  <issue id="Autofill" severity="informational" />
+
+  <!-- We’re not using AGP 5 yet, and once we are, we’ll use compose anyway -->
+  <issue id="NonConstantResourceId" severity="informational" />
+
+  <!-- It’s only used for testing -->
+  <issue id="TrustAllX509TrustManager" severity="informational" />
+
+  <!-- TODO for the future -->
+  <issue id="DataExtractionRules" severity="informational" />
+</lint>
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..e77b474c1407546ab8b7898fbfedfd615be124ad
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+rootProject.name = "chatconcept"
+
+pluginManagement {
+  includeBuild("gradle/convention")
+  repositories {
+    google()
+    mavenCentral()
+    gradlePluginPortal()
+  }
+}
+
+dependencyResolutionManagement {
+  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+  repositories {
+    google()
+    mavenCentral()
+  }
+}
+
+include(":app")