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/archive/2026-10-21-permalink-sharing/.openspec.yaml b/openspec/changes/archive/2026-10-21-permalink-sharing/.openspec.yaml new file mode 100644 index 00000000000..c1bc902799f --- /dev/null +++ b/openspec/changes/archive/2026-10-21-permalink-sharing/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-19 diff --git a/openspec/changes/archive/2026-10-21-permalink-sharing/design.md b/openspec/changes/archive/2026-10-21-permalink-sharing/design.md new file mode 100644 index 00000000000..0db802c52ae --- /dev/null +++ b/openspec/changes/archive/2026-10-21-permalink-sharing/design.md @@ -0,0 +1,78 @@ +## 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.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)`. + +## 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 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 +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 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 `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 + +- [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.~~ **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/archive/2026-10-21-permalink-sharing/proposal.md b/openspec/changes/archive/2026-10-21-permalink-sharing/proposal.md new file mode 100644 index 00000000000..4cb2902ebce --- /dev/null +++ b/openspec/changes/archive/2026-10-21-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)` 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 + +### 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 `Enhancement` entry under `changelog/unreleased/`. +- No new dependencies. \ No newline at end of file diff --git a/openspec/changes/archive/2026-10-21-permalink-sharing/specs/files-sharing/permalink/spec.md b/openspec/changes/archive/2026-10-21-permalink-sharing/specs/files-sharing/permalink/spec.md new file mode 100644 index 00000000000..4a9f99804dc --- /dev/null +++ b/openspec/changes/archive/2026-10-21-permalink-sharing/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. + +## 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. 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 diff --git a/openspec/changes/archive/2026-10-21-permalink-sharing/tasks.md b/openspec/changes/archive/2026-10-21-permalink-sharing/tasks.md new file mode 100644 index 00000000000..a74c4e1417e --- /dev/null +++ b/openspec/changes/archive/2026-10-21-permalink-sharing/tasks.md @@ -0,0 +1,88 @@ +## 1. oCIS Share reachability (owncloudApp — FilterFileMenuOptionsUseCase) + +- [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) + +- [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. +- [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 + +- [x] 3.1 Draft the user-facing release notes entry summarising the oCIS Share/permalink change for end users. + +## 4. Changelog + +- [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 + +- [ ] 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`) | | | +| 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)** | | | | | +| 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 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 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..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,8 +75,8 @@ class CapabilityViewModel( ) ) - fun isOcisServer(): Boolean = runBlocking(coroutineDispatcherProvider.io) { - getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))?.isSpacesAllowed() == true + fun getStoredCapabilities(): OCCapability? = runBlocking(coroutineDispatcherProvider.io) { + getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName)) } fun checkMultiPersonal(): Boolean = runBlocking(CoroutinesDispatcherProvider().io) { 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/java/com/owncloud/android/presentation/sharing/ShareActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareActivity.kt index 7df4fb58717..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 @@ -35,11 +35,14 @@ 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 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 @@ -81,8 +84,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) } @@ -92,11 +96,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) @@ -154,7 +158,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()) @@ -169,7 +173,19 @@ class ShareActivity : FileActivity(), OcsShareFragmentListener, GraphShareFragme binding.itemSize.text = DisplayUtils.bytesToHumanReadable(file.length, this, true) } - binding.permanentLinkButton.isVisible = false + val privateLinkDisabled = capabilities.filesPrivateLinks == CapabilityBooleanType.FALSE + 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 + } + } + } } /************************************************************************************************************** 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!) 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)