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
4 changes: 3 additions & 1 deletion apps/dav/lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ public function __construct(
}

/**
* @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
* @return array{dav: array{chunking: string, public_shares_chunking: bool, search_supports_creation_time: bool, search_supports_upload_time: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
*/
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 Down
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('{DAV:}creationdate', 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 '{DAV:}creationdate':
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 '{DAV:}creationdate':
return 'creation_time';
case '{http://nextcloud.org/ns}upload_time':
return 'upload_time';
case FilesPlugin::SIZE_PROPERTYNAME:
Expand Down
10 changes: 9 additions & 1 deletion apps/dav/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"type": "object",
"required": [
"chunking",
"public_shares_chunking"
"public_shares_chunking",
"search_supports_creation_time",
"search_supports_upload_time"
],
"properties": {
"chunking": {
Expand All @@ -39,6 +41,12 @@
"public_shares_chunking": {
"type": "boolean"
},
"search_supports_creation_time": {
"type": "boolean"
},
"search_supports_upload_time": {
"type": "boolean"
},
"bulkupload": {
"type": "string"
},
Expand Down
6 changes: 6 additions & 0 deletions apps/dav/tests/unit/CapabilitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public function testGetCapabilities(): void {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
],
];
$this->assertSame($expected, $capabilities->getCapabilities());
Expand All @@ -51,6 +53,8 @@ public function testGetCapabilitiesWithBulkUpload(): void {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
'bulkupload' => '1.0',
],
];
Expand All @@ -72,6 +76,8 @@ public function testGetCapabilitiesWithAbsence(): void {
'dav' => [
'chunking' => '1.0',
'public_shares_chunking' => true,
'search_supports_creation_time' => true,
'search_supports_upload_time' => true,
'absence-supported' => true,
'absence-replacement' => true,
],
Expand Down
22 changes: 22 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,21 @@ export default defineComponent({
return this.source.attributes.favorite === 1
},
isRecentlyCreated(): boolean {
if (!this.source.crtime) {
return false
}
const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
return this.source.crtime > oneDayAgo
},
isRecentView(): boolean {
return this.$route?.params?.view === 'recent'
},
userConfig(): UserConfig {
return this.userConfigStore.userConfig
},
Expand Down
71 changes: 71 additions & 0 deletions apps/files/src/components/FileEntry/RecentlyCreatedIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!--
- 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')" :path="mdiPlus" />
</template>

<script lang="ts">
import { mdiPlus } from '@mdi/js'
import { 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(.recently-created-marker-icon svg path) {
* stroke: var(--color-background-hover);
* }
* ```
*/
export default defineComponent({
name: 'RecentlyCreatedIcon',
components: {
NcIconSvgWrapper,
},
setup() {
return {
mdiPlus,
}
},
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 @@ -247,6 +247,12 @@ public function getFolderContentsById($fileId) {
* @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 @@ -152,7 +152,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 @@ -180,6 +180,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
10 changes: 9 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1504,7 +1504,9 @@
"type": "object",
"required": [
"chunking",
"public_shares_chunking"
"public_shares_chunking",
"search_supports_creation_time",
"search_supports_upload_time"
],
"properties": {
"chunking": {
Expand All @@ -1513,6 +1515,12 @@
"public_shares_chunking": {
"type": "boolean"
},
"search_supports_creation_time": {
"type": "boolean"
},
"search_supports_upload_time": {
"type": "boolean"
},
"bulkupload": {
"type": "string"
},
Expand Down
Loading