From 4bc1983df00b95df2c5c169bfe0fdb936be6835f Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 11:34:27 +0200 Subject: [PATCH 1/9] chore: add permalink-sharing openspec change Signed-off-by: Jorge Aguado Recio --- .../changes/permalink-sharing/.openspec.yaml | 2 + openspec/changes/permalink-sharing/design.md | 73 ++++++++++++++++ .../changes/permalink-sharing/proposal.md | 29 +++++++ .../specs/files-sharing/permalink/spec.md | 72 ++++++++++++++++ openspec/changes/permalink-sharing/tasks.md | 86 +++++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 openspec/changes/permalink-sharing/.openspec.yaml create mode 100644 openspec/changes/permalink-sharing/design.md create mode 100644 openspec/changes/permalink-sharing/proposal.md create mode 100644 openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md create mode 100644 openspec/changes/permalink-sharing/tasks.md diff --git a/openspec/changes/permalink-sharing/.openspec.yaml b/openspec/changes/permalink-sharing/.openspec.yaml new file mode 100644 index 00000000000..c1bc902799f --- /dev/null +++ b/openspec/changes/permalink-sharing/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-19 diff --git a/openspec/changes/permalink-sharing/design.md b/openspec/changes/permalink-sharing/design.md new file mode 100644 index 00000000000..f6af61cf399 --- /dev/null +++ b/openspec/changes/permalink-sharing/design.md @@ -0,0 +1,73 @@ +## Context + +See proposal.md — Why. Two independent pieces must change, both scoped to oCIS: + +1. **`Share` reachability** lives in `FilterFileMenuOptionsUseCase` (owncloudApp), a use case shared by every entry point that builds the file context menu (`MainFileListViewModel`, `FileDetailsViewModel`, and the four preview ViewModels). Its current `SHARE` gate — `!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && hasResharePermission` — was designed for oC10. Because it is shared, any relaxation must be guarded so oC10 keeps the exact same gate. +2. **The permalink button** in the oCIS Share header exists in `members_activity.xml` as `permanentLinkButton`, but `ShareActivity.setupFileHeader()` hard-codes it hidden (`ShareActivity.kt:172`) and sets no listener. oC10 uses a different screen (`OcsShareFileFragment`) where the private link already works via `showOrHidePrivateLink()`. + +Relevant existing facts the design relies on: +- `OCCapability.isSpacesAllowed()` (`spaces?.enabled == true`) is the established oCIS/oC10 discriminator across the app. +- `ShareActivity` already resolves `capabilityViewModel.isOcisServer()` (which calls `isSpacesAllowed()`) and branches `setupOcisLayout()` vs `setupLegacyLayout()`. `setupFileHeader()` runs only on the oCIS branch. +- `ShareActivity` already overrides `copyOrSendPrivateLink(file)` → `fileOperationsHelper.copyOrSendPrivateLink(file)`. +- oC10's rule (`OcsShareFileFragment.showOrHidePrivateLink()`): hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`. +- `FilterFileMenuOptionsUseCase` already fetches `capability = capabilityRepository.getStoredCapabilities(params.accountName)`. + +## Goals / Non-Goals + +**Goals:** +- Make `Share` unconditionally reachable for a single, non-available-offline selection on oCIS, without changing the oC10 gate. +- Make the oCIS permalink button visible and functional under the same show/hide rule oC10 already uses. + +**Non-Goals:** +- No change to oC10 (`FilterFileMenuOptionsUseCase` oC10 branch, `OcsShareFileFragment`, `share_file_layout.xml`). +- No change to `FilterFileMenuOptionsUseCase.Params`, its callers, or its Koin factory. +- No change to the section-level gating inside the Share screen. +- No data/domain/network layer changes. + +## Decisions + +### D1: Branch the `SHARE` gate by server type inside the use case +Reuse the already-fetched `capability`: `val isOcis = capability?.isSpacesAllowed() == true`. Then: + +```kotlin +val shareReachable = if (isOcis) { + !onlyAvailableOfflineFiles && isSingleSelection(files) +} else { + !onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && + resharingAllowed && hasResharePermission +} +if (shareReachable) optionsToShow.add(FileMenuOption.SHARE) +``` + +- **Why in the use case** rather than at the call sites: the gate and all its inputs already live here; the six callers pass identical `Params` and would otherwise each need the oCIS branch duplicated. +- **Why keep the oC10 branch verbatim**: the change must be behavior-preserving for oC10. Consequently `resharingAllowed`, `hasResharePermission`, `anyFileSharedWithMe`, `shareViaLinkOrWithUsersAllowed`, the `capability` lookup, the `capabilityRepository` dependency, and the `shareViaLinkAllowed`/`shareWithUsersAllowed` `Params` fields are all retained (still read by the oC10 branch), so detekt's zero-tolerance "unused" rules stay satisfied and no caller/Koin edits are needed. +- **Alternative rejected**: removing the oC10-only inputs and reducing the gate to `!onlyAvailableOfflineFiles && isSingleSelection(files)` for everyone. Rejected because it changes oC10 behavior (an item "shared with me" with resharing disabled would newly show `Share`), which is explicitly out of scope. + +### D2: Implement the oCIS permalink button in `ShareActivity.setupFileHeader()` +Replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule, then wire the click to the existing override: + +```kotlin +val privateLinkDisabled = + capabilityViewModel.getStoredCapabilities()?.filesPrivateLinks == CapabilityBooleanType.FALSE +if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { + binding.permanentLinkButton.isVisible = false +} else { + binding.permanentLinkButton.isVisible = true + binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) } +} +``` + +- **Why here**: `setupFileHeader()` already runs only on the oCIS layout branch, so no extra oCIS guard is required; the button and the `copyOrSendPrivateLink` plumbing already exist. +- **Why mirror oC10's rule** instead of inventing one: keeps permalink visibility consistent across both screens (hidden when no link or `filesPrivateLinks == FALSE`). +- **Capability access**: `CapabilityViewModel` already exposes stored capabilities for the account (used by `isOcisServer()`); read `filesPrivateLinks` through it. If no accessor returns the full `OCCapability`, add a thin read-only getter on `CapabilityViewModel` rather than re-querying Room from the Activity — confirm the exact accessor during apply (see Open Questions). +- **Reference**: `SpaceMembersActivity.kt:74` wires the same button via `setOnClickListener`. + +## Risks / Trade-offs + +- [oC10 regression from the shared use case] → Guard the relaxation behind `isOcis` so the oC10 branch stays byte-for-byte unchanged; validated by the manual oC10 regression QA cases in `tasks.md` (item shared-with-me, resharing false, no `R` → `Share` absent). +- [`capability` is null when stored capabilities are missing] → `capability?.isSpacesAllowed() == true` evaluates to `false`, so the account falls back to the oC10 branch — the pre-change behavior, which is the safe default. +- [Detekt flags a now-unused symbol] → None expected, because the oC10 branch keeps reading every retained symbol; verify with `./gradlew detekt` during apply. + +## Open Questions + +- Which existing `CapabilityViewModel` accessor returns the stored `OCCapability` (or specifically `filesPrivateLinks`) for the account, or whether a small read-only getter must be added. This is an implementation detail that does not affect the specs, the approach, or the task breakdown; resolve it when editing `ShareActivity`. \ No newline at end of file diff --git a/openspec/changes/permalink-sharing/proposal.md b/openspec/changes/permalink-sharing/proposal.md new file mode 100644 index 00000000000..e3f8e1c65d7 --- /dev/null +++ b/openspec/changes/permalink-sharing/proposal.md @@ -0,0 +1,29 @@ +## Why + +On oCIS the permalink can become unreachable. The `Share` screen is the only entry point to it, and the `FileMenuOption.SHARE` gate in `FilterFileMenuOptionsUseCase` — designed for oC10 — hides `Share` whenever the `resharing` capability is `false` (always the case on oCIS) or the item carries no `R` permission (empty `oc:permissions` in custom spaces). Even when `Share` is reachable, the oCIS header's `permanent_link_button` is hard-coded hidden and wired to nothing, so the permalink is never shown. oC10 is unaffected and must stay exactly as it is. + +## What Changes + +- On oCIS accounts (identified by the `spaces` capability, `OCCapability.isSpacesAllowed()`), the `Share` menu option becomes reachable for any single selected item, independent of the `resharing`/`sharing` capabilities, the `R` permission, and the `share_via_link_feature`/`share_with_users_feature` setup flags. It stays hidden in the available-offline shortcut and for multiselection. +- On oC10 the `Share` gate is left unchanged (no observable difference). +- On oCIS, the `permanent_link_button` in the Share header (`members_activity.xml`, driven by `ShareActivity.setupFileHeader()`) is made visible and wired to `copyOrSendPrivateLink(file)`, shown only when the item has a private link and the `filesPrivateLinks` capability is not `FALSE` — mirroring oC10's `showOrHidePrivateLink()` and the `SpaceMembersActivity` reference. +- The capability/feature-flag gating of the "share with users" and "share by link" sections *inside* the Share screen is untouched on both server types. + +## Capabilities + +### New Capabilities +- `files-sharing/permalink`: Reachability of the `Share` screen on oCIS and the display/behavior of the permalink (private link) button in the oCIS Share header, including the show/hide rule and the copy/send action. + +### Modified Capabilities + + +## Impact + +- **owncloudApp** (presentation): + - `usecases/files/FilterFileMenuOptionsUseCase.kt` — split the `SHARE` gate by server type; the oCIS branch reduces to `!onlyAvailableOfflineFiles && isSingleSelection(files)`, the oC10 branch is unchanged. All existing inputs (`capabilityRepository`, `resharingAllowed`, `hasResharePermission`, the `shareViaLinkAllowed`/`shareWithUsersAllowed` `Params`) are retained because the oC10 branch still needs them — no caller or Koin changes. + - `presentation/sharing/ShareActivity.kt` — `setupFileHeader()` shows and wires `permanentLinkButton`. + - No unit tests are added: the change touches only the presentation layer (owncloudApp), where unit tests are not established per project convention. The oCIS reachability and permalink behavior are covered by the manual QA cases in `tasks.md`. +- **owncloudDomain / owncloudData / owncloudComLibrary**: no changes. `OCFile.privateLink` is already populated from the `oc:privatelink` PROPFIND property and `OCCapability.isSpacesAllowed()` already exists. +- **Release notes**: user-facing entry summarising the oCIS Share/permalink change. +- **Changelog**: new `Bugfix` entry under `changelog/unreleased/`. +- No new dependencies. \ No newline at end of file diff --git a/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md b/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md new file mode 100644 index 00000000000..eb49e7a8506 --- /dev/null +++ b/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md @@ -0,0 +1,72 @@ +## Purpose + +Defines when the `Share` screen is reachable on oCIS and how the permalink (private link) button in the oCIS Share header is shown and acted upon, so that a user can always reach and copy an item's permalink while oC10 behavior stays unchanged. + +## ADDED Requirements + +### Requirement: Share screen reachable for a single item on oCIS + +On an oCIS account, the `Share` menu option SHALL be offered for a single selected item regardless of the `resharing`/`sharing` capability values, the item's `R` (reshare) permission, and the `share_via_link_feature`/`share_with_users_feature` setup flags. Reachability of the `Share` screen SHALL NOT depend on any of those values on oCIS. + +An account is treated as oCIS when its stored capabilities report the `spaces` capability as enabled; otherwise the account is treated as oC10. + +#### Scenario: Single item in an oCIS personal space with resharing disabled +- **WHEN** an oCIS account has a single item selected in its personal space and the `resharing` capability is `false` +- **THEN** the `Share` menu option is offered + +#### Scenario: Single item in an oCIS custom space with no reshare permission +- **WHEN** an oCIS account has a single item selected in a custom space, the item's permissions carry no `R` flag, and the `resharing` capability is `false` +- **THEN** the `Share` menu option is offered + +#### Scenario: oCIS with both share feature flags disabled +- **WHEN** an oCIS account has a single item selected and both `share_via_link_feature` and `share_with_users_feature` are `false` +- **THEN** the `Share` menu option is offered + +### Requirement: Share screen stays restricted in available-offline and multiselection on oCIS + +On an oCIS account, the `Share` menu option SHALL NOT be offered in the available-offline shortcut, and SHALL NOT be offered when more than one item is selected. + +#### Scenario: Available-offline shortcut on oCIS +- **WHEN** an oCIS account is in the available-offline shortcut +- **THEN** the `Share` menu option is not offered + +#### Scenario: Multiselection on oCIS +- **WHEN** an oCIS account has more than one item selected +- **THEN** the `Share` menu option is not offered + +### Requirement: oC10 Share reachability unchanged + +On an oC10 account, the conditions under which the `Share` menu option is offered SHALL remain exactly as they were before this change: it is offered only when the selection is not the available-offline shortcut, at least one of the share-via-link / share-with-users features is enabled, resharing is allowed for the selection, and the single selected item carries the reshare permission. + +#### Scenario: oC10 gate unchanged for an item shared with the user +- **WHEN** an oC10 account has a single item that is shared with the user, the `resharing` capability is `false`, and the item carries no reshare permission +- **THEN** the `Share` menu option is not offered, exactly as before this change + +#### Scenario: oC10 gate unchanged when conditions are met +- **WHEN** an oC10 account has a single selected item that satisfies the pre-existing share-feature, resharing, and reshare-permission conditions outside the available-offline shortcut +- **THEN** the `Share` menu option is offered, exactly as before this change + +### Requirement: Permalink button shown and functional in the oCIS Share header + +On an oCIS account, the Share header SHALL display the permalink (private link) button when the selected item has a non-empty private link and the `filesPrivateLinks` capability is not `FALSE`. When the button is displayed and the user activates it, the system SHALL copy or send that item's private link, reusing the existing private-link copy/send action. + +#### Scenario: Item with a private link and private links not disabled +- **WHEN** the oCIS Share header is shown for an item whose private link is non-empty and the `filesPrivateLinks` capability is not `FALSE` +- **THEN** the permalink button is visible +- **AND** activating it copies or sends that item's private link + +#### Scenario: Private links capability disabled +- **WHEN** the oCIS Share header is shown and the `filesPrivateLinks` capability is `FALSE` +- **THEN** the permalink button is hidden + +#### Scenario: Item without a private link +- **WHEN** the oCIS Share header is shown for an item whose private link is null or empty +- **THEN** the permalink button is hidden + +### Requirement: Share screen section gating unchanged + +The visibility of the "share with users" and "share by link" sections inside the Share screen SHALL continue to be governed by their existing capabilities and build-time feature flags, on both oCIS and oC10. This change SHALL NOT alter that section-level gating. + +#### Scenario: Sections still governed by their own rules +- **WHEN** the Share screen is opened on either server type +- **THEN** the "share with users" and "share by link" sections are shown or hidden according to their existing capabilities and feature flags, unchanged by this change \ No newline at end of file diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md new file mode 100644 index 00000000000..d49c5136a08 --- /dev/null +++ b/openspec/changes/permalink-sharing/tasks.md @@ -0,0 +1,86 @@ +## 1. oCIS Share reachability (owncloudApp — FilterFileMenuOptionsUseCase) + +- [ ] 1.1 In `FilterFileMenuOptionsUseCase.run()`, derive `val isOcis = capability?.isSpacesAllowed() == true` from the already-fetched `capability`; verify by reading the file that `isOcis` is computed before the `SHARE` block and reuses the existing `capability` lookup (no new repository call). +- [ ] 1.2 Replace the `SHARE` gate (`FilterFileMenuOptionsUseCase.kt:90-91`) with a server-type branch: oCIS → `!onlyAvailableOfflineFiles && isSingleSelection(files)`, oC10 → the existing `!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && hasResharePermission`; verify by reading the diff that the oC10 branch is byte-for-byte the previous condition. +- [ ] 1.3 Confirm no symbol became unused: `resharingAllowed`, `hasResharePermission`, `anyFileSharedWithMe`, `shareViaLinkOrWithUsersAllowed`, the `capability` lookup, `capabilityRepository`, and the `shareViaLinkAllowed`/`shareWithUsersAllowed` `Params` fields are all still referenced by the oC10 branch; verify with `./gradlew :owncloudApp:detektOriginalDebug` (or `./gradlew detekt`) reporting zero issues. +- [ ] 1.4 Confirm no caller or DI change is required — `FilterFileMenuOptionsUseCase.Params` and the `UseCaseModule` factory are unchanged; verify by grepping callers (`MainFileListViewModel`, `FileDetailsViewModel`, `PreviewVideoViewModel`, `PreviewAudioViewModel`, `PreviewTextViewModel`, `PreviewImageViewModel`) still compile against the same `Params` and that `./gradlew :owncloudApp:assembleOriginalDebug` succeeds. + +## 2. oCIS permalink button (owncloudApp — ShareActivity) + +- [ ] 2.1 Expose the stored `filesPrivateLinks` capability to `ShareActivity` — reuse an existing `CapabilityViewModel` accessor if one returns the `OCCapability`, otherwise add a thin read-only getter on `CapabilityViewModel` backed by `GetStoredCapabilitiesUseCase` (mirroring `isOcisServer()`); verify by reading that `ShareActivity.setupFileHeader()` can read `filesPrivateLinks` without querying Room directly. +- [ ] 2.2 In `ShareActivity.setupFileHeader()`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` (`ShareActivity.kt:172`) with the oC10 rule: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. +- [ ] 2.3 When the button is shown, wire `binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) }` (reference `SpaceMembersActivity.kt:74`); verify manually on an oCIS account that tapping the permalink button in the Share header copies/sends the item's private link. +- [ ] 2.4 Manual check — hidden states: on oCIS, open the Share header for an item with no private link and for an account where `filesPrivateLinks` is `FALSE`; verify the permalink button is not shown in either case. + +## 3. Release notes + +- [ ] 3.1 Draft the user-facing release notes entry summarising the oCIS Share/permalink change for end users. + +## 4. Changelog + +- [ ] 4.1 Add `changelog/unreleased/` with a `Bugfix` entry (title under 80 chars, present-perfect-passive body) linking `https://github.com/owncloud/android/issues/4939` and the PR URL; verify the file matches the CLAUDE.md changelog format. + +## 5. Final verification + +- [ ] 5.1 Run `./gradlew detekt ktlintCheck lintDebug` and `./gradlew testDebugUnitTest testMdmDebugUnitTest --continue`; verify all pass (detekt zero-tolerance). Note: no new unit tests are added, as this change touches only the presentation layer (owncloudApp), where unit tests are not established per project convention; the oCIS reachability and permalink behavior are covered by the manual QA cases below. + +## Permalink sharing reachability + +**Server(s)**:
+**Device(s)**:
+**Execution date**:
+**Tester**:
+ +| Test Case | Steps | Expected | Result | Comments | +|---|---|---|---|---| +| **Share reachability (oCIS)** | | | | | +| Share reachable — personal space | 1. On an oCIS account, open the file list for the personal space
2. Open the file menu for a single item | The `Share` option is shown even when `resharing` is `false` and the item has no `R` permission | | | +| Share reachable — custom space | 1. On an oCIS account, open a custom space with items whose `oc:permissions` is empty
2. Open the file menu for a single item | The `Share` option is shown | | | +| Share hidden — available offline | 1. On an oCIS account, open the available-offline shortcut
2. Open the file menu for a single item | The `Share` option is not shown | | | +| Share hidden — multiselection | 1. On an oCIS account, select two or more items
2. Open the multiselection menu | The `Share` option is not shown | | | +| **Permalink button (oCIS)** | | | | | +| Button shown | 1. On an oCIS account where `filesPrivateLinks` is not `FALSE`
2. Open the Share screen for an item that has a private link | The permalink (permanent link) button is shown in the Share header | | | +| Copy/send permalink | 1. Open the Share header for an item with a private link
2. Tap the permalink button | The item's private link is copied/sent (same behavior as oC10 `copyOrSendPrivateLink`) | | | +| Button hidden — no private link | 1. Open the Share header for an item with no private link | The permalink button is not shown | | | +| Button hidden — capability FALSE | 1. Use an account where `filesPrivateLinks` is `FALSE`
2. Open the Share header for any item | The permalink button is not shown | | | +| **Regression (oC10)** | | | | | +| Share gate unchanged | 1. On an oC10 account, open the file menu for various items (shared-with-me, no `R`, resharing off/on) | The `Share` option visibility is identical to the current behavior (no observable difference) | | | +| Permalink unchanged | 1. On an oC10 account, open the Share screen for an item with/without a private link | The permalink button shows/hides exactly as before | | | +| **Accessibility** | **One device** | | | | +| Talkback | 1. Enable Talkback
2. Navigate to the permalink button in the Share header | The permalink button is correctly announced and actionable | | | +| Keyboard | 1. Attach a physical keyboard via Bluetooth
2. Navigate to the permalink button | The button is reachable and actionable | | | + +### Gherkin scenario (E2E suite proposal) + +```gherkin +Feature: Permalink sharing reachability on oCIS + + Scenario: Share option is reachable for a single item on oCIS + Given an oCIS account where the "resharing" capability is false + And a single item whose "oc:permissions" has no "R" permission + When I open the file menu for that item + Then the "Share" option is shown + + Scenario: Share option is hidden for multiselection on oCIS + Given an oCIS account + When I select two or more items and open the menu + Then the "Share" option is not shown + + Scenario: Permalink button copies the private link on oCIS + Given an oCIS account where the "filesPrivateLinks" capability is not FALSE + And an item that has a private link + When I open the Share header for that item + Then the permalink button is shown + And tapping the permalink button copies or sends the item's private link + + Scenario: Permalink button is hidden when the item has no private link + Given an oCIS account + And an item that has no private link + When I open the Share header for that item + Then the permalink button is not shown + + Scenario: oC10 Share gate is unchanged + Given an oC10 account + When I open the file menu for items with varying resharing/permission state + Then the "Share" option visibility is identical to the previous behavior +``` \ No newline at end of file From c2d679013bfe7ba1323611f538f0e67a1cf8ac91 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 11:48:58 +0200 Subject: [PATCH 2/9] feat: make share reachable for a single item on oCIS Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/tasks.md | 8 ++++---- .../usecases/files/FilterFileMenuOptionsUseCase.kt | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index d49c5136a08..c37f408ba44 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -1,9 +1,9 @@ ## 1. oCIS Share reachability (owncloudApp — FilterFileMenuOptionsUseCase) -- [ ] 1.1 In `FilterFileMenuOptionsUseCase.run()`, derive `val isOcis = capability?.isSpacesAllowed() == true` from the already-fetched `capability`; verify by reading the file that `isOcis` is computed before the `SHARE` block and reuses the existing `capability` lookup (no new repository call). -- [ ] 1.2 Replace the `SHARE` gate (`FilterFileMenuOptionsUseCase.kt:90-91`) with a server-type branch: oCIS → `!onlyAvailableOfflineFiles && isSingleSelection(files)`, oC10 → the existing `!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && hasResharePermission`; verify by reading the diff that the oC10 branch is byte-for-byte the previous condition. -- [ ] 1.3 Confirm no symbol became unused: `resharingAllowed`, `hasResharePermission`, `anyFileSharedWithMe`, `shareViaLinkOrWithUsersAllowed`, the `capability` lookup, `capabilityRepository`, and the `shareViaLinkAllowed`/`shareWithUsersAllowed` `Params` fields are all still referenced by the oC10 branch; verify with `./gradlew :owncloudApp:detektOriginalDebug` (or `./gradlew detekt`) reporting zero issues. -- [ ] 1.4 Confirm no caller or DI change is required — `FilterFileMenuOptionsUseCase.Params` and the `UseCaseModule` factory are unchanged; verify by grepping callers (`MainFileListViewModel`, `FileDetailsViewModel`, `PreviewVideoViewModel`, `PreviewAudioViewModel`, `PreviewTextViewModel`, `PreviewImageViewModel`) still compile against the same `Params` and that `./gradlew :owncloudApp:assembleOriginalDebug` succeeds. +- [x] 1.1 In `FilterFileMenuOptionsUseCase.run()`, derive `val isOcis = capability?.isSpacesAllowed() == true` from the already-fetched `capability`; verify by reading the file that `isOcis` is computed before the `SHARE` block and reuses the existing `capability` lookup (no new repository call). +- [x] 1.2 Replace the `SHARE` gate (`FilterFileMenuOptionsUseCase.kt:90-91`) with a server-type branch: oCIS → `!onlyAvailableOfflineFiles && isSingleSelection(files)`, oC10 → the existing `!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && hasResharePermission`; verify by reading the diff that the oC10 branch is byte-for-byte the previous condition. +- [x] 1.3 Confirm no symbol became unused: `resharingAllowed`, `hasResharePermission`, `anyFileSharedWithMe`, `shareViaLinkOrWithUsersAllowed`, the `capability` lookup, `capabilityRepository`, and the `shareViaLinkAllowed`/`shareWithUsersAllowed` `Params` fields are all still referenced by the oC10 branch; verify with `./gradlew :owncloudApp:detektOriginalDebug` (or `./gradlew detekt`) reporting zero issues. +- [x] 1.4 Confirm no caller or DI change is required — `FilterFileMenuOptionsUseCase.Params` and the `UseCaseModule` factory are unchanged; verify by grepping callers (`MainFileListViewModel`, `FileDetailsViewModel`, `PreviewVideoViewModel`, `PreviewAudioViewModel`, `PreviewTextViewModel`, `PreviewImageViewModel`) still compile against the same `Params` and that `./gradlew :owncloudApp:assembleOriginalDebug` succeeds. ## 2. oCIS permalink button (owncloudApp — ShareActivity) diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt index c6714de81cd..81e0ee7130f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt @@ -65,6 +65,7 @@ class FilterFileMenuOptionsUseCase( false } val resharingAllowed = capability?.let { !anyFileSharedWithMe(files) || it.filesSharingResharing.isTrue } ?: false + val isOcis = capability?.isSpacesAllowed() == true val displaySelectAll = params.displaySelectAll val displaySelectInverse = params.displaySelectInverse val onlyAvailableOfflineFiles = params.onlyAvailableOfflineFiles @@ -87,8 +88,13 @@ class FilterFileMenuOptionsUseCase( optionsToShow.add(FileMenuOption.SELECT_INVERSE) } // Share - if (!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && - hasResharePermission) { + val shareReachable = if (isOcis) { + !onlyAvailableOfflineFiles && isSingleSelection(files) + } else { + !onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && + hasResharePermission + } + if (shareReachable) { optionsToShow.add(FileMenuOption.SHARE) } // Open with (different to preview!) From 4b15efa4441f42caccc7376d61de23394cbc76a3 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 12:43:20 +0200 Subject: [PATCH 3/9] feat: show and wire the permalink button in the oCIS share header Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/tasks.md | 8 ++++---- .../presentation/capabilities/CapabilityViewModel.kt | 4 ++++ .../android/presentation/sharing/ShareActivity.kt | 9 ++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index c37f408ba44..edb425d23ca 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -7,10 +7,10 @@ ## 2. oCIS permalink button (owncloudApp — ShareActivity) -- [ ] 2.1 Expose the stored `filesPrivateLinks` capability to `ShareActivity` — reuse an existing `CapabilityViewModel` accessor if one returns the `OCCapability`, otherwise add a thin read-only getter on `CapabilityViewModel` backed by `GetStoredCapabilitiesUseCase` (mirroring `isOcisServer()`); verify by reading that `ShareActivity.setupFileHeader()` can read `filesPrivateLinks` without querying Room directly. -- [ ] 2.2 In `ShareActivity.setupFileHeader()`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` (`ShareActivity.kt:172`) with the oC10 rule: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. -- [ ] 2.3 When the button is shown, wire `binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) }` (reference `SpaceMembersActivity.kt:74`); verify manually on an oCIS account that tapping the permalink button in the Share header copies/sends the item's private link. -- [ ] 2.4 Manual check — hidden states: on oCIS, open the Share header for an item with no private link and for an account where `filesPrivateLinks` is `FALSE`; verify the permalink button is not shown in either case. +- [x] 2.1 Expose the stored `filesPrivateLinks` capability to `ShareActivity` — reuse an existing `CapabilityViewModel` accessor if one returns the `OCCapability`, otherwise add a thin read-only getter on `CapabilityViewModel` backed by `GetStoredCapabilitiesUseCase` (mirroring `isOcisServer()`); verify by reading that `ShareActivity.setupFileHeader()` can read `filesPrivateLinks` without querying Room directly. +- [x] 2.2 In `ShareActivity.setupFileHeader()`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` (`ShareActivity.kt:172`) with the oC10 rule: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. +- [x] 2.3 When the button is shown, wire `binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) }` (reference `SpaceMembersActivity.kt:74`); verify manually on an oCIS account that tapping the permalink button in the Share header copies/sends the item's private link. +- [x] 2.4 Manual check — hidden states: on oCIS, open the Share header for an item with no private link and for an account where `filesPrivateLinks` is `FALSE`; verify the permalink button is not shown in either case. ## 3. Release notes diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt index 0ce70ae3fff..161e3cfd429 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt @@ -79,6 +79,10 @@ class CapabilityViewModel( getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))?.isSpacesAllowed() == true } + fun getStoredCapabilities(): OCCapability? = runBlocking(coroutineDispatcherProvider.io) { + getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) + } + fun checkMultiPersonal(): Boolean = runBlocking(CoroutinesDispatcherProvider().io) { val capabilities = withContext(CoroutinesDispatcherProvider().io) { getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index 7df4fb58717..5cacb36cd1e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt @@ -40,6 +40,7 @@ import androidx.fragment.app.transaction import com.owncloud.android.R import com.owncloud.android.databinding.MembersActivityBinding import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.sharing.shares.model.MemberPermission @@ -169,7 +170,13 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme binding.itemSize.text = DisplayUtils.bytesToHumanReadable(file.length, this, true) } - binding.permanentLinkButton.isVisible = false + val privateLinkDisabled = capabilityViewModel.getStoredCapabilities()?.filesPrivateLinks == CapabilityBooleanType.FALSE + if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { + binding.permanentLinkButton.isVisible = false + } else { + binding.permanentLinkButton.isVisible = true + binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) } + } } /************************************************************************************************************** From 6204a0954bc2ab50eacc80c5663e3bb9a9ee6013 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 13:06:23 +0200 Subject: [PATCH 4/9] refactor: read stored capabilities once in ShareActivity Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/design.md | 9 ++++----- openspec/changes/permalink-sharing/tasks.md | 4 ++-- .../capabilities/CapabilityViewModel.kt | 4 ---- .../android/presentation/sharing/ShareActivity.kt | 14 ++++++++------ 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/openspec/changes/permalink-sharing/design.md b/openspec/changes/permalink-sharing/design.md index f6af61cf399..fc26d5fb7e6 100644 --- a/openspec/changes/permalink-sharing/design.md +++ b/openspec/changes/permalink-sharing/design.md @@ -7,7 +7,7 @@ See proposal.md — Why. Two independent pieces must change, both scoped to oCIS Relevant existing facts the design relies on: - `OCCapability.isSpacesAllowed()` (`spaces?.enabled == true`) is the established oCIS/oC10 discriminator across the app. -- `ShareActivity` already resolves `capabilityViewModel.isOcisServer()` (which calls `isSpacesAllowed()`) and branches `setupOcisLayout()` vs `setupLegacyLayout()`. `setupFileHeader()` runs only on the oCIS branch. +- `ShareActivity.onCreate()` fetches the stored `OCCapability` once via `capabilityViewModel.getStoredCapabilities()`, branches `setupOcisLayout()` vs `setupLegacyLayout()` on `isSpacesAllowed()`, and passes the capabilities down to `setupFileHeader()`, which runs only on the oCIS branch. - `ShareActivity` already overrides `copyOrSendPrivateLink(file)` → `fileOperationsHelper.copyOrSendPrivateLink(file)`. - oC10's rule (`OcsShareFileFragment.showOrHidePrivateLink()`): hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`. - `FilterFileMenuOptionsUseCase` already fetches `capability = capabilityRepository.getStoredCapabilities(params.accountName)`. @@ -47,8 +47,7 @@ if (shareReachable) optionsToShow.add(FileMenuOption.SHARE) Replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule, then wire the click to the existing override: ```kotlin -val privateLinkDisabled = - capabilityViewModel.getStoredCapabilities()?.filesPrivateLinks == CapabilityBooleanType.FALSE +val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { binding.permanentLinkButton.isVisible = false } else { @@ -59,7 +58,7 @@ if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { - **Why here**: `setupFileHeader()` already runs only on the oCIS layout branch, so no extra oCIS guard is required; the button and the `copyOrSendPrivateLink` plumbing already exist. - **Why mirror oC10's rule** instead of inventing one: keeps permalink visibility consistent across both screens (hidden when no link or `filesPrivateLinks == FALSE`). -- **Capability access**: `CapabilityViewModel` already exposes stored capabilities for the account (used by `isOcisServer()`); read `filesPrivateLinks` through it. If no accessor returns the full `OCCapability`, add a thin read-only getter on `CapabilityViewModel` rather than re-querying Room from the Activity — confirm the exact accessor during apply (see Open Questions). +- **Capability access**: a thin read-only `getStoredCapabilities()` getter (backed by `GetStoredCapabilitiesUseCase`) was added to `CapabilityViewModel`, replacing the previous `isOcisServer()` boolean accessor. `onCreate()` reads the stored `OCCapability` once through it, branches on `isSpacesAllowed()`, and passes the capabilities into `setupFileHeader(binding, capabilities)`; `filesPrivateLinks` is read from that parameter rather than re-querying Room or the view model inside the method. - **Reference**: `SpaceMembersActivity.kt:74` wires the same button via `setOnClickListener`. ## Risks / Trade-offs @@ -70,4 +69,4 @@ if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { ## Open Questions -- Which existing `CapabilityViewModel` accessor returns the stored `OCCapability` (or specifically `filesPrivateLinks`) for the account, or whether a small read-only getter must be added. This is an implementation detail that does not affect the specs, the approach, or the task breakdown; resolve it when editing `ShareActivity`. \ No newline at end of file +- ~~Which existing `CapabilityViewModel` accessor returns the stored `OCCapability` (or specifically `filesPrivateLinks`) for the account, or whether a small read-only getter must be added.~~ **Resolved:** no existing accessor returned the full `OCCapability`, so a read-only `getStoredCapabilities()` getter was added to `CapabilityViewModel`, backed by `GetStoredCapabilitiesUseCase`; `onCreate()` calls it once and passes the result into `setupFileHeader()`. The former `isOcisServer()` boolean accessor was removed, as `getStoredCapabilities()?.isSpacesAllowed()` now covers both the layout branch and the permalink rule. \ No newline at end of file diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index edb425d23ca..6bc0911a228 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -7,8 +7,8 @@ ## 2. oCIS permalink button (owncloudApp — ShareActivity) -- [x] 2.1 Expose the stored `filesPrivateLinks` capability to `ShareActivity` — reuse an existing `CapabilityViewModel` accessor if one returns the `OCCapability`, otherwise add a thin read-only getter on `CapabilityViewModel` backed by `GetStoredCapabilitiesUseCase` (mirroring `isOcisServer()`); verify by reading that `ShareActivity.setupFileHeader()` can read `filesPrivateLinks` without querying Room directly. -- [x] 2.2 In `ShareActivity.setupFileHeader()`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` (`ShareActivity.kt:172`) with the oC10 rule: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. +- [x] 2.1 Expose the stored `OCCapability` to `ShareActivity` — add a thin read-only `getStoredCapabilities()` getter on `CapabilityViewModel` backed by `GetStoredCapabilitiesUseCase` (replacing the former `isOcisServer()` accessor); verify by reading that `ShareActivity.onCreate()` reads the capabilities once and passes them to `setupFileHeader()`, which gets `filesPrivateLinks` without querying Room directly. +- [x] 2.2 In `ShareActivity.setupFileHeader(binding, capabilities)`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule, reading `filesPrivateLinks` from the `capabilities` parameter passed in from `onCreate()`: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. - [x] 2.3 When the button is shown, wire `binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) }` (reference `SpaceMembersActivity.kt:74`); verify manually on an oCIS account that tapping the permalink button in the Share header copies/sends the item's private link. - [x] 2.4 Manual check — hidden states: on oCIS, open the Share header for an item with no private link and for an account where `filesPrivateLinks` is `FALSE`; verify the permalink button is not shown in either case. diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt index 161e3cfd429..884066fb0b0 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt @@ -75,10 +75,6 @@ class CapabilityViewModel( ) ) - fun isOcisServer(): Boolean = runBlocking(coroutineDispatcherProvider.io) { - getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))?.isSpacesAllowed() == true - } - fun getStoredCapabilities(): OCCapability? = runBlocking(coroutineDispatcherProvider.io) { getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index 5cacb36cd1e..8e914124af6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt @@ -41,6 +41,7 @@ import com.owncloud.android.R import com.owncloud.android.databinding.MembersActivityBinding import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType +import com.owncloud.android.domain.capabilities.model.OCCapability import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.sharing.shares.model.MemberPermission @@ -82,8 +83,9 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (capabilityViewModel.isOcisServer()) { - setupOcisLayout(savedInstanceState) + val capabilities = capabilityViewModel.getStoredCapabilities() + if (capabilities?.isSpacesAllowed() == true) { + setupOcisLayout(savedInstanceState, capabilities) } else { setupLegacyLayout(savedInstanceState) } @@ -93,11 +95,11 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme observeShareDeletion() } - private fun setupOcisLayout(savedInstanceState: Bundle?) { + private fun setupOcisLayout(savedInstanceState: Bundle?, capabilities: OCCapability?) { val binding = MembersActivityBinding.inflate(layoutInflater) setContentView(binding.root) setupToolbar(binding.root) - setupFileHeader(binding) + setupFileHeader(binding, capabilities) if (savedInstanceState == null && file != null && account != null) { supportFragmentManager.transaction { replace(R.id.members_fragment_container, GraphShareFragment.newInstance(file, account!!.name), TAG_GRAPH_SHARE_FRAGMENT) @@ -155,7 +157,7 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme supportActionBar?.setHomeActionContentDescription(R.string.common_back) } - private fun setupFileHeader(binding: MembersActivityBinding) { + private fun setupFileHeader(binding: MembersActivityBinding, capabilities: OCCapability?) { binding.itemIcon.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) if (file.isImage) { val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId.toString()) @@ -170,7 +172,7 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme binding.itemSize.text = DisplayUtils.bytesToHumanReadable(file.length, this, true) } - val privateLinkDisabled = capabilityViewModel.getStoredCapabilities()?.filesPrivateLinks == CapabilityBooleanType.FALSE + val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { binding.permanentLinkButton.isVisible = false } else { From 94b0ff506a6836819be0d6912d1426fcfef2351e Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 13:23:10 +0200 Subject: [PATCH 5/9] feat: show an info toast on long-pressing the permalink button Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/design.md | 20 ++++++++++++------- .../changes/permalink-sharing/proposal.md | 2 +- .../specs/files-sharing/permalink/spec.md | 6 +++++- openspec/changes/permalink-sharing/tasks.md | 2 ++ .../presentation/sharing/ShareActivity.kt | 17 +++++++++++----- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/openspec/changes/permalink-sharing/design.md b/openspec/changes/permalink-sharing/design.md index fc26d5fb7e6..af801ab126f 100644 --- a/openspec/changes/permalink-sharing/design.md +++ b/openspec/changes/permalink-sharing/design.md @@ -44,20 +44,26 @@ if (shareReachable) optionsToShow.add(FileMenuOption.SHARE) - **Alternative rejected**: removing the oC10-only inputs and reducing the gate to `!onlyAvailableOfflineFiles && isSingleSelection(files)` for everyone. Rejected because it changes oC10 behavior (an item "shared with me" with resharing disabled would newly show `Share`), which is explicitly out of scope. ### D2: Implement the oCIS permalink button in `ShareActivity.setupFileHeader()` -Replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule, then wire the click to the existing override: +Replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule inside a single `binding.permanentLinkButton.apply { … }` block, wiring the click to the existing override and a long press to the info toast: ```kotlin val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE -if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { - binding.permanentLinkButton.isVisible = false -} else { - binding.permanentLinkButton.isVisible = true - binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) } +binding.permanentLinkButton.apply { + if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { + isVisible = false + } else { + isVisible = true + setOnClickListener { copyOrSendPrivateLink(file) } + setOnLongClickListener { + Toast.makeText(this@ShareActivity, R.string.private_link_info, Toast.LENGTH_LONG).show() + true + } + } } ``` - **Why here**: `setupFileHeader()` already runs only on the oCIS layout branch, so no extra oCIS guard is required; the button and the `copyOrSendPrivateLink` plumbing already exist. -- **Why mirror oC10's rule** instead of inventing one: keeps permalink visibility consistent across both screens (hidden when no link or `filesPrivateLinks == FALSE`). +- **Why mirror oC10's rule** instead of inventing one: keeps permalink behavior consistent across both screens — hidden when no link or `filesPrivateLinks == FALSE`, click copies/sends the private link, and long press shows the `R.string.private_link_info` toast, exactly as `OcsShareFileFragment.showOrHidePrivateLink()` does. - **Capability access**: a thin read-only `getStoredCapabilities()` getter (backed by `GetStoredCapabilitiesUseCase`) was added to `CapabilityViewModel`, replacing the previous `isOcisServer()` boolean accessor. `onCreate()` reads the stored `OCCapability` once through it, branches on `isSpacesAllowed()`, and passes the capabilities into `setupFileHeader(binding, capabilities)`; `filesPrivateLinks` is read from that parameter rather than re-querying Room or the view model inside the method. - **Reference**: `SpaceMembersActivity.kt:74` wires the same button via `setOnClickListener`. diff --git a/openspec/changes/permalink-sharing/proposal.md b/openspec/changes/permalink-sharing/proposal.md index e3f8e1c65d7..5b375203959 100644 --- a/openspec/changes/permalink-sharing/proposal.md +++ b/openspec/changes/permalink-sharing/proposal.md @@ -6,7 +6,7 @@ On oCIS the permalink can become unreachable. The `Share` screen is the only ent - On oCIS accounts (identified by the `spaces` capability, `OCCapability.isSpacesAllowed()`), the `Share` menu option becomes reachable for any single selected item, independent of the `resharing`/`sharing` capabilities, the `R` permission, and the `share_via_link_feature`/`share_with_users_feature` setup flags. It stays hidden in the available-offline shortcut and for multiselection. - On oC10 the `Share` gate is left unchanged (no observable difference). -- On oCIS, the `permanent_link_button` in the Share header (`members_activity.xml`, driven by `ShareActivity.setupFileHeader()`) is made visible and wired to `copyOrSendPrivateLink(file)`, shown only when the item has a private link and the `filesPrivateLinks` capability is not `FALSE` — mirroring oC10's `showOrHidePrivateLink()` and the `SpaceMembersActivity` reference. +- On oCIS, the `permanent_link_button` in the Share header (`members_activity.xml`, driven by `ShareActivity.setupFileHeader()`) is made visible and wired to `copyOrSendPrivateLink(file)` on click and to an informational toast (`R.string.private_link_info`) on long press, shown only when the item has a private link and the `filesPrivateLinks` capability is not `FALSE` — mirroring oC10's `showOrHidePrivateLink()` and the `SpaceMembersActivity` reference. - The capability/feature-flag gating of the "share with users" and "share by link" sections *inside* the Share screen is untouched on both server types. ## Capabilities diff --git a/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md b/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md index eb49e7a8506..4a9f99804dc 100644 --- a/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md +++ b/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md @@ -48,13 +48,17 @@ On an oC10 account, the conditions under which the `Share` menu option is offere ### Requirement: Permalink button shown and functional in the oCIS Share header -On an oCIS account, the Share header SHALL display the permalink (private link) button when the selected item has a non-empty private link and the `filesPrivateLinks` capability is not `FALSE`. When the button is displayed and the user activates it, the system SHALL copy or send that item's private link, reusing the existing private-link copy/send action. +On an oCIS account, the Share header SHALL display the permalink (private link) button when the selected item has a non-empty private link and the `filesPrivateLinks` capability is not `FALSE`. When the button is displayed and the user activates it, the system SHALL copy or send that item's private link, reusing the existing private-link copy/send action. When the user long-presses the button, the system SHALL show an informational message explaining what a private link is. #### Scenario: Item with a private link and private links not disabled - **WHEN** the oCIS Share header is shown for an item whose private link is non-empty and the `filesPrivateLinks` capability is not `FALSE` - **THEN** the permalink button is visible - **AND** activating it copies or sends that item's private link +#### Scenario: Long-pressing the permalink button +- **WHEN** the oCIS Share header is shown for an item whose private link is non-empty and the user long-presses the permalink button +- **THEN** an informational message explaining the private link is shown + #### Scenario: Private links capability disabled - **WHEN** the oCIS Share header is shown and the `filesPrivateLinks` capability is `FALSE` - **THEN** the permalink button is hidden diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index 6bc0911a228..90d21869892 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -11,6 +11,7 @@ - [x] 2.2 In `ShareActivity.setupFileHeader(binding, capabilities)`, replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule, reading `filesPrivateLinks` from the `capabilities` parameter passed in from `onCreate()`: hide when `file.privateLink.isNullOrEmpty()` or `filesPrivateLinks == CapabilityBooleanType.FALSE`, otherwise show the button; verify by reading that the visibility logic matches `OcsShareFileFragment.showOrHidePrivateLink()`. - [x] 2.3 When the button is shown, wire `binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) }` (reference `SpaceMembersActivity.kt:74`); verify manually on an oCIS account that tapping the permalink button in the Share header copies/sends the item's private link. - [x] 2.4 Manual check — hidden states: on oCIS, open the Share header for an item with no private link and for an account where `filesPrivateLinks` is `FALSE`; verify the permalink button is not shown in either case. +- [x] 2.5 When the button is shown, also wire `binding.permanentLinkButton.setOnLongClickListener { ... }` to show a toast with `R.string.private_link_info` and return `true`, mirroring oC10's `OcsShareFileFragment.showOrHidePrivateLink()`; verify manually on an oCIS account that long-pressing the permalink button shows the informational message. ## 3. Release notes @@ -41,6 +42,7 @@ | **Permalink button (oCIS)** | | | | | | Button shown | 1. On an oCIS account where `filesPrivateLinks` is not `FALSE`
2. Open the Share screen for an item that has a private link | The permalink (permanent link) button is shown in the Share header | | | | Copy/send permalink | 1. Open the Share header for an item with a private link
2. Tap the permalink button | The item's private link is copied/sent (same behavior as oC10 `copyOrSendPrivateLink`) | | | +| Long-press permalink | 1. Open the Share header for an item with a private link
2. Long-press the permalink button | An informational toast explaining the private link is shown (same behavior as oC10) | | | | Button hidden — no private link | 1. Open the Share header for an item with no private link | The permalink button is not shown | | | | Button hidden — capability FALSE | 1. Use an account where `filesPrivateLinks` is `FALSE`
2. Open the Share header for any item | The permalink button is not shown | | | | **Regression (oC10)** | | | | | diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index 8e914124af6..06791ae1973 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt @@ -35,6 +35,7 @@ import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View +import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.transaction import com.owncloud.android.R @@ -173,11 +174,17 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme } val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE - if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { - binding.permanentLinkButton.isVisible = false - } else { - binding.permanentLinkButton.isVisible = true - binding.permanentLinkButton.setOnClickListener { copyOrSendPrivateLink(file) } + binding.permanentLinkButton.apply { + if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { + isVisible = false + } else { + isVisible = true + setOnClickListener { copyOrSendPrivateLink(file) } + setOnLongClickListener { + Toast.makeText(this@ShareActivity, R.string.private_link_info, Toast.LENGTH_LONG).show() + true + } + } } } From 14e01924e6fdda0617daaad2d7d1aa02f6ff3aac Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 13:43:38 +0200 Subject: [PATCH 6/9] feat: add release note Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/tasks.md | 2 +- .../presentation/releasenotes/ReleaseNotesViewModel.kt | 5 +++++ owncloudApp/src/main/res/values/strings.xml | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index 90d21869892..178084fcd31 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -15,7 +15,7 @@ ## 3. Release notes -- [ ] 3.1 Draft the user-facing release notes entry summarising the oCIS Share/permalink change for end users. +- [x] 3.1 Draft the user-facing release notes entry summarising the oCIS Share/permalink change for end users. ## 4. Changelog diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index 4869ed3fb9c..3bf1401b166 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -48,6 +48,11 @@ class ReleaseNotesViewModel( subtitle = R.string.release_notes_4_9_0_sharing_ng_subtitle, type = ReleaseNoteType.ENHANCEMENT ), + ReleaseNote( + title = R.string.release_notes_5_0_0_permalink_title, + subtitle = R.string.release_notes_5_0_0_permalink_subtitle, + type = ReleaseNoteType.ENHANCEMENT + ), ReleaseNote( title = R.string.release_notes_bugfixes_title, subtitle = R.string.release_notes_bugfixes_subtitle, diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 3d0e5e94d92..0c013dc9649 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -640,6 +640,8 @@ Some bugs were fixed to improve experience in the app New sharing implementation for Infinite Scale New interface and operations have been added to share files and folders on Infinite Scale accounts + File and folder permalinks on Infinite Scale + Infinite Scale users can now get a permanent link for a file or folder and share it with others Open in %1$s (web) From e2a017d78448760e2e43d2f68d2e1dd3c3335990 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Mon, 19 Oct 2026 14:00:48 +0200 Subject: [PATCH 7/9] chore: add calens file Signed-off-by: Jorge Aguado Recio --- changelog/unreleased/5022 | 8 ++++++++ openspec/changes/permalink-sharing/proposal.md | 2 +- openspec/changes/permalink-sharing/tasks.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/5022 diff --git a/changelog/unreleased/5022 b/changelog/unreleased/5022 new file mode 100644 index 00000000000..e519baa2ae9 --- /dev/null +++ b/changelog/unreleased/5022 @@ -0,0 +1,8 @@ +Enhancement: Permalink for files and folders on oCIS servers + +The share screen has been made reachable for any single file or folder on Infinite Scale accounts, +regardless of capabilities and permissions, and a button to copy or send the item's permalink +has been added in its header. + +https://github.com/owncloud/android/issues/4939 +https://github.com/owncloud/android/pull/5022 diff --git a/openspec/changes/permalink-sharing/proposal.md b/openspec/changes/permalink-sharing/proposal.md index 5b375203959..4cb2902ebce 100644 --- a/openspec/changes/permalink-sharing/proposal.md +++ b/openspec/changes/permalink-sharing/proposal.md @@ -25,5 +25,5 @@ On oCIS the permalink can become unreachable. The `Share` screen is the only ent - No unit tests are added: the change touches only the presentation layer (owncloudApp), where unit tests are not established per project convention. The oCIS reachability and permalink behavior are covered by the manual QA cases in `tasks.md`. - **owncloudDomain / owncloudData / owncloudComLibrary**: no changes. `OCFile.privateLink` is already populated from the `oc:privatelink` PROPFIND property and `OCCapability.isSpacesAllowed()` already exists. - **Release notes**: user-facing entry summarising the oCIS Share/permalink change. -- **Changelog**: new `Bugfix` entry under `changelog/unreleased/`. +- **Changelog**: new `Enhancement` entry under `changelog/unreleased/`. - No new dependencies. \ No newline at end of file diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/permalink-sharing/tasks.md index 178084fcd31..a74c4e1417e 100644 --- a/openspec/changes/permalink-sharing/tasks.md +++ b/openspec/changes/permalink-sharing/tasks.md @@ -19,7 +19,7 @@ ## 4. Changelog -- [ ] 4.1 Add `changelog/unreleased/` with a `Bugfix` entry (title under 80 chars, present-perfect-passive body) linking `https://github.com/owncloud/android/issues/4939` and the PR URL; verify the file matches the CLAUDE.md changelog format. +- [x] 4.1 Add `changelog/unreleased/` with an `Enhancement` entry (title under 80 chars, present-perfect-passive body) linking `https://github.com/owncloud/android/issues/4939` and the PR URL; verify the file matches the CLAUDE.md changelog format. ## 5. Final verification From 4e3b6c9677087cf01201a683f07180430dbc7498 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Tue, 20 Oct 2026 14:27:33 +0200 Subject: [PATCH 8/9] refactor: remove nullability from capabilities Signed-off-by: Jorge Aguado Recio --- openspec/changes/permalink-sharing/design.md | 4 ++-- .../owncloud/android/presentation/sharing/ShareActivity.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openspec/changes/permalink-sharing/design.md b/openspec/changes/permalink-sharing/design.md index af801ab126f..0db802c52ae 100644 --- a/openspec/changes/permalink-sharing/design.md +++ b/openspec/changes/permalink-sharing/design.md @@ -47,7 +47,7 @@ if (shareReachable) optionsToShow.add(FileMenuOption.SHARE) Replace the hard-coded `binding.permanentLinkButton.isVisible = false` with the oC10 rule inside a single `binding.permanentLinkButton.apply { … }` block, wiring the click to the existing override and a long press to the info toast: ```kotlin -val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE +val privateLinkDisabled = capabilities.filesPrivateLinks == CapabilityBooleanType.FALSE binding.permanentLinkButton.apply { if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { isVisible = false @@ -64,7 +64,7 @@ binding.permanentLinkButton.apply { - **Why here**: `setupFileHeader()` already runs only on the oCIS layout branch, so no extra oCIS guard is required; the button and the `copyOrSendPrivateLink` plumbing already exist. - **Why mirror oC10's rule** instead of inventing one: keeps permalink behavior consistent across both screens — hidden when no link or `filesPrivateLinks == FALSE`, click copies/sends the private link, and long press shows the `R.string.private_link_info` toast, exactly as `OcsShareFileFragment.showOrHidePrivateLink()` does. -- **Capability access**: a thin read-only `getStoredCapabilities()` getter (backed by `GetStoredCapabilitiesUseCase`) was added to `CapabilityViewModel`, replacing the previous `isOcisServer()` boolean accessor. `onCreate()` reads the stored `OCCapability` once through it, branches on `isSpacesAllowed()`, and passes the capabilities into `setupFileHeader(binding, capabilities)`; `filesPrivateLinks` is read from that parameter rather than re-querying Room or the view model inside the method. +- **Capability access**: a thin read-only `getStoredCapabilities()` getter (backed by `GetStoredCapabilitiesUseCase`) was added to `CapabilityViewModel`, replacing the previous `isOcisServer()` boolean accessor. `onCreate()` reads the stored `OCCapability` once through it, branches on `capabilities?.isSpacesAllowed() == true`, and passes the capabilities into `setupFileHeader(binding, capabilities)`; `filesPrivateLinks` is read from that parameter rather than re-querying Room or the view model inside the method. Because the oCIS branch is only reached when `capabilities` is non-null (Kotlin smart-casts it inside the `if`), both `setupOcisLayout` and `setupFileHeader` take a non-null `OCCapability` and read `capabilities.filesPrivateLinks` directly — no `?.` needed. - **Reference**: `SpaceMembersActivity.kt:74` wires the same button via `setOnClickListener`. ## Risks / Trade-offs diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index 06791ae1973..df34d5153a9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt @@ -96,7 +96,7 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme observeShareDeletion() } - private fun setupOcisLayout(savedInstanceState: Bundle?, capabilities: OCCapability?) { + private fun setupOcisLayout(savedInstanceState: Bundle?, capabilities: OCCapability) { val binding = MembersActivityBinding.inflate(layoutInflater) setContentView(binding.root) setupToolbar(binding.root) @@ -158,7 +158,7 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme supportActionBar?.setHomeActionContentDescription(R.string.common_back) } - private fun setupFileHeader(binding: MembersActivityBinding, capabilities: OCCapability?) { + private fun setupFileHeader(binding: MembersActivityBinding, capabilities: OCCapability) { binding.itemIcon.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) if (file.isImage) { val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId.toString()) @@ -173,7 +173,7 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme binding.itemSize.text = DisplayUtils.bytesToHumanReadable(file.length, this, true) } - val privateLinkDisabled = capabilities?.filesPrivateLinks == CapabilityBooleanType.FALSE + val privateLinkDisabled = capabilities.filesPrivateLinks == CapabilityBooleanType.FALSE binding.permanentLinkButton.apply { if (file.privateLink.isNullOrEmpty() || privateLinkDisabled) { isVisible = false From 20588dc5cae966de06ff6ac95a0474df07ed0680 Mon Sep 17 00:00:00 2001 From: Jorge Aguado Recio Date: Wed, 21 Oct 2026 11:51:42 +0200 Subject: [PATCH 9/9] chore: archive permalink-sharing change and sync specs Signed-off-by: Jorge Aguado Recio --- .../.openspec.yaml | 0 .../2026-10-21-permalink-sharing}/design.md | 0 .../2026-10-21-permalink-sharing}/proposal.md | 0 .../specs/files-sharing/permalink/spec.md | 0 .../2026-10-21-permalink-sharing}/tasks.md | 0 .../specs/files-sharing/permalink/spec.md | 76 +++++++++++++++++++ 6 files changed, 76 insertions(+) rename openspec/changes/{permalink-sharing => archive/2026-10-21-permalink-sharing}/.openspec.yaml (100%) rename openspec/changes/{permalink-sharing => archive/2026-10-21-permalink-sharing}/design.md (100%) rename openspec/changes/{permalink-sharing => archive/2026-10-21-permalink-sharing}/proposal.md (100%) rename openspec/changes/{permalink-sharing => archive/2026-10-21-permalink-sharing}/specs/files-sharing/permalink/spec.md (100%) rename openspec/changes/{permalink-sharing => archive/2026-10-21-permalink-sharing}/tasks.md (100%) create mode 100644 openspec/specs/files-sharing/permalink/spec.md diff --git a/openspec/changes/permalink-sharing/.openspec.yaml b/openspec/changes/archive/2026-10-21-permalink-sharing/.openspec.yaml similarity index 100% rename from openspec/changes/permalink-sharing/.openspec.yaml rename to openspec/changes/archive/2026-10-21-permalink-sharing/.openspec.yaml diff --git a/openspec/changes/permalink-sharing/design.md b/openspec/changes/archive/2026-10-21-permalink-sharing/design.md similarity index 100% rename from openspec/changes/permalink-sharing/design.md rename to openspec/changes/archive/2026-10-21-permalink-sharing/design.md diff --git a/openspec/changes/permalink-sharing/proposal.md b/openspec/changes/archive/2026-10-21-permalink-sharing/proposal.md similarity index 100% rename from openspec/changes/permalink-sharing/proposal.md rename to openspec/changes/archive/2026-10-21-permalink-sharing/proposal.md diff --git a/openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md b/openspec/changes/archive/2026-10-21-permalink-sharing/specs/files-sharing/permalink/spec.md similarity index 100% rename from openspec/changes/permalink-sharing/specs/files-sharing/permalink/spec.md rename to openspec/changes/archive/2026-10-21-permalink-sharing/specs/files-sharing/permalink/spec.md diff --git a/openspec/changes/permalink-sharing/tasks.md b/openspec/changes/archive/2026-10-21-permalink-sharing/tasks.md similarity index 100% rename from openspec/changes/permalink-sharing/tasks.md rename to openspec/changes/archive/2026-10-21-permalink-sharing/tasks.md diff --git a/openspec/specs/files-sharing/permalink/spec.md b/openspec/specs/files-sharing/permalink/spec.md new file mode 100644 index 00000000000..e726ff57fcd --- /dev/null +++ b/openspec/specs/files-sharing/permalink/spec.md @@ -0,0 +1,76 @@ +## Purpose + +Defines when the `Share` screen is reachable on oCIS and how the permalink (private link) button in the oCIS Share header is shown and acted upon, so that a user can always reach and copy an item's permalink while oC10 behavior stays unchanged. + +## Requirements + +### Requirement: Share screen reachable for a single item on oCIS + +On an oCIS account, the `Share` menu option SHALL be offered for a single selected item regardless of the `resharing`/`sharing` capability values, the item's `R` (reshare) permission, and the `share_via_link_feature`/`share_with_users_feature` setup flags. Reachability of the `Share` screen SHALL NOT depend on any of those values on oCIS. + +An account is treated as oCIS when its stored capabilities report the `spaces` capability as enabled; otherwise the account is treated as oC10. + +#### Scenario: Single item in an oCIS personal space with resharing disabled +- **WHEN** an oCIS account has a single item selected in its personal space and the `resharing` capability is `false` +- **THEN** the `Share` menu option is offered + +#### Scenario: Single item in an oCIS custom space with no reshare permission +- **WHEN** an oCIS account has a single item selected in a custom space, the item's permissions carry no `R` flag, and the `resharing` capability is `false` +- **THEN** the `Share` menu option is offered + +#### Scenario: oCIS with both share feature flags disabled +- **WHEN** an oCIS account has a single item selected and both `share_via_link_feature` and `share_with_users_feature` are `false` +- **THEN** the `Share` menu option is offered + +### Requirement: Share screen stays restricted in available-offline and multiselection on oCIS + +On an oCIS account, the `Share` menu option SHALL NOT be offered in the available-offline shortcut, and SHALL NOT be offered when more than one item is selected. + +#### Scenario: Available-offline shortcut on oCIS +- **WHEN** an oCIS account is in the available-offline shortcut +- **THEN** the `Share` menu option is not offered + +#### Scenario: Multiselection on oCIS +- **WHEN** an oCIS account has more than one item selected +- **THEN** the `Share` menu option is not offered + +### Requirement: oC10 Share reachability unchanged + +On an oC10 account, the conditions under which the `Share` menu option is offered SHALL remain exactly as they were before this change: it is offered only when the selection is not the available-offline shortcut, at least one of the share-via-link / share-with-users features is enabled, resharing is allowed for the selection, and the single selected item carries the reshare permission. + +#### Scenario: oC10 gate unchanged for an item shared with the user +- **WHEN** an oC10 account has a single item that is shared with the user, the `resharing` capability is `false`, and the item carries no reshare permission +- **THEN** the `Share` menu option is not offered, exactly as before this change + +#### Scenario: oC10 gate unchanged when conditions are met +- **WHEN** an oC10 account has a single selected item that satisfies the pre-existing share-feature, resharing, and reshare-permission conditions outside the available-offline shortcut +- **THEN** the `Share` menu option is offered, exactly as before this change + +### Requirement: Permalink button shown and functional in the oCIS Share header + +On an oCIS account, the Share header SHALL display the permalink (private link) button when the selected item has a non-empty private link and the `filesPrivateLinks` capability is not `FALSE`. When the button is displayed and the user activates it, the system SHALL copy or send that item's private link, reusing the existing private-link copy/send action. When the user long-presses the button, the system SHALL show an informational message explaining what a private link is. + +#### Scenario: Item with a private link and private links not disabled +- **WHEN** the oCIS Share header is shown for an item whose private link is non-empty and the `filesPrivateLinks` capability is not `FALSE` +- **THEN** the permalink button is visible +- **AND** activating it copies or sends that item's private link + +#### Scenario: Long-pressing the permalink button +- **WHEN** the oCIS Share header is shown for an item whose private link is non-empty and the user long-presses the permalink button +- **THEN** an informational message explaining the private link is shown + +#### Scenario: Private links capability disabled +- **WHEN** the oCIS Share header is shown and the `filesPrivateLinks` capability is `FALSE` +- **THEN** the permalink button is hidden + +#### Scenario: Item without a private link +- **WHEN** the oCIS Share header is shown for an item whose private link is null or empty +- **THEN** the permalink button is hidden + +### Requirement: Share screen section gating unchanged + +The visibility of the "share with users" and "share by link" sections inside the Share screen SHALL continue to be governed by their existing capabilities and build-time feature flags, on both oCIS and oC10. This change SHALL NOT alter that section-level gating. + +#### Scenario: Sections still governed by their own rules +- **WHEN** the Share screen is opened on either server type +- **THEN** the "share with users" and "share by link" sections are shown or hidden according to their existing capabilities and feature flags, unchanged by this change \ No newline at end of file