Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/dav/lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
}

/**
* @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}

Check failure on line 23 in apps/dav/lib/Capabilities.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidReturnType

apps/dav/lib/Capabilities.php:23:13: InvalidReturnType: The declared return type 'array{dav: array{'absence-replacement'?: bool, 'absence-supported'?: bool, bulkupload?: string, chunking: string, public_shares_chunking: bool}}' for OCA\DAV\Capabilities::getCapabilities is incorrect, got 'array{dav: array{'absence-replacement'?: true, 'absence-supported'?: true, bulkupload?: '1.0', chunking: '1.0', public_shares_chunking: true, search_supports_creation_time: true, search_supports_upload_time: true}}' which is different due to additional array shape fields (search_supports_creation_time, search_supports_upload_time) (see https://psalm.dev/011)
*/
public function getCapabilities() {
$capabilities = [
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
]
];
if ($this->config->getSystemValueBool('bulkupload.enabled', true)) {
Expand All @@ -36,6 +38,6 @@
$capabilities['dav']['absence-supported'] = true;
$capabilities['dav']['absence-replacement'] = true;
}
return $capabilities;

Check failure on line 41 in apps/dav/lib/Capabilities.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidReturnStatement

apps/dav/lib/Capabilities.php:41:10: InvalidReturnStatement: The inferred type 'array{dav: array{'absence-replacement'?: true, 'absence-supported'?: true, bulkupload?: '1.0', chunking: '1.0', public_shares_chunking: true, search_supports_creation_time: true, search_supports_upload_time: true}}' does not match the declared return type 'array{dav: array{'absence-replacement'?: bool, 'absence-supported'?: bool, bulkupload?: string, chunking: string, public_shares_chunking: bool}}' for OCA\DAV\Capabilities::getCapabilities due to additional array shape fields (search_supports_creation_time, search_supports_upload_time) (see https://psalm.dev/128)
}
}
5 changes: 5 additions & 0 deletions apps/dav/lib/Files/FileSearchBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public function getPropertyDefinitionsForScope(string $href, ?string $path): arr
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}creation_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
Expand Down Expand Up @@ -299,6 +300,8 @@ private function getSearchResultProperty(SearchResult $result, SearchPropertyDef
return $node->getName();
case '{DAV:}getlastmodified':
return $node->getLastModified();
case '{http://nextcloud.org/ns}creation_time':
return $node->getNode()->getCreationTime();
case '{http://nextcloud.org/ns}upload_time':
return $node->getNode()->getUploadTime();
case FilesPlugin::SIZE_PROPERTYNAME:
Expand Down Expand Up @@ -461,6 +464,8 @@ private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
return 'mimetype';
case '{DAV:}getlastmodified':
return 'mtime';
case '{http://nextcloud.org/ns}creation_time':
return 'creation_time';
case '{http://nextcloud.org/ns}upload_time':
return 'upload_time';
case FilesPlugin::SIZE_PROPERTYNAME:
Expand Down
30 changes: 30 additions & 0 deletions apps/files/src/components/FileEntry/FileEntryPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<FavoriteIcon v-once />
</span>

<!-- Recently created icon -->
<span v-else-if="isRecentView && isRecentlyCreated" class="files-list__row-icon-recently-created">
<RecentlyCreatedIcon v-once />
</span>

<component
:is="fileOverlay"
v-if="fileOverlay"
Expand Down Expand Up @@ -71,6 +76,7 @@ import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import CollectivesIcon from './CollectivesIcon.vue'
import FavoriteIcon from './FavoriteIcon.vue'
import RecentlyCreatedIcon from './RecentlyCreatedIcon.vue'
import { usePreviewImage } from '../../composables/usePreviewImage.ts'
import logger from '../../logger.ts'
import { isLivePhoto } from '../../services/LivePhotos.ts'
Expand All @@ -91,6 +97,7 @@ export default defineComponent({
LinkIcon,
NetworkIcon,
TagIcon,
RecentlyCreatedIcon,
},

props: {
Expand Down Expand Up @@ -138,6 +145,29 @@ export default defineComponent({
return this.source.attributes.favorite === 1
},

isRecentlyCreated(): boolean {
if (this.source.attributes.upload_time) {
return false
}

const creationDate = this.source.attributes.creationdate
? new Date(this.source.attributes.creationdate)
: null

if (!creationDate) {
return false
}

const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)

return creationDate > oneDayAgo
},

isRecentView(): boolean {
return this.$route?.params?.view === 'recent'
},

userConfig(): UserConfig {
return this.userConfigStore.userConfig
},
Expand Down
78 changes: 78 additions & 0 deletions apps/files/src/components/FileEntry/RecentlyCreatedIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcIconSvgWrapper class="recently-created-marker-icon" :name="t('files', 'Recently created')" :svg="PlusSvg" />
</template>

<script lang="ts">
import PlusSvg from '@mdi/svg/svg/plus.svg?raw'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'

/**
* A recently created icon to be used for overlaying recently created entries like the file preview / icon
* It has a stroke around the icon to ensure enough contrast for accessibility.
*
* If the background has a hover state you might want to also apply it to the stroke like this:
* ```scss
* .parent:hover :deep(.favorite-marker-icon svg path) {
* stroke: var(--color-background-hover);
* }
* ```
*/
export default defineComponent({
name: 'RecentlyCreatedIcon',
components: {
NcIconSvgWrapper,
},

data() {
return {
PlusSvg,
}
},

async mounted() {
await this.$nextTick()
// MDI default viewBox is "0 0 24 24" but we add a stroke of 10px so we must adjust it
const el = this.$el.querySelector('svg')
el?.setAttribute?.('viewBox', '-4 -4 30 30')
},

methods: {
t,
},
})
</script>

<style lang="scss" scoped>
.recently-created-marker-icon {
color: var(--color-element-success);
// Override NcIconSvgWrapper defaults (clickable area)
min-width: unset !important;
min-height: unset !important;

:deep() {
svg {
// We added a stroke for a11y so we must increase the size to include the stroke
width: 20px !important;
height: 20px !important;

// Override NcIconSvgWrapper defaults of 20px
max-width: unset !important;
max-height: unset !important;

// Show a border around the icon for better contrast
path {
stroke: var(--color-main-background);
stroke-width: 8px;
stroke-linejoin: round;
paint-order: stroke;
}
}
}
}
</style>
10 changes: 6 additions & 4 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ export default defineComponent({
& > span {
justify-content: flex-start;

&:not(.files-list__row-icon-favorite) svg {
&:not(.files-list__row-icon-favorite):not(.files-list__row-icon-recently-created) svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
Expand Down Expand Up @@ -791,7 +791,8 @@ export default defineComponent({
}
}

&-favorite {
&-favorite,
&-recently-created {
position: absolute;
top: 0px;
inset-inline-end: -10px;
Expand Down Expand Up @@ -993,8 +994,9 @@ export default defineComponent({
}
}

// Star icon in the top right
.files-list__row-icon-favorite {
// Icon in the top right
.files-list__row-icon-favorite,
.files-list__row-icon-recently-created {
position: absolute;
top: 0;
inset-inline-end: 0;
Expand Down
4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lib/private/Files/Cache/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = nul
* @throws \RuntimeException
*/
public function put($file, array $data) {
// do not carry over creation_time to file versions, as each new version would otherwise
// create a filecache_extended entry with the same creation_time as the original file
if (str_starts_with($file, 'files_versions/')) {
unset($data['creation_time']);
}

if (($id = $this->getId($file)) > -1) {
$this->update($id, $data);
return $id;
Expand Down
2 changes: 1 addition & 1 deletion lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array

$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());

$joinExtendedCache = in_array('upload_time', $requestedFields);
$joinExtendedCache = in_array('creation_time', $requestedFields) || in_array('upload_time', $requestedFields);

$query = $builder->selectFileCache('file', $joinExtendedCache);

Expand Down
2 changes: 2 additions & 0 deletions lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class SearchBuilder {
'share_with' => 'string',
'share_type' => 'integer',
'owner' => 'string',
'creation_time' => 'integer',
'upload_time' => 'integer',
];

Expand Down Expand Up @@ -258,6 +259,7 @@ private function validateComparison(ISearchComparison $operator) {
'share_with' => ['eq'],
'share_type' => ['eq'],
'owner' => ['eq'],
'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
];

Expand Down
1 change: 1 addition & 0 deletions lib/private/Files/Node/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public function newFile($path, $content = null) {
throw new NotPermittedException('Could not create path "' . $fullPath . '"');
}
$node = new File($this->root, $this->view, $fullPath, null, $this);
$this->view->putFileInfo($fullPath, ['creation_time' => time()]);
$this->sendHooks(['postWrite', 'postCreate'], [$node]);
return $node;
}
Expand Down
Loading