Skip to content

Improve browser extension #375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
- Prevent articles to be read on scroll before initial scroll to top on page load.
- Force re-authentication before managing tokens.
- Allow users to change their passwords.
- Browser extension:
- Ask before resetting options.
- Can open articles and feeds details.
- Prevent extension popup to become too wide.
- Can test the options on the settings page.
- Support tag hierarchy.

## 24.12.6

Expand Down
2 changes: 1 addition & 1 deletion browser-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "legadilo-browser-extension",
"version": "0.1.0",
"version": "0.2.0",
"dependencies": {
"bootstrap": "^5.3.3",
"bootstrap5-tags": "^1.7.5",
Expand Down
5 changes: 5 additions & 0 deletions browser-extension/src/action.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
padding: 0.5rem;
}

#article-container,
#feed-container {
max-width: 300px;
}

#action-selector-container {
max-width: 300px;
padding: 0.5rem;
Expand Down
12 changes: 12 additions & 0 deletions browser-extension/src/action.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@
src="./vendor/bs-icons/bookmark-fill.svg"
alt="Unmark as for later" />
</button>
<a id="open-article-details"
class="btn btn-outline-secondary btn-sm"
role="button"
href="">
<img class="bi" src="./vendor/bs-icons/eye.svg" alt="Open article details" />
</a>
</div>
</div>
<div id="feed-container">
Expand Down Expand Up @@ -143,6 +149,12 @@ <h2 id="feed-title"></h2>
</select>
</div>
<button class="btn btn-primary btn-sm">Update</button>
<a id="open-feed-details"
class="btn btn-outline-secondary btn-sm"
role="button"
href="">
<img class="bi" src="./vendor/bs-icons/eye.svg" alt="Open feed" />
</a>
</form>
</div>
<div id="error-container" class="alert alert-danger">
Expand Down
39 changes: 27 additions & 12 deletions browser-extension/src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,35 @@ const hideLoader = () => {
document.querySelector("#loading-indicator-container").style.display = "none";
};

const createTagInstance = (element, tags, selectedTags) => {
const tagsHierarchy = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.sub_tags }), {});

return Tags.init(element, {
allowNew: true,
allowClear: true,
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
selected: selectedTags.map((tag) => tag.slug),
onSelectItem(item, instance) {
if (!Array.isArray(tagsHierarchy[item.value])) {
return;
}

const alreadyAddedItems = instance.getSelectedValues();
tagsHierarchy[item.value]
.filter((tag) => !alreadyAddedItems.includes(tag.slug))
.forEach((tag) => instance.addItem(tag.title, tag.slug));
},
});
};

const displayArticle = (article, tags) => {
document.querySelector("#article-container").style.display = "block";

document.querySelector("#saved-article-title").value = article.title;
document.querySelector("#saved-article-reading-time").value = article.reading_time;

if (articleTagsInstance === null) {
articleTagsInstance = Tags.init("#saved-article-tags", {
allowNew: true,
allowClear: true,
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
selected: article.tags.map((tag) => tag.slug),
});
articleTagsInstance = createTagInstance("#saved-article-tags", tags, article.tags);
}

if (article.is_read) {
Expand All @@ -160,6 +176,8 @@ const displayArticle = (article, tags) => {
document.querySelector("#mark-article-as-for-later").style.display = "block";
document.querySelector("#unmark-article-as-for-later").style.display = "none";
}

document.querySelector("#open-article-details").href = article.details_url;
};

const hideArticle = () => {
Expand All @@ -173,6 +191,8 @@ const displayFeed = (feed, tags, categories) => {
document.querySelector("#feed-refresh-delay").value = feed.refresh_delay;
document.querySelector("#feed-article-retention-time").value = feed.article_retention_time;

document.querySelector("#open-feed-details").href = feed.details_url;

const categorySelector = document.querySelector("#feed-category");
// Clean all existing choices.
categorySelector.innerHTML = "";
Expand All @@ -189,12 +209,7 @@ const displayFeed = (feed, tags, categories) => {
categorySelector.value = feed.category ? feed.category.id : "";

if (feedTagsInstance === null) {
feedTagsInstance = Tags.init("#feed-tags", {
allowNew: true,
allowClear: true,
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
selected: feed.tags.map((tag) => tag.slug),
});
feedTagsInstance = createTagInstance("#feed-tags", tags, feed.tags);
}

document.querySelector("#update-feed-form").addEventListener("submit", (event) => {
Expand Down
21 changes: 21 additions & 0 deletions browser-extension/src/legadilo.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ export const DEFAULT_OPTIONS = {
accessToken: "",
};

export const testCredentials = async ({ instanceUrl, userEmail, tokenId, tokenSecret }) => {
try {
const resp = await fetch(`${instanceUrl}/api/users/tokens/`, {
"Content-Type": "application/json",
method: "POST",
body: JSON.stringify({
email: userEmail,
application_token_uuid: tokenId,
application_token_secret: tokenSecret,
}),
});
await resp.json();

return resp.status === 200;
} catch (error) {
console.error(error);

return false;
}
};

export const saveArticle = async ({ link, title, content }) => {
if (!/^https?:\/\//.test(link)) {
throw new Error("Invalid url");
Expand Down
2 changes: 1 addition & 1 deletion browser-extension/src/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Legadilo",
"version": "0.1.0",
"version": "0.2.0",

"description": "Open the legadilo browser extension to easily subscribe to feeds & save articles in Legadilo",

Expand Down
21 changes: 18 additions & 3 deletions browser-extension/src/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<meta charset="utf-8" />
<title>Legadilo Options</title>
<link rel="stylesheet" type="text/css" href="./vendor/bootstrap.css" />
<script src="./vendor/bootstrap.js"></script>
</head>
<body>
<form id="options-form">
Expand Down Expand Up @@ -48,10 +47,26 @@
type="text"
required />
</div>
<button id="reset-options" class="btn btn-danger" type="button">Reset to default</button>
<button type="submit" class="btn btn-primary">Save</button>
<div class="d-flex justify-content-between">
<div>
<button id="test-options" class="btn btn-outline-secondary" type="button">Test</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
<div>
<button id="reset-options" class="btn btn-danger" type="button">Reset to default</button>
</div>
</div>
</form>
<p id="status"></p>
<dialog id="confirm-dialog">
<div class="modal-header">
<h2 class="modal-title">Do you confirm?</h2>
</div>
<button id="confirm-dialog-cancel-btn"
class="btn btn-outline-secondary"
type="button">No</button>
<button id="confirm-dialog-confirm-btn" class="btn btn-danger" type="button">Yes</button>
</dialog>
<script src="./options.js" type="module"></script>
</body>
</html>
60 changes: 56 additions & 4 deletions browser-extension/src/options.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DEFAULT_OPTIONS, loadOptions, storeOptions } from "./legadilo.js";
import { DEFAULT_OPTIONS, loadOptions, storeOptions, testCredentials } from "./legadilo.js";

/**
* @param event {SubmitEvent}
Expand All @@ -15,8 +15,12 @@ const saveOptions = async (event) => {
});

// Update status to let user know options were saved.
displayMessage("Options saved.");
};

const displayMessage = (text) => {
const status = document.getElementById("status");
status.textContent = "Options saved.";
status.textContent = text;
setTimeout(() => {
status.textContent = "";
}, 750);
Expand All @@ -35,10 +39,58 @@ const setOptions = (options = {}) => {
document.getElementById("token-secret").value = options.tokenSecret;
};

const resetOptions = () => {
setOptions(DEFAULT_OPTIONS);
const resetOptions = async () => {
if (await askConfirmation()) {
setOptions(DEFAULT_OPTIONS);
}
};

const testOptions = async () => {
const data = new FormData(document.getElementById("options-form"));

if (
await testCredentials({
instanceUrl: data.get("instance-url"),
userEmail: data.get("user-email"),
tokenId: data.get("token-id"),
tokenSecret: data.get("token-secret"),
})
) {
displayMessage("Instance URL and token are valid");
} else {
displayMessage("Failed to connect with supplied URL and tokens");
}
};

const askConfirmation = () => {
const confirmDialog = document.getElementById("confirm-dialog");

let resolveDeferred;
const deferred = new Promise((resolve) => (resolveDeferred = resolve));
const cancelBtn = document.getElementById("confirm-dialog-cancel-btn");
const confirmBtn = document.getElementById("confirm-dialog-confirm-btn");
const cancel = () => {
confirmDialog.close();
resolveDeferred(false);
cancelBtn.removeEventListener("click", cancel);
confirmBtn.removeEventListener("click", confirm);
};
const confirm = () => {
confirmDialog.close();
resolveDeferred(true);
cancelBtn.removeEventListener("click", cancel);
confirmBtn.removeEventListener("click", confirm);
};

cancelBtn.addEventListener("click", cancel);
confirmBtn.addEventListener("click", confirm);

confirmDialog.showModal();

return deferred;
};

document.addEventListener("DOMContentLoaded", restoreOptions);
document.getElementById("options-form").addEventListener("submit", saveOptions);
document.getElementById("reset-options").addEventListener("click", resetOptions);
document.getElementById("test-options").addEventListener("click", testOptions);
10 changes: 10 additions & 0 deletions browser-extension/src/vendor/bs-icons/eye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions legadilo/feeds/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from asgiref.sync import sync_to_async
from django.db import IntegrityError, transaction
from django.shortcuts import aget_object_or_404
from django.urls import reverse
from ninja import ModelSchema, Router, Schema
from ninja.errors import ValidationError as NinjaValidationError
from ninja.pagination import paginate
Expand Down Expand Up @@ -61,6 +62,12 @@ class Meta:
class OutFeedSchema(ModelSchema):
category: OutFeedCategorySchema | None
tags: list[OutTagSchema]
details_url: str

@staticmethod
def resolve_details_url(obj, context) -> str:
url = reverse("feeds:feed_articles", kwargs={"feed_id": obj.id, "feed_slug": obj.slug})
return context["request"].build_absolute_uri(url)

class Meta:
model = Feed
Expand Down
4 changes: 2 additions & 2 deletions legadilo/feeds/models/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def update_feed(self, feed: Feed, feed_metadata: FeedData):
articles = [
article for article in feed_metadata.articles if article.link not in deleted_feed_links
]
created_articles = Article.objects.update_or_create_from_articles_list(
save_result = Article.objects.save_from_list_of_data(
feed.user,
articles,
feed.tags.all(),
Expand All @@ -298,7 +298,7 @@ def update_feed(self, feed: Feed, feed_metadata: FeedData):
feed=feed,
)
FeedArticle.objects.bulk_create(
[FeedArticle(article=article, feed=feed) for article in created_articles],
[FeedArticle(article=result.article, feed=feed) for result in save_result],
ignore_conflicts=True,
)

Expand Down
1 change: 1 addition & 0 deletions legadilo/feeds/tests/snapshots/test_api/test_get/feed.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"article_retention_time": 0,
"category": null,
"description": "",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"article_retention_time": 0,
"category": null,
"description": "",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"article_retention_time": 0,
"category": null,
"description": "",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"title": "Category title"
},
"description": "Some feed description",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"article_retention_time": 0,
"category": null,
"description": "Some feed description",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"article_retention_time": 0,
"category": null,
"description": "",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"title": "Category title"
},
"description": "",
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
"disabled_at": null,
"disabled_reason": "",
"enabled": true,
Expand Down
Loading
Loading