Character count in firefox plugin
Build LinkLog Development Image / development-image (push) Successful in 8s
Build LinkLog Development Image / development-image (push) Successful in 8s
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* The feed now loads a user-selectable number of entries per page (default 25, configurable via `LINKLOG_FRONTEND_LOADPOSTS`) with Previous/Next and numbered page navigation
|
||||
* Moved tag/user filtering, search, and sorting to the backend, so the feed API returns only the matching, paginated results
|
||||
* The New Entry page shows a live character count and warns when the assembled post would exceed the configurable `LINKLOG_MAX_POST_CHARACTERS` limit (default 500), to help avoid failed Mastodon posts
|
||||
* The Firefox extension popup now shows the same live character count and over-limit warning as the New Entry page (version 0.3.0)
|
||||
|
||||
### Fixed
|
||||
* Restored tag and user filtering on the feed: the decorative header arc no longer intercepts clicks on the filter dropdowns
|
||||
|
||||
@@ -1610,4 +1610,10 @@ In the new-entry form, provide a warning when the total ammount of characters is
|
||||
|
||||
### Assistant outcome
|
||||
Added a `LINKLOG_MAX_POST_CHARACTERS` setting (default 500) exposed via `GET /api/public/config`. The New Entry page now shows a live `used/limit characters` counter that recalculates on every relevant input (title, URL, comment, tags, checkbox toggles) by assembling the estimated Mastodon post text the same way the backend's Mastodon plugin does, and switches to a warning style when the limit is exceeded. Verified with the full backend test suite (56 passed) and a live smoke test of `/api/public/config` and the rendered New Entry page.
|
||||
|
||||
### User
|
||||
Implement the same functionality in the firefox plugin
|
||||
|
||||
### Assistant outcome
|
||||
Added the same live character-count warning to the Firefox extension popup: it fetches `/api/public/config` for the limit and the user's `/api/user/plugins/mastodon` post prefix, estimates the assembled Mastodon post length the same way as the New Entry page and backend plugin, and shows a localized `used/limit characters` counter (with an over-limit warning message) that updates on every relevant input and tag toggle. Added the `characterCount`/`characterCountOverLimit` translations to all five locale files (en-US, de, es, fr, nl) and verified all locale JSON files parse and `popup.js` passes a Node syntax check.
|
||||
# Chat Log
|
||||
|
||||
@@ -314,6 +314,7 @@ If the user uses the filters and/or search in the toolbox then those should limi
|
||||
259. Make sure the CHANGELOG reflects all changes since commit 74b2c400c6
|
||||
260. Add a skill to maintain the changelog.md
|
||||
261. In the new-entry form, provide a warning when the total ammount of characters is over LINKLOG_MAX_POST_CHARACTERS=500 (also indicat the number of characters used/500 count). This is to prevent Mastodon posts from failing - so all characters should be counted.
|
||||
262. Implement the same functionality in the firefox plugin
|
||||
|
||||
## Future entries
|
||||
|
||||
|
||||
Binary file not shown.
@@ -53,5 +53,7 @@
|
||||
"loginFailed": {"message": "Anmeldung fehlgeschlagen"},
|
||||
"loggedInSuccessfully": {"message": "Erfolgreich angemeldet"},
|
||||
"unableToLogIn": {"message": "Anmeldung nicht möglich. Überprüfe Backend-URL und Zugangsdaten."},
|
||||
"signedOut": {"message": "Abgemeldet"}
|
||||
"signedOut": {"message": "Abgemeldet"},
|
||||
"characterCount": {"message": "$USED$/$LIMIT$ Zeichen", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
|
||||
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ Zeichen - überschreitet das Mastodon-Beitragslimit um $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
|
||||
}
|
||||
|
||||
@@ -181,5 +181,30 @@
|
||||
},
|
||||
"signedOut": {
|
||||
"message": "Signed out"
|
||||
},
|
||||
"characterCount": {
|
||||
"message": "$USED$/$LIMIT$ characters",
|
||||
"placeholders": {
|
||||
"used": {
|
||||
"content": "$1"
|
||||
},
|
||||
"limit": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"characterCountOverLimit": {
|
||||
"message": "$USED$/$LIMIT$ characters - exceeds the Mastodon post limit by $OVER$",
|
||||
"placeholders": {
|
||||
"used": {
|
||||
"content": "$1"
|
||||
},
|
||||
"limit": {
|
||||
"content": "$2"
|
||||
},
|
||||
"over": {
|
||||
"content": "$3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,5 +53,7 @@
|
||||
"loginFailed": {"message": "Error de inicio de sesión"},
|
||||
"loggedInSuccessfully": {"message": "Sesión iniciada correctamente"},
|
||||
"unableToLogIn": {"message": "No se pudo iniciar sesión. Comprueba la URL y las credenciales."},
|
||||
"signedOut": {"message": "Sesión cerrada"}
|
||||
"signedOut": {"message": "Sesión cerrada"},
|
||||
"characterCount": {"message": "$USED$/$LIMIT$ caracteres", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
|
||||
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ caracteres: supera el límite de publicación de Mastodon en $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,7 @@
|
||||
"loginFailed": {"message": "Échec de la connexion"},
|
||||
"loggedInSuccessfully": {"message": "Connexion réussie"},
|
||||
"unableToLogIn": {"message": "Connexion impossible. Vérifiez l’URL du serveur et vos identifiants."},
|
||||
"signedOut": {"message": "Déconnecté"}
|
||||
"signedOut": {"message": "Déconnecté"},
|
||||
"characterCount": {"message": "$USED$/$LIMIT$ caractères", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
|
||||
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ caractères : dépasse la limite de publication Mastodon de $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,7 @@
|
||||
"loginFailed": {"message": "Inloggen mislukt"},
|
||||
"loggedInSuccessfully": {"message": "Succesvol ingelogd"},
|
||||
"unableToLogIn": {"message": "Inloggen mislukt. Controleer de backend-URL en inloggegevens."},
|
||||
"signedOut": {"message": "Uitgelogd"}
|
||||
"signedOut": {"message": "Uitgelogd"},
|
||||
"characterCount": {"message": "$USED$/$LIMIT$ tekens", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
|
||||
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ tekens - overschrijdt de Mastodon-postlimiet met $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extensionName__",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "__MSG_extensionDescription__",
|
||||
"default_locale": "en-US",
|
||||
"permissions": [
|
||||
|
||||
@@ -149,6 +149,18 @@ button {
|
||||
color: #f38ba8;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
margin: -6px 0 10px;
|
||||
color: #a6adc8;
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.char-count.over-limit {
|
||||
color: #f38ba8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-session {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #585b70;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<span data-i18n="commentLabel">Comment</span>
|
||||
<textarea id="comment" name="comment" rows="3" data-i18n-placeholder="commentPlaceholder" placeholder="Your comment"></textarea>
|
||||
</label>
|
||||
<div id="character-count" class="char-count" aria-live="polite"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend data-i18n="tagsLabel">Tags</legend>
|
||||
|
||||
+82
-1
@@ -14,10 +14,14 @@ const authWarning = document.getElementById('auth-warning');
|
||||
const warningSettingsButton = document.getElementById('warning-settings');
|
||||
const authSession = document.getElementById('auth-session');
|
||||
const refetchTitleButton = document.getElementById('refetch-title');
|
||||
const characterCountEl = document.getElementById('character-count');
|
||||
const sessionStore = browser.storage.session;
|
||||
|
||||
const t = window.linklogI18n;
|
||||
|
||||
let maxPostCharacters = 500;
|
||||
let mastodonPostPrefix = 'From my #LinkLog: ';
|
||||
|
||||
function normalizeBackendOrigin(backendUrl) {
|
||||
try {
|
||||
const url = new URL(backendUrl);
|
||||
@@ -192,9 +196,11 @@ async function loadExistingTags() {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = tag;
|
||||
checkbox.addEventListener('change', updateCharacterCount);
|
||||
label.append(checkbox, document.createTextNode(` ${tag}`));
|
||||
return label;
|
||||
}));
|
||||
updateCharacterCount();
|
||||
}
|
||||
|
||||
function getTags() {
|
||||
@@ -203,6 +209,79 @@ function getTags() {
|
||||
return [...new Set([...selected, ...newTags])].slice(0, 10);
|
||||
}
|
||||
|
||||
// Load public app settings (e.g. the Mastodon post length limit) once a backend is configured.
|
||||
async function loadConfig() {
|
||||
const settings = await getSettings();
|
||||
if (!settings.backendUrl || !(await hasBackendPermission(settings.backendUrl))) return;
|
||||
try {
|
||||
const response = await fetch(`${settings.backendUrl}/api/public/config`);
|
||||
if (!response.ok) return;
|
||||
const config = await response.json();
|
||||
if (config.max_post_characters) maxPostCharacters = config.max_post_characters;
|
||||
} catch (error) {
|
||||
// Character-count warning is advisory; ignore config load failures.
|
||||
}
|
||||
updateCharacterCount();
|
||||
}
|
||||
|
||||
// Load the user's configured Mastodon post prefix, so the estimated post length matches the server.
|
||||
async function loadMastodonPrefix() {
|
||||
const settings = await getSettings();
|
||||
if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl))) return;
|
||||
try {
|
||||
const response = await fetch(`${settings.backendUrl}/api/user/plugins/mastodon`, {
|
||||
headers: {'Authorization': `Bearer ${settings.accessToken}`},
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const config = await response.json();
|
||||
if (config.post_prefix) {
|
||||
mastodonPostPrefix = config.post_prefix;
|
||||
} else if (config.hashtag) {
|
||||
mastodonPostPrefix = `#${String(config.hashtag).trim().replace(/^#/, '')} `;
|
||||
}
|
||||
} catch (error) {
|
||||
// Character-count warning is advisory; ignore config load failures.
|
||||
}
|
||||
updateCharacterCount();
|
||||
}
|
||||
|
||||
// Format a UTC timestamp the same way the Mastodon plugin does, so the estimated post length matches the server.
|
||||
function formatMastodonTimestamp(date) {
|
||||
const months = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
return `${date.getUTCFullYear()} ${months[date.getUTCMonth()]} ${pad(date.getUTCDate())} - ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
|
||||
}
|
||||
|
||||
// Estimate the assembled Mastodon post length, mirroring backend/app/services/plugin_manager.py.
|
||||
function estimatePostLength() {
|
||||
const title = titleInput.value.trim();
|
||||
const comment = commentInput.value.trim();
|
||||
const url = removeKnownTrackingParams(urlInput.value.trim());
|
||||
const parts = [mastodonPostPrefix.trim()];
|
||||
if (title) parts.push(title);
|
||||
if (comment) parts.push(comment);
|
||||
if (title) parts.push(`Logged on ${formatMastodonTimestamp(new Date())} UTC from: ${url}`);
|
||||
const tags = getTags();
|
||||
if (tags.length) parts.push(tags.join(' '));
|
||||
return parts.join('\n\n').length;
|
||||
}
|
||||
|
||||
function updateCharacterCount() {
|
||||
const length = estimatePostLength();
|
||||
const overLimit = length > maxPostCharacters;
|
||||
characterCountEl.textContent = overLimit
|
||||
? t('characterCountOverLimit', [String(length), String(maxPostCharacters), String(length - maxPostCharacters)])
|
||||
: t('characterCount', [String(length), String(maxPostCharacters)]);
|
||||
characterCountEl.classList.toggle('over-limit', overLimit);
|
||||
}
|
||||
|
||||
[titleInput, urlInput, commentInput, newTagsInput].forEach((input) => {
|
||||
input.addEventListener('input', updateCharacterCount);
|
||||
});
|
||||
|
||||
function removeKnownTrackingParams(urlString) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
@@ -361,6 +440,8 @@ refetchTitleButton.addEventListener('click', (e) => {
|
||||
titleInput.value = '';
|
||||
fetchTitle(url, { force: true });
|
||||
});
|
||||
populateCurrentTab().then(checkExistingLink);
|
||||
populateCurrentTab().then(checkExistingLink).then(updateCharacterCount);
|
||||
loadExistingTags();
|
||||
updateFeedLink();
|
||||
loadConfig();
|
||||
loadMastodonPrefix();
|
||||
|
||||
Reference in New Issue
Block a user