Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/5015
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: List existing public links on oCIS servers

The existing public links of a file or folder have been listed, showing each link's name,
permission and expiration date, with a chain icon to share the link's URL.

https://github.com/owncloud/android/issues/4956
https://github.com/owncloud/android/pull/5015
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-09
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Context

The Sharing NG flow (`Graph*`-prefixed classes under `owncloudApp/.../presentation/sharing/`) already fetches an item's permissions from the Graph API and parses them end-to-end. The whole data pipeline exists:

- Networking (`owncloudComLibrary`): `GetRemoteGraphSharesForFileOperation(spaceId, itemId)` calls `graph/v1beta1/drives/<spaceId>/items/<itemId>/permissions` and parses the response with Moshi into `PermissionsResponse` (`spaces/responses/SpacesResponse.kt`). `LinkInfoResponse` already exposes `@libre.graph.displayName`, `type`, `webUrl`.
- Data (`owncloudData`): `OCRemoteShareDataSource.getGraphShares(...)` maps `PermissionsResponse.toModel()`, already **splitting** the `value` array — entries with `grantedToV2 == null` become `OCLink`s (parsing displayName/type/webUrl/hasPassword/expiration) and populate `OCPermissions.links`.
- Domain (`owncloudDomain`): `GetGraphSharesAsyncUseCase(accountName, spaceId, itemId)` returns `OCPermissions`, whose model (`sharing/shares/model/OCPermissions.kt`) already carries `links: List<OCLink>`. `OCLink` (`links/model/OCLink.kt`) and `OCLinkType` (`view/edit/upload/createOnly/internal`) already exist.

The sharing panel (`GraphShareFragment` + `GraphShareViewModel`) inflates `members_fragment.xml`, the **same layout** used by the spaces members screen (`SpaceMembersFragment`, issue #4752). That layout already contains the full public-links section (`public_links_section`, `public_links_title`, `public_links_warning`, `public_links_recycler_view`, `no_public_links_message`). The spaces screen renders links into it via `SpaceLinksAdapter` (renamed to `PublicLinksAdapter` under a shared `presentation/links` package by this change — see Decision 2) (binds `List<OCLink>`, inflates `public_link_item.xml`, maps type via `OCLinkType.toStringResId()`).

What is missing is purely presentation wiring:
- `GraphShareViewModel`/`GraphShareFragment` read only `OCPermissions.roles` and `.members`; they ignore `.links`.
- The public-links section views that already exist in `GraphShareFragment`'s layout are never populated.
- The warning in `members_fragment.xml` hardcodes `share_warning_about_forwarding_space_public_links` ("...access to the space").

Relevant strings in `owncloudApp/src/main/res/values/strings.xml`: `share_via_link_section_title` ("Public links", header), `share_warning_about_forwarding_space_public_links` ("...the space.", current), `share_warning_about_forwarding_public_links` ("...the file/folder.", generic), `share_no_public_links` ("No public links created yet.").

Affected flavors: all three (`original` / `mdm` / `qa`) — this is shared presentation code.

## Goals / Non-Goals

**Goals:**
- Render all existing public links of the selected file/folder in the sharing panel's public-links section (name, permission, expiration if present, copy-link icon).
- Reuse the existing data pipeline (`GetGraphSharesAsyncUseCase` → `OCPermissions.links`) with no changes below the presentation layer.
- Reuse the spaces links row UI (`public_link_item.xml` + `SpaceLinksAdapter`) as the layout reference.
- Replace the warning text with one that references the item type (file vs folder).

**Non-Goals:**
- Creating, editing, or deleting public links (hide/disable those affordances in this flow).
- Displaying `hasPassword` status (kept in `OCLink`, not shown).
- Any change to networking, data, or domain layers (the pipeline already provides `links`).
- Classic WebDAV/OCS shares (`PublicShareDialogFragment` and friends are untouched).

## Decisions

**1. Expose `links` from `GraphShareViewModel` and render in `GraphShareFragment`.**
The ViewModel already receives the full `OCPermissions` via `_shares`. The Fragment's `observeShares()` currently reads only `.roles`/`.members`. Extend it to also pass `OCPermissions.links` to a links adapter bound to `public_links_recycler_view`, and toggle `no_public_links_message` / section visibility the way `SpaceMembersFragment.showOrHideEmptyView` / `showSpaceLinks()` already do. No new UseCase or ViewModel flow is required — the data rides on the existing shares fetch (which is already triggered on panel open), satisfying the "fetch on open" requirement with no extra network call.

**2. Rename the links row UI to a shared `PublicLinks…` package and reuse it.**
The spaces links row UI (`SpaceLinksAdapter` + `SpaceLinksDiffUtil`, with nested `SpaceLinksViewHolder` / `SpaceLinksAdapterListener`, previously under `presentation/spaces/links`) is renamed to `PublicLinksAdapter` + `PublicLinksDiffUtil` (`PublicLinksViewHolder` / `PublicLinksAdapterListener`; the `setSpaceLinks`/`spaceLinks` members become `setPublicLinks`/`publicLinks`) and moved to a shared top-level package `presentation/links`, since it is now used by both the spaces members screen and the Sharing NG panel. `GraphShareFragment` reuses `PublicLinksAdapter` read-only: it enables only the share-link (chain icon) action and suppresses edit/remove, which are out of scope. `SpaceMembersFragment` is updated to the new package and names; its behavior is unchanged. (`SpaceLinksViewModel`, part of the spaces add-link flow, is not used here and is left untouched.)

**3. Permission label mapping lives in the adapter/row binding.**
The link name is rendered directly from `link["@libre.graph.displayName"]` with no fallback (the server guarantees a non-empty name). The `OCLinkType` → label mapping (`view`→"Can view", `edit`→"Can edit", `createOnly`→"Secret file drop") reuses the existing `OCLinkType.toStringResId()` extension. Verify its mappings match the required labels; if `createOnly` does not already map to "Secret file drop", add/adjust the string mapping (not the API enum).

**4. Expiration row shown only when present.**
`OCLink.expirationDateTime` is nullable; the row binding shows the expiration row only when it is non-null, mirroring the existing `public_link_item.xml` behavior.

**5. Item-type-aware warning text.**
The acceptance criteria require the warning to read "...the file" or "...the folder" matching the selected item's type. The current layout hardcodes the "space" string. Replace the `public_links_warning` text at runtime in `GraphShareFragment` (not in the shared layout XML, so `SpaceMembersFragment` keeps its "space" wording). Introduce two strings — "Anyone with the link has access to the file" and "Anyone with the link has access to the folder" — and select based on `OCFile.isFolder`. The existing generic `share_warning_about_forwarding_public_links` ("...the file/folder.") is a weaker fit than the type-specific wording the acceptance criteria call for, so prefer the two type-specific strings. New strings are added only to the base `values/strings.xml`; translations go to Transifex (no translation PRs).

**6. drive-id / item-id source unchanged.**
`GraphShareViewModel.getGraphShares()` already derives drive-id = `file.spaceId` and item-id = `file.remoteId` from the `OCFile`, emitting `IncompleteFileDataException` if either is null. No change needed.

**Koin modules:** No new bindings expected (reuses existing ViewModel/UseCase/Repository/DataSource graph). If a dedicated `GraphLinksAdapter` is introduced it is a plain UI class, not a Koin dependency.

**Room:** No schema changes — Graph links are fetched live and never persisted.

## Risks / Trade-offs

- **Shared layout coupling:** `members_fragment.xml` is shared by the spaces members screen and the sharing panel. Changing the warning text in XML would affect both; mitigated by setting the warning text programmatically in `GraphShareFragment` only.
- **Shared adapter rename reaches the spaces screen:** renaming `SpaceLinks…`→`PublicLinks…` and moving the package means `SpaceMembersFragment` (issue #4752) must be updated to the new names; mitigated by a pure rename/move with no behavior change, verified by the spaces-screen regression test.
- **Permission label parity:** If `OCLinkType.toStringResId()` does not already yield exactly "Can view" / "Can edit" / "Secret file drop", the mapping must be reconciled; verify against the spaces screen to avoid regressing its labels.
- **`createOnly`/upload semantics:** The Graph API `createOnly` type must map to "Secret file drop"; confirm the existing enum/string mapping distinguishes `createOnly` from `upload` so the label is correct.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Why

In the Sharing NG flow, the sharing panel for a file or folder does not show public links that already exist for that item. Users who created a link previously — from any client — have no way to see it, so they cannot tell which links are active. Additionally, the warning text under the `PUBLIC LINKS` header incorrectly refers to "the space" even when the shared item is a file or folder.

## What Changes

- The sharing panel fetches an item's permissions from the Graph API (`GET /graph/v1beta1/drives/<drive-id>/items/<item-id>/permissions`, the same endpoint already used for shares) and lists all existing public links for that item.
- Only entries in the response `value` array that contain a `link` object are treated as public links; share entries without a `link` object are excluded.
- Each listed link shows:
- its name — `link["@libre.graph.displayName"]`;
- its permission label — `link.type` mapped to `Can edit` (edit), `Can view` (view), `Secret file drop` (createOnly);
- its expiration date — only when `expirationDateTime` is present;
- a chain icon on the right that shares `link.webUrl` through the Android share sheet when tapped.
- The warning text under `PUBLIC LINKS` changes from "Anyone with the link has access to the space" to reference the shared item, reading "Anyone with the link has access to the file" or "...the folder" depending on the item type.
- `hasPassword` is carried in the data model (not displayed) for use by a future edit flow.

Out of scope: creating, editing, or deleting public links; displaying password-protection status; classic WebDAV shares.

## Capabilities

### New Capabilities
- `files-sharing/public-links`: Retrieving and displaying the existing public links of a file or folder in the Sharing NG panel, including each link's name, permission, expiration, and share-link action, plus the item-type-aware warning text.

### Modified Capabilities
<!-- None: no existing capability specs defined yet. -->

## Impact

- **owncloudApp** (presentation): sharing panel Fragment/ViewModel render the public-links list and the item-type-aware warning; `strings.xml` warning text updated/parameterized.
- **owncloudDomain** (business): UseCase + domain model for an item's public links; repository interface for retrieving them (if not already covered by the shares flow).
- **owncloudData** (data): repository/data-source mapping of the permissions response `value` array into public-link domain models.
- **owncloudComLibrary** (networking): Moshi models for the permissions response `link` object fields, if not already present.
- No new third-party dependencies; reuses the existing Graph API permissions integration and the spaces' links row UI (issue #4752), which is renamed to `PublicLinksAdapter`/`PublicLinksDiffUtil` and moved to a shared `presentation/links` package so both screens share it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
## Purpose

Defines how the Sharing NG panel retrieves and displays the public links that already exist for a file or folder, so a user can see which links are active, read each link's details, and copy a link's URL.

## ADDED Requirements

### Requirement: Fetch existing public links when opening the sharing panel

When the sharing panel is opened for a file or folder, the system SHALL request that item's permissions from the Graph API permissions endpoint and derive the item's public links from the response. The system SHALL treat only entries in the response `value` array that contain a `link` object as public links; entries without a `link` object SHALL be excluded.

#### Scenario: Permissions fetched on panel open
- **WHEN** a user opens the sharing panel for a file or folder
- **THEN** the system requests the item's permissions from the Graph API permissions endpoint for that item's drive and item identifiers

#### Scenario: Only entries with a link object are listed
- **WHEN** the permissions response contains a mix of entries, some with a `link` object and some without
- **THEN** only the entries that contain a `link` object are shown in the `PUBLIC LINKS` section
- **AND** entries without a `link` object are excluded from the public-links list

#### Scenario: No public links exist
- **WHEN** the permissions response contains no entry with a `link` object
- **THEN** the `PUBLIC LINKS` section shows no link entries

### Requirement: Display each public link's name

Each public link entry SHALL display the name taken from `link["@libre.graph.displayName"]`.

#### Scenario: Named link
- **WHEN** a public link has a `link["@libre.graph.displayName"]`
- **THEN** the entry displays that value as its name

### Requirement: Display each public link's permission

Each public link entry SHALL display a permission label derived from `link.type`: `edit` SHALL be shown as "Can edit", `view` SHALL be shown as "Can view", and `createOnly` SHALL be shown as "Secret file drop".

#### Scenario: Edit permission
- **WHEN** a public link has `link.type` equal to `edit`
- **THEN** the entry displays "Can edit" as its permission

#### Scenario: View permission
- **WHEN** a public link has `link.type` equal to `view`
- **THEN** the entry displays "Can view" as its permission

#### Scenario: Create-only permission
- **WHEN** a public link has `link.type` equal to `createOnly`
- **THEN** the entry displays "Secret file drop" as its permission

### Requirement: Display each public link's expiration date

Each public link entry SHALL display an expiration date only when the response provides `expirationDateTime` for that link. When `expirationDateTime` is absent, the entry SHALL NOT display an expiration date row.

#### Scenario: Link with expiration
- **WHEN** a public link has an `expirationDateTime` value
- **THEN** the entry displays that expiration date

#### Scenario: Link without expiration
- **WHEN** a public link has no `expirationDateTime` value
- **THEN** the entry displays no expiration date row

### Requirement: Share a public link's URL

Each public link entry SHALL present a chain icon on the right side of the entry. When the user taps the chain icon, the system SHALL share that link's `link.webUrl` through the Android share sheet.

#### Scenario: Share link URL
- **WHEN** a user taps the chain icon on a public link entry
- **THEN** the system opens the Android share sheet offering that link's `link.webUrl`

### Requirement: Item-type-aware public-links warning text

The warning text under the `PUBLIC LINKS` header SHALL reference the type of the shared item. When the shared item is a file, the text SHALL read "Anyone with the link has access to the file". When the shared item is a folder, the text SHALL read "Anyone with the link has access to the folder".

#### Scenario: Warning for a file
- **WHEN** the shared item is a file
- **THEN** the warning text reads "Anyone with the link has access to the file"

#### Scenario: Warning for a folder
- **WHEN** the shared item is a folder
- **THEN** the warning text reads "Anyone with the link has access to the folder"
Loading
Loading