Skip to content

Commit

Permalink
feat: Add username label on shared photos
Browse files Browse the repository at this point in the history
Fixes #955

Signed-off-by: Jo Van Bulck <[email protected]>
  • Loading branch information
jovanbulck committed Nov 16, 2024
1 parent f1e2669 commit 9a8b709
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 3 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function w($base, $param)
['name' => 'Map#clusters', 'url' => '/api/map/clusters', 'verb' => 'GET'],
['name' => 'Map#init', 'url' => '/api/map/init', 'verb' => 'GET'],

['name' => 'Uid#name', 'url' => '/api/uid/name/{id}', 'verb' => 'GET'],

['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],

['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
Expand Down
55 changes: 55 additions & 0 deletions lib/Controller/UidController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Varun Patil <[email protected]>
* @author Varun Patil <[email protected]>
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OCA\Memories\Controller;

use OCA\Memories\Util;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;

class UidController extends GenericApiController
{
/**
* @NoAdminRequired
*
* @NoCSRFRequired
*
* @PublicPage
*
* Get display name for a Nextcloud user id
*
* @param string fileid
*/
public function name(
string $uid,
): Http\Response {
return Util::guardEx(static function () use ($uid) {
$userManager = \OC::$server->get(\OCP\IUserManager::class);
$user = $userManager->get($uid);

return new JSONResponse([
'user_display' => $user ? $user->getDisplayName() : null,
], Http::STATUS_OK);
});
}
}
1 change: 1 addition & 0 deletions lib/Db/TimelineQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TimelineQuery
'f.etag', 'f.name AS basename',
'f.size', 'm.epoch', // auid
'mimetypes.mimetype',
'm.uid',
];

protected ?TimelineRoot $_root = null; // cache
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/TimelineQueryDays.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OCA\Memories\ClustersBackend;
use OCA\Memories\Exif;
use OCA\Memories\Settings\SystemConfig;
use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

Expand Down Expand Up @@ -315,6 +316,10 @@ private function postProcessDayPhoto(array &$row, bool $monthView = false): void
unset($row['liveid']);
}

if ($row['uid'] === Util::getUID()) {
unset($row['uid']);
}

// Favorite field, may not be present
if ($row['categoryid'] ?? null) {
$row['isfavorite'] = 1;
Expand Down
3 changes: 2 additions & 1 deletion lib/Db/TimelineQuerySingleItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function getSingleItem(int $fileId): ?array
public function getInfoById(int $id, bool $basic): array
{
$qb = $this->connection->getQueryBuilder();
$qb->select('fileid', 'dayid', 'datetaken', 'w', 'h')
$qb->select('fileid', 'dayid', 'datetaken', 'w', 'h', 'uid')
->from('memories')
->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT)))
;
Expand All @@ -63,6 +63,7 @@ public function getInfoById(int $id, bool $basic): array
'w' => (int) $row['w'],
'h' => (int) $row['h'],
'datetaken' => Util::sqlUtcToTimestamp($row['datetaken']),
'uid' => $row['uid'],
];

// Return if only basic info is needed
Expand Down
2 changes: 2 additions & 0 deletions lib/Db/TimelineWrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function processFile(
// Get parameters
$mtime = $file->getMtime();
$fileId = $file->getId();
$uid = $file->getOwner()?->getUID();
$isvideo = Index::isVideo($file);

// Get previous row
Expand Down Expand Up @@ -172,6 +173,7 @@ public function processFile(
'orphan' => $query->createNamedParameter(false, IQueryBuilder::PARAM_BOOL),
'buid' => $query->createNamedParameter($buid, IQueryBuilder::PARAM_STR),
'parent' => $query->createNamedParameter($file->getParent()->getId(), IQueryBuilder::PARAM_INT),
'uid' => $query->createNamedParameter($uid, IQueryBuilder::PARAM_STR),
];

// There is no easy way to UPSERT in standard SQL
Expand Down
120 changes: 120 additions & 0 deletions lib/Migration/Version800001Date20241026171056.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Varun Patil <[email protected]>
* @author Varun Patil <[email protected]>
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OCA\Memories\Migration;

use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* FIXME Auto-generated migration step: Please modify to your needs!
*/
class Version800001Date20241026171056 extends SimpleMigrationStep
{
public function __construct(private IDBConnection $dbc) {}

/**
* @param \Closure(): ISchemaWrapper $schemaClosure
*/
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {}

/**
* @param \Closure(): ISchemaWrapper $schemaClosure
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('memories')) {
throw new \Exception('Memories table does not exist');
}

$table = $schema->getTable('memories');

if (!$table->hasColumn('uid')) {
$table->addColumn('uid', 'string', [
'notnull' => false,
'length' => 64,
]);
}

return $schema;
}

/**
* @param \Closure(): ISchemaWrapper $schemaClosure
*/
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
// create database triggers; this will never throw
\OCA\Memories\Db\AddMissingIndices::createFilecacheTriggers($output);

// migrate uid values from filecache
try {
$output->info('Migrating values for uid from filecache');

$platform = $this->dbc->getDatabasePlatform();

// copy existing parent values from filecache
if (preg_match('/mysql|mariadb/i', $platform::class)) {
$this->dbc->executeQuery(
'UPDATE *PREFIX*memories AS m
JOIN *PREFIX*filecache AS f ON f.fileid = m.fileid
JOIN *PREFIX*storages AS s ON f.storage = s.numeric_id
SET m.uid = SUBSTRING_INDEX(s.id, \'::\', -1)
WHERE s.id LIKE \'home::%\'
',
);
} elseif (preg_match('/postgres/i', $platform::class)) {
$this->dbc->executeQuery(
'UPDATE *PREFIX*memories AS m
SET uid = split_part(s.id, \'::\', 2)
FROM *PREFIX*filecache AS f
JOIN *PREFIX*storages AS s ON f.storage = s.numeric_id
WHERE f.fileid = m.fileid
AND s.id LIKE \'home::%\'
',
);
} elseif (preg_match('/sqlite/i', $platform::class)) {
$this->dbc->executeQuery(
'UPDATE memories AS m
SET uid = SUBSTR(s.id, INSTR(s.id, \'::\') + 2)
FROM filecache AS f
JOIN storages AS s ON f.storage = s.numeric_id
WHERE f.fileid = m.fileid
AND s.id LIKE \'home::%\'
',
);
} else {
throw new \Exception('Unsupported '.$platform::class);
}

$output->info('Values for uid migrated successfully');
} catch (\Exception $e) {
$output->warning('Failed to copy uid values from fileid: '.$e->getMessage());
$output->warning('Please run occ memories:index -f');
}
}
}
24 changes: 23 additions & 1 deletion src/components/Metadata.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
<AlbumsList class="albums" :albums="albums" />
</div>

<div v-if="uid">
<div class="section-title">{{ t('memories', 'Shared By') }}</div>
<div class="top-field">
<div class="icon">
<NcAvatar key="uid" :user="uid" :showUserStatus="false" :size="24" />
</div>

<div class="text">
<span class="title">{{ userDisplay }}</span>
</div>
</div>
</div>

<div class="section-title">{{ t('memories', 'Metadata') }}</div>
<div v-for="field of topFields" :key="field.title" :class="`top-field top-field--${field.id}`">
<div class="icon">
Expand Down Expand Up @@ -90,9 +103,10 @@ import TagIcon from 'vue-material-design-icons/Tag.vue';
import * as utils from '@services/utils';
import * as dav from '@services/dav';
import { API } from '@services/API';
import type { IAlbum, IFace, IImageInfo, IPhoto, IExif } from '@typings';
const NcAvatar = () => import('@nextcloud/vue/dist/Components/NcAvatar.js');
interface TopField {
id?: string;
title: string;
Expand All @@ -110,6 +124,7 @@ export default defineComponent({
AlbumsList,
Cluster,
EditIcon,
NcAvatar,
},
mixins: [UserConfig],
Expand All @@ -120,6 +135,8 @@ export default defineComponent({
exif: {} as IExif,
baseInfo: {} as IImageInfo,
error: false,
uid: null as string | null,
userDisplay: '',
loading: 0,
state: 0,
Expand Down Expand Up @@ -419,6 +436,11 @@ export default defineComponent({
this.filename = this.baseInfo.basename;
this.exif = this.baseInfo.exif ?? {};
// set user info
this.uid = this.baseInfo.uid ?? null;
if (this.uid == utils.uid) this.uid = null;
this.userDisplay = await utils.getUserDisplayName(this.uid);
return this.baseInfo;
},
Expand Down
33 changes: 32 additions & 1 deletion src/components/frame/Photo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@
</div>
</div>

<div class="flag bottom-left">
<div class="flag bottom-right">
<StarIcon :size="22" v-if="data.flag & c.FLAG_IS_FAVORITE" />
<LocalIcon :size="22" v-if="data.flag & c.FLAG_IS_LOCAL" />
</div>

<div class="flag bottom-left">
<AccountMultipleIcon :size="22" v-if="data.uid" />
<span class="username" v-if="data.uid">{{ owner }}</span>
</div>

<div
class="img-outer fill-block"
:class="{ 'memories-livephoto': data.liveid }"
Expand Down Expand Up @@ -81,6 +86,7 @@ import StarIcon from 'vue-material-design-icons/Star.vue';
import VideoIcon from 'vue-material-design-icons/PlayCircleOutline.vue';
import LocalIcon from 'vue-material-design-icons/CloudOff.vue';
import RawIcon from 'vue-material-design-icons/Raw.vue';
import AccountMultipleIcon from 'vue-material-design-icons/AccountMultiple.vue';
import type { IDay, IPhoto } from '@typings';
import type XImg from '@components/XImg.vue';
Expand All @@ -96,6 +102,7 @@ export default defineComponent({
StarIcon,
LocalIcon,
RawIcon,
AccountMultipleIcon,
},
props: {
Expand Down Expand Up @@ -126,6 +133,7 @@ export default defineComponent({
requested: false,
},
faceSrc: null as string | null,
userDisplay: null as string | null,
}),
watch: {
Expand Down Expand Up @@ -173,6 +181,10 @@ export default defineComponent({
return null;
},
owner(): string {
return this.userDisplay ?? this.data.uid ?? '';
},
videoUrl(): string | null {
if (this.data.liveid) {
return utils.getLivePhotoVideoUrl(this.data, true);
Expand Down Expand Up @@ -260,9 +272,14 @@ export default defineComponent({
);
},
async addUserDisplay() {
this.userDisplay = await utils.getUserDisplayName(this.data.uid ?? null);
},
/** Post load tasks */
load() {
this.addFaceRect();
this.addUserDisplay();
},
/** Error in loading image */
Expand Down Expand Up @@ -417,6 +434,20 @@ $icon-size: $icon-half-size * 2;
}
}
&.bottom-right {
bottom: var(--icon-dist);
right: var(--icon-dist);
.p-outer.selected > & {
transform: translate($icon-size, -$icon-size);
}
}
> .username {
font-size: 0.75em;
font-weight: bold;
margin-left: 3px;
}
> .video {
display: flex;
line-height: 22px; // force text height to match
Expand Down
Loading

0 comments on commit 9a8b709

Please sign in to comment.