Skip to content

Commit 105c058

Browse files
committed
Merge branch 'improve-browser-extension'
2 parents 5e44f30 + 15115af commit 105c058

File tree

38 files changed

+494
-156
lines changed

38 files changed

+494
-156
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22

33
## Unreleased
44

5+
## 25.01.1
6+
57
- Correct footer background on dark mode.
68
- Prevent articles to be read on scroll before initial scroll to top on page load.
79
- Force re-authentication before managing tokens.
810
- Allow users to change their passwords.
11+
- Browser extension:
12+
- Ask before resetting options.
13+
- Can open articles and feeds details.
14+
- Prevent extension popup to become too wide.
15+
- Can test the options on the settings page.
16+
- Support tag hierarchy.
917

1018
## 24.12.6
1119

browser-extension/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "legadilo-browser-extension",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"dependencies": {
55
"bootstrap": "^5.3.3",
66
"bootstrap5-tags": "^1.7.5",

browser-extension/src/action.css

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
padding: 0.5rem;
1111
}
1212

13+
#article-container,
14+
#feed-container {
15+
max-width: 300px;
16+
}
17+
1318
#action-selector-container {
1419
max-width: 300px;
1520
padding: 0.5rem;

browser-extension/src/action.html

+12
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@
9696
src="./vendor/bs-icons/bookmark-fill.svg"
9797
alt="Unmark as for later" />
9898
</button>
99+
<a id="open-article-details"
100+
class="btn btn-outline-secondary btn-sm"
101+
role="button"
102+
href="">
103+
<img class="bi" src="./vendor/bs-icons/eye.svg" alt="Open article details" />
104+
</a>
99105
</div>
100106
</div>
101107
<div id="feed-container">
@@ -143,6 +149,12 @@ <h2 id="feed-title"></h2>
143149
</select>
144150
</div>
145151
<button class="btn btn-primary btn-sm">Update</button>
152+
<a id="open-feed-details"
153+
class="btn btn-outline-secondary btn-sm"
154+
role="button"
155+
href="">
156+
<img class="bi" src="./vendor/bs-icons/eye.svg" alt="Open feed" />
157+
</a>
146158
</form>
147159
</div>
148160
<div id="error-container" class="alert alert-danger">

browser-extension/src/action.js

+27-12
Original file line numberDiff line numberDiff line change
@@ -124,19 +124,35 @@ const hideLoader = () => {
124124
document.querySelector("#loading-indicator-container").style.display = "none";
125125
};
126126

127+
const createTagInstance = (element, tags, selectedTags) => {
128+
const tagsHierarchy = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.sub_tags }), {});
129+
130+
return Tags.init(element, {
131+
allowNew: true,
132+
allowClear: true,
133+
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
134+
selected: selectedTags.map((tag) => tag.slug),
135+
onSelectItem(item, instance) {
136+
if (!Array.isArray(tagsHierarchy[item.value])) {
137+
return;
138+
}
139+
140+
const alreadyAddedItems = instance.getSelectedValues();
141+
tagsHierarchy[item.value]
142+
.filter((tag) => !alreadyAddedItems.includes(tag.slug))
143+
.forEach((tag) => instance.addItem(tag.title, tag.slug));
144+
},
145+
});
146+
};
147+
127148
const displayArticle = (article, tags) => {
128149
document.querySelector("#article-container").style.display = "block";
129150

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

133154
if (articleTagsInstance === null) {
134-
articleTagsInstance = Tags.init("#saved-article-tags", {
135-
allowNew: true,
136-
allowClear: true,
137-
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
138-
selected: article.tags.map((tag) => tag.slug),
139-
});
155+
articleTagsInstance = createTagInstance("#saved-article-tags", tags, article.tags);
140156
}
141157

142158
if (article.is_read) {
@@ -160,6 +176,8 @@ const displayArticle = (article, tags) => {
160176
document.querySelector("#mark-article-as-for-later").style.display = "block";
161177
document.querySelector("#unmark-article-as-for-later").style.display = "none";
162178
}
179+
180+
document.querySelector("#open-article-details").href = article.details_url;
163181
};
164182

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

194+
document.querySelector("#open-feed-details").href = feed.details_url;
195+
176196
const categorySelector = document.querySelector("#feed-category");
177197
// Clean all existing choices.
178198
categorySelector.innerHTML = "";
@@ -189,12 +209,7 @@ const displayFeed = (feed, tags, categories) => {
189209
categorySelector.value = feed.category ? feed.category.id : "";
190210

191211
if (feedTagsInstance === null) {
192-
feedTagsInstance = Tags.init("#feed-tags", {
193-
allowNew: true,
194-
allowClear: true,
195-
items: tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.title }), {}),
196-
selected: feed.tags.map((tag) => tag.slug),
197-
});
212+
feedTagsInstance = createTagInstance("#feed-tags", tags, feed.tags);
198213
}
199214

200215
document.querySelector("#update-feed-form").addEventListener("submit", (event) => {

browser-extension/src/legadilo.js

+21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ export const DEFAULT_OPTIONS = {
88
accessToken: "",
99
};
1010

11+
export const testCredentials = async ({ instanceUrl, userEmail, tokenId, tokenSecret }) => {
12+
try {
13+
const resp = await fetch(`${instanceUrl}/api/users/tokens/`, {
14+
"Content-Type": "application/json",
15+
method: "POST",
16+
body: JSON.stringify({
17+
email: userEmail,
18+
application_token_uuid: tokenId,
19+
application_token_secret: tokenSecret,
20+
}),
21+
});
22+
await resp.json();
23+
24+
return resp.status === 200;
25+
} catch (error) {
26+
console.error(error);
27+
28+
return false;
29+
}
30+
};
31+
1132
export const saveArticle = async ({ link, title, content }) => {
1233
if (!/^https?:\/\//.test(link)) {
1334
throw new Error("Invalid url");

browser-extension/src/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Legadilo",
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55

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

browser-extension/src/options.html

+18-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<meta charset="utf-8" />
55
<title>Legadilo Options</title>
66
<link rel="stylesheet" type="text/css" href="./vendor/bootstrap.css" />
7-
<script src="./vendor/bootstrap.js"></script>
87
</head>
98
<body>
109
<form id="options-form">
@@ -48,10 +47,26 @@
4847
type="text"
4948
required />
5049
</div>
51-
<button id="reset-options" class="btn btn-danger" type="button">Reset to default</button>
52-
<button type="submit" class="btn btn-primary">Save</button>
50+
<div class="d-flex justify-content-between">
51+
<div>
52+
<button id="test-options" class="btn btn-outline-secondary" type="button">Test</button>
53+
<button type="submit" class="btn btn-primary">Save</button>
54+
</div>
55+
<div>
56+
<button id="reset-options" class="btn btn-danger" type="button">Reset to default</button>
57+
</div>
58+
</div>
5359
</form>
5460
<p id="status"></p>
61+
<dialog id="confirm-dialog">
62+
<div class="modal-header">
63+
<h2 class="modal-title">Do you confirm?</h2>
64+
</div>
65+
<button id="confirm-dialog-cancel-btn"
66+
class="btn btn-outline-secondary"
67+
type="button">No</button>
68+
<button id="confirm-dialog-confirm-btn" class="btn btn-danger" type="button">Yes</button>
69+
</dialog>
5570
<script src="./options.js" type="module"></script>
5671
</body>
5772
</html>

browser-extension/src/options.js

+56-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DEFAULT_OPTIONS, loadOptions, storeOptions } from "./legadilo.js";
1+
import { DEFAULT_OPTIONS, loadOptions, storeOptions, testCredentials } from "./legadilo.js";
22

33
/**
44
* @param event {SubmitEvent}
@@ -15,8 +15,12 @@ const saveOptions = async (event) => {
1515
});
1616

1717
// Update status to let user know options were saved.
18+
displayMessage("Options saved.");
19+
};
20+
21+
const displayMessage = (text) => {
1822
const status = document.getElementById("status");
19-
status.textContent = "Options saved.";
23+
status.textContent = text;
2024
setTimeout(() => {
2125
status.textContent = "";
2226
}, 750);
@@ -35,10 +39,58 @@ const setOptions = (options = {}) => {
3539
document.getElementById("token-secret").value = options.tokenSecret;
3640
};
3741

38-
const resetOptions = () => {
39-
setOptions(DEFAULT_OPTIONS);
42+
const resetOptions = async () => {
43+
if (await askConfirmation()) {
44+
setOptions(DEFAULT_OPTIONS);
45+
}
46+
};
47+
48+
const testOptions = async () => {
49+
const data = new FormData(document.getElementById("options-form"));
50+
51+
if (
52+
await testCredentials({
53+
instanceUrl: data.get("instance-url"),
54+
userEmail: data.get("user-email"),
55+
tokenId: data.get("token-id"),
56+
tokenSecret: data.get("token-secret"),
57+
})
58+
) {
59+
displayMessage("Instance URL and token are valid");
60+
} else {
61+
displayMessage("Failed to connect with supplied URL and tokens");
62+
}
63+
};
64+
65+
const askConfirmation = () => {
66+
const confirmDialog = document.getElementById("confirm-dialog");
67+
68+
let resolveDeferred;
69+
const deferred = new Promise((resolve) => (resolveDeferred = resolve));
70+
const cancelBtn = document.getElementById("confirm-dialog-cancel-btn");
71+
const confirmBtn = document.getElementById("confirm-dialog-confirm-btn");
72+
const cancel = () => {
73+
confirmDialog.close();
74+
resolveDeferred(false);
75+
cancelBtn.removeEventListener("click", cancel);
76+
confirmBtn.removeEventListener("click", confirm);
77+
};
78+
const confirm = () => {
79+
confirmDialog.close();
80+
resolveDeferred(true);
81+
cancelBtn.removeEventListener("click", cancel);
82+
confirmBtn.removeEventListener("click", confirm);
83+
};
84+
85+
cancelBtn.addEventListener("click", cancel);
86+
confirmBtn.addEventListener("click", confirm);
87+
88+
confirmDialog.showModal();
89+
90+
return deferred;
4091
};
4192

4293
document.addEventListener("DOMContentLoaded", restoreOptions);
4394
document.getElementById("options-form").addEventListener("submit", saveOptions);
4495
document.getElementById("reset-options").addEventListener("click", resetOptions);
96+
document.getElementById("test-options").addEventListener("click", testOptions);
Loading

legadilo/feeds/api.py

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from asgiref.sync import sync_to_async
2323
from django.db import IntegrityError, transaction
2424
from django.shortcuts import aget_object_or_404
25+
from django.urls import reverse
2526
from ninja import ModelSchema, Router, Schema
2627
from ninja.errors import ValidationError as NinjaValidationError
2728
from ninja.pagination import paginate
@@ -61,6 +62,12 @@ class Meta:
6162
class OutFeedSchema(ModelSchema):
6263
category: OutFeedCategorySchema | None
6364
tags: list[OutTagSchema]
65+
details_url: str
66+
67+
@staticmethod
68+
def resolve_details_url(obj, context) -> str:
69+
url = reverse("feeds:feed_articles", kwargs={"feed_id": obj.id, "feed_slug": obj.slug})
70+
return context["request"].build_absolute_uri(url)
6471

6572
class Meta:
6673
model = Feed

legadilo/feeds/models/feed.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ def update_feed(self, feed: Feed, feed_metadata: FeedData):
284284
articles = [
285285
article for article in feed_metadata.articles if article.link not in deleted_feed_links
286286
]
287-
created_articles = Article.objects.update_or_create_from_articles_list(
287+
save_result = Article.objects.save_from_list_of_data(
288288
feed.user,
289289
articles,
290290
feed.tags.all(),
@@ -298,7 +298,7 @@ def update_feed(self, feed: Feed, feed_metadata: FeedData):
298298
feed=feed,
299299
)
300300
FeedArticle.objects.bulk_create(
301-
[FeedArticle(article=article, feed=feed) for article in created_articles],
301+
[FeedArticle(article=result.article, feed=feed) for result in save_result],
302302
ignore_conflicts=True,
303303
)
304304

legadilo/feeds/tests/snapshots/test_api/test_get/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"article_retention_time": 0,
33
"category": null,
44
"description": "",
5+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
56
"disabled_at": null,
67
"disabled_reason": "",
78
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_list/feeds.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"article_retention_time": 0,
66
"category": null,
77
"description": "",
8+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
89
"disabled_at": null,
910
"disabled_reason": "",
1011
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_subscribe_to_already_subscribed_feed/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"article_retention_time": 0,
33
"category": null,
44
"description": "",
5+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
56
"disabled_at": null,
67
"disabled_reason": "",
78
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_subscribe_to_feed/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"title": "Category title"
77
},
88
"description": "Some feed description",
9+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
910
"disabled_at": null,
1011
"disabled_reason": "",
1112
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_subscribe_to_feed_with_just_url/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"article_retention_time": 0,
33
"category": null,
44
"description": "Some feed description",
5+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
56
"disabled_at": null,
67
"disabled_reason": "",
78
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_unset_category/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"article_retention_time": 0,
33
"category": null,
44
"description": "",
5+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
56
"disabled_at": null,
67
"disabled_reason": "",
78
"enabled": true,

legadilo/feeds/tests/snapshots/test_api/test_update/feed.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"title": "Category title"
77
},
88
"description": "",
9+
"details_url": "https://example.com/feeds/articles/1-feed-slug/",
910
"disabled_at": null,
1011
"disabled_reason": "",
1112
"enabled": true,

0 commit comments

Comments
 (0)