diff --git a/lib/ClustersBackend/AlbumsBackend.php b/lib/ClustersBackend/AlbumsBackend.php index 79a803793..ee457954f 100644 --- a/lib/ClustersBackend/AlbumsBackend.php +++ b/lib/ClustersBackend/AlbumsBackend.php @@ -121,8 +121,14 @@ public function getClustersInternal(int $fileid = 0): array ); }; + // Transformation to select the shared flag + $sharedFlag = function (IQueryBuilder &$query): void { + $this->albumsQuery->transformSharedFlag($query); + }; + // Transformations to apply to own albums $transformOwned = [ + $sharedFlag, $materialize, $ownCover, $materialize, $etag('last_added_photo'), $etag('cover'), ]; diff --git a/lib/Db/AlbumsQuery.php b/lib/Db/AlbumsQuery.php index 2a198789d..ac4b0d88b 100644 --- a/lib/Db/AlbumsQuery.php +++ b/lib/Db/AlbumsQuery.php @@ -300,6 +300,20 @@ public function getAlbumPhotos(int $albumId, ?int $limit, ?int $fileid): array return $result; } + /** + * Query transformation to add a "shared" flag to the list + * of albums (whether the album has any shared collaborators). + */ + public function transformSharedFlag(IQueryBuilder &$query): void + { + $sSq = $query->getConnection()->getQueryBuilder(); + $sSq->select($sSq->expr()->literal(1)) + ->from($this->collaboratorsTable(), 'pc') + ->where($sSq->expr()->eq('pc.album_id', 'pa.album_id')) + ; + $query->selectAlias(SQL::exists($query, $sSq), 'shared'); + } + /** * Get the various collaborator IDs that a user has. * This includes the groups the user is in and the user itself. diff --git a/src/components/frame/Cluster.vue b/src/components/frame/Cluster.vue index 2eab8e87b..413fd6310 100644 --- a/src/components/frame/Cluster.vue +++ b/src/components/frame/Cluster.vue @@ -86,21 +86,7 @@ export default defineComponent({ subtitle() { if (dav.clusterIs.album(this.data)) { - let text: string; - if (this.data.count === 0) { - text = this.t('memories', 'No items'); - } else { - text = this.n('memories', '{n} item', '{n} items', this.data.count, { n: this.data.count }); - } - - if (this.data.user !== utils.uid) { - const sharer = this.t('memories', 'Shared by {user}', { - user: this.data.user_display || this.data.user, - }); - text = `${text} / ${sharer}`; - } - - return text; + return dav.getAlbumSubtitle(this.data); } return String(); diff --git a/src/components/modal/AlbumCreateModal.vue b/src/components/modal/AlbumCreateModal.vue index 144843e49..70a74ed23 100644 --- a/src/components/modal/AlbumCreateModal.vue +++ b/src/components/modal/AlbumCreateModal.vue @@ -24,6 +24,7 @@ import Modal from './Modal.vue'; import ModalMixin from './ModalMixin'; import AlbumForm from './AlbumForm.vue'; +import * as utils from '@services/utils'; import * as dav from '@services/dav'; export default defineComponent({ @@ -81,6 +82,9 @@ export default defineComponent({ } else { await this.$router.replace(route); } + } else { + // refresh timeline for metadata changes + utils.bus.emit('memories:timeline:soft-refresh', null); } }, }, diff --git a/src/components/modal/AlbumShareModal.vue b/src/components/modal/AlbumShareModal.vue index fb7f2cbf9..7a3e51eb7 100644 --- a/src/components/modal/AlbumShareModal.vue +++ b/src/components/modal/AlbumShareModal.vue @@ -57,6 +57,7 @@ import Modal from './Modal.vue'; import ModalMixin from './ModalMixin'; import AlbumCollaborators from './AlbumCollaborators.vue'; +import * as utils from '@services/utils'; import * as dav from '@services/dav'; export default defineComponent({ @@ -151,6 +152,9 @@ export default defineComponent({ } } + // Refresh timeline for metadata changes + utils.bus.emit('memories:timeline:soft-refresh', null); + // Close modal await this.close(); } catch (error) { diff --git a/src/components/modal/AlbumsList.vue b/src/components/modal/AlbumsList.vue index 95680ba3d..e61ebb6d8 100644 --- a/src/components/modal/AlbumsList.vue +++ b/src/components/modal/AlbumsList.vue @@ -37,6 +37,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'; const NcListItem = () => import('@nextcloud/vue/dist/Components/NcListItem.js'); import * as utils from '@services/utils'; +import * as dav from '@services/dav'; import type { IAlbum, IPhoto } from '@typings'; @@ -107,17 +108,7 @@ export default defineComponent({ }, getSubtitle(album: IAlbum) { - let text = this.n('memories', '%n item', '%n items', album.count); - - if (album.user !== utils.uid) { - text += - ' / ' + - this.t('memories', 'Shared by {user}', { - user: album.user_display || album.user, - }); - } - - return text; + return dav.getAlbumSubtitle(album); }, }, }); diff --git a/src/components/top-matter/AlbumDynamicTopMatter.vue b/src/components/top-matter/AlbumDynamicTopMatter.vue new file mode 100644 index 000000000..fae77a1d2 --- /dev/null +++ b/src/components/top-matter/AlbumDynamicTopMatter.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/components/top-matter/DynamicTopMatter.vue b/src/components/top-matter/DynamicTopMatter.vue index 2e41ab4f6..e671b53f0 100644 --- a/src/components/top-matter/DynamicTopMatter.vue +++ b/src/components/top-matter/DynamicTopMatter.vue @@ -10,10 +10,10 @@ import { defineComponent, type Component } from 'vue'; import UserMixin from '@mixins/UserConfig'; +import AlbumDynamicTopMatter from './AlbumDynamicTopMatter.vue'; import FolderDynamicTopMatter from './FolderDynamicTopMatter.vue'; import PlacesDynamicTopMatterVue from './PlacesDynamicTopMatter.vue'; import OnThisDay from './OnThisDay.vue'; - import * as strings from '@services/strings'; // Auto-hide top header on public shares if redundant @@ -40,6 +40,8 @@ export default defineComponent({ return FolderDynamicTopMatter; } else if (this.routeIsPlaces) { return PlacesDynamicTopMatterVue; + } else if (this.routeIsAlbums) { + return AlbumDynamicTopMatter; } else if (this.routeIsBase && this.config.enable_top_memories) { return OnThisDay; } diff --git a/src/services/dav/albums.ts b/src/services/dav/albums.ts index 39d7e05c4..b093d2e49 100644 --- a/src/services/dav/albums.ts +++ b/src/services/dav/albums.ts @@ -4,7 +4,7 @@ import axios from '@nextcloud/axios'; import { showError } from '@nextcloud/dialogs'; import { getLanguage } from '@nextcloud/l10n'; -import { translate as t } from '@services/l10n'; +import { translate as t, translatePlural as n } from '@services/l10n'; import { API } from '@services/API'; import client from '@services/dav/client'; import staticConfig from '@services/static-config'; @@ -12,6 +12,15 @@ import * as utils from '@services/utils'; import type { IAlbum, IFileInfo, IPhoto } from '@typings'; +export type IDavAlbum = { + location: string; + collaborators: { + id: string; + label: string; + type: number; + }[]; +}; + /** * Get DAV path for album */ @@ -190,7 +199,7 @@ export async function updateAlbum(album: any, { albumName, properties }: any) { * @param user Owner of album * @param name Name of album (or ID) */ -export async function getAlbum(user: string, name: string, extraProps = {}) { +export async function getAlbum(user: string, name: string, extraProps = {}): Promise { const req = ` `; - let album = (await client.stat(`/photos/${user}/albums/${name}`, { + let album = (await client.stat(getAlbumPath(user, name), { data: req, details: true, })) as any; @@ -217,6 +226,12 @@ export async function getAlbum(user: string, name: string, extraProps = {}) { }; const c = album?.collaborators?.collaborator; album.collaborators = c ? (Array.isArray(c) ? c : [c]) : []; + + // Sort collaborators by type + album.collaborators.sort((a: any, b: any) => { + return (a.type ?? -1) - (b.type ?? -1); + }); + return album; } @@ -258,3 +273,24 @@ export function getAlbumFileInfos(photos: IPhoto[], albumUser: string, albumName } as IFileInfo; }); } + +export function getAlbumSubtitle(album: IAlbum) { + let text: string; + if (album.count === 0) { + text = t('memories', 'No items'); + } else { + text = n('memories', '{n} item', '{n} items', album.count, { n: album.count }); + } + + if (album.user !== utils.uid) { + const sharer = t('memories', 'Shared by {user}', { + user: album.user_display || album.user, + }); + text = `${text} | ${sharer}`; + } else if (album.shared) { + const shared = t('memories', 'Shared Album'); + text = `${text} | ${shared}`; + } + + return text; +} diff --git a/src/typings/cluster.d.ts b/src/typings/cluster.d.ts index bc4658082..47dd214ff 100644 --- a/src/typings/cluster.d.ts +++ b/src/typings/cluster.d.ts @@ -46,6 +46,8 @@ declare module '@typings' { last_added_photo_etag: string; /** Record ID of the latest update */ update_id: number; + /** Album is shared with other users */ + shared: boolean; } export interface IFace extends ICluster {