Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Load preview URL for download-disabled shares #2484

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion css/viewer-main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-DXSti9TM.chunk.css';
@import './main-BbBb0x7S.chunk.css';
66 changes: 66 additions & 0 deletions cypress/e2e/download-forbidden.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* SPDX-License: AGPL-3.0-or-later
* SPDX-: Nextcloud GmbH and Nextcloud contributors
*/

import type { User } from '@nextcloud/cypress'
import { ShareType } from '@nextcloud/sharing'

describe('Disable download button if forbidden', { testIsolation: true }, () => {
let sharee: User

before(() => {
cy.createRandomUser().then((user) => { sharee = user })
cy.createRandomUser().then((user) => {
// Upload test files
cy.createFolder(user, '/Photos')
cy.uploadFile(user, 'image1.jpg', 'image/jpeg', '/Photos/image1.jpg')

cy.login(user)
cy.createShare('/Photos',
{ shareWith: sharee.userId, shareType: ShareType.User, attributes: [{ scope: 'permissions', key: 'download', value: false }] },
)
cy.logout()
})
})

beforeEach(() => {
cy.login(sharee)
cy.visit('/apps/files')
cy.openFile('Photos')
})

it('See the shared folder and images in files list', () => {
cy.getFile('image1.jpg', { timeout: 10000 })
.should('contain', 'image1 .jpg')
})

it('See the image can be shown', () => {
cy.getFile('image1.jpg').should('be.visible')
cy.openFile('image1.jpg')
cy.get('body > .viewer').should('be.visible')

cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
})

it('See the title on the viewer header but not the Download nor the menu button', () => {
cy.getFile('image1.jpg').should('be.visible')
cy.openFile('image1.jpg')
cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg')

cy.get('[role="dialog"]')
.should('be.visible')
.find('button[aria-label="Actions"]')
.click()

cy.get('[role="menu"]:visible')
.find('button')
.should('have.length', 2)
.each(($el) => {
expect($el.text()).to.match(/(Full screen|Open sidebar)/i)
})
})
})
6 changes: 2 additions & 4 deletions cypress/e2e/sharing/download-share-disabled.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,14 @@ describe(`Download ${fileName} in viewer`, function() {
cy.get('body > .viewer').should('be.visible')
})

// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('Does not see a loading animation', function() {
it('Does not see a loading animation', function() {
cy.get('body > .viewer', { timeout: 10000 })
.should('be.visible')
.and('have.class', 'modal-mask')
.and('not.have.class', 'icon-loading')
})

// TODO: FIX DOWNLOAD DISABLED SHARES
it.skip('See the title on the viewer header but not the Download nor the menu button', function() {
it('See the title on the viewer header but not the Download nor the menu button', function() {
cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg')
cy.get('body a[download="image1.jpg"]').should('not.exist')
cy.get('body > .viewer .modal-header button.action-item__menutoggle').should('not.exist')
Expand Down
61 changes: 39 additions & 22 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
*
*/

import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path'
import axios from '@nextcloud/axios'
import { addCommands } from '@nextcloud/cypress'
import { Permission } from '@nextcloud/files'
import { ShareType } from '@nextcloud/sharing'
import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/command'
import { basename } from 'path'

addCommands()
addCompareSnapshotCommand()
Expand All @@ -34,7 +35,7 @@
/**
* cy.uploadedFile - uploads a file from the fixtures folder
*
* @param {User} user the owner of the file, e.g. admin

Check warning on line 38 in cypress/support/commands.ts

View workflow job for this annotation

GitHub Actions / NPM lint

The type 'User' is undefined
* @param {string} fixture the fixture file name, e.g. image1.jpg
* @param {string} mimeType e.g. image/png
* @param {string} [target] the target of the file relative to the user root
Expand Down Expand Up @@ -127,32 +128,48 @@
},
)

interface ShareOptions {
shareType: number
shareWith?: string
permissions: number
attributes?: { value: boolean, key: string, scope: string}[]
}

Cypress.Commands.add('createShare', (path: string, shareOptions?: ShareOptions) => {
return cy.request('/csrftoken').then(({ body }) => {
const requesttoken = body.token

return cy.request({
method: 'POST',
url: '../ocs/v2.php/apps/files_sharing/api/v1/shares?format=json',
headers: {
requesttoken,
},
body: {
path,
permissions: Permission.READ,
...shareOptions,
attributes: shareOptions?.attributes && JSON.stringify(shareOptions.attributes),
},
}).then(({ body }) => {
const shareToken = body.ocs?.data?.token
if (shareToken === undefined) {
throw new Error('Invalid OCS response')
}
cy.log('Share link created', shareToken)
return cy.wrap(shareToken)
})
})
})

/**
* Create a share link and return the share url
*
* @param {string} path the file/folder path
* @return {string} the share link url
*/
Cypress.Commands.add('createLinkShare', path => {
return cy.window().then(async window => {
try {
const request = await axios.post(`${Cypress.env('baseUrl')}/ocs/v2.php/apps/files_sharing/api/v1/shares`, {
path,
shareType: window.OC.Share.SHARE_TYPE_LINK,
}, {
headers: {
requesttoken: window.OC.requestToken,
},
})
if (!('ocs' in request.data) || !('token' in request.data.ocs.data && request.data.ocs.data.token.length > 0)) {
throw request
}
cy.log('Share link created', request.data.ocs.data.token)
return cy.wrap(request.data.ocs.data.token)
} catch (error) {
console.error(error)
}
}).should('have.length', 15)
return cy.createShare(path, { shareType: ShareType.Link })
})

Cypress.Commands.overwrite('compareSnapshot', (originalFn, subject, name, options) => {
Expand Down
2 changes: 1 addition & 1 deletion js/vendor.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ version: 3.3.1
license: GPL-3.0-or-later

@nextcloud/files
version: 3.9.0
version: 3.8.0
license: AGPL-3.0-or-later

@nextcloud/initial-state
Expand Down
92 changes: 46 additions & 46 deletions js/viewer-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer-main.mjs.map

Large diffs are not rendered by default.

125 changes: 70 additions & 55 deletions src/components/Images.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,63 +28,63 @@
:fileid="fileid"
@close="onClose" />

<template v-else-if="data !== null">
<img v-if="!livePhotoCanBePlayed"
ref="image"
:alt="alt"
<IconImageBroken v-if="!data" :size="64" />

<img v-else-if="!livePhotoCanBePlayed"
ref="image"
:alt="alt"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
@wheel.stop.prevent="updateZoom"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove">

<template v-else-if="livePhoto">
<video v-show="livePhotoCanBePlayed"
ref="video"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:src="data"
:style="imgStyle"
@error.capture.prevent.stop.once="onFail"
@load="updateImgSize"
:playsinline="true"
:poster="data"
:src="livePhotoSrc"
preload="metadata"
@canplaythrough="doneLoadingLivePhoto"
@loadedmetadata="updateImgSize"
@wheel.stop.prevent="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove">

<template v-if="livePhoto">
<video v-show="livePhotoCanBePlayed"
ref="video"
:class="{
dragging,
loaded,
zoomed: zoomRatio > 1
}"
:style="imgStyle"
:playsinline="true"
:poster="data"
:src="livePhotoSrc"
preload="metadata"
@canplaythrough="doneLoadingLivePhoto"
@loadedmetadata="updateImgSize"
@wheel.stop.prevent="updateZoom"
@error.capture.prevent.stop.once="onFail"
@dblclick.prevent="onDblclick"
@pointerdown.prevent="pointerDown"
@pointerup.prevent="pointerUp"
@pointermove.prevent="pointerMove"
@ended="stopLivePhoto" />
<button v-if="width !== 0"
class="live-photo_play_button"
:style="{left: `calc(50% - ${width/2}px)`}"
:disabled="!livePhotoCanBePlayed"
:aria-description="t('viewer', 'Play the live photo')"
@click="playLivePhoto"
@pointerenter="playLivePhoto"
@focus="playLivePhoto"
@pointerleave="stopLivePhoto"
@blur="stopLivePhoto">
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
<NcLoadingIcon v-else />
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
{{ t('viewer', 'LIVE') }}
</button>
</template>
@pointermove.prevent="pointerMove"
@ended="stopLivePhoto" />
<button v-if="width !== 0"
class="live-photo_play_button"
:style="{left: `calc(50% - ${width/2}px)`}"
:disabled="!livePhotoCanBePlayed"
:aria-description="t('viewer', 'Play the live photo')"
@click="playLivePhoto"
@pointerenter="playLivePhoto"
@focus="playLivePhoto"
@pointerleave="stopLivePhoto"
@blur="stopLivePhoto">
<PlayCircleOutline v-if="livePhotoCanBePlayed" />
<NcLoadingIcon v-else />
<!-- TRANSLATORS Label of the button used at the top left corner of live photos to play them -->
{{ t('viewer', 'LIVE') }}
</button>
</template>
</div>
</template>
Expand All @@ -93,6 +93,7 @@
import Vue from 'vue'
import AsyncComputed from 'vue-async-computed'
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue'
import IconImageBroken from 'vue-material-design-icons/ImageBroken.vue'

import axios from '@nextcloud/axios'
import { basename } from '@nextcloud/paths'
Expand All @@ -102,13 +103,15 @@ import { NcLoadingIcon } from '@nextcloud/vue'
import ImageEditor from './ImageEditor.vue'
import { findLivePhotoPeerFromFileId } from '../utils/livePhotoUtils'
import { getDavPath } from '../utils/fileUtils'
import { getPreviewIfAny } from '../utils/previewUtils'

Vue.use(AsyncComputed)

export default {
name: 'Images',

components: {
IconImageBroken,
ImageEditor,
PlayCircleOutline,
NcLoadingIcon,
Expand Down Expand Up @@ -192,23 +195,35 @@ export default {

// Load the raw gif instead of the static preview
if (this.mime === 'image/gif') {
// if the source failed fallback to the preview
if (this.fallback) {
return this.previewPath
}
return this.src
}

// If there is no preview and we have a direct source
// load it instead
if (this.source && !this.hasPreview && !this.previewUrl) {
return this.source
// First try the preview if any
if (!this.fallback && this.previewPath) {
return this.previewPath
}

// If loading the preview failed once, let's load the original file
if (this.fallback) {
return this.src
}
return this.src
},

return this.previewPath
async previewPath() {
return await getPreviewIfAny({
...this.$attrs,
fileid: this.fileid,
filename: this.filename,
previewUrl: this.previewUrl,
hasPreview: this.hasPreview,
davPath: this.davPath,
etag: this.$attrs.etag,
})
},
},

watch: {
active(val, old) {
// the item was hidden before and is now the current view
Expand Down
15 changes: 13 additions & 2 deletions src/mixins/Mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
*
*/
import debounce from 'debounce'
import PreviewUrl from '../mixins/PreviewUrl.js'
import parsePath from 'path-parse'
import { getDavPath } from '../utils/fileUtils.ts'

export default {
inheritAttrs: false,
mixins: [PreviewUrl],
props: {
// Is the current component shown
active: {
Expand Down Expand Up @@ -115,6 +114,18 @@ export default {
},

computed: {
/**
* Absolute dav remote path of the file
*
* @return {string}
*/
davPath() {
return getDavPath({
filename: this.filename,
basename: this.basename,
})
},

name() {
return parsePath(this.basename).name
},
Expand Down
Loading
Loading