aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-20 10:30:02 +0200
committerGitHub <noreply@github.com>2020-10-20 10:30:02 +0200
commitd8030c8155ee4c20573848b2444f6df0b65d1662 (patch)
treeb6c0b8208f004e1b2b37b1af54e8d4c40310d56e
parent9b3c1270bcbe4f8e30e0160da8badd43dd94871a (diff)
parent21e72da9ee34cec56b10c83ae0c75b4bf320dfcb (diff)
downloadShaarli-d8030c8155ee4c20573848b2444f6df0b65d1662.tar.gz
Shaarli-d8030c8155ee4c20573848b2444f6df0b65d1662.tar.zst
Shaarli-d8030c8155ee4c20573848b2444f6df0b65d1662.zip
Merge pull request #1584 from ArthurHoaro/feature/async-thumbnail-retrieval
Asynchronous retrieval of bookmark's thumbnails
-rw-r--r--application/bookmark/Bookmark.php18
-rw-r--r--application/front/controller/admin/ManageShaareController.php3
-rw-r--r--application/front/controller/visitor/BookmarkListController.php10
-rw-r--r--assets/common/js/metadata.js106
-rw-r--r--tests/bookmark/BookmarkTest.php44
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php10
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php55
-rw-r--r--tests/front/controller/visitor/BookmarkListControllerTest.php57
-rw-r--r--tpl/default/linklist.html11
-rw-r--r--tpl/vintage/linklist.html7
10 files changed, 279 insertions, 42 deletions
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
index ea565d1f..4810c5e6 100644
--- a/application/bookmark/Bookmark.php
+++ b/application/bookmark/Bookmark.php
@@ -378,6 +378,24 @@ class Bookmark
378 } 378 }
379 379
380 /** 380 /**
381 * Return true if:
382 * - the bookmark's thumbnail is not already set to false (= not found)
383 * - it's not a note
384 * - it's an HTTP(S) link
385 * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
386 *
387 * @return bool True if the bookmark's thumbnail needs to be retrieved.
388 */
389 public function shouldUpdateThumbnail(): bool
390 {
391 return $this->thumbnail !== false
392 && !$this->isNote()
393 && startsWith(strtolower($this->url), 'http')
394 && (null === $this->thumbnail || !is_file($this->thumbnail))
395 ;
396 }
397
398 /**
381 * Get the Sticky. 399 * Get the Sticky.
382 * 400 *
383 * @return bool 401 * @return bool
diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
index df2f1631..908ebae3 100644
--- a/application/front/controller/admin/ManageShaareController.php
+++ b/application/front/controller/admin/ManageShaareController.php
@@ -129,7 +129,8 @@ class ManageShaareController extends ShaarliAdminController
129 $bookmark->setTagsString($request->getParam('lf_tags')); 129 $bookmark->setTagsString($request->getParam('lf_tags'));
130 130
131 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE 131 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
132 && false === $bookmark->isNote() 132 && true !== $this->container->conf->get('general.enable_async_metadata', true)
133 && $bookmark->shouldUpdateThumbnail()
133 ) { 134 ) {
134 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); 135 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
135 } 136 }
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
index 18368751..a8019ead 100644
--- a/application/front/controller/visitor/BookmarkListController.php
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -169,14 +169,11 @@ class BookmarkListController extends ShaarliVisitorController
169 */ 169 */
170 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool 170 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
171 { 171 {
172 // Logged in, thumbnails enabled, not a note, is HTTP 172 // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated
173 // and (never retrieved yet or no valid cache file)
174 if ($this->container->loginManager->isLoggedIn() 173 if ($this->container->loginManager->isLoggedIn()
174 && true !== $this->container->conf->get('general.enable_async_metadata', true)
175 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE 175 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
176 && false !== $bookmark->getThumbnail() 176 && $bookmark->shouldUpdateThumbnail()
177 && !$bookmark->isNote()
178 && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
179 && startsWith(strtolower($bookmark->getUrl()), 'http')
180 ) { 177 ) {
181 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); 178 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
182 $this->container->bookmarkService->set($bookmark, $writeDatastore); 179 $this->container->bookmarkService->set($bookmark, $writeDatastore);
@@ -198,6 +195,7 @@ class BookmarkListController extends ShaarliVisitorController
198 'page_max' => '', 195 'page_max' => '',
199 'search_tags' => '', 196 'search_tags' => '',
200 'result_count' => '', 197 'result_count' => '',
198 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
201 ]; 199 ];
202 } 200 }
203 201
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js
index 5200b481..2b013364 100644
--- a/assets/common/js/metadata.js
+++ b/assets/common/js/metadata.js
@@ -1,5 +1,19 @@
1import he from 'he'; 1import he from 'he';
2 2
3/**
4 * This script is used to retrieve bookmarks metadata asynchronously:
5 * - title, description and keywords while creating a new bookmark
6 * - thumbnails while visiting the bookmark list
7 *
8 * Note: it should only be included if the user is logged in
9 * and the setting general.enable_async_metadata is enabled.
10 */
11
12/**
13 * Removes given input loaders - used in edit link template.
14 *
15 * @param {object} loaders List of input DOM element that need to be cleared
16 */
3function clearLoaders(loaders) { 17function clearLoaders(loaders) {
4 if (loaders != null && loaders.length > 0) { 18 if (loaders != null && loaders.length > 0) {
5 [...loaders].forEach((loader) => { 19 [...loaders].forEach((loader) => {
@@ -8,32 +22,82 @@ function clearLoaders(loaders) {
8 } 22 }
9} 23}
10 24
25/**
26 * AJAX request to update the thumbnail of a bookmark with the provided ID.
27 * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it.
28 *
29 * @param {string} basePath Shaarli subfolder for XHR requests
30 * @param {object} divElement Main <div> DOM element containing the thumbnail placeholder
31 * @param {int} id Bookmark ID to update
32 */
33function updateThumb(basePath, divElement, id) {
34 const xhr = new XMLHttpRequest();
35 xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`);
36 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
37 xhr.responseType = 'json';
38 xhr.onload = () => {
39 if (xhr.status !== 200) {
40 alert(`An error occurred. Return code: ${xhr.status}`);
41 } else {
42 const { response } = xhr;
43
44 if (response.thumbnail !== false) {
45 const imgElement = divElement.querySelector('img');
46
47 imgElement.src = response.thumbnail;
48 imgElement.dataset.src = response.thumbnail;
49 imgElement.style.opacity = '1';
50 divElement.classList.remove('hidden');
51 }
52 }
53 };
54 xhr.send();
55}
56
11(() => { 57(() => {
58 const basePath = document.querySelector('input[name="js_base_path"]').value;
12 const loaders = document.querySelectorAll('.loading-input'); 59 const loaders = document.querySelectorAll('.loading-input');
60
61 /*
62 * METADATA FOR EDIT BOOKMARK PAGE
63 */
13 const inputTitle = document.querySelector('input[name="lf_title"]'); 64 const inputTitle = document.querySelector('input[name="lf_title"]');
14 if (inputTitle != null && inputTitle.value.length > 0) { 65 if (inputTitle != null) {
15 clearLoaders(loaders); 66 if (inputTitle.value.length > 0) {
16 return; 67 clearLoaders(loaders);
17 } 68 return;
69 }
18 70
19 const url = document.querySelector('input[name="lf_url"]').value; 71 const url = document.querySelector('input[name="lf_url"]').value;
20 const basePath = document.querySelector('input[name="js_base_path"]').value;
21 72
22 const xhr = new XMLHttpRequest(); 73 const xhr = new XMLHttpRequest();
23 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); 74 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
24 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 75 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
25 xhr.onload = () => { 76 xhr.onload = () => {
26 const result = JSON.parse(xhr.response); 77 const result = JSON.parse(xhr.response);
27 Object.keys(result).forEach((key) => { 78 Object.keys(result).forEach((key) => {
28 if (result[key] !== null && result[key].length) { 79 if (result[key] !== null && result[key].length) {
29 const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); 80 const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
30 if (element != null && element.value.length === 0) { 81 if (element != null && element.value.length === 0) {
31 element.value = he.decode(result[key]); 82 element.value = he.decode(result[key]);
83 }
32 } 84 }
33 } 85 });
34 }); 86 clearLoaders(loaders);
35 clearLoaders(loaders); 87 };
36 };
37 88
38 xhr.send(); 89 xhr.send();
90 }
91
92 /*
93 * METADATA FOR THUMBNAIL RETRIEVAL
94 */
95 const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]');
96 if (thumbsToLoad != null) {
97 [...thumbsToLoad].forEach((divElement) => {
98 const { id } = divElement.closest('[data-id]').dataset;
99
100 updateThumb(basePath, divElement, id);
101 });
102 }
39})(); 103})();
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
index 4c7ae4c0..4c1ae25d 100644
--- a/tests/bookmark/BookmarkTest.php
+++ b/tests/bookmark/BookmarkTest.php
@@ -347,4 +347,48 @@ class BookmarkTest extends TestCase
347 $bookmark->deleteTag('nope'); 347 $bookmark->deleteTag('nope');
348 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); 348 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
349 } 349 }
350
351 /**
352 * Test shouldUpdateThumbnail() with bookmarks needing an update.
353 */
354 public function testShouldUpdateThumbnail(): void
355 {
356 $bookmark = (new Bookmark())->setUrl('http://domain.tld/with-image');
357
358 static::assertTrue($bookmark->shouldUpdateThumbnail());
359
360 $bookmark = (new Bookmark())
361 ->setUrl('http://domain.tld/with-image')
362 ->setThumbnail('unknown file')
363 ;
364
365 static::assertTrue($bookmark->shouldUpdateThumbnail());
366 }
367
368 /**
369 * Test shouldUpdateThumbnail() with bookmarks that should not update.
370 */
371 public function testShouldNotUpdateThumbnail(): void
372 {
373 $bookmark = (new Bookmark());
374
375 static::assertFalse($bookmark->shouldUpdateThumbnail());
376
377 $bookmark = (new Bookmark())
378 ->setUrl('ftp://domain.tld/other-protocol', ['ftp'])
379 ;
380
381 static::assertFalse($bookmark->shouldUpdateThumbnail());
382
383 $bookmark = (new Bookmark())
384 ->setUrl('http://domain.tld/with-image')
385 ->setThumbnail(__FILE__)
386 ;
387
388 static::assertFalse($bookmark->shouldUpdateThumbnail());
389
390 $bookmark = (new Bookmark())->setUrl('/shaare/abcdef');
391
392 static::assertFalse($bookmark->shouldUpdateThumbnail());
393 }
350} 394}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
index 4fd88480..eafa54eb 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
@@ -144,12 +144,14 @@ class DisplayCreateFormTest extends TestCase
144 144
145 // Make sure that PluginManager hook is triggered 145 // Make sure that PluginManager hook is triggered
146 $this->container->pluginManager 146 $this->container->pluginManager
147 ->expects(static::at(0)) 147 ->expects(static::atLeastOnce())
148 ->method('executeHooks') 148 ->method('executeHooks')
149 ->withConsecutive(['render_editlink'], ['render_includes'])
149 ->willReturnCallback(function (string $hook, array $data): array { 150 ->willReturnCallback(function (string $hook, array $data): array {
150 static::assertSame('render_editlink', $hook); 151 if ('render_editlink' === $hook) {
151 static::assertSame('', $data['link']['title']); 152 static::assertSame('', $data['link']['title']);
152 static::assertSame('', $data['link']['description']); 153 static::assertSame('', $data['link']['description']);
154 }
153 155
154 return $data; 156 return $data;
155 }) 157 })
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
index 37542c26..1adeef5a 100644
--- a/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
+++ b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
@@ -209,7 +209,7 @@ class SaveBookmarkTest extends TestCase
209 /** 209 /**
210 * Test save a bookmark - try to retrieve the thumbnail 210 * Test save a bookmark - try to retrieve the thumbnail
211 */ 211 */
212 public function testSaveBookmarkWithThumbnail(): void 212 public function testSaveBookmarkWithThumbnailSync(): void
213 { 213 {
214 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash']; 214 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
215 215
@@ -224,7 +224,13 @@ class SaveBookmarkTest extends TestCase
224 224
225 $this->container->conf = $this->createMock(ConfigManager::class); 225 $this->container->conf = $this->createMock(ConfigManager::class);
226 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { 226 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
227 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 227 if ($key === 'thumbnails.mode') {
228 return Thumbnailer::MODE_ALL;
229 } elseif ($key === 'general.enable_async_metadata') {
230 return false;
231 }
232
233 return $default;
228 }); 234 });
229 235
230 $this->container->thumbnailer = $this->createMock(Thumbnailer::class); 236 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
@@ -275,6 +281,51 @@ class SaveBookmarkTest extends TestCase
275 } 281 }
276 282
277 /** 283 /**
284 * Test save a bookmark - do not attempt to retrieve thumbnails if async mode is enabled.
285 */
286 public function testSaveBookmarkWithThumbnailAsync(): void
287 {
288 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
289
290 $request = $this->createMock(Request::class);
291 $request
292 ->method('getParam')
293 ->willReturnCallback(function (string $key) use ($parameters): ?string {
294 return $parameters[$key] ?? null;
295 })
296 ;
297 $response = new Response();
298
299 $this->container->conf = $this->createMock(ConfigManager::class);
300 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
301 if ($key === 'thumbnails.mode') {
302 return Thumbnailer::MODE_ALL;
303 } elseif ($key === 'general.enable_async_metadata') {
304 return true;
305 }
306
307 return $default;
308 });
309
310 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
311 $this->container->thumbnailer->expects(static::never())->method('get');
312
313 $this->container->bookmarkService
314 ->expects(static::once())
315 ->method('addOrSet')
316 ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
317 static::assertNull($bookmark->getThumbnail());
318
319 return $bookmark;
320 })
321 ;
322
323 $result = $this->controller->save($request, $response);
324
325 static::assertSame(302, $result->getStatusCode());
326 }
327
328 /**
278 * Change the password with a wrong existing password 329 * Change the password with a wrong existing password
279 */ 330 */
280 public function testSaveBookmarkFromBookmarklet(): void 331 public function testSaveBookmarkFromBookmarklet(): void
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php
index 0c95df97..5ca92507 100644
--- a/tests/front/controller/visitor/BookmarkListControllerTest.php
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -307,7 +307,13 @@ class BookmarkListControllerTest extends TestCase
307 $this->container->conf 307 $this->container->conf
308 ->method('get') 308 ->method('get')
309 ->willReturnCallback(function (string $key, $default) { 309 ->willReturnCallback(function (string $key, $default) {
310 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 310 if ($key === 'thumbnails.mode') {
311 return Thumbnailer::MODE_ALL;
312 } elseif ($key === 'general.enable_async_metadata') {
313 return false;
314 }
315
316 return $default;
311 }) 317 })
312 ; 318 ;
313 319
@@ -357,7 +363,13 @@ class BookmarkListControllerTest extends TestCase
357 $this->container->conf 363 $this->container->conf
358 ->method('get') 364 ->method('get')
359 ->willReturnCallback(function (string $key, $default) { 365 ->willReturnCallback(function (string $key, $default) {
360 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default; 366 if ($key === 'thumbnails.mode') {
367 return Thumbnailer::MODE_ALL;
368 } elseif ($key === 'general.enable_async_metadata') {
369 return false;
370 }
371
372 return $default;
361 }) 373 })
362 ; 374 ;
363 375
@@ -379,6 +391,47 @@ class BookmarkListControllerTest extends TestCase
379 } 391 }
380 392
381 /** 393 /**
394 * Test getting a permalink with thumbnail update with async setting: no update should run.
395 */
396 public function testThumbnailUpdateFromPermalinkAsync(): void
397 {
398 $request = $this->createMock(Request::class);
399 $response = new Response();
400
401 $this->container->loginManager = $this->createMock(LoginManager::class);
402 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
403
404 $this->container->conf = $this->createMock(ConfigManager::class);
405 $this->container->conf
406 ->method('get')
407 ->willReturnCallback(function (string $key, $default) {
408 if ($key === 'thumbnails.mode') {
409 return Thumbnailer::MODE_ALL;
410 } elseif ($key === 'general.enable_async_metadata') {
411 return true;
412 }
413
414 return $default;
415 })
416 ;
417
418 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
419 $this->container->thumbnailer->expects(static::never())->method('get');
420
421 $this->container->bookmarkService
422 ->expects(static::once())
423 ->method('findByHash')
424 ->willReturn((new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
425 ;
426 $this->container->bookmarkService->expects(static::never())->method('set');
427 $this->container->bookmarkService->expects(static::never())->method('save');
428
429 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
430
431 static::assertSame(200, $result->getStatusCode());
432 }
433
434 /**
382 * Trigger legacy controller in link list controller: permalink 435 * Trigger legacy controller in link list controller: permalink
383 */ 436 */
384 public function testLegacyControllerPermalink(): void 437 public function testLegacyControllerPermalink(): void
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index beab0eac..48cd9aad 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -135,8 +135,12 @@
135 135
136 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 136 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
137 <div class="linklist-item-title"> 137 <div class="linklist-item-title">
138 {if="$thumbnails_enabled && !empty($value.thumbnail)"} 138 {if="$thumbnails_enabled && $value.thumbnail !== false"}
139 <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"> 139 <div
140 class="linklist-item-thumbnail {if="$value.thumbnail === null"}hidden{/if}"
141 style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"
142 {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}
143 >
140 <div class="thumbnail"> 144 <div class="thumbnail">
141 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 145 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
142 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> 146 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
@@ -158,7 +162,7 @@
158 </div> 162 </div>
159 163
160 <h2> 164 <h2>
161 <a href="{$value.real_url}"> 165 <a href="{$value.real_url}" class="linklist-real-url">
162 {if="strpos($value.url, $value.shorturl) === false"} 166 {if="strpos($value.url, $value.shorturl) === false"}
163 <i class="fa fa-external-link" aria-hidden="true"></i> 167 <i class="fa fa-external-link" aria-hidden="true"></i>
164 {else} 168 {else}
@@ -308,5 +312,6 @@
308 312
309{include="page.footer"} 313{include="page.footer"}
310<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script> 314<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
315{if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
311</body> 316</body>
312</html> 317</html>
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html
index 00896eb5..90f5cf8f 100644
--- a/tpl/vintage/linklist.html
+++ b/tpl/vintage/linklist.html
@@ -77,10 +77,10 @@
77 {/if} 77 {/if}
78 <ul> 78 <ul>
79 {loop="$links"} 79 {loop="$links"}
80 <li{if="$value.class"} class="{$value.class}"{/if}> 80 <li{if="$value.class"} class="{$value.class}"{/if} data-id="{$value.id}">
81 <a id="{$value.shorturl}"></a> 81 <a id="{$value.shorturl}"></a>
82 {if="$thumbnails_enabled && !empty($value.thumbnail)"} 82 {if="$thumbnails_enabled && $value.thumbnail !== false"}
83 <div class="thumbnail"> 83 <div class="thumbnail" {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}>
84 <a href="{$value.real_url}"> 84 <a href="{$value.real_url}">
85 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 85 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
86 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy" 86 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
@@ -153,6 +153,7 @@
153 153
154 {include="page.footer"} 154 {include="page.footer"}
155<script src="{$asset_path}/js/thumbnails.min.js#"></script> 155<script src="{$asset_path}/js/thumbnails.min.js#"></script>
156{if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
156 157
157</body> 158</body>
158</html> 159</html>