Skip to content

Commit

Permalink
Speed up navigation (#824)
Browse files Browse the repository at this point in the history
* use client-side navigation

* update tests

* add setting for enabling link prefetching

* do not prefetch bookmark details

* theme progress bar

* cleanup behaviors

* update test
  • Loading branch information
sissbruecker authored Sep 14, 2024
1 parent 3ae9cf0 commit c929e8f
Show file tree
Hide file tree
Showing 29 changed files with 281 additions and 142 deletions.
10 changes: 7 additions & 3 deletions bookmarks/frontend/behaviors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ const mutationObserver = new MutationObserver((mutations) => {
});
});

mutationObserver.observe(document.body, {
childList: true,
subtree: true,
window.addEventListener("turbo:load", () => {
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});

applyBehaviors(document.body);
});

export class Behavior {
Expand Down
1 change: 1 addition & 0 deletions bookmarks/frontend/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@hotwired/turbo";
import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit";
import "./behaviors/confirm-button";
Expand Down
25 changes: 15 additions & 10 deletions bookmarks/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,33 @@ class CustomRemoteUserMiddleware(RemoteUserMiddleware):
header = settings.LD_AUTH_PROXY_USERNAME_HEADER


default_global_settings = GlobalSettings()

standard_profile = UserProfile()
standard_profile.enable_favicons = True


class UserProfileMiddleware:
class LinkdingMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
# add global settings to request
try:
global_settings = GlobalSettings.get()
except:
global_settings = default_global_settings
request.global_settings = global_settings

# add user profile to request
if request.user.is_authenticated:
request.user_profile = request.user.profile
else:
# check if a custom profile for guests exists, otherwise use standard profile
guest_profile = None
try:
global_settings = GlobalSettings.get()
if global_settings.guest_profile_user:
guest_profile = global_settings.guest_profile_user.profile
except:
pass

request.user_profile = guest_profile or standard_profile
if global_settings.guest_profile_user:
request.user_profile = global_settings.guest_profile_user.profile
else:
request.user_profile = standard_profile

response = self.get_response(request)

Expand Down
18 changes: 18 additions & 0 deletions bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2024-09-14 07:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("bookmarks", "0038_globalsettings_guest_profile_user"),
]

operations = [
migrations.AddField(
model_name="globalsettings",
name="enable_link_prefetch",
field=models.BooleanField(default=False),
),
]
3 changes: 2 additions & 1 deletion bookmarks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ class GlobalSettings(models.Model):
guest_profile_user = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, null=True, blank=True
)
enable_link_prefetch = models.BooleanField(default=False, null=False)

@classmethod
def get(cls):
Expand All @@ -532,7 +533,7 @@ def save(self, *args, **kwargs):
class GlobalSettingsForm(forms.ModelForm):
class Meta:
model = GlobalSettings
fields = ["landing_page", "guest_profile_user"]
fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]

def __init__(self, *args, **kwargs):
super(GlobalSettingsForm, self).__init__(*args, **kwargs)
Expand Down
7 changes: 6 additions & 1 deletion bookmarks/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ span.confirmation {
.divider {
border-bottom: solid 1px var(--secondary-border-color);
margin: var(--unit-5) 0;
}
}

/* Turbo progress bar */
.turbo-progress-bar {
background-color: var(--primary-color);
}
13 changes: 7 additions & 6 deletions bookmarks/templates/bookmarks/bookmark_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{% if bookmark_list.show_url %}
<div class="url-path truncate">
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="url-display">
class="url-display">
{{ bookmark_item.url }}
</a>
</div>
Expand Down Expand Up @@ -66,9 +66,9 @@
{% if bookmark_item.display_date %}
{% if bookmark_item.web_archive_snapshot_url %}
<a href="{{ bookmark_item.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}"
rel="noopener">
title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}"
rel="noopener">
{{ bookmark_item.display_date }}
</a>
{% else %}
Expand All @@ -79,8 +79,9 @@
{# View link is visible for both owned and shared bookmarks #}
{% if bookmark_list.show_view_action %}
<a ld-fetch="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}"
ld-on="click" ld-target="body|append"
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a>
ld-on="click" ld-target="body|append"
data-turbo-prefetch="false"
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a>
{% endif %}
{% if bookmark_item.is_editable %}
{# Bookmark owner actions #}
Expand Down
6 changes: 5 additions & 1 deletion bookmarks/templates/bookmarks/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
{% if request.user_profile.custom_css %}
<style>{{ request.user_profile.custom_css }}</style>
{% endif %}
<meta name="turbo-cache-control" content="no-preview">
{% if not request.global_settings.enable_link_prefetch %}
<meta name="turbo-prefetch" content="false">
{% endif %}
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</head>
<body ld-global-shortcuts>

Expand Down Expand Up @@ -129,6 +134,5 @@ <h1>LINKDING</h1>
{% block content %}
{% endblock %}
</div>
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions bookmarks/templates/bookmarks/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@

{# Replace search input with auto-complete component #}
<script type="application/javascript">
window.addEventListener("load", function () {
(function init() {
const currentTagsString = '{{ tags_string }}';
const currentTags = currentTagsString.split(' ');
const uniqueTags = [...new Set(currentTags)]
Expand All @@ -104,5 +104,5 @@
}
})
input.replaceWith(wrapper.firstElementChild);
});
})();
</script>
64 changes: 38 additions & 26 deletions bookmarks/templates/settings/general.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h2>Profile</h2>
<p>
<a href="{% url 'change_password' %}">Change password</a>
</p>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
<form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
Expand Down Expand Up @@ -247,7 +247,7 @@ <h2>Profile</h2>
{% if global_settings_form %}
<section class="content-area">
<h2>Global settings</h2>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
<form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %}
<div class="form-group">
<label for="{{ global_settings_form.landing_page.id_for_label }}" class="form-label">Landing page</label>
Expand All @@ -266,6 +266,16 @@ <h2>Global settings</h2>
a dedicated user for this purpose. By default, a standard profile with fixed settings is used.
</div>
</div>
<div class="form-group">
<label for="{{ global_settings_form.enable_link_prefetch.id_for_label }}" class="form-checkbox">
{{ global_settings_form.enable_link_prefetch }}
<i class="form-icon"></i> Enable prefetching links on hover
</label>
<div class="form-input-hint">
Prefetches internal links when hovering over them. This can improve the perceived performance when
navigating application, but also increases the load on the server as well as bandwidth usage.
</div>
</div>

<div class="form-group">
<input type="submit" name="update_global_settings" value="Save" class="btn btn-primary btn-wide mt-2">
Expand Down Expand Up @@ -306,7 +316,7 @@ <h2>Import</h2>
<section class="content-area">
<h2>Export</h2>
<p>Export all bookmarks in Netscape HTML format.</p>
<a class="btn btn-primary" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a>
<a class="btn btn-primary" target="_blank" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a>
{% if export_error %}
<div class="has-error">
<p class="form-input-hint">
Expand Down Expand Up @@ -344,35 +354,37 @@ <h2>About</h2>
</div>

<script>
const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}");
const bookmarkDescriptionDisplay = document.getElementById("{{ form.bookmark_description_display.id_for_label }}");
const bookmarkDescriptionMaxLines = document.getElementById("{{ form.bookmark_description_max_lines.id_for_label }}");
(function init() {
const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}");
const bookmarkDescriptionDisplay = document.getElementById("{{ form.bookmark_description_display.id_for_label }}");
const bookmarkDescriptionMaxLines = document.getElementById("{{ form.bookmark_description_max_lines.id_for_label }}");

// Automatically disable public bookmark sharing if bookmark sharing is disabled
function updatePublicSharing() {
if (enableSharing.checked) {
enablePublicSharing.disabled = false;
} else {
enablePublicSharing.disabled = true;
enablePublicSharing.checked = false;
// Automatically disable public bookmark sharing if bookmark sharing is disabled
function updatePublicSharing() {
if (enableSharing.checked) {
enablePublicSharing.disabled = false;
} else {
enablePublicSharing.disabled = true;
enablePublicSharing.checked = false;
}
}
}

updatePublicSharing();
enableSharing.addEventListener("change", updatePublicSharing);
updatePublicSharing();
enableSharing.addEventListener("change", updatePublicSharing);

// Automatically hide the bookmark description max lines input if the description display is set to inline
function updateBookmarkDescriptionMaxLines() {
if (bookmarkDescriptionDisplay.value === "inline") {
bookmarkDescriptionMaxLines.closest(".form-group").classList.add("d-hide");
} else {
bookmarkDescriptionMaxLines.closest(".form-group").classList.remove("d-hide");
// Automatically hide the bookmark description max lines input if the description display is set to inline
function updateBookmarkDescriptionMaxLines() {
if (bookmarkDescriptionDisplay.value === "inline") {
bookmarkDescriptionMaxLines.closest(".form-group").classList.add("d-hide");
} else {
bookmarkDescriptionMaxLines.closest(".form-group").classList.remove("d-hide");
}
}
}

updateBookmarkDescriptionMaxLines();
bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines);
updateBookmarkDescriptionMaxLines();
bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines);
})();
</script>

{% endblock %}
10 changes: 5 additions & 5 deletions bookmarks/templates/settings/integrations.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ <h2>REST API</h2>
<h2>RSS Feeds</h2>
<p>The following URLs provide RSS feeds for your bookmarks:</p>
<ul style="list-style-position: outside;">
<li><a href="{{ all_feed_url }}">All bookmarks</a></li>
<li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li>
<li><a href="{{ shared_feed_url }}">Shared bookmarks</a></li>
<li><a href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span>
<li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li>
<li><a target="_blank" href="{{ unread_feed_url }}">Unread bookmarks</a></li>
<li><a target="_blank" href="{{ shared_feed_url }}">Shared bookmarks</a></li>
<li><a target="_blank" href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span>
</li>
</ul>
<p>
Expand All @@ -80,7 +80,7 @@ <h2>RSS Feeds</h2>
credential.</strong>
Any party with access to these URLs can read all your bookmarks.
If you think that a URL was compromised you can delete the feed token for your user in the <a
href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
target="_blank" href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
After deleting the feed token, new URLs will be generated when you reload this settings page.
</p>
</section>
Expand Down
13 changes: 8 additions & 5 deletions bookmarks/tests/test_bookmark_archived_view_performance.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.contrib.auth.models import User
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS

from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin


Expand All @@ -20,9 +20,12 @@ def get_connection(self):
return connections[DEFAULT_DB_ALIAS]

def test_should_not_increase_number_of_queries_per_bookmark(self):
# create global settings
GlobalSettings.get()

# create initial bookmarks
num_initial_bookmarks = 10
for index in range(num_initial_bookmarks):
for _ in range(num_initial_bookmarks):
self.setup_bookmark(user=self.user, is_archived=True)

# capture number of queries
Expand All @@ -37,7 +40,7 @@ def test_should_not_increase_number_of_queries_per_bookmark(self):

# add more bookmarks
num_additional_bookmarks = 10
for index in range(num_additional_bookmarks):
for _ in range(num_additional_bookmarks):
self.setup_bookmark(user=self.user, is_archived=True)

# assert num queries doesn't increase
Expand Down
13 changes: 8 additions & 5 deletions bookmarks/tests/test_bookmark_index_view_performance.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.contrib.auth.models import User
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS

from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin


Expand All @@ -18,9 +18,12 @@ def get_connection(self):
return connections[DEFAULT_DB_ALIAS]

def test_should_not_increase_number_of_queries_per_bookmark(self):
# create global settings
GlobalSettings.get()

# create initial bookmarks
num_initial_bookmarks = 10
for index in range(num_initial_bookmarks):
for _ in range(num_initial_bookmarks):
self.setup_bookmark(user=self.user)

# capture number of queries
Expand All @@ -35,7 +38,7 @@ def test_should_not_increase_number_of_queries_per_bookmark(self):

# add more bookmarks
num_additional_bookmarks = 10
for index in range(num_additional_bookmarks):
for _ in range(num_additional_bookmarks):
self.setup_bookmark(user=self.user)

# assert num queries doesn't increase
Expand Down
Loading

0 comments on commit c929e8f

Please sign in to comment.