diff --git a/build.gradle b/build.gradle
index 16cff00e..972590d4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,7 @@ buildscript {
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'org.jetbrains.kotlin.plugin.compose' version '2.3.20' apply false
+ id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion" apply false
id 'com.android.library' version '9.1.1' apply false
id 'org.jetbrains.kotlin.android' version "$kotlinVersion" apply false
id 'com.android.legacy-kapt' version '9.1.1' apply false
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a225095f..bfe8235f 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -92,6 +92,7 @@
+
@@ -236,6 +237,7 @@
+
@@ -324,6 +326,11 @@
+
+
+
+
+
@@ -17849,6 +17856,11 @@
+
+
+
+
+
@@ -19234,6 +19246,11 @@
+
+
+
+
+
@@ -20699,6 +20716,11 @@
+
+
+
+
+
@@ -21402,6 +21424,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 c35d96aa..e06fc6ad 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/java/com/nextcloud/android/common/sample/MainActivity.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt
index ed29a272..f46c2704 100644
--- a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt
+++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt
@@ -85,6 +85,17 @@ class MainActivity : AppCompatActivity() {
material.colorMaterialButtonPrimaryBorderless(negativeButton)
}
+ binding.testApiBtn.setOnClickListener {
+ val baseUrl = binding.baseUrl.text?.toString().orEmpty()
+ val username = binding.username.text?.toString().orEmpty()
+ val token = binding.token.text?.toString().orEmpty()
+ mainViewModel.testPredefinedStatuses(baseUrl, username, token)
+ }
+
+ mainViewModel.apiTestResult.observe(this) { result ->
+ 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..220effdc 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,37 @@ 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.api.ApiCredentials
+import com.nextcloud.android.common.ui.network.model.ApiResult
+import com.nextcloud.android.common.ui.network.api.ApiHttpClient
+import com.nextcloud.android.common.ui.network.UserStatusService
+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 = ApiCredentials(baseUrl, username, token)
+ val client = ApiHttpClient.create(credentials, enableLogging = true)
+ val service = UserStatusService(client)
+
+ when (val result = service.fetchPredefinedStatuses()) {
+ is ApiResult.Success ->
+ apiTestResult.value =
+ "✅ Success (${result.data.size} statuses):\n" +
+ result.data.joinToString("\n") { "${it.icon} ${it.message}" }
+
+ is ApiResult.Error ->
+ apiTestResult.value =
+ "❌ Error ${result.error.ocs.meta.statusCode}: ${result.error.ocs.meta.message}"
+ }
+ }
+ }
+}
\ No newline at end of file
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 1c11fea6..45b88a2a 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'
id 'com.android.library'
id 'com.android.built-in-kotlin'
id 'com.android.legacy-kapt'
@@ -45,12 +46,15 @@ android {
}
dependencies {
+ implementation 'androidx.compose.ui:ui-tooling-preview:1.10.6'
+ debugImplementation 'androidx.compose.ui:ui-tooling:1.10.6'
kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}"
implementation(platform("androidx.compose:compose-bom:2026.03.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("com.vanniktech:ui:0.10.0")
@@ -60,6 +64,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.7.3")
+
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/network/PredefinedStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt
new file mode 100644
index 00000000..35918558
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.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.network
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ClearAt(
+ val type: String,
+ val time: Int
+)
+
+@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/UserStatusService.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt
new file mode 100644
index 00000000..9fa314ee
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.api.ApiHttpClient
+import com.nextcloud.android.common.ui.network.model.ApiResult
+import com.nextcloud.android.common.ui.network.model.Meta
+import com.nextcloud.android.common.ui.network.model.Ocs
+import com.nextcloud.android.common.ui.network.model.OcsResponse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import okhttp3.Request
+
+class UserStatusService(private val client: ApiHttpClient) {
+
+ private val json = Json { ignoreUnknownKeys = true }
+
+ suspend fun fetchPredefinedStatuses(): ApiResult> =
+ withContext(Dispatchers.IO) {
+ val url =
+ "${client.credentials.baseURL.trimEnd('/')}" +
+ "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses"
+
+ val request =
+ Request
+ .Builder()
+ .url(url)
+ .header("Accept", "application/json")
+ .build()
+
+ try {
+ val response = client.okHttpClient.newCall(request).execute()
+ val body = response.body?.string().orEmpty()
+
+ if (response.isSuccessful) {
+ val parsed = json.decodeFromString>>(body)
+ ApiResult.Success(parsed.ocs.data)
+ } else {
+ val error = json.decodeFromString>(body)
+ ApiResult.Error(error)
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(
+ OcsResponse(
+ Ocs(
+ meta =
+ Meta(
+ status = "error",
+ statusCode = -1,
+ message = e.message ?: "Unknown error",
+ totalItems = "",
+ itemsPerPage = ""
+ ),
+ data = e.message ?: "Unknown error"
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt
new file mode 100644
index 00000000..f6770090
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.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.api
+
+data class ApiCredentials(
+ val baseURL: String,
+ val username: String,
+ val token: String
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt
new file mode 100644
index 00000000..499207a8
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt
@@ -0,0 +1,74 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.network.api
+
+import okhttp3.Credentials
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import java.util.concurrent.TimeUnit
+
+class ApiHttpClient private constructor(
+ val okHttpClient: OkHttpClient,
+ val credentials: ApiCredentials
+) {
+ companion object {
+ private const val CONNECT_TIMEOUT_SECONDS = 30L
+ private const val READ_TIMEOUT_SECONDS = 30L
+ private const val WRITE_TIMEOUT_SECONDS = 30L
+
+ fun create(
+ credentials: ApiCredentials,
+ enableLogging: Boolean = false
+ ): ApiHttpClient {
+ val authInterceptor = AuthInterceptor(credentials)
+
+ 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)
+ .addInterceptor(loggingInterceptor)
+ .build()
+
+ return ApiHttpClient(okHttpClient, credentials)
+ }
+ }
+
+ private class AuthInterceptor(private val credentials: ApiCredentials) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val basicCredentials = Credentials.basic(credentials.username, credentials.token)
+
+ val request = chain.request()
+ .newBuilder()
+ .header("Authorization", basicCredentials)
+ .header("OCS-APIRequest", "true")
+ .url(buildUrl(chain.request().url.toString(), credentials.baseURL))
+ .build()
+
+ return chain.proceed(request)
+ }
+
+ private fun buildUrl(requestUrl: String, baseUrl: String): String {
+ return if (requestUrl.startsWith("http://") || requestUrl.startsWith("https://")) {
+ requestUrl
+ } else {
+ "${baseUrl.trimEnd('/')}/${requestUrl.trimStart('/')}"
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt
new file mode 100644
index 00000000..9ab9328b
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt
@@ -0,0 +1,13 @@
+/*
+ * 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 ApiResult {
+ data class Success(val data: T) : ApiResult()
+ data class Error(val error: OcsResponse) : ApiResult()
+}
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..d0c361e5
--- /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/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt
new file mode 100644
index 00000000..6a2c51ea
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt
@@ -0,0 +1,738 @@
+/*
+ * Nextcloud Android Common Library
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
+ */
+
+package com.nextcloud.android.common.ui.share
+
+import android.content.ClipData
+import android.content.res.Configuration
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.Button
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.FilledTonalButton
+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.ModalBottomSheet
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+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.LocalClipboard
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.platform.toClipEntry
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.nextcloud.android.common.ui.R
+import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission
+import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields
+import com.nextcloud.android.common.ui.share.repository.MockShareRepository
+import kotlinx.coroutines.launch
+
+
+@Composable
+private fun ShareView(viewModel: ShareViewModel) {
+ val errorMessageId by viewModel.errorMessageId.collectAsState()
+ var bottomSheetState by remember { mutableStateOf(ShareBottomSheetState.Idle) }
+ val shares by viewModel.shares.collectAsState()
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(errorMessageId) {
+ errorMessageId?.let {
+ snackbarHostState.showSnackbar(context.getString(it))
+ viewModel.updateErrorMessage(null)
+ }
+ }
+
+ Scaffold(floatingActionButton = {
+ FloatingActionButton(
+ onClick = { bottomSheetState = ShareBottomSheetState.New(UnifiedShare.new()) },
+ ) {
+ Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add")
+ }
+ }, snackbarHost = {
+ SnackbarHost(snackbarHostState)
+ }) {
+ LazyColumn(modifier = Modifier.padding(it)) {
+ itemsIndexed(shares) { index, share ->
+ val type = when (index) {
+ 0 -> {
+ UnifiedSharesListItemType.Top
+ }
+
+ shares.lastIndex -> {
+ UnifiedSharesListItemType.Bottom
+ }
+
+ else -> {
+ UnifiedSharesListItemType.Mid
+ }
+ }
+
+ UnifiedSharesListItem(share, type, onSelectShare = { share ->
+ bottomSheetState = ShareBottomSheetState.Edit(share)
+ }, onDeleteShare = {
+ viewModel.delete(share)
+ }, onSendEmail = {
+ // TODO:
+ })
+ }
+ }
+ }
+
+ when (bottomSheetState) {
+ is ShareBottomSheetState.Edit -> {
+ val state = (bottomSheetState as ShareBottomSheetState.Edit)
+ AddOrEditShareBottomSheet(
+ title = stringResource(R.string.share_view_bottom_sheet_edit_title, state.share.label),
+ share = state.share,
+ onCreateOrEdit = {
+
+ },
+ onDismiss = { bottomSheetState = ShareBottomSheetState.Idle }
+ )
+ }
+
+ is ShareBottomSheetState.New -> {
+ val state = (bottomSheetState as ShareBottomSheetState.New)
+ AddOrEditShareBottomSheet(
+ title = stringResource(R.string.share_view_bottom_sheet_new_title),
+ share = state.newShare,
+ onCreateOrEdit = {
+
+ },
+ onDismiss = { bottomSheetState = ShareBottomSheetState.Idle }
+ )
+ }
+
+ ShareBottomSheetState.Idle -> Unit
+ }
+}
+
+// TODO: Use like inner tags whenever user add a new people to the search and it
+// should look like User 1, Group 1 etc.
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AddOrEditShareBottomSheet(
+ title: String,
+ share: UnifiedShare,
+ onCreateOrEdit: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ val scrollState = rememberScrollState()
+
+ var category by remember { mutableStateOf(share.category) }
+ var permission by remember { mutableStateOf(share.permission ?: UnifiedSharePermission.CanView) }
+ var searchQuery by remember { mutableStateOf("") }
+ var note by remember { mutableStateOf(share.note) }
+
+ // Toggle states for collapse/expand
+ var showInvitedSettings by remember { mutableStateOf(false) }
+ var showAnyoneSettings by remember { mutableStateOf(false) }
+
+ val clipboard = LocalClipboard.current
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val availablePermissions = remember {
+ listOf(
+ UnifiedSharePermission.CanView,
+ UnifiedSharePermission.CanEdit,
+ UnifiedSharePermission.FileDrop,
+ UnifiedSharePermission.Custom.getFromPermission(share.permission)
+ )
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ 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)
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ ShareCategoryButtonGroup(
+ selectedCategory = category,
+ onCategoryChange = { category = it }
+ )
+
+ if (category == UnifiedShareCategory.Invited) {
+ InvitedShareContent(
+ searchQuery = searchQuery,
+ onSearchChange = { searchQuery = it },
+ permission = permission,
+ availablePermissions = availablePermissions,
+ onPermissionChange = { permission = it },
+ )
+
+ CollapsibleSettingsSection(
+ isExpanded = showInvitedSettings,
+ onToggle = { showInvitedSettings = !showInvitedSettings }
+ ) {
+ InvitedInlineSettings(share)
+ }
+ } else {
+ AnyoneShareContent(
+ permission = permission,
+ availablePermissions = availablePermissions,
+ onPermissionChange = { permission = it },
+ )
+
+ if (permission is UnifiedSharePermission.Custom) {
+ val customPermissions = permission as UnifiedSharePermission.Custom
+
+ customPermissionFields.forEach { field ->
+ SettingsSwitchRow(
+ label = stringResource(field.labelRes),
+ checked = field.getValue(customPermissions),
+ onCheckedChange = { permission = field.setValue(customPermissions, it) }
+ )
+ }
+ }
+
+ CollapsibleSettingsSection(
+ isExpanded = showAnyoneSettings,
+ onToggle = { showAnyoneSettings = !showAnyoneSettings }
+ ) {
+ AnyoneInlineSettings(share)
+ }
+ }
+
+ NoteToRecipients(note = note, onNoteChange = { note = it })
+
+ ShareActionButtons(
+ share = share,
+ isSendEnabled = searchQuery.isNotBlank(),
+ onCopyClick = {
+ val label = context.getString(R.string.share_view_copy_to_clipboard_label)
+
+ scope.launch {
+ val clipData =
+ ClipData.newPlainText(label, it)
+ clipboard.setClipEntry(clipData.toClipEntry())
+ }
+ },
+ onSendClick = {
+ onCreateOrEdit()
+ }
+ )
+ }
+ }
+}
+
+@Composable
+private fun CollapsibleSettingsSection(
+ 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 = stringResource(R.string.share_view_advanced_settings),
+ 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()
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ShareCategoryButtonGroup(
+ selectedCategory: UnifiedShareCategory,
+ onCategoryChange: (UnifiedShareCategory) -> Unit
+) {
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ UnifiedShareCategory.entries.forEachIndexed { index, option ->
+ SegmentedButton(
+ selected = selectedCategory == option,
+ onClick = { onCategoryChange(option) },
+ shape = SegmentedButtonDefaults.itemShape(
+ index = index,
+ count = UnifiedShareCategory.entries.size
+ )
+ ) {
+ Text(option.name)
+ }
+ }
+ }
+}
+
+@Composable
+private fun InvitedShareContent(
+ searchQuery: String,
+ onSearchChange: (String) -> Unit,
+ permission: UnifiedSharePermission,
+ availablePermissions: List,
+ onPermissionChange: (UnifiedSharePermission) -> Unit,
+
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = onSearchChange,
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text(stringResource(R.string.share_view_invited_category_label)) },
+ placeholder = { Text(stringResource(R.string.share_view_invited_category_placeholder)) },
+ singleLine = true,
+ shape = RoundedCornerShape(8.dp)
+ )
+
+ PermissionDropdown(
+ label = stringResource(R.string.share_view_invited_category_participants),
+ selectedPermission = permission,
+ availablePermissions = availablePermissions,
+ onPermissionChange = onPermissionChange
+ )
+ }
+}
+
+@Composable
+private fun NoteToRecipients(
+ note: String,
+ onNoteChange: (String) -> Unit
+) {
+ OutlinedTextField(
+ value = note,
+ onValueChange = onNoteChange,
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text(stringResource(R.string.share_view_note_text_field_placeholder)) },
+ shape = RoundedCornerShape(8.dp)
+ )
+}
+
+@Composable
+private fun AnyoneShareContent(
+ permission: UnifiedSharePermission,
+ availablePermissions: List,
+ onPermissionChange: (UnifiedSharePermission) -> Unit,
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ PermissionDropdown(
+ label = stringResource(R.string.share_view_permission_dropdown_label),
+ selectedPermission = permission,
+ availablePermissions = availablePermissions,
+ onPermissionChange = onPermissionChange
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun PermissionDropdown(
+ label: String,
+ selectedPermission: UnifiedSharePermission,
+ availablePermissions: List,
+ onPermissionChange: (UnifiedSharePermission) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ OutlinedTextField(
+ value = stringResource(selectedPermission.getTextId()),
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(label) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+ modifier = Modifier
+ .menuAnchor()
+ .fillMaxWidth()
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ availablePermissions.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(stringResource(option.getTextId())) },
+ onClick = {
+ onPermissionChange(option)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun InvitedInlineSettings(share: UnifiedShare) {
+ var shareWithOthers by remember { mutableStateOf(share.recipients.isNotEmpty()) }
+ var editFile by remember { mutableStateOf((share.permission as? UnifiedSharePermission.CanEdit) != null) }
+ var hasExpiration by remember { mutableStateOf(false) } // TODO
+ var hideDownload by remember { mutableStateOf(false) } // TODO
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_invited_category_share_with_others_switch),
+ shareWithOthers
+ ) { shareWithOthers = it }
+ SettingsSwitchRow(stringResource(R.string.share_view_invited_category_edit_file_switch), editFile) { editFile = it }
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_invited_category_expiration_date_switch),
+ hasExpiration
+ ) { hasExpiration = it }
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_invited_category_hide_and_download_switch),
+ hideDownload
+ ) { hideDownload = it }
+}
+
+@Composable
+private fun AnyoneInlineSettings(share: UnifiedShare) {
+ var hasPassword by remember { mutableStateOf(share.password.isNotEmpty()) }
+ var hasExpiration by remember { mutableStateOf(false) }
+ var limitDownloads by remember { mutableStateOf(share.limit != null) }
+
+ var hideDownloads by remember { mutableStateOf(false) }
+ var videoVerification by remember { mutableStateOf(false) }
+ var showFilesInGridView by remember { mutableStateOf(false) }
+
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp),
+ label = { Text(stringResource(R.string.share_view_anyone_category_label)) },
+ placeholder = { Text(stringResource(R.string.share_view_anyone_category_label_placeholder)) },
+ singleLine = true
+ )
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_expiration_date_switch),
+ hasExpiration
+ ) { hasExpiration = it }
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_password_switch),
+ hasPassword
+ ) { hasPassword = it }
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_limit_downloads_switch),
+ limitDownloads
+ ) { limitDownloads = it }
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_hide_downloads_switch),
+ hideDownloads
+ ) { hideDownloads = it }
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_video_verification_switch),
+ videoVerification
+ ) { videoVerification = it }
+
+ SettingsSwitchRow(
+ stringResource(R.string.share_view_anyone_category_grid_view_switch),
+ showFilesInGridView
+ ) { showFilesInGridView = it }
+}
+
+@Composable
+private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(text = label, style = MaterialTheme.typography.bodyLarge)
+ Switch(checked = checked, onCheckedChange = onCheckedChange)
+ }
+}
+
+@Composable
+private fun ShareActionButtons(
+ share: UnifiedShare,
+ isSendEnabled: Boolean,
+ onCopyClick: (String) -> Unit,
+ onSendClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ ) {
+ if (share.category == UnifiedShareCategory.Invited) {
+ FilledTonalButton(
+ onClick = { onCopyClick("TODO") },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text(stringResource(R.string.share_view_copy_action))
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Button(
+ onClick = onSendClick,
+ modifier = Modifier.weight(1f),
+ enabled = isSendEnabled
+ ) {
+ Text(stringResource(R.string.share_view_send_action))
+ }
+ } else {
+ Button(
+ onClick = { onCopyClick("TODO") },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.share_view_create_public_link))
+ }
+ }
+ }
+}
+
+enum class UnifiedSharesListItemType {
+ 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)
+ }
+ }
+}
+
+// NOTE: To just create a public link anyone tab + just send DOES SAME THING
+@Composable
+private fun UnifiedSharesListItem(
+ share: UnifiedShare,
+ type: UnifiedSharesListItemType,
+ onSelectShare: (UnifiedShare) -> Unit,
+ onDeleteShare: (UnifiedShare) -> Unit,
+ onSendEmail: (UnifiedShare) -> Unit
+) {
+ var showContextMenu by remember { mutableStateOf(false) }
+ val haptics = LocalHapticFeedback.current
+
+ ListItem(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(type.getShape())
+ .combinedClickable(
+ onClick = { onSelectShare(share) },
+ onLongClick = {
+ haptics.performHapticFeedback(HapticFeedbackType.LongPress)
+ showContextMenu = true
+ },
+ )
+ .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
+ leadingContent = {
+ share.type?.let {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center
+ ) {
+ it.Icon()
+ }
+ }
+ },
+ headlineContent = {
+ Text(
+ text = share.label,
+ style = MaterialTheme.typography.titleSmall
+ )
+ },
+ supportingContent = {
+ share.permission?.getTextId()?.let {
+ Text(
+ text = stringResource(it),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ },
+ trailingContent = {
+ Box {
+ IconButton(onClick = { showContextMenu = true }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "More options")
+ }
+
+ DropdownMenu(
+ expanded = showContextMenu,
+ onDismissRequest = { showContextMenu = false }
+ ) {
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.share_view_list_item_edit)) },
+ onClick = {
+ showContextMenu = false
+ onSelectShare(share)
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.share_view_list_item_send_email)) },
+ onClick = {
+ onSendEmail(share)
+ showContextMenu = false
+ }
+ )
+
+ HorizontalDivider()
+
+ DropdownMenuItem(
+ text = {
+ Text(
+ stringResource(R.string.share_view_list_item_delete),
+ color = MaterialTheme.colorScheme.error
+ )
+ },
+ onClick = {
+ onDeleteShare(share)
+ showContextMenu = false
+ }
+ )
+ }
+ }
+ },
+ colors = ListItemDefaults.colors(
+ containerColor = Color.Transparent
+ )
+ )
+}
+
+@Preview(name = "UnifiedShareView – light", showBackground = true)
+@Composable
+private fun PreviewLight() {
+ PreviewTheme {
+ ShareView(viewModel = ShareViewModel(MockShareRepository()))
+ }
+}
+
+@Preview(name = "UnifiedShareView – dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun PreviewDark() {
+ PreviewTheme(darkTheme = true) {
+ ShareView(viewModel = ShareViewModel(MockShareRepository()))
+ }
+}
+
+@Composable
+private fun PreviewTheme(
+ darkTheme: Boolean = false,
+ content: @Composable () -> Unit
+) {
+ MaterialTheme {
+ Surface(content = content)
+ }
+}
+
+fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme) {
+ // TODO: REPLACE
+ val viewModel = ShareViewModel(repository = MockShareRepository())
+
+ setContent {
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = {
+ ShareView(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..5621326e
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.ApiResult
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare
+import com.nextcloud.android.common.ui.share.repository.ShareRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class ShareViewModel(
+ private val repository: ShareRepository
+) : ViewModel() {
+
+ private val _shares = MutableStateFlow>(emptyList())
+ val shares: StateFlow> = _shares
+
+ private val _loading = MutableStateFlow(false)
+ val loading: StateFlow = _loading
+
+ private val _errorMessageId = MutableStateFlow(null)
+ val errorMessageId: StateFlow = _errorMessageId
+
+ init {
+ loadShares()
+ }
+
+ // region private methods
+ private fun loadShares() {
+ viewModelScope.launch(Dispatchers.IO) {
+ _loading.value = true
+ _errorMessageId.value = null
+
+ when (val result = repository.fetchShares()) {
+ is ApiResult.Success -> {
+ _shares.update { result.data }
+ }
+
+ is ApiResult.Error -> {
+ _errorMessageId.value = R.string.share_view_fetch_error_message
+ }
+ }
+
+ _loading.value = false
+ }
+ }
+ // endregion
+
+ // region public methods
+ fun create(share: UnifiedShare) {
+ viewModelScope.launch(Dispatchers.IO) {
+ /*
+ val request = CreateShareRequest()
+ repository.createShare()
+ */
+
+ }
+ }
+
+ fun delete(share: UnifiedShare) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val id = share.id
+ if (id == null) {
+ _errorMessageId.update {
+ R.string.share_view_delete_error_id_not_found_message
+ }
+ return@launch
+ }
+
+ val result = repository.deleteShare(share.id)
+ if (result is ApiResult.Error) {
+ _errorMessageId.update {
+ R.string.share_view_delete_error_message
+ }
+ return@launch
+ }
+ }
+ }
+
+ fun updateErrorMessage(value: Int?) {
+ _errorMessageId.update {
+ value
+ }
+ }
+ // endregion
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt
new file mode 100644
index 00000000..54fa9da7
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.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.create
+
+import com.nextcloud.android.common.ui.share.model.api.user.ShareUser
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CreateShareRequest(
+ val data: ShareDataRequest
+)
+
+@Serializable
+data class ShareDataRequest(
+ val sources: List,
+ val recipients: List,
+ val properties: Map>>
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt
new file mode 100644
index 00000000..1d2ec2f2
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.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.model.api.create
+
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.user.ShareUser
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ShareDataResponse(
+ val sources: List,
+ val recipients: List,
+ val properties: Map>>,
+
+ val id: String,
+
+ @SerialName("last_updated")
+ val lastUpdated: Long,
+
+ val owner: Owner
+)
+
+fun ShareDataResponse.toUnifiedShare(): UnifiedShare {
+ val primarySource = sources.firstOrNull()
+ return UnifiedShare(
+ id = id,
+ sources = sources,
+ recipients = recipients,
+ properties = properties,
+ lastUpdated = lastUpdated,
+ owner = owner,
+ type = UnifiedShareType.toUnifiedShareType(primarySource?.type),
+ category = UnifiedShareCategory.Invited, // TODO map from properties
+ permission = UnifiedSharePermission.CanView, // TODO map from properties
+ label = primarySource?.displayName ?: "Unknown",
+ note = "", // TODO map from properties
+ password = "", // TODO map from properties
+ limit = null // TODO map from properties
+ )
+}
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..cb75c5d7
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+
+@Serializable
+data class Owner(
+ @SerialName("user_id")
+ val userId: String,
+
+ @SerialName("display_name")
+ val displayName: String
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt
new file mode 100644
index 00000000..a9bba350
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ShareRecipients(
+ val type: String,
+ val value: String,
+
+ @SerialName("display_name")
+ val displayName: String,
+
+ @SerialName("display_name_unique")
+ val displayNameUnique: String,
+
+ @SerialName("icon_url_light")
+ val iconUrlLight: String,
+
+ @SerialName("icon_url_dark")
+ val iconUrlDark: String
+)
\ No newline at end of file
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt
new file mode 100644
index 00000000..926177af
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.update
+
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.user.ShareUser
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class UpdateShareRequest(
+ val data: UpdateShareData
+)
+
+@Serializable
+data class UpdateShareData(
+ val sources: List,
+ val recipients: List,
+ val properties: Map>>,
+
+ val id: String,
+
+ @SerialName("last_updated")
+ val lastUpdated: Long,
+
+ val owner: Owner
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt
new file mode 100644
index 00000000..c0709a89
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.user
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ShareUser(
+ val type: String,
+ val value: String,
+
+ @SerialName("display_name")
+ val displayName: String
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt
new file mode 100644
index 00000000..a5bfd600
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.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.share.model.ui
+
+sealed class ShareBottomSheetState {
+ data object Idle: ShareBottomSheetState()
+ data class New(val newShare: UnifiedShare): ShareBottomSheetState()
+ data class Edit(val share: UnifiedShare): ShareBottomSheetState()
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt
new file mode 100644
index 00000000..e93bd3d4
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.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 UnifiedShareCategory {
+ Invited, Anyone
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt
new file mode 100644
index 00000000..46a600be
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt
@@ -0,0 +1,13 @@
+/*
+ * 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
+
+data class UnifiedShareDownloadLimit(
+ val limit: Int,
+ val downloadCount: Int
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt
new file mode 100644
index 00000000..b6511314
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.R
+
+sealed class UnifiedSharePermission {
+ // file drop only for folder
+ data object FileDrop : UnifiedSharePermission()
+
+ data object CanView : UnifiedSharePermission()
+ data object CanEdit : UnifiedSharePermission()
+
+ // create only for folder
+ data class Custom(var read: Boolean, var edit: Boolean, var delete: Boolean, var create: Boolean) :
+ UnifiedSharePermission() {
+ companion object {
+ fun getFromPermission(permission: UnifiedSharePermission?): Custom {
+ return Custom(
+ permission?.customPermissionRead() == true,
+ permission?.customPermissionEdit() == true,
+ permission?.customPermissionDelete() == true,
+ permission?.customPermissionCreate() == true
+ )
+ }
+ }
+ }
+
+ fun getTextId(): Int {
+ return when(this) {
+ FileDrop -> R.string.share_permission_file_drop
+ CanView -> R.string.share_permission_can_view
+ CanEdit -> R.string.share_permission_can_edit
+ is Custom -> R.string.share_permission_custom
+ }
+ }
+
+ fun customFlag(selector: Custom.() -> Boolean): Boolean =
+ (this as? Custom)?.selector() ?: false
+}
+
+fun UnifiedSharePermission?.customPermissionRead(): Boolean = this?.customFlag { read } ?: false
+fun UnifiedSharePermission?.customPermissionEdit(): Boolean = this?.customFlag { edit } ?: false
+fun UnifiedSharePermission?.customPermissionDelete(): Boolean = this?.customFlag { delete } ?: false
+fun UnifiedSharePermission?.customPermissionCreate(): Boolean = this?.customFlag { create } ?: false
+
+data class CustomPermissionField(
+ val labelRes: Int,
+ val getValue: (UnifiedSharePermission.Custom) -> Boolean,
+ val setValue: (UnifiedSharePermission.Custom, Boolean) -> UnifiedSharePermission.Custom
+)
+
+val customPermissionFields = listOf(
+ CustomPermissionField(
+ labelRes = R.string.share_view_view_files_switch,
+ getValue = { it.read },
+ setValue = { p, v -> p.copy(read = v) }
+ ),
+ CustomPermissionField(
+ labelRes = R.string.share_view_edit_files_switch,
+ getValue = { it.edit },
+ setValue = { p, v -> p.copy(edit = v) }
+ ),
+ CustomPermissionField(
+ labelRes = R.string.share_view_create_files_switch,
+ getValue = { it.create },
+ setValue = { p, v -> p.copy(create = v) }
+ ),
+ CustomPermissionField(
+ labelRes = R.string.share_view_delete_files_switch,
+ getValue = { it.delete },
+ setValue = { p, v -> p.copy(delete = v) }
+ ),
+)
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt
new file mode 100644
index 00000000..ec3a8acc
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.painterResource
+import com.nextcloud.android.common.ui.R
+
+enum class UnifiedShareType {
+ InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail;
+
+ @Composable
+ fun Icon() {
+ val iconId = when (this) {
+ InternalUser -> R.drawable.ic_user
+ InternalGroup -> R.drawable.ic_group
+ InternalLink -> R.drawable.ic_email
+ ExternalLink -> R.drawable.ic_link
+ ExternalFederated -> R.drawable.ic_group
+ ExternalMail -> R.drawable.ic_email
+ }
+
+ Icon(painterResource(iconId), contentDescription = "share type icon")
+ }
+
+ companion object {
+ fun toUnifiedShareType(value: String?): UnifiedShareType {
+ return when (value?.lowercase()) {
+ "user" -> InternalUser
+ "group" -> InternalGroup
+ "link" -> InternalLink
+ "federated" -> ExternalFederated
+ "mail" -> ExternalMail
+ else -> ExternalLink
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt
new file mode 100644
index 00000000..5690cb5f
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.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.ui
+
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.user.ShareUser
+
+data class UnifiedShare(
+ val id: String?,
+ val sources: List,
+ val recipients: List,
+ val properties: Map>>,
+
+ val lastUpdated: Long,
+ val owner: Owner?,
+
+ val permission: UnifiedSharePermission?,
+ val type: UnifiedShareType?,
+ val category: UnifiedShareCategory,
+ val label: String,
+ val note: String = "",
+ val password: String = "",
+ val limit: UnifiedShareDownloadLimit? = null
+) {
+ companion object {
+ fun new(): UnifiedShare {
+ return UnifiedShare(
+ id = null,
+ sources = listOf(),
+ recipients = listOf(),
+ properties = mapOf(),
+ lastUpdated = -1,
+ owner = null,
+ permission = null,
+ type = null,
+ category = UnifiedShareCategory.Invited,
+ label = "",
+ note = "",
+ password = "",
+ limit = null
+ )
+ }
+ }
+}
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..053d951c
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt
@@ -0,0 +1,281 @@
+/*
+ * 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.ApiResult
+import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest
+import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse
+import com.nextcloud.android.common.ui.share.model.api.owner.Owner
+import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients
+import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest
+import com.nextcloud.android.common.ui.share.model.api.user.ShareUser
+import com.nextcloud.android.common.ui.share.model.ui.*
+
+class MockShareRepository : ShareRepository {
+ override suspend fun fetchRecipients(
+ recipientType: String,
+ query: String,
+ limit: Int,
+ offset: Int
+ ): ApiResult> {
+
+ val mock = listOf(
+ ShareRecipients(
+ type = recipientType,
+ value = "alice@company.com",
+ displayName = "Alice Johnson",
+ displayNameUnique = "Alice Johnson (Company)",
+ iconUrlLight = "https://mock/icons/user_light.png",
+ iconUrlDark = "https://mock/icons/user_dark.png"
+ ),
+
+ ShareRecipients(
+ type = recipientType,
+ value = "marketing",
+ displayName = "Marketing Team",
+ displayNameUnique = "Marketing Team (Group)",
+ iconUrlLight = "https://mock/icons/group_light.png",
+ iconUrlDark = "https://mock/icons/group_dark.png"
+ ),
+
+ ShareRecipients(
+ type = recipientType,
+ value = "john@external.com",
+ displayName = "John External",
+ displayNameUnique = "John External (External)",
+ iconUrlLight = "https://mock/icons/external_light.png",
+ iconUrlDark = "https://mock/icons/external_dark.png"
+ )
+ )
+
+ return ApiResult.Success(mock)
+ }
+
+ override suspend fun createShare(
+ request: CreateShareRequest
+ ): ApiResult {
+
+ val response = ShareDataResponse(
+ sources = request.data.sources,
+ recipients = request.data.recipients,
+ properties = request.data.properties,
+ id = "mock-share-${System.currentTimeMillis()}",
+ lastUpdated = System.currentTimeMillis(),
+ owner = Owner(
+ userId = "mock-user",
+ displayName = "Mock User"
+ )
+ )
+
+ return ApiResult.Success(response)
+ }
+
+ override suspend fun fetchShare(id: String): ApiResult {
+
+ val mock = ShareDataResponse(
+ sources = emptyList(),
+ recipients = listOf(
+ ShareUser(
+ type = "user",
+ value = "alice@company.com",
+ displayName = "Alice Johnson"
+ )
+ ),
+ properties = emptyMap(),
+ id = id,
+ lastUpdated = 0,
+ owner = Owner(
+ userId = "alice",
+ displayName = "Alice Johnson"
+ )
+ )
+
+ return ApiResult.Success(mock)
+ }
+
+ override suspend fun updateShare(
+ id: String,
+ request: UpdateShareRequest
+ ): ApiResult {
+
+ val updated = ShareDataResponse(
+ sources = request.data.sources,
+ recipients = request.data.recipients,
+ properties = request.data.properties,
+ id = id,
+ lastUpdated = System.currentTimeMillis(),
+ owner = request.data.owner
+ )
+
+ return ApiResult.Success(updated)
+ }
+
+ override suspend fun deleteShare(id: String): ApiResult {
+ return ApiResult.Success(Unit)
+ }
+
+ override suspend fun fetchShares(
+ sourceType: String?,
+ lastShareId: String?,
+ limit: Int
+ ): ApiResult> {
+ val data = listOf(
+ UnifiedShare(
+ id = "1",
+ sources = emptyList(),
+ recipients = listOf(
+ ShareUser(
+ type = "user",
+ value = "alice@company.com",
+ displayName = "Alice Johnson"
+ )
+ ),
+ properties = emptyMap(),
+ lastUpdated = 0,
+ owner = Owner(
+ userId = "alice",
+ displayName = "Alice Johnson"
+ ),
+
+ permission = UnifiedSharePermission.CanView,
+ label = "Alice Johnson",
+ note = "Design review – please check latest changes",
+ password = "",
+ type = UnifiedShareType.InternalUser,
+ category = UnifiedShareCategory.Invited,
+ limit = UnifiedShareDownloadLimit(
+ limit = 100,
+ downloadCount = 12
+ )
+ ),
+
+ UnifiedShare(
+ id = "2",
+ sources = emptyList(),
+ recipients = listOf(
+ ShareUser(
+ type = "group",
+ value = "marketing",
+ displayName = "Marketing Team"
+ )
+ ),
+ properties = emptyMap(),
+ lastUpdated = 0,
+ owner = Owner(
+ userId = "system",
+ displayName = "System"
+ ),
+
+ permission = UnifiedSharePermission.CanEdit,
+ label = "Marketing Team",
+ note = "",
+ password = "",
+ type = UnifiedShareType.InternalGroup,
+ category = UnifiedShareCategory.Invited,
+ limit = UnifiedShareDownloadLimit(
+ limit = 0,
+ downloadCount = 0
+ )
+ ),
+
+ UnifiedShare(
+ id = "3",
+ sources = listOf(
+ ShareUser(
+ type = "link",
+ value = "https://nextcloud.com/s/abc123",
+ displayName = "Public Link"
+ )
+ ),
+ recipients = emptyList(),
+ properties = emptyMap(),
+ lastUpdated = 1710000000,
+ owner = Owner(
+ userId = "system",
+ displayName = "System"
+ ),
+
+ permission = UnifiedSharePermission.Custom(
+ read = true,
+ edit = false,
+ delete = false,
+ create = false
+ ),
+ label = "Public Link",
+ note = "Public link for client review",
+ password = "1234",
+ type = UnifiedShareType.InternalLink,
+ category = UnifiedShareCategory.Anyone,
+ limit = UnifiedShareDownloadLimit(
+ limit = 50,
+ downloadCount = 5
+ )
+ ),
+
+ UnifiedShare(
+ id = "4",
+ sources = emptyList(),
+ recipients = listOf(
+ ShareUser(
+ type = "mail",
+ value = "john@external.com",
+ displayName = "John External"
+ )
+ ),
+ properties = emptyMap(),
+ lastUpdated = 0,
+ owner = Owner(
+ userId = "john",
+ displayName = "John External"
+ ),
+
+ permission = UnifiedSharePermission.CanView,
+ label = "John External",
+ note = "External partner access",
+ password = "",
+ type = UnifiedShareType.ExternalMail,
+ category = UnifiedShareCategory.Anyone,
+ limit = UnifiedShareDownloadLimit(
+ limit = 20,
+ downloadCount = 2
+ )
+ ),
+
+ UnifiedShare(
+ id = "5",
+ sources = emptyList(),
+ recipients = listOf(
+ ShareUser(
+ type = "federated",
+ value = "partner@nextcloud.org",
+ displayName = "Partner Cloud"
+ )
+ ),
+ properties = emptyMap(),
+ lastUpdated = 0,
+ owner = Owner(
+ userId = "partner",
+ displayName = "Partner Cloud"
+ ),
+
+ permission = UnifiedSharePermission.FileDrop,
+ label = "Partner Cloud",
+ note = "Federated sharing with partner instance",
+ password = "",
+ type = UnifiedShareType.ExternalFederated,
+ category = UnifiedShareCategory.Anyone,
+ limit = UnifiedShareDownloadLimit(
+ limit = 0,
+ downloadCount = 0
+ )
+ )
+ )
+
+ return ApiResult.Success(data)
+ }
+}
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..6551f27f
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.ApiResult
+import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest
+import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse
+import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients
+import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare
+
+class ShareRemoteRepository: ShareRepository {
+
+ // TODO: ALL OCS-APIRequest //boolean header
+
+ /**
+ * Searches for recipients
+ */
+ override suspend fun fetchRecipients(
+ recipientType: String,
+ query: String,
+ limit: Int,
+ offset: Int
+ ): ApiResult> {
+ /*
+ GET
+ /ocs/v2.php/apps/sharing/api/v1/recipients
+
+ */
+
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun createShare(request: CreateShareRequest): ApiResult {
+ /*
+ POST
+ /ocs/v2.php/apps/sharing/api/v1/share
+ */
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun fetchShare(id: String): ApiResult {
+ /*
+ POST
+ /ocs/v2.php/apps/sharing/api/v1/share/{id}
+ */
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult {
+ /*
+ PUT
+ /ocs/v2.php/apps/sharing/api/v1/share/{id}
+ */
+ TODO("Not yet implemented")
+ }
+
+ override suspend fun deleteShare(id: String): ApiResult {
+ /*
+ DELETE
+ /ocs/v2.php/apps/sharing/api/v1/share/{id}
+ */
+ TODO("Not yet implemented")
+ }
+
+ /**
+ * @param sourceType
+ * Optional filter to return only shares matching a specific source type.
+ * When null, shares of all source types are returned.
+ *
+ * @param lastShareId
+ * Pagination cursor representing the last known share ID.
+ * Only shares with an ID greater than this value will be returned.
+ * When null, results start from the first available share.
+ */
+ override suspend fun fetchShares(
+ sourceType: String?,
+ lastShareId: String?,
+ limit: Int
+ ): ApiResult> {
+ /*
+ GET
+ /ocs/v2.php/apps/sharing/api/v1/shares
+ */
+ TODO("Not yet implemented")
+ }
+}
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..3267d9db
--- /dev/null
+++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.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.share.repository
+
+import com.nextcloud.android.common.ui.network.model.ApiResult
+import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest
+import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse
+import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients
+import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest
+import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare
+
+interface ShareRepository {
+ suspend fun fetchRecipients(
+ recipientType: String,
+ query: String,
+ limit: Int = 10,
+ offset: Int = 0
+ ): ApiResult>
+
+ suspend fun createShare(request: CreateShareRequest): ApiResult
+
+ suspend fun fetchShare(id: String): ApiResult
+
+ suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult
+
+ suspend fun deleteShare(id: String): ApiResult
+
+ suspend fun fetchShares(
+ sourceType: String? = null,
+ lastShareId: String? = null,
+ limit: Int = 100
+ ): ApiResult>
+}
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..f341dc87
--- /dev/null
+++ b/ui/src/main/res/values/strings.xml
@@ -0,0 +1,60 @@
+
+
+
+ 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
+
+ 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
+
+ Failed to fetch shares
+
+ Share not found, cannot delete
+ Failed to delete share
+
+
\ No newline at end of file