diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 61256ce9..9623579d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -96,6 +96,7 @@
+
@@ -240,6 +241,7 @@
+
@@ -329,6 +331,11 @@
+
+
+
+
+
@@ -20771,6 +20778,11 @@
+
+
+
+
+
diff --git a/material-color-utilities/build.gradle b/material-color-utilities/build.gradle
index 19996c1f..d1e3b6ac 100644
--- a/material-color-utilities/build.gradle
+++ b/material-color-utilities/build.gradle
@@ -1,3 +1,4 @@
+
/*
* Nextcloud Android Common Library
*
diff --git a/sample/build.gradle b/sample/build.gradle
index e6501918..5c3b135f 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -45,6 +45,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.10.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation project(path: ':ui')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index bc5dab68..153a8ae0 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
+ android:usesCleartextTraffic="true"
android:theme="@style/Theme.Androidcommon">
+ Toast.makeText(this, result, Toast.LENGTH_LONG).show()
+ }
+
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
mainViewModel.color.observe(this) { applyTheme(it) }
diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt
index 457600c7..4744a557 100644
--- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt
+++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt
@@ -9,7 +9,41 @@ package com.nextcloud.android.common.sample
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.android.common.ui.network.auth.ServerCredentials
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
+import com.nextcloud.android.common.ui.network.UserStatusRepository
+import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
val color = MutableLiveData()
+ val apiTestResult = MutableLiveData()
+
+ fun testPredefinedStatuses(
+ baseUrl: String,
+ username: String,
+ token: String
+ ) {
+ viewModelScope.launch {
+ val credentials = ServerCredentials(baseUrl, username, token)
+ val client = NextcloudHttpClient.create(credentials, enableLogging = true)
+ val service = UserStatusRepository(client)
+
+ when (val result = service.fetchPredefinedStatuses()) {
+ is NetworkResult.Success ->
+ apiTestResult.value =
+ "✅ Success (${result.data.size} statuses):\n" +
+ result.data.joinToString("\n") { "${it.icon} ${it.message}" }
+
+ is NetworkResult.ServerError ->
+ apiTestResult.value =
+ "❌ Error ${result.response.ocs.meta.statusCode}: ${result.response.ocs.meta.message}"
+
+ is NetworkResult.NetworkException ->
+ apiTestResult.value =
+ "❌ Exception: ${result.throwable.message}"
+ }
+ }
+ }
}
diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml
index 23447605..2e4f232b 100644
--- a/sample/src/main/res/layout/activity_main.xml
+++ b/sample/src/main/res/layout/activity_main.xml
@@ -193,6 +193,67 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/circular_progress_bar" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
index 1b957aa2..f7db289e 100644
--- a/sample/src/main/res/values/strings.xml
+++ b/sample/src/main/res/values/strings.xml
@@ -19,6 +19,10 @@
Suggestion Chip
Filter Chip
Color
+ Base URL
+ Username
+ App token
+ Test User Status API
Theming
UI Module
\ No newline at end of file
diff --git a/ui/build.gradle b/ui/build.gradle
index 52a7e6aa..07297dd8 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -7,6 +7,7 @@
plugins {
id 'org.jetbrains.kotlin.plugin.compose'
+ id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion"
id 'com.android.library'
id 'com.android.built-in-kotlin'
id 'com.android.legacy-kapt'
@@ -15,7 +16,7 @@ plugins {
android {
defaultConfig {
- minSdk = 21
+ minSdk = 26
compileSdk = 36
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -45,12 +46,19 @@ android {
}
dependencies {
+ implementation 'androidx.compose.ui:ui-tooling-preview:1.11.0'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.11.0'
kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}"
implementation(platform("androidx.compose:compose-bom:2026.05.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-core")
+
+ implementation("io.coil-kt.coil3:coil-compose:3.4.0")
+ implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
+ implementation("io.coil-kt.coil3:coil-svg:3.4.0")
implementation("com.vanniktech:ui:0.10.0")
@@ -60,6 +68,12 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
+ implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.2"))
+ implementation("com.squareup.okhttp3:okhttp")
+ implementation("com.squareup.okhttp3:logging-interceptor")
+
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
+
implementation project(':core')
api project(':material-color-utilities')
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index b830d265..447589b1 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -5,6 +5,6 @@
~ SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: MIT
-->
-
+
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt
new file mode 100644
index 00000000..3d28ffc2
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt
@@ -0,0 +1,64 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ContentUnavailableView(title: String, description: String? = null, iconId: Int? = null) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ iconId?.let {
+ Image(
+ painter = painterResource(iconId),
+ modifier = Modifier.size(48.dp),
+ contentDescription = "",
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center
+ )
+
+ description?.let {
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = description,
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt
new file mode 100644
index 00000000..bc1eb79b
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt
@@ -0,0 +1,43 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonPrimitive
+
+object ClearAtTimeSerializer : KSerializer {
+ override val descriptor = PrimitiveSerialDescriptor("ClearAtTime", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): String {
+ val jsonDecoder = decoder as? JsonDecoder ?: return decoder.decodeString()
+ return (jsonDecoder.decodeJsonElement() as? JsonPrimitive)?.content ?: ""
+ }
+
+ override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value)
+}
+
+@Serializable
+data class ClearAt(
+ val type: String,
+ @Serializable(with = ClearAtTimeSerializer::class)
+ val time: String
+)
+
+@Serializable
+data class PredefinedStatus(
+ val id: String,
+ val icon: String,
+ val message: String,
+ val clearAt: ClearAt? = null
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt
new file mode 100644
index 00000000..7c36bdf9
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network
+
+import com.nextcloud.android.common.ui.network.http.HttpMethod
+import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.network.model.OcsResponse
+import com.nextcloud.android.common.ui.network.serialization.OCSSerializer
+
+class UserStatusRepository(private val client: NextcloudHttpClient) {
+
+ private companion object {
+ private const val PREDEFINED_STATUSES_ENDPOINT =
+ "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses"
+ }
+
+ suspend fun fetchPredefinedStatuses(): NetworkResult> =
+ client.executeRequest(
+ endpoint = PREDEFINED_STATUSES_ENDPOINT,
+ method = HttpMethod.GET
+ ) { body ->
+ OCSSerializer.json.decodeFromString>>(body).ocs.data
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt
new file mode 100644
index 00000000..07c7b485
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt
@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.auth
+
+import okhttp3.Credentials
+import okhttp3.Interceptor
+import okhttp3.Response
+
+class AuthInterceptor(private val credentials: ServerCredentials) : Interceptor {
+
+ private companion object {
+ private const val HTTP_PREFIX = "http://"
+ private const val HTTPS_PREFIX = "https://"
+ private const val DELIMITER = '/'
+
+ private const val HEADER_AUTHORIZATION = "Authorization"
+ private const val HEADER_OCS_REQUEST = "OCS-APIRequest"
+ private const val HEADER_OCS_REQUEST_VALUE = "true"
+ }
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val basicCredentials = Credentials.basic(credentials.username, credentials.token)
+
+ val request = chain.request()
+ .newBuilder()
+ .header(HEADER_AUTHORIZATION, basicCredentials)
+ .header(HEADER_OCS_REQUEST, HEADER_OCS_REQUEST_VALUE)
+ .url(resolveUrl(chain.request().url.toString()))
+ .build()
+
+ return chain.proceed(request)
+ }
+
+ /**
+ * Prepends [ServerCredentials.baseURL] to relative URLs.
+ * Absolute URLs (starting with http/https) are passed through unchanged.
+ */
+ private fun resolveUrl(requestUrl: String): String =
+ if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) {
+ requestUrl
+ } else {
+ "${credentials.baseURL.trimEnd(DELIMITER)}/${requestUrl.trimStart(DELIMITER)}"
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt
new file mode 100644
index 00000000..b299353c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt
@@ -0,0 +1,14 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.auth
+
+data class ServerCredentials(
+ val baseURL: String,
+ val username: String,
+ val token: String
+)
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt
new file mode 100644
index 00000000..f5901b8d
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt
@@ -0,0 +1,15 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.http
+
+enum class HttpMethod(val type: String) {
+ GET("GET"),
+ POST("POST"),
+ PUT("PUT"),
+ DELETE("DELETE")
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt
new file mode 100644
index 00000000..8996bfb2
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt
@@ -0,0 +1,76 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.http
+
+import com.nextcloud.android.common.ui.network.auth.AuthInterceptor
+import com.nextcloud.android.common.ui.network.auth.ServerCredentials
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.network.model.OcsResponse
+import com.nextcloud.android.common.ui.network.serialization.OCSSerializer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.RequestBody
+import okhttp3.logging.HttpLoggingInterceptor
+import java.util.concurrent.TimeUnit
+
+class NextcloudHttpClient private constructor(
+ val okHttpClient: OkHttpClient,
+ val credentials: ServerCredentials
+) {
+ companion object {
+ private const val CONNECT_TIMEOUT_SECONDS = 90L
+ private const val READ_TIMEOUT_SECONDS = 90L
+ private const val WRITE_TIMEOUT_SECONDS = 90L
+
+ fun create(
+ credentials: ServerCredentials,
+ enableLogging: Boolean = false
+ ): NextcloudHttpClient {
+ val loggingInterceptor = HttpLoggingInterceptor().apply {
+ level = if (enableLogging) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+ }
+
+ val okHttpClient = OkHttpClient.Builder()
+ .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .addInterceptor(AuthInterceptor(credentials))
+ .addInterceptor(loggingInterceptor)
+ .build()
+
+ return NextcloudHttpClient(okHttpClient, credentials)
+ }
+ }
+
+ private val baseUrl get() = credentials.baseURL.trimEnd('/')
+
+ suspend fun executeRequest(
+ endpoint: String,
+ method: HttpMethod,
+ body: RequestBody? = null,
+ parse: (String) -> T
+ ): NetworkResult = withContext(Dispatchers.IO) {
+ val request = buildOcsRequest("$baseUrl$endpoint", method, body)
+ try {
+ val response = okHttpClient.newCall(request).execute()
+ val responseBody = response.body.string()
+ if (response.isSuccessful) {
+ NetworkResult.Success(parse(responseBody))
+ } else {
+ NetworkResult.ServerError(OCSSerializer.json.decodeFromString>(responseBody))
+ }
+ } catch (e: Exception) {
+ NetworkResult.fromException(e)
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt
new file mode 100644
index 00000000..7e7560cd
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt
@@ -0,0 +1,27 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.http
+
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Request
+import okhttp3.RequestBody
+
+val JSON_CONTENT_TYPE = "application/json; charset=utf-8".toMediaType()
+
+private const val HEADER_ACCEPT = "Accept"
+private const val HEADER_ACCEPT_VALUE = "application/json"
+
+fun buildOcsRequest(
+ url: String,
+ method: HttpMethod,
+ body: RequestBody? = null
+): Request = Request.Builder()
+ .url(url)
+ .header(HEADER_ACCEPT, HEADER_ACCEPT_VALUE)
+ .method(method.type, body)
+ .build()
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt
new file mode 100644
index 00000000..932761b6
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt
@@ -0,0 +1,22 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.model
+
+sealed class NetworkResult {
+ data class Success(val data: T) : NetworkResult()
+
+ data class ServerError(val response: OcsResponse) : NetworkResult()
+
+ data class NetworkException(val throwable: Throwable) : NetworkResult()
+
+ val isSuccess: Boolean get() = this is Success
+
+ companion object {
+ fun fromException(e: Throwable): NetworkException = NetworkException(e)
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt
new file mode 100644
index 00000000..0b5c39db
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt
@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class OcsResponse(
+ val ocs: Ocs
+)
+
+@Serializable
+data class Ocs(
+ val meta: Meta,
+ val data: T
+)
+
+@Serializable
+data class Meta(
+ val status: String,
+
+ @SerialName("statuscode")
+ val statusCode: Int,
+
+ val message: String,
+
+ @SerialName("totalitems")
+ val totalItems: String = "",
+
+ @SerialName("itemsperpage")
+ val itemsPerPage: String = ""
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt
new file mode 100644
index 00000000..aae4ab75
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt
@@ -0,0 +1,14 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.serialization
+
+import kotlinx.serialization.json.Json
+
+object OCSSerializer {
+ val json = Json { ignoreUnknownKeys = true }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt
new file mode 100644
index 00000000..2fd80eef
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt
@@ -0,0 +1,297 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+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.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil3.ImageLoader
+import coil3.compose.setSingletonImageLoaderFactory
+import coil3.svg.SvgDecoder
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.component.ContentUnavailableView
+import com.nextcloud.android.common.ui.network.auth.ServerCredentials
+import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
+import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet
+import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog
+import com.nextcloud.android.common.ui.share.component.dialog.DiscardDraftShareDialog
+import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState
+import com.nextcloud.android.common.ui.share.model.ui.ShareItemType
+import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState
+import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
+
+@Composable
+private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) {
+ val errorMessageId by viewModel.errorMessageId.collectAsState()
+ val screenState by viewModel.state.collectAsState()
+ val activeShare by viewModel.activeShare.collectAsState()
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val snackbarHostState = remember { SnackbarHostState() }
+ var showDiscardDraftDialog by remember { mutableStateOf(false) }
+
+ if (showDiscardDraftDialog) {
+ DiscardDraftShareDialog(
+ onKeep = { showDiscardDraftDialog = false },
+ onDiscard = {
+ showDiscardDraftDialog = false
+ activeShare?.let { viewModel.deleteShare(it.id) }
+ viewModel.setActiveShare(null)
+ }
+ )
+ }
+
+ LaunchedEffect(errorMessageId) {
+ errorMessageId?.let {
+ snackbarHostState.showSnackbar(context.getString(it))
+ viewModel.updateErrorMessage(null)
+ }
+ }
+
+ Scaffold(
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = {
+ scope.launch {
+ viewModel.createDraftShare()?.let {
+ viewModel.addSource(it.id, sourceId)
+ }
+ }
+ },
+ ) {
+ Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add")
+ }
+ },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ containerColor = Color.Transparent
+ ) { paddingValues ->
+ when (val state = screenState) {
+ is ShareScreenState.Loading -> {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ is ShareScreenState.Empty -> {
+ ContentUnavailableView(
+ iconId = R.drawable.ic_person_add,
+ title = stringResource(R.string.share_view_empty_title),
+ )
+ }
+ is ShareScreenState.Loaded -> {
+ LazyColumn(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ itemsIndexed(state.shares, key = { _, share -> share.id }) { index, share ->
+ val type = ShareItemType.type(index, state.shares.lastIndex)
+
+ if (index == 0) {
+ Spacer(modifier = Modifier.height(16.dp))
+ } else {
+ Spacer(modifier = Modifier.height(2.dp))
+ }
+
+ ShareItem(
+ share = share,
+ type = type,
+ onSelectShare = { selected -> viewModel.setActiveShare(selected) },
+ onDeleteShare = { viewModel.deleteShare(it.id) },
+ onSendEmail = { }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ activeShare?.let {
+ AddOrEditShareBottomSheet(
+ share = it,
+ sharingCapabilities = sharingCapabilities,
+ viewModel = viewModel,
+ onDismissDraft = { showDiscardDraftDialog = true }
+ )
+ }
+}
+
+@Composable
+private fun ShareItem(
+ share: Share,
+ type: ShareItemType,
+ onSelectShare: (Share) -> Unit,
+ onDeleteShare: (Share) -> Unit,
+ onSendEmail: (Share) -> Unit
+) {
+ var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) }
+ val haptics = LocalHapticFeedback.current
+
+ if (overlayState == ShareItemOverlayState.DeleteConfirmation) {
+ DeleteShareConfirmationDialog(
+ onConfirm = {
+ overlayState = ShareItemOverlayState.None
+ onDeleteShare(share)
+ },
+ onDismiss = { overlayState = ShareItemOverlayState.None }
+ )
+ }
+
+ ListItem(
+ modifier = Modifier
+ .fillMaxWidth(0.9f)
+ .clip(type.getShape())
+ .combinedClickable(
+ onClick = { onSelectShare(share) },
+ onLongClick = {
+ haptics.performHapticFeedback(HapticFeedbackType.LongPress)
+ overlayState = ShareItemOverlayState.ContextMenu
+ },
+ )
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
+ headlineContent = {
+ val headline = if (share.recipients.isNotEmpty()) {
+ share.recipients.first().displayName
+ } else {
+ ""
+ }
+
+ Text(
+ text = headline,
+ style = MaterialTheme.typography.titleSmall
+ )
+ },
+ supportingContent = {
+ Text(
+ text = share.shareState.name,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ trailingContent = {
+ Box {
+ IconButton(onClick = { overlayState = ShareItemOverlayState.ContextMenu }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "More options")
+ }
+
+ DropdownMenu(
+ expanded = overlayState == ShareItemOverlayState.ContextMenu,
+ onDismissRequest = { overlayState = ShareItemOverlayState.None }
+ ) {
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.share_view_list_item_edit)) },
+ onClick = {
+ overlayState = ShareItemOverlayState.None
+ onSelectShare(share)
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.share_view_list_item_send_email)) },
+ onClick = {
+ onSendEmail(share)
+ overlayState = ShareItemOverlayState.None
+ }
+ )
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = {
+ Text(
+ stringResource(R.string.share_view_list_item_delete),
+ color = MaterialTheme.colorScheme.error
+ )
+ },
+ onClick = {
+ overlayState = ShareItemOverlayState.DeleteConfirmation
+ }
+ )
+ }
+ }
+ },
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent)
+ )
+}
+
+private val json = Json { ignoreUnknownKeys = true }
+
+fun ComposeView.initShareScreen(
+ sourceId: String,
+ sharingJson: String,
+ credentials: ServerCredentials,
+ colorScheme: ColorScheme
+) {
+ val sharingCapabilities = json.decodeFromString(sharingJson)
+ val nextcloudHttpClient = NextcloudHttpClient.create(credentials)
+ val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient))
+
+ setContent {
+ setSingletonImageLoaderFactory { context ->
+ ImageLoader.Builder(context)
+ .components { add(SvgDecoder.Factory()) }
+ .build()
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = {
+ ShareScreen(sourceId, sharingCapabilities, viewModel)
+ }
+ )
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt
new file mode 100644
index 00000000..42298b3e
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt
@@ -0,0 +1,276 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest
+import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest
+import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+import com.nextcloud.android.common.ui.share.model.api.state.ShareState
+import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState
+import com.nextcloud.android.common.ui.share.repository.ShareRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class ShareViewModel(
+ private val repository: ShareRepository
+) : ViewModel() {
+
+ private val _state = MutableStateFlow(ShareScreenState.Loading)
+ val state: StateFlow = _state
+
+ private val _activeShare = MutableStateFlow(null)
+ val activeShare: StateFlow = _activeShare
+
+ private val _searchQuery = MutableStateFlow("")
+
+ private val _recipientSearchResults = MutableStateFlow>(emptyList())
+ val recipientSearchResults: StateFlow> = _recipientSearchResults
+
+ private val _errorMessageId = MutableStateFlow(null)
+ val errorMessageId: StateFlow = _errorMessageId
+
+ private val pendingProperties = mutableMapOf()
+
+ private val currentShares: List
+ get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList()
+
+ init {
+ fetchShares()
+ initSearchQuery()
+ }
+
+ // region search query
+ @OptIn(FlowPreview::class)
+ private fun initSearchQuery() {
+ viewModelScope.launch {
+ _searchQuery
+ .debounce(300L)
+ .distinctUntilChanged()
+ .filter { it.isNotBlank() }
+ .collect { query -> executeSearch(query) }
+ }
+ }
+
+ fun onSearchQueryChanged(query: String) {
+ _searchQuery.value = query
+ }
+
+ private suspend fun executeSearch(query: String) {
+ val result = repository.fetchRecipients(null, query, 10, 0)
+ if (result is NetworkResult.Success) {
+ _recipientSearchResults.value = result.data
+ }
+ }
+ // endregion
+
+ // region shares list
+ fun fetchShares(
+ sourceClass: String? = null,
+ lastShareID: String? = null,
+ limit: Int = 50
+ ) {
+ viewModelScope.launch(Dispatchers.IO) {
+ _state.update { ShareScreenState.Loading }
+ _errorMessageId.update { null }
+
+ val result = repository.fetchShares(sourceClass, lastShareID, limit)
+ handleResult(result, R.string.share_view_fetch_error_message)?.let { fetched ->
+ _state.update {
+ if (fetched.isEmpty()) ShareScreenState.Empty
+ else ShareScreenState.Loaded(fetched)
+ }
+ }
+ }
+ }
+
+ fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()) {
+ viewModelScope.launch(Dispatchers.IO) {
+ _errorMessageId.update { null }
+
+ val result = repository.fetchShare(id, request)
+ handleResult(result, R.string.share_view_fetch_error_message)?.let { share ->
+ _activeShare.update { share }
+ replaceInList(share)
+ }
+ }
+ }
+ // endregion
+
+ // region create
+ suspend fun createDraftShare(): Share? = withContext(Dispatchers.IO) {
+ _errorMessageId.update { null }
+
+ val result = repository.createDraftShare()
+ val draft = handleResult(result, R.string.share_view_create_error_message)
+
+ if (draft != null) {
+ _activeShare.update { draft }
+ _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) }
+ }
+
+ draft
+ }
+ // endregion
+
+ // region state
+ fun updateState(id: String, shareState: ShareState) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.updateShareState(id, UpdateShareStateRequest(shareState))
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+ // endregion
+
+ // region sources
+ fun addSource(id: String, value: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType"
+ val result = repository.addShareSource(id, AddSourceRequest(clazz, value))
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+
+ fun removeSource(id: String, clazz: String, value: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.removeShareSource(id, clazz, value)
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+ // endregion
+
+ // region recipients
+ fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance))
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+
+ fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.removeShareRecipient(id, clazz, value, instance)
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+ // endregion
+
+ // region properties
+ fun updateProperty(id: String, clazz: String, value: String?) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value))
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+
+ fun updatePropertyLocally(clazz: String, value: String) {
+ pendingProperties[clazz] = value
+ }
+
+ fun commitPendingProperties(shareId: String) {
+ if (pendingProperties.isEmpty()) return
+
+ viewModelScope.launch(Dispatchers.IO) {
+ pendingProperties.forEach { (clazz, value) ->
+ repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))
+ }
+ pendingProperties.clear()
+ }
+ }
+ // endregion
+
+ // region permissions
+ fun updatePermission(id: String, clazz: String, enabled: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled))
+ handleResult(result, R.string.share_view_update_error_message)?.let { updated ->
+ _activeShare.update { updated }
+ replaceInList(updated)
+ }
+ }
+ }
+ // endregion
+
+ // region delete
+ fun deleteShare(id: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val result = repository.deleteShare(id)
+ handleResult(result, R.string.share_view_delete_error_message)?.let {
+ val remaining = currentShares.filterNot { it.id == id }
+ _state.update {
+ if (remaining.isEmpty()) ShareScreenState.Empty
+ else ShareScreenState.Loaded(remaining)
+ }
+ if (_activeShare.value?.id == id) _activeShare.update { null }
+ }
+ }
+ }
+ // endregion
+
+ // region ui helpers
+ fun updateErrorMessage(value: Int?) {
+ _errorMessageId.update { value }
+ }
+
+ fun setActiveShare(value: Share?) {
+ _activeShare.update { value }
+ }
+ // endregion
+
+ // region private
+ private fun replaceInList(updated: Share) {
+ val shares = currentShares.ifEmpty { return }
+ _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) }
+ }
+
+ private fun handleResult(result: NetworkResult, errorId: Int): T? {
+ return when (result) {
+ is NetworkResult.Success -> result.data
+ is NetworkResult.ServerError,
+ is NetworkResult.NetworkException -> {
+ _errorMessageId.update { errorId }
+ null
+ }
+ }
+ }
+ // endregion
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt
new file mode 100644
index 00000000..affe1da5
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt
@@ -0,0 +1,60 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material3.Icon
+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.unit.dp
+
+@Composable
+fun CollapsibleShareSection(
+ label: String,
+ isExpanded: Boolean,
+ onToggle: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onToggle() }
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ AnimatedVisibility(visible = isExpanded) {
+ Column { content() }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt
new file mode 100644
index 00000000..9dfd0a6f
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt
@@ -0,0 +1,230 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.InputChip
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import coil3.request.ImageRequest
+import coil3.svg.SvgDecoder
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.share.ShareViewModel
+import com.nextcloud.android.common.ui.share.model.api.icon.Icon
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RecipientSearchField(
+ share: Share,
+ viewModel: ShareViewModel
+) {
+ var query by remember { mutableStateOf("") }
+ var expanded by remember { mutableStateOf(false) }
+ val results by viewModel.recipientSearchResults.collectAsState()
+ val chipScrollState = rememberScrollState()
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if (share.recipients.isNotEmpty()) {
+ RecipientChipRow(
+ recipients = share.recipients,
+ chipScrollState = chipScrollState,
+ onRemove = { recipient ->
+ viewModel.removeRecipient(
+ id = share.id,
+ clazz = recipient.clazz,
+ value = recipient.value,
+ instance = recipient.instance
+ )
+ }
+ )
+ }
+
+ RecipientSearchDropdown(
+ query = query,
+ expanded = expanded,
+ onQueryChange = {
+ query = it
+ expanded = true
+ viewModel.onSearchQueryChanged(it)
+ },
+ onExpandedChange = { expanded = it },
+ onDismiss = { expanded = false }
+ ) {
+ RecipientDropdownContent(
+ results = results,
+ onSelect = { recipient ->
+ viewModel.addRecipient(share.id, recipient.clazz, recipient.value)
+ query = ""
+ expanded = false
+ }
+ )
+ }
+ }
+}
+
+@Composable
+private fun RecipientChipRow(
+ recipients: List,
+ chipScrollState: androidx.compose.foundation.ScrollState,
+ onRemove: (Recipient) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(chipScrollState),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ recipients.forEach { recipient ->
+ InputChip(
+ selected = true,
+ onClick = { },
+ label = { Text(recipient.displayName) },
+ leadingIcon = {
+ recipient.icon?.let {
+ RecipientIcon(
+ icon = it,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ },
+ trailingIcon = {
+ IconButton(
+ onClick = { onRemove(recipient) },
+ modifier = Modifier.size(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "remove recipient"
+ )
+ }
+ }
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun RecipientSearchDropdown(
+ query: String,
+ expanded: Boolean,
+ onQueryChange: (String) -> Unit,
+ onExpandedChange: (Boolean) -> Unit,
+ onDismiss: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ ExposedDropdownMenuBox(
+ expanded = expanded && query.isNotBlank(),
+ onExpandedChange = onExpandedChange
+ ) {
+ OutlinedTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ label = { Text(stringResource(R.string.share_view_invited_category_label)) },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true)
+ .fillMaxWidth(),
+ singleLine = true
+ )
+
+ if (query.isNotBlank()) {
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismiss
+ ) {
+ content()
+ }
+ }
+ }
+}
+
+@Composable
+private fun RecipientDropdownContent(
+ results: List,
+ onSelect: (Recipient) -> Unit
+) {
+ if (results.isEmpty()) {
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = stringResource(R.string.share_view_recipient_search_field_empty_result),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ onClick = {},
+ enabled = false
+ )
+ } else {
+ results.forEach { recipient ->
+ DropdownMenuItem(
+ leadingIcon = {
+ recipient.icon?.let {
+ RecipientIcon(
+ icon = it,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ },
+ text = { Text(recipient.displayName) },
+ onClick = { onSelect(recipient) }
+ )
+ }
+ }
+}
+
+@Composable
+private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) {
+ val isDark = isSystemInDarkTheme()
+ val url = if (isDark) icon.dark ?: icon.light else icon.light ?: icon.dark
+
+ if (url != null) {
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(url)
+ .decoderFactory(SvgDecoder.Factory())
+ .build(),
+ contentDescription = null,
+ modifier = modifier,
+ )
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt
new file mode 100644
index 00000000..8af52f43
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt
@@ -0,0 +1,50 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ShareSwitch(
+ label: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 48.dp)
+ .padding(vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(1f)
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange
+ )
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt
new file mode 100644
index 00000000..9897bdba
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt
@@ -0,0 +1,209 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+
+package com.nextcloud.android.common.ui.share.component.bottomsheet
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.share.ShareViewModel
+import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection
+import com.nextcloud.android.common.ui.share.component.RecipientSearchField
+import com.nextcloud.android.common.ui.share.component.ShareSwitch
+import com.nextcloud.android.common.ui.share.component.property.SharePropertyView
+import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities
+import com.nextcloud.android.common.ui.share.model.api.property.clazz
+import com.nextcloud.android.common.ui.share.model.api.property.priority
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+import com.nextcloud.android.common.ui.share.model.api.state.ShareState
+import com.nextcloud.android.common.ui.share.model.ui.ShareCategory
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddOrEditShareBottomSheet(
+ share: Share,
+ sharingCapabilities: SharingCapabilities,
+ viewModel: ShareViewModel,
+ onDismissDraft: () -> Unit = {}
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ val scrollState = rememberScrollState()
+ val categories = remember { ShareCategory.entries.toList() }
+ var selectedCategory by remember { mutableStateOf(categories.first()) }
+ var showAdvancedSettings by remember { mutableStateOf(false) }
+ var expandedCategories by remember { mutableStateOf(emptySet()) }
+
+ ModalBottomSheet(
+ onDismissRequest = {
+ if (share.shareState == ShareState.DRAFT) {
+ onDismissDraft()
+ } else {
+ viewModel.commitPendingProperties(share.id)
+ viewModel.setActiveShare(null)
+ }
+ },
+ sheetState = sheetState,
+ containerColor = MaterialTheme.colorScheme.surface,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .padding(bottom = 32.dp)
+ .verticalScroll(scrollState),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ ShareTitle(share)
+
+ CategorySelector(
+ categories = categories,
+ selectedCategory = selectedCategory,
+ onCategorySelected = { selectedCategory = it }
+ )
+
+ if (selectedCategory == ShareCategory.Invited) {
+ RecipientSearchField(share, viewModel)
+ }
+
+ PermissionCategories(
+ share = share,
+ sharingCapabilities = sharingCapabilities,
+ expandedCategories = expandedCategories,
+ onToggleCategory = { categoryName ->
+ expandedCategories = if (expandedCategories.contains(categoryName)) {
+ expandedCategories - categoryName
+ } else {
+ expandedCategories + categoryName
+ }
+ },
+ viewModel = viewModel
+ )
+
+ if (share.properties.isNotEmpty()) {
+ AdvancedSettingsSection(
+ share = share,
+ isExpanded = showAdvancedSettings,
+ onToggle = { showAdvancedSettings = !showAdvancedSettings },
+ viewModel = viewModel
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShareTitle(share: Share) {
+ val context = LocalContext.current
+ Text(
+ text = share.title(context),
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+}
+
+@Composable
+private fun CategorySelector(
+ categories: List,
+ selectedCategory: ShareCategory,
+ onCategorySelected: (ShareCategory) -> Unit
+) {
+ SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
+ categories.forEachIndexed { index, category ->
+ SegmentedButton(
+ selected = selectedCategory == category,
+ onClick = { onCategorySelected(category) },
+ shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size)
+ ) {
+ Text(category.name)
+ }
+ }
+ }
+}
+
+@Composable
+private fun PermissionCategories(
+ share: Share,
+ sharingCapabilities: SharingCapabilities,
+ expandedCategories: Set,
+ onToggleCategory: (String) -> Unit,
+ viewModel: ShareViewModel
+) {
+ sharingCapabilities.permissionCategories
+ .sortedBy { it.priority }
+ .forEach { sharingCapability ->
+ key(sharingCapability.class_field) {
+ CollapsibleShareSection(
+ label = sharingCapability.displayName,
+ isExpanded = sharingCapability.displayName in expandedCategories,
+ onToggle = { onToggleCategory(sharingCapability.displayName) },
+ ) {
+ share.permissions
+ .filter { permission -> permission.category == sharingCapability.class_field }
+ .sortedBy { it.displayName }
+ .forEach { permission ->
+ key(permission.clazz) {
+ ShareSwitch(
+ label = permission.displayName,
+ checked = permission.enabled,
+ onCheckedChange = { isChecked ->
+ viewModel.updatePermission(share.id, permission.clazz, isChecked)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AdvancedSettingsSection(
+ share: Share,
+ isExpanded: Boolean,
+ onToggle: () -> Unit,
+ viewModel: ShareViewModel
+) {
+ CollapsibleShareSection(
+ label = stringResource(R.string.share_view_advanced_settings),
+ isExpanded = isExpanded,
+ onToggle = onToggle
+ ) {
+ share.properties.sortedBy { it.priority }.forEach { property ->
+ key(property.clazz) {
+ SharePropertyView(
+ shareId = share.id,
+ property = property,
+ viewModel = viewModel
+ )
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt
new file mode 100644
index 00000000..cf16a865
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt
@@ -0,0 +1,46 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.android.common.ui.share.component.dialog
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.nextcloud.android.common.ui.R
+
+@Composable
+fun DeleteShareConfirmationDialog(
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(R.string.share_view_delete_confirm_title)) },
+ text = { Text(stringResource(R.string.share_view_delete_confirm_message)) },
+ confirmButton = {
+ FilledTonalButton(
+ onClick = onConfirm,
+ colors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Text(stringResource(R.string.common_ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.common_cancel))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt
new file mode 100644
index 00000000..85031c3c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt
@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.android.common.ui.share.component.dialog
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.nextcloud.android.common.ui.R
+
+@Composable
+fun DiscardDraftShareDialog(
+ onKeep: () -> Unit,
+ onDiscard: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onKeep,
+ title = { Text(stringResource(R.string.share_view_discard_draft_title)) },
+ text = { Text(stringResource(R.string.share_view_discard_draft_message)) },
+ confirmButton = {
+ FilledTonalButton(onClick = onKeep) {
+ Text(stringResource(R.string.share_view_discard_draft_keep))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDiscard) {
+ Text(stringResource(R.string.share_view_discard_draft_delete))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt
new file mode 100644
index 00000000..974e381e
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt
@@ -0,0 +1,118 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component.property
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import com.nextcloud.android.common.ui.share.ShareViewModel
+import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker
+import com.nextcloud.android.common.ui.share.component.ShareSwitch
+import com.nextcloud.android.common.ui.share.model.api.property.Property
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyString
+import kotlinx.coroutines.delay
+
+@Composable
+fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) {
+ when (property) {
+ is PropertyBoolean -> {
+ var checkedValue by remember(property.clazz) { mutableStateOf(property.isTrue()) }
+
+ ShareSwitch(
+ label = property.displayName,
+ checked = checkedValue,
+ onCheckedChange = { isChecked ->
+ checkedValue = isChecked
+ viewModel.updatePropertyLocally(property.clazz, isChecked.toString())
+ }
+ )
+ }
+
+ is PropertyString -> {
+ var textValue by remember(property.clazz) { mutableStateOf(property.value ?: "") }
+
+ DebouncedPropertyUpdater(
+ value = textValue,
+ originalValue = property.value ?: "",
+ onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) }
+ )
+
+ OutlinedTextField(
+ value = textValue,
+ onValueChange = { textValue = it },
+ label = { Text(property.displayName) },
+ modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
+ singleLine = true
+ )
+ }
+
+ is PropertyPassword -> {
+ var passwordValue by remember(property.clazz) { mutableStateOf(property.value ?: "") }
+
+ DebouncedPropertyUpdater(
+ value = passwordValue,
+ originalValue = property.value ?: "",
+ onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) }
+ )
+
+ OutlinedTextField(
+ value = passwordValue,
+ onValueChange = { passwordValue = it },
+ label = { Text(property.displayName) },
+ placeholder = property.hint?.let { { Text(it) } },
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ singleLine = true
+ )
+ }
+
+ is PropertyDate -> {
+ ShareDatePicker(property, onDateSelected = { dateValue ->
+ viewModel.updateProperty(shareId, property.clazz, dateValue)
+ })
+ }
+
+ is PropertyEnum -> {
+ // TODO: Implement ExposedDropdownMenuBox using property.validValues
+ Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray)
+ }
+ }
+}
+
+@Composable
+private fun DebouncedPropertyUpdater(
+ value: String,
+ originalValue: String,
+ delayMillis: Long = 400L,
+ onDebounceComplete: (String) -> Unit
+) {
+ LaunchedEffect(value) {
+ if (value != originalValue) {
+ delay(delayMillis)
+ onDebounceComplete(value)
+ }
+ }
+}
+
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt
new file mode 100644
index 00000000..a99fae18
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt
@@ -0,0 +1,119 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component.property.datepicker
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.DatePicker
+import androidx.compose.material3.DatePickerDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberDatePickerState
+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.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate
+
+@Composable
+fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) {
+ val formatter = ShareDateFormatter()
+ var showDatePicker by remember { mutableStateOf(false) }
+ var dateValue by remember { mutableStateOf(property.value ?: "") }
+
+
+ Box(modifier = Modifier.fillMaxWidth()) {
+ OutlinedTextField(
+ value = dateValue,
+ onValueChange = {},
+ readOnly = true,
+ enabled = true,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text(formatter.getDisplayName(property)) },
+ trailingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_calendar),
+ contentDescription = null
+ )
+ }
+ )
+
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .clickable { showDatePicker = true }
+ )
+ }
+
+ if (showDatePicker) {
+ DatePickerModal(formatter, onDateSelected = {
+ dateValue = it ?: ""
+ onDateSelected(dateValue)
+ }, onDismiss = {
+ showDatePicker = false
+ })
+ }
+}
+
+@Composable
+private fun DatePickerModal(
+ formatter: ShareDateFormatter,
+ onDateSelected: (String?) -> Unit,
+ onDismiss: () -> Unit
+) {
+ val datePickerState = rememberDatePickerState()
+
+ DatePickerDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ TextButton(onClick = {
+ onDateSelected(formatter.formatDate(datePickerState))
+ onDismiss()
+ }) {
+ Text(stringResource(R.string.common_ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.common_cancel))
+ }
+ }
+ ) {
+ DatePicker(state = datePickerState)
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ShareDatePickerPreview() {
+ MaterialTheme {
+ ShareDatePicker(
+ property = PropertyDate(
+ clazz = "expiry",
+ displayName = "Expiration Date",
+ priority = 1,
+ required = false,
+ value = "2026-12-31",
+ minDate = "2026-01-01",
+ maxDate = "2027-12-31"
+ ),
+ onDateSelected = {}
+ )
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt
new file mode 100644
index 00000000..7caca750
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt
@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.component.property.datepicker.util
+
+import androidx.compose.material3.DatePickerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.getSelectedDate
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate
+import java.time.format.DateTimeFormatter
+
+class ShareDateFormatter {
+ companion object {
+ private const val PATTERN = "MM-dd-yyyy"
+ }
+
+ fun getDisplayName(property: PropertyDate): String {
+ return property.displayName + " ($PATTERN)"
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ fun formatDate(datePickerState: DatePickerState): String? {
+ val date = datePickerState.getSelectedDate()
+ val formatter = DateTimeFormatter.ofPattern(PATTERN)
+ return date?.format(formatter)
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt
new file mode 100644
index 00000000..545f925c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt
@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.capabilities
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SharingCapabilities(
+ @SerialName("api_versions")
+ val apiVersions: List,
+ val legacy: Legacy,
+ @SerialName("permission_categories")
+ val permissionCategories: List,
+)
+
+@Serializable
+data class Legacy(
+ @SerialName("max_sources")
+ val maxSources: Long,
+ @SerialName("max_recipients")
+ val maxRecipients: Long,
+)
+
+@Serializable
+data class PermissionCategory(
+ @SerialName("class")
+ val class_field: String,
+ @SerialName("display_name")
+ val displayName: String,
+ val hint: String?,
+ val icon: String?,
+ val priority: Long,
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt
new file mode 100644
index 00000000..4343042a
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt
@@ -0,0 +1,17 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.icon
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Icon(
+ val svg: String? = null,
+ val light: String? = null,
+ val dark: String? = null
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt
new file mode 100644
index 00000000..d5646004
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt
@@ -0,0 +1,25 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.owner
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import com.nextcloud.android.common.ui.share.model.api.icon.Icon
+
+@Serializable
+data class Owner(
+ @SerialName("user_id")
+ val userId: String,
+
+ val instance: String? = null,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val icon: Icon
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt
new file mode 100644
index 00000000..eae0441a
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt
@@ -0,0 +1,24 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.permission
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Permission(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val category: String?,
+
+ val enabled: Boolean
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt
new file mode 100644
index 00000000..2aeb3d8c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt
@@ -0,0 +1,143 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.property
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonClassDiscriminator
+
+@OptIn(ExperimentalSerializationApi::class)
+@JsonClassDiscriminator("type")
+@Serializable
+sealed class Property
+
+val Property.priority: Int
+ get() = when (this) {
+ is PropertyBoolean -> priority
+ is PropertyDate -> priority
+ is PropertyEnum -> priority
+ is PropertyPassword -> priority
+ is PropertyString -> priority
+ }
+
+val Property.clazz: String
+ get() = when (this) {
+ is PropertyBoolean -> clazz
+ is PropertyDate -> clazz
+ is PropertyEnum -> clazz
+ is PropertyPassword -> clazz
+ is PropertyString -> clazz
+ }
+
+@Serializable
+@SerialName("boolean")
+data class PropertyBoolean(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val hint: String? = null,
+
+ val priority: Int,
+
+ val required: Boolean,
+
+ val value: String? = null
+) : Property() {
+ fun isTrue(): Boolean = (value == "true")
+}
+
+@Serializable
+@SerialName("date")
+data class PropertyDate(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val hint: String? = null,
+
+ val priority: Int,
+
+ val required: Boolean,
+
+ val value: String? = null,
+
+ @SerialName("min_date")
+ val minDate: String? = null,
+
+ @SerialName("max_date")
+ val maxDate: String? = null
+) : Property()
+
+@Serializable
+@SerialName("enum")
+data class PropertyEnum(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val hint: String? = null,
+
+ val priority: Int,
+
+ val required: Boolean,
+
+ val value: String? = null,
+
+ @SerialName("valid_values")
+ val validValues: List
+) : Property()
+
+@Serializable
+@SerialName("password")
+data class PropertyPassword(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val hint: String? = null,
+
+ val priority: Int,
+
+ val required: Boolean,
+
+ val value: String? = null
+) : Property()
+
+@Serializable
+@SerialName("string")
+data class PropertyString(
+ @SerialName("class")
+ val clazz: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val hint: String? = null,
+
+ val priority: Int,
+
+ val required: Boolean,
+
+ val value: String? = null,
+
+ @SerialName("min_length")
+ val minLength: Int? = null,
+
+ @SerialName("max_length")
+ val maxLength: Int? = null
+) : Property()
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt
new file mode 100644
index 00000000..36d876b1
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt
@@ -0,0 +1,27 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.recipients
+
+import com.nextcloud.android.common.ui.share.model.api.icon.Icon
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Recipient(
+ @SerialName("class")
+ val clazz: String,
+
+ val value: String,
+
+ val instance: String? = null,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ val icon: Icon? = null
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt
new file mode 100644
index 00000000..b0502ec6
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt
@@ -0,0 +1,58 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.request
+
+import com.nextcloud.android.common.ui.share.model.api.state.ShareState
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonElement
+
+@Serializable
+data class GetShareRequest(
+ val secret: String? = null,
+ val arguments: Map = emptyMap()
+)
+
+@Serializable
+data class UpdateShareStateRequest(
+ val shareState: ShareState
+)
+
+@Serializable
+data class AddSourceRequest(
+ @SerialName("class")
+ val clazz: String,
+
+ val value: String
+)
+
+@Serializable
+data class AddRecipientRequest(
+ @SerialName("class")
+ val clazz: String,
+
+ val value: String,
+
+ val instance: String? = null
+)
+
+@Serializable
+data class UpdateSharePropertyRequest(
+ @SerialName("class")
+ val clazz: String,
+
+ val value: String? = null
+)
+
+@Serializable
+data class UpdateSharePermissionRequest(
+ @SerialName("class")
+ val clazz: String,
+
+ val enabled: Boolean
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt
new file mode 100644
index 00000000..3d2032e4
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt
@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.share
+
+import android.content.Context
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.permission.Permission
+import com.nextcloud.android.common.ui.share.model.api.property.Property
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.source.Source
+import com.nextcloud.android.common.ui.share.model.api.state.ShareState
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Share(
+ val id: String,
+
+ val owner: Owner,
+
+ @SerialName("last_updated")
+ val lastUpdated: Long,
+
+ @SerialName("state")
+ val shareState: ShareState,
+
+ val sources: List,
+
+ val recipients: List,
+
+ val properties: List,
+
+ val permissions: List
+) {
+ fun title(context: Context): String {
+ return if (shareState == ShareState.DRAFT) {
+ context.getString(R.string.share_view_bottom_sheet_new_title)
+ } else {
+ // TODO do not hardcode
+ context.getString(R.string.share_view_bottom_sheet_edit_title, "")
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt
new file mode 100644
index 00000000..b5dfa16f
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt
@@ -0,0 +1,22 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.source
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Source(
+ @SerialName("class")
+ val clazz: String,
+
+ val value: String,
+
+ @SerialName("display_name")
+ val displayName: String
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt
new file mode 100644
index 00000000..2846dc06
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt
@@ -0,0 +1,23 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.api.state
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class ShareState {
+ @SerialName("active")
+ ACTIVE,
+
+ @SerialName("draft")
+ DRAFT,
+
+ @SerialName("deleted")
+ DELETED
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt
new file mode 100644
index 00000000..491b1b57
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt
@@ -0,0 +1,12 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.ui
+
+enum class ShareCategory {
+ Invited, Anyone
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt
new file mode 100644
index 00000000..0e50bb29
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt
@@ -0,0 +1,14 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.android.common.ui.share.model.ui
+
+sealed class ShareItemOverlayState {
+ data object None : ShareItemOverlayState()
+ data object ContextMenu : ShareItemOverlayState()
+ data object DeleteConfirmation : ShareItemOverlayState()
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt
new file mode 100644
index 00000000..f3c08a5f
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt
@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.ui
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+
+enum class ShareItemType {
+ Top, Mid, Bottom;
+
+ @Composable
+ fun getShape(): RoundedCornerShape {
+ return when (this) {
+ Top -> RoundedCornerShape(12.dp, 12.dp, 4.dp, 4.dp)
+ Mid -> RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp)
+ Bottom -> RoundedCornerShape(4.dp, 4.dp, 12.dp, 12.dp)
+ }
+ }
+
+ companion object {
+ fun type(index: Int, lastIndex: Int): ShareItemType {
+ return when (index) {
+ 0 -> Top
+ lastIndex -> Bottom
+ else -> Mid
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt
new file mode 100644
index 00000000..aae0f0dd
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt
@@ -0,0 +1,16 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.model.ui
+
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+
+sealed class ShareScreenState {
+ data object Empty: ShareScreenState()
+ data object Loading: ShareScreenState()
+ data class Loaded(val shares: List): ShareScreenState()
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt
new file mode 100644
index 00000000..f6a63048
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt
@@ -0,0 +1,432 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.repository
+
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.share.model.api.icon.Icon
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.permission.Permission
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword
+import com.nextcloud.android.common.ui.share.model.api.property.PropertyString
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest
+import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest
+import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+import com.nextcloud.android.common.ui.share.model.api.source.Source
+import com.nextcloud.android.common.ui.share.model.api.state.ShareState
+
+class MockShareRepository : ShareRepository {
+
+ private val mockOwner = Owner(
+ userId = "alice",
+ displayName = "Alice Johnson",
+ icon = Icon(
+ light = "https://mock/icons/user_light.png",
+ dark = "https://mock/icons/user_dark.png"
+ )
+ )
+
+ private val mockPermissions = listOf(
+ Permission(
+ clazz = "read",
+ displayName = "Read",
+ category = "access",
+ enabled = true
+ ),
+ Permission(
+ clazz = "update",
+ displayName = "Update",
+ category = "access",
+ enabled = false
+ ),
+ Permission(
+ clazz = "delete",
+ displayName = "Delete",
+ category = "access",
+ enabled = false
+ )
+ )
+
+ private val mockProperties = listOf(
+ PropertyString(
+ clazz = "note",
+ displayName = "Note",
+ priority = 10,
+ required = false,
+ value = "Design review – please check latest changes"
+ ),
+ PropertyPassword(
+ clazz = "password",
+ displayName = "Password",
+ priority = 20,
+ required = false,
+ value = null
+ ),
+ PropertyDate(
+ clazz = "expiration_date",
+ displayName = "Expiration date",
+ priority = 30,
+ required = false,
+ value = null,
+ minDate = "2026-01-01",
+ maxDate = "2027-01-01"
+ ),
+ PropertyBoolean(
+ clazz = "hide_download",
+ displayName = "Hide download",
+ priority = 40,
+ required = false,
+ value = "false"
+ )
+ )
+
+ private fun buildShare(
+ id: String,
+ sources: List,
+ recipients: List,
+ shareState: ShareState = ShareState.ACTIVE,
+ lastUpdated: Long = System.currentTimeMillis(),
+ owner: Owner = mockOwner
+ ) = Share(
+ id = id,
+ owner = owner,
+ lastUpdated = lastUpdated,
+ shareState = shareState,
+ sources = sources,
+ recipients = recipients,
+ properties = mockProperties,
+ permissions = mockPermissions
+ )
+
+ private val mockShares = mutableListOf(
+ buildShare(
+ id = "1",
+ sources = listOf(
+ Source(clazz = "file", value = "/Photos/vacation.jpg", displayName = "vacation.jpg")
+ ),
+ recipients = listOf(
+ Recipient(
+ clazz = "user",
+ value = "alice@company.com",
+ displayName = "Alice Johnson",
+ icon = Icon(
+ light = "https://mock/icons/user_light.png",
+ dark = "https://mock/icons/user_dark.png"
+ )
+ )
+ )
+ ),
+ buildShare(
+ id = "2",
+ sources = listOf(
+ Source(clazz = "file", value = "/Documents/report.pdf", displayName = "report.pdf")
+ ),
+ recipients = listOf(
+ Recipient(
+ clazz = "group",
+ value = "marketing",
+ displayName = "Marketing Team",
+ icon = Icon(
+ light = "https://mock/icons/group_light.png",
+ dark = "https://mock/icons/group_dark.png"
+ )
+ )
+ ),
+ owner = Owner(
+ userId = "system",
+ displayName = "System",
+ icon = Icon(
+ light = "https://mock/icons/system_light.png",
+ dark = "https://mock/icons/system_dark.png"
+ )
+ )
+ ),
+ buildShare(
+ id = "3",
+ sources = listOf(
+ Source(clazz = "link", value = "https://nextcloud.com/s/abc123", displayName = "Public Link")
+ ),
+ recipients = emptyList(),
+ lastUpdated = 1710000000L,
+ owner = Owner(
+ userId = "system",
+ displayName = "System",
+ icon = Icon(
+ light = "https://mock/icons/system_light.png",
+ dark = "https://mock/icons/system_dark.png"
+ )
+ )
+ ),
+ buildShare(
+ id = "4",
+ sources = listOf(
+ Source(clazz = "file", value = "/Projects/brief.docx", displayName = "brief.docx")
+ ),
+ recipients = listOf(
+ Recipient(
+ clazz = "mail",
+ value = "john@external.com",
+ displayName = "John External",
+ icon = Icon(
+ light = "https://mock/icons/external_light.png",
+ dark = "https://mock/icons/external_dark.png"
+ )
+ )
+ ),
+ owner = Owner(
+ userId = "john",
+ displayName = "John External",
+ icon = Icon(
+ light = "https://mock/icons/user_light.png",
+ dark = "https://mock/icons/user_dark.png"
+ )
+ )
+ ),
+ buildShare(
+ id = "5",
+ sources = listOf(
+ Source(clazz = "file", value = "/Shared/assets.zip", displayName = "assets.zip")
+ ),
+ recipients = listOf(
+ Recipient(
+ clazz = "federated",
+ value = "partner@nextcloud.org",
+ instance = "nextcloud.org",
+ displayName = "Partner Cloud",
+ icon = Icon(
+ light = "https://mock/icons/federated_light.png",
+ dark = "https://mock/icons/federated_dark.png"
+ )
+ )
+ ),
+ owner = Owner(
+ userId = "partner",
+ displayName = "Partner Cloud",
+ icon = Icon(
+ light = "https://mock/icons/user_light.png",
+ dark = "https://mock/icons/user_dark.png"
+ )
+ )
+ )
+ )
+
+ override suspend fun fetchRecipients(
+ recipientTypeClass: String?,
+ query: String,
+ limit: Int,
+ offset: Int
+ ): NetworkResult> {
+ val all = listOf(
+ Recipient(
+ clazz = "user",
+ value = "alice@company.com",
+ displayName = "Alice Johnson",
+ icon = Icon(
+ light = "https://mock/icons/user_light.png",
+ dark = "https://mock/icons/user_dark.png"
+ )
+ ),
+ Recipient(
+ clazz = "group",
+ value = "marketing",
+ displayName = "Marketing Team",
+ icon = Icon(
+ light = "https://mock/icons/group_light.png",
+ dark = "https://mock/icons/group_dark.png"
+ )
+ ),
+ Recipient(
+ clazz = "mail",
+ value = "john@external.com",
+ displayName = "John External",
+ icon = Icon(
+ light = "https://mock/icons/external_light.png",
+ dark = "https://mock/icons/external_dark.png"
+ )
+ ),
+ Recipient(
+ clazz = "federated",
+ value = "partner@nextcloud.org",
+ instance = "nextcloud.org",
+ displayName = "Partner Cloud",
+ icon = Icon(
+ light = "https://mock/icons/federated_light.png",
+ dark = "https://mock/icons/federated_dark.png"
+ )
+ )
+ )
+
+ val filtered = all
+ .filter { recipientTypeClass == null || it.clazz == recipientTypeClass }
+ .filter { it.displayName.contains(query, ignoreCase = true) || it.value.contains(query, ignoreCase = true) }
+ .drop(offset)
+ .take(limit)
+
+ return NetworkResult.Success(filtered)
+ }
+
+ override suspend fun createDraftShare(): NetworkResult {
+ val share = buildShare(
+ id = "mock-share-${System.currentTimeMillis()}",
+ sources = emptyList(),
+ recipients = emptyList(),
+ shareState = ShareState.DRAFT
+ )
+ mockShares.add(share)
+ return NetworkResult.Success(share)
+ }
+
+ override suspend fun fetchShare(
+ id: String,
+ request: GetShareRequest
+ ): NetworkResult {
+ val share = mockShares.find { it.id == id }
+ ?: return NetworkResult.Success(
+ buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ )
+ return NetworkResult.Success(share)
+ }
+
+ override suspend fun deleteShare(id: String): NetworkResult {
+ mockShares.removeAll { it.id == id }
+ return NetworkResult.Success(Unit)
+ }
+
+ override suspend fun fetchShares(
+ sourceClass: String?,
+ lastShareID: String?,
+ limit: Int
+ ): NetworkResult> {
+ var result = mockShares.toList()
+
+ if (sourceClass != null) {
+ result = result.filter { share -> share.sources.any { it.clazz == sourceClass } }
+ }
+
+ if (lastShareID != null) {
+ val index = result.indexOfFirst { it.id == lastShareID }
+ if (index >= 0) result = result.drop(index + 1)
+ }
+
+ return NetworkResult.Success(result.take(limit))
+ }
+
+ override suspend fun updateShareState(
+ id: String,
+ request: UpdateShareStateRequest
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val updated = (if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()))
+ .copy(shareState = request.shareState, lastUpdated = System.currentTimeMillis())
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun addShareSource(
+ id: String,
+ request: AddSourceRequest
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val newSource = Source(clazz = request.clazz, value = request.value, displayName = request.value)
+ val updated = current.copy(
+ sources = current.sources + newSource,
+ lastUpdated = System.currentTimeMillis()
+ )
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun removeShareSource(
+ id: String,
+ clazz: String,
+ value: String
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val updated = current.copy(
+ sources = current.sources.filterNot { it.clazz == clazz && it.value == value },
+ lastUpdated = System.currentTimeMillis()
+ )
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun addShareRecipient(
+ id: String,
+ request: AddRecipientRequest
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val newRecipient = Recipient(
+ clazz = request.clazz,
+ value = request.value,
+ instance = request.instance,
+ displayName = request.value
+ )
+ val updated = current.copy(
+ recipients = current.recipients + newRecipient,
+ lastUpdated = System.currentTimeMillis()
+ )
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun removeShareRecipient(
+ id: String,
+ clazz: String,
+ value: String,
+ instance: String?
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val updated = current.copy(
+ recipients = current.recipients.filterNot {
+ it.clazz == clazz && it.value == value && it.instance == instance
+ },
+ lastUpdated = System.currentTimeMillis()
+ )
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun updateShareProperty(
+ id: String,
+ request: UpdateSharePropertyRequest
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val updated = current.copy(lastUpdated = System.currentTimeMillis())
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+
+ override suspend fun updateSharePermission(
+ id: String,
+ request: UpdateSharePermissionRequest
+ ): NetworkResult {
+ val index = mockShares.indexOfFirst { it.id == id }
+ val current =
+ if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())
+ val updatedPermissions = current.permissions.map {
+ if (it.clazz == request.clazz) it.copy(enabled = request.enabled) else it
+ }
+ val updated = current.copy(
+ permissions = updatedPermissions,
+ lastUpdated = System.currentTimeMillis()
+ )
+ if (index >= 0) mockShares[index] = updated
+ return NetworkResult.Success(updated)
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt
new file mode 100644
index 00000000..20ceac7c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt
@@ -0,0 +1,189 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share.repository
+
+import com.nextcloud.android.common.ui.network.http.HttpMethod
+import com.nextcloud.android.common.ui.network.http.JSON_CONTENT_TYPE
+import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.network.model.OcsResponse
+import com.nextcloud.android.common.ui.network.serialization.OCSSerializer
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest
+import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest
+import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+import okhttp3.RequestBody.Companion.toRequestBody
+
+class ShareRemoteRepository(
+ private val client: NextcloudHttpClient,
+ private val json: kotlinx.serialization.json.Json = OCSSerializer.json
+) : ShareRepository {
+
+ private companion object {
+ private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share"
+ private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares"
+ private const val RECIPIENTS_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/recipients"
+ }
+
+ override suspend fun fetchRecipients(
+ recipientTypeClass: String?,
+ query: String,
+ limit: Int,
+ offset: Int
+ ): NetworkResult> {
+ val queryParams = buildString {
+ append("?query=$query&limit=$limit&offset=$offset")
+ recipientTypeClass?.let { append("&recipientTypeClass=$it") }
+ }
+ return client.executeRequest(
+ endpoint = "$RECIPIENTS_ENDPOINT$queryParams",
+ method = HttpMethod.GET
+ ) { body ->
+ json.decodeFromString>>(body).ocs.data
+ }
+ }
+
+ override suspend fun createDraftShare(): NetworkResult =
+ client.executeRequest(
+ endpoint = SHARE_ENDPOINT,
+ method = HttpMethod.POST,
+ body = ByteArray(0).toRequestBody()
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun fetchShare(
+ id: String,
+ request: GetShareRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id",
+ method = HttpMethod.POST,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun deleteShare(id: String): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id",
+ method = HttpMethod.DELETE
+ ) { }
+
+ override suspend fun fetchShares(
+ sourceClass: String?,
+ lastShareID: String?,
+ limit: Int
+ ): NetworkResult> {
+ val queryParams = buildString {
+ append("?limit=$limit")
+ sourceClass?.let { append("&sourceClass=$it") }
+ lastShareID?.let { append("&lastShareID=$it") }
+ }
+ return client.executeRequest(
+ endpoint = "$SHARES_ENDPOINT$queryParams",
+ method = HttpMethod.GET
+ ) { body ->
+ json.decodeFromString>>(body).ocs.data
+ }
+ }
+
+ override suspend fun updateShareState(
+ id: String,
+ request: UpdateShareStateRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/state",
+ method = HttpMethod.PUT,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun addShareSource(
+ id: String,
+ request: AddSourceRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/source",
+ method = HttpMethod.POST,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun removeShareSource(
+ id: String,
+ clazz: String,
+ value: String
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/source?class=$clazz&value=$value",
+ method = HttpMethod.DELETE
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun addShareRecipient(
+ id: String,
+ request: AddRecipientRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/recipient",
+ method = HttpMethod.POST,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun removeShareRecipient(
+ id: String,
+ clazz: String,
+ value: String,
+ instance: String?
+ ): NetworkResult {
+ val queryParams = buildString {
+ append("?class=$clazz&value=$value")
+ instance?.let { append("&instance=$it") }
+ }
+ return client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/recipient$queryParams",
+ method = HttpMethod.DELETE
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+ }
+
+ override suspend fun updateShareProperty(
+ id: String,
+ request: UpdateSharePropertyRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/property",
+ method = HttpMethod.PUT,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+
+ override suspend fun updateSharePermission(
+ id: String,
+ request: UpdateSharePermissionRequest
+ ): NetworkResult =
+ client.executeRequest(
+ endpoint = "$SHARE_ENDPOINT/$id/enabled",
+ method = HttpMethod.PUT,
+ body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE)
+ ) { body ->
+ json.decodeFromString>(body).ocs.data
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt
new file mode 100644
index 00000000..d10524b8
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt
@@ -0,0 +1,80 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+package com.nextcloud.android.common.ui.share.repository
+
+import com.nextcloud.android.common.ui.network.model.NetworkResult
+import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient
+import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest
+import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest
+import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest
+import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest
+import com.nextcloud.android.common.ui.share.model.api.share.Share
+
+interface ShareRepository {
+
+ suspend fun fetchRecipients(
+ recipientTypeClass: String?,
+ query: String,
+ limit: Int,
+ offset: Int
+ ): NetworkResult>
+
+ suspend fun createDraftShare(): NetworkResult
+
+ suspend fun fetchShare(
+ id: String,
+ request: GetShareRequest = GetShareRequest()
+ ): NetworkResult
+
+ suspend fun deleteShare(id: String): NetworkResult
+
+ suspend fun fetchShares(
+ sourceClass: String?,
+ lastShareID: String?,
+ limit: Int
+ ): NetworkResult>
+
+ suspend fun updateShareState(
+ id: String,
+ request: UpdateShareStateRequest
+ ): NetworkResult
+
+ suspend fun addShareSource(
+ id: String,
+ request: AddSourceRequest
+ ): NetworkResult
+
+ suspend fun removeShareSource(
+ id: String,
+ clazz: String,
+ value: String
+ ): NetworkResult
+
+ suspend fun addShareRecipient(
+ id: String,
+ request: AddRecipientRequest
+ ): NetworkResult
+
+ suspend fun removeShareRecipient(
+ id: String,
+ clazz: String,
+ value: String,
+ instance: String? = null
+ ): NetworkResult
+
+ suspend fun updateShareProperty(
+ id: String,
+ request: UpdateSharePropertyRequest
+ ): NetworkResult
+
+ suspend fun updateSharePermission(
+ id: String,
+ request: UpdateSharePermissionRequest
+ ): NetworkResult
+}
diff --git a/ui/src/main/res/drawable/ic_calendar.xml b/ui/src/main/res/drawable/ic_calendar.xml
new file mode 100644
index 00000000..9e205862
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_circles.xml b/ui/src/main/res/drawable/ic_circles.xml
new file mode 100644
index 00000000..5b07aff7
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_circles.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_email.xml b/ui/src/main/res/drawable/ic_email.xml
new file mode 100644
index 00000000..3319f67e
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_email.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_group.xml b/ui/src/main/res/drawable/ic_group.xml
new file mode 100644
index 00000000..e68f08e7
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_group.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_link.xml b/ui/src/main/res/drawable/ic_link.xml
new file mode 100644
index 00000000..3cb49187
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_link.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_person_add.xml b/ui/src/main/res/drawable/ic_person_add.xml
new file mode 100644
index 00000000..db9f2514
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_person_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/ui/src/main/res/drawable/ic_talk.xml b/ui/src/main/res/drawable/ic_talk.xml
new file mode 100644
index 00000000..e55ac5d9
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_talk.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/ui/src/main/res/drawable/ic_user.xml b/ui/src/main/res/drawable/ic_user.xml
new file mode 100644
index 00000000..d6267b2f
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_user.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
new file mode 100644
index 00000000..540a7009
--- /dev/null
+++ b/ui/src/main/res/values/strings.xml
@@ -0,0 +1,74 @@
+
+
+
+ OK
+ Cancel
+
+ Create a new share
+ Share %s
+
+ File drop
+ Can view
+ Can edit
+ Custom permissions
+
+ View files
+ Edit files
+ Create files
+ Delete files
+
+ Settings
+ Add people
+ Name, team, email or federated ID
+ Participants
+
+ Not shared
+ No result
+ Note to recipients
+
+ Anyone with the link
+
+ Share with others
+ Edit file
+ Expiration date
+ Hide download and sync options
+
+ Label
+ Optional name for this link
+ Expiration date
+ Password protection
+ Limit downloads
+ Hide downloads
+ Video verification
+
+ Show files in grid view
+ Copied
+ Copy link
+ Send
+ Create public link
+
+ Edit
+ Send email
+ Delete
+ Delete share?
+ This share will be permanently removed.
+
+ Failed to fetch shares.
+
+ Discard new share?
+ This share hasn\'t been saved yet. Do you want to discard it?
+ Keep
+ Discard
+
+ Share not found, cannot delete.
+ Failed to delete share.
+
+ Failed to create share.
+ Failed to update share.
+
+
\ No newline at end of file