Skip to content

Commit e791ba1

Browse files
authored
Merge pull request #4036 from nextcloud/enh/reference-widget
Introduce reference link and picker
2 parents 89020b7 + bac8c84 commit e791ba1

9 files changed

Lines changed: 236 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ All notable changes to this project will be documented in this file.
2424
- Delete polls without the need to archive them first
2525
- Collapsible poll description
2626
- Transfer polls to another owner by the current poll owner or the administration
27-
- Added reference provider for link previews
27+
- Added reference provider for link previews and smart picker
2828

2929
## [7.4.1] - 2024-03-07
3030
### New

lib/AppInfo/Application.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,22 @@
4848
use OCA\Polls\Listener\GroupDeletedListener;
4949
use OCA\Polls\Listener\OptionListener;
5050
use OCA\Polls\Listener\PollListener;
51+
use OCA\Polls\Listener\PollsReferenceListener;
5152
use OCA\Polls\Listener\ShareListener;
5253
use OCA\Polls\Listener\UserDeletedListener;
5354
use OCA\Polls\Listener\VoteListener;
5455
use OCA\Polls\Middleware\RequestAttributesMiddleware;
5556
use OCA\Polls\Model\Settings\AppSettings;
5657
use OCA\Polls\Model\Settings\SystemSettings;
5758
use OCA\Polls\Notification\Notifier;
59+
use OCA\Polls\Provider\ReferenceProvider;
5860
use OCA\Polls\Provider\SearchProvider;
59-
use OCA\Polls\Reference\PollReferenceProvider;
6061
use OCA\Polls\UserSession;
6162
use OCP\AppFramework\App;
6263
use OCP\AppFramework\Bootstrap\IBootContext;
6364
use OCP\AppFramework\Bootstrap\IBootstrap;
6465
use OCP\AppFramework\Bootstrap\IRegistrationContext;
66+
use OCP\Collaboration\Reference\RenderReferenceEvent;
6567
use OCP\Group\Events\GroupDeletedEvent;
6668
use OCP\IAppConfig;
6769
use OCP\IDBConnection;
@@ -89,6 +91,7 @@ public function register(IRegistrationContext $context): void {
8991
include_once __DIR__ . '/../../vendor/autoload.php';
9092
$this->registerServices($context);
9193

94+
$context->registerEventListener(RenderReferenceEvent::class, PollsReferenceListener::class);
9295
$context->registerMiddleWare(RequestAttributesMiddleware::class);
9396
$context->registerNotifierService(Notifier::class);
9497

@@ -128,7 +131,8 @@ public function register(IRegistrationContext $context): void {
128131

129132
$context->registerSearchProvider(SearchProvider::class);
130133
$context->registerDashboardWidget(PollWidget::class);
131-
$context->registerReferenceProvider(PollReferenceProvider::class);
134+
$context->registerReferenceProvider(ReferenceProvider::class);
135+
132136
}
133137

134138
/**
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Polls\Listener;
10+
11+
use OCA\Polls\AppInfo\Application;
12+
use OCP\Collaboration\Reference\RenderReferenceEvent;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Util;
16+
17+
/**
18+
* @template-implements IEventListener<Event>
19+
*/
20+
class PollsReferenceListener implements IEventListener {
21+
public function handle(Event $event): void {
22+
if (!$event instanceof RenderReferenceEvent) {
23+
return;
24+
}
25+
26+
Util::addScript(Application::APP_ID, 'polls-reference');
27+
}
28+
}
Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66
* SPDX-License-Identifier: AGPL-3.0-or-later
77
*/
88

9-
namespace OCA\Polls\Reference;
9+
namespace OCA\Polls\Provider;
1010

1111
use Exception;
1212
use OCA\Polls\AppInfo\Application;
1313
use OCA\Polls\Exceptions\ForbiddenException;
1414
use OCA\Polls\Exceptions\NotFoundException;
1515
use OCA\Polls\Service\PollService;
16+
use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
1617
use OCP\Collaboration\Reference\IReference;
17-
use OCP\Collaboration\Reference\IReferenceProvider;
18+
use OCP\Collaboration\Reference\ISearchableReferenceProvider;
1819
use OCP\Collaboration\Reference\Reference;
1920
use OCP\IL10N;
2021
use OCP\IURLGenerator;
2122

22-
class PollReferenceProvider implements IReferenceProvider {
23+
class ReferenceProvider extends ADiscoverableReferenceProvider implements ISearchableReferenceProvider {
2324

2425
/** @psalm-suppress PossiblyUnusedMethod */
2526
public function __construct(
@@ -38,7 +39,7 @@ public function matchReference(string $referenceText): bool {
3839
return ($this->extractPollId($referenceText) !== 0);
3940
}
4041

41-
private function extractPollId($referenceText): int {
42+
public function extractPollId(string $referenceText): int {
4243
$matchingUrls = [
4344
$this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID . '/vote'), // poll url base without index.php
4445
$this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID . '/vote'), // poll url base with index.php
@@ -60,6 +61,10 @@ private function extractPollId($referenceText): int {
6061
public function resolveReference(string $referenceText): ?IReference {
6162
if ($this->matchReference($referenceText)) {
6263
$pollId = $this->extractPollId($referenceText);
64+
$expired = false;
65+
$expiry = 0;
66+
$participated = false;
67+
6368

6469
if ($pollId) {
6570
try {
@@ -69,6 +74,9 @@ public function resolveReference(string $referenceText): ?IReference {
6974
$ownerId = $poll->getUser()->getId();
7075
$ownerDisplayName = $poll->getUser()->getDisplayName();
7176
$url = $poll->getVoteUrl();
77+
$expired = $poll->getExpired();
78+
$expiry = $poll->getExpire();
79+
$participated = $poll->getCurrentUserVotes() ? true : false;
7280

7381
} catch (NotFoundException $e) {
7482
$pollId = 0;
@@ -91,15 +99,11 @@ public function resolveReference(string $referenceText): ?IReference {
9199
return null;
92100
}
93101

94-
$imageUrl = $this->urlGenerator->getAbsoluteURL(
95-
$this->urlGenerator->imagePath(Application::APP_ID, 'polls.svg')
96-
);
97-
98102
$reference = new Reference($referenceText);
99103
$reference->setTitle($title);
100104
$reference->setDescription($description ? $description : $this->l10n->t('No description available.'));
101-
$reference->setImageUrl($imageUrl);
102-
$reference->setRichObject(Application::APP_ID . '_poll_widget', [
105+
$reference->setImageUrl($this->getIconUrl());
106+
$reference->setRichObject(Application::APP_ID . '_reference_widget', [
103107
'id' => $pollId,
104108
'poll' => [
105109
'id' => $pollId,
@@ -108,6 +112,9 @@ public function resolveReference(string $referenceText): ?IReference {
108112
'ownerDisplayName' => $ownerDisplayName,
109113
'ownerId' => $ownerId,
110114
'url' => $url,
115+
'expired' => $expired,
116+
'expiry' => $expiry,
117+
'participated' => $participated,
111118
],
112119
]);
113120
return $reference;
@@ -129,4 +136,22 @@ public function getCachePrefix(string $referenceId): string {
129136
public function getCacheKey(string $referenceId): ?string {
130137
return $this->userId ?? '';
131138
}
139+
public function getId(): string {
140+
return Application::APP_ID;
141+
}
142+
public function getTitle(): string {
143+
return $this->l10n->t('Poll');
144+
}
145+
146+
public function getIconUrl(): string {
147+
return $this->urlGenerator->imagePath(Application::APP_ID, 'polls.svg');
148+
}
149+
150+
public function getOrder(): int {
151+
return 51;
152+
}
153+
154+
public function getSupportedSearchProviderIds(): array {
155+
return ['search-poll'];
156+
}
132157
}

lib/Provider/SearchProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131
}
3232

3333
public function getId(): string {
34-
return 'poll';
34+
return 'search-poll';
3535
}
3636

3737
public function getName(): string {

src/components/PollList/PollItem.vue

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<script setup lang="ts">
77
import { RouterLink } from 'vue-router'
88
import { computed } from 'vue'
9-
import moment from '@nextcloud/moment'
9+
import { DateTime } from 'luxon'
1010
import { t } from '@nextcloud/l10n'
1111
import {
1212
usePollStore,
@@ -41,12 +41,13 @@ const closeToClosing = computed(
4141
() =>
4242
!poll.status.isExpired
4343
&& poll.configuration.expire
44-
&& moment.unix(poll.configuration.expire).diff() < 86400000,
44+
&& DateTime.fromMillis(poll.configuration.expire * 1000).diffNow('hours')
45+
.hours < 36,
4546
)
4647
4748
const timeExpirationRelative = computed(() => {
4849
if (poll.configuration.expire) {
49-
return moment.unix(poll.configuration.expire).fromNow()
50+
return DateTime.fromMillis(poll.configuration.expire * 1000).toRelative()
5051
}
5152
return t('polls', 'never')
5253
})
@@ -67,14 +68,16 @@ const expiryClass = computed(() => {
6768
return StatusResults.Success
6869
})
6970
70-
const timeCreatedRelative = computed(() =>
71-
moment.unix(poll.status.created).fromNow(),
71+
const timeCreatedRelative = computed(
72+
() => DateTime.fromMillis(poll.status.created * 1000).toRelative() as string,
7273
)
7374
7475
const descriptionLine = computed(() => {
7576
if (poll.status.isArchived) {
76-
return t('polls', 'Archived {relativeTIme}', {
77-
relativeTIme: moment.unix(poll.status.archivedDate).fromNow(),
77+
return t('polls', 'Archived {relativeTime}', {
78+
relativeTime: DateTime.fromMillis(
79+
poll.status.archivedDate * 1000,
80+
).toRelative() as string,
7881
})
7982
}
8083
return t('polls', 'Started {relativeTime} from {ownerName}', {

src/polls-reference.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2022 Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { createApp } from 'vue'
7+
import { pinia } from './stores/index.ts'
8+
import { registerWidget } from '@nextcloud/vue/components/NcRichText'
9+
import Reference from './views/Reference.vue'
10+
import './assets/scss/polls-icon.scss'
11+
12+
registerWidget('polls_reference_widget', async (el, { richObject }) => {
13+
const PollsReference = createApp(Reference, {
14+
richObject,
15+
})
16+
.use(pinia)
17+
.mount(el)
18+
return PollsReference
19+
})

src/views/Reference.vue

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2022 Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
8+
import { PollsAppIcon } from '../components/AppIcons'
9+
import ExpirationIcon from 'vue-material-design-icons/CalendarEnd.vue'
10+
import BadgeSmallDiv from '../components/Base/modules/BadgeSmallDiv.vue'
11+
import { t } from '@nextcloud/l10n'
12+
import { DateTime } from 'luxon'
13+
import { StatusResults } from '../Types'
14+
15+
type RichObject = {
16+
id: number
17+
poll: {
18+
id: number
19+
title: string
20+
description: string
21+
ownerDisplayName: string
22+
ownerId: string
23+
url: string
24+
participated: boolean
25+
expiry: number
26+
expired: boolean
27+
}
28+
}
29+
30+
interface Props {
31+
richObject?: RichObject
32+
}
33+
34+
const { richObject } = defineProps<Props>()
35+
// const expiryClass2 = (() => {
36+
// if (!richObject?.poll?.expiry) {
37+
// return ''
38+
// }
39+
// if (DateTime.fromMillis(richObject.poll.expiry * 1000).diffNow('hours').hours < 36) {
40+
// return StatusResults.Warning
41+
// }
42+
// return StatusResults.Success
43+
// })
44+
const expiryClass = richObject?.poll?.expiry
45+
? DateTime.fromMillis(richObject.poll.expiry * 1000).diffNow('hours').hours < 36
46+
? StatusResults.Warning
47+
: StatusResults.Success
48+
: ''
49+
</script>
50+
51+
<template>
52+
<div v-if="richObject" class="polls_widget">
53+
<div class="widget_header">
54+
<PollsAppIcon :size="20" class="title-icon" />
55+
<a class="title" :href="richObject.poll.url" target="_blank">
56+
{{ richObject.poll.title }}
57+
</a>
58+
<BadgeSmallDiv v-if="richObject.poll.participated" class="success">
59+
{{ t('polls', 'participated') }}
60+
</BadgeSmallDiv>
61+
<BadgeSmallDiv v-else-if="richObject.poll.expired" class="error">
62+
{{ t('polls', 'closed') }}
63+
</BadgeSmallDiv>
64+
<BadgeSmallDiv
65+
v-else-if="richObject.poll.expiry > 0"
66+
:class="expiryClass">
67+
<template #icon>
68+
<ExpirationIcon :size="16" />
69+
</template>
70+
{{ DateTime.fromMillis(richObject.poll.expiry * 1000).toRelative() }}
71+
</BadgeSmallDiv>
72+
</div>
73+
<div class="description">
74+
<span class="clamped">
75+
{{ richObject.poll.description }}
76+
</span>
77+
</div>
78+
<div class="widget_footer">
79+
<span>{{ t('polls', 'By:') }}</span>
80+
<NcUserBubble
81+
:user="richObject.poll.ownerId"
82+
:display-name="richObject.poll.ownerDisplayName" />
83+
<span
84+
v-if="richObject.poll.expiry > 0 && !richObject.poll.expired"
85+
class="expiration">
86+
{{ t('polls', 'Ends in') }}
87+
{{ DateTime.fromMillis(richObject.poll.expiry * 1000).toRelative() }}
88+
</span>
89+
</div>
90+
</div>
91+
</template>
92+
93+
<style lang="scss" scoped>
94+
.polls_widget {
95+
padding: 0.6rem;
96+
}
97+
.widget_header,
98+
.widget_footer {
99+
display: flex;
100+
column-gap: 0.3rem;
101+
}
102+
103+
.badge-small {
104+
flex: 0;
105+
}
106+
.polls_app_icon {
107+
flex: 0 0 1.4rem;
108+
}
109+
.title {
110+
flex: 1;
111+
font-weight: bold;
112+
padding-left: 0.6rem;
113+
text-wrap: nowrap;
114+
overflow: hidden;
115+
text-overflow: ellipsis;
116+
}
117+
.description {
118+
margin-left: 1.4rem;
119+
padding: 0.6rem;
120+
}
121+
.owner {
122+
margin-left: 1.4rem;
123+
padding-left: 0.6rem;
124+
}
125+
.clamped {
126+
display: -webkit-box !important;
127+
-webkit-line-clamp: 4;
128+
line-clamp: 4;
129+
-webkit-box-orient: vertical;
130+
text-wrap: wrap;
131+
overflow: clip !important;
132+
text-overflow: ellipsis !important;
133+
padding: 0 !important;
134+
}
135+
</style>

0 commit comments

Comments
 (0)