diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2fef5ea
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# DuckDuckSocial
+A simple Firefox extension that appends Mastodon search result cards to a duckduckgo search result page.
+
+https://addons.mozilla.org/en-US/firefox/addon/duckducksocial/
+
+
+## Features
+- Appends Mastodon search result cards to a duckduckgo search result page.
+- Shows images and videos in the cards.
+- Customizable Date and Time format.
+- OAuth authentication with Mastodon API (currently web only)
+- Word Filter (optionally automatically remove terms from search term)
diff --git a/background.js b/background.js
index 3f712d4..2ee990b 100644
--- a/background.js
+++ b/background.js
@@ -1,8 +1,10 @@
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'searchMastodon') {
+ console.log('Searching Mastodon for:', message.searchTerm);
// Load saved settings from browser.storage.local
- browser.storage.local.get(['client_id', 'client_secret', 'access_token', 'apiKey', 'domain', 'numPosts'])
- .then(({ client_id, client_secret, access_token, apiKey, domain, numPosts = 5 }) => {
+ browser.storage.local.get(['client_id', 'client_secret', 'access_token', 'apiKey', 'domain', 'dateType', 'dropWords', 'numPosts'])
+ .then(({ client_id, client_secret, access_token, apiKey, domain, dateType, dropWords, numPosts = 5 }) => {
+ console.log('Loaded settings:', client_id, client_secret, access_token, apiKey, domain, dateType, dropWords, numPosts);
if ((!access_token && !apiKey) || !domain) {
sendResponse({ success: false, error: 'Missing access token, API key, or domain.' });
return;
@@ -35,9 +37,9 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
} else if (message.action === 'getSettings') {
- browser.storage.local.get(['domain', 'numPosts'])
- .then(({ domain, numPosts = 5 }) => {
- sendResponse({ domain, numPosts });
+ browser.storage.local.get(['domain', 'numPosts', 'dropWords', 'dateType'])
+ .then(({ domain, numPosts = 5, dropWords, dateType }) => {
+ sendResponse({ domain, numPosts, dropWords, dateType });
})
.catch(error => {
sendResponse({ success: false, error: 'Failed to retrieve settings.' });
@@ -50,7 +52,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Register the app and start the OAuth flow
registerApp(domain).then(appRegistration => {
- console.log('App registration successful:', appRegistration);
+ console.log('App registration successful');
appRegistrate = appRegistration;
return launchOAuthFlow(appRegistration, domain);
}).then(redirectUrl => {
@@ -146,9 +148,8 @@ async function exchangeCodeForToken(code, domain, appRegistration) {
// Validates the redirect URL
async function validate(redirectUrl, domain, appRegistration) {
try {
- console.log('Redirect URL:', redirectUrl);
-
if (redirectUrl) {
+ console.log('Generated Redirect URL');
const code = new URL(redirectUrl).searchParams.get('code');
if (code) {
@@ -164,4 +165,4 @@ async function validate(redirectUrl, domain, appRegistration) {
console.error('Validation error:', error);
throw error;
}
-}
+}
\ No newline at end of file
diff --git a/content.js b/content.js
index 7463e85..5b8283e 100644
--- a/content.js
+++ b/content.js
@@ -1,16 +1,30 @@
// Extract the search term from DuckDuckGo's URL
const urlParams = new URLSearchParams(window.location.search);
const searchTerm = urlParams.get('q');
-
+console.log("Search Term: ", searchTerm);
if (searchTerm) {
// Request the settings from the background script
browser.runtime.sendMessage({ action: 'getSettings' })
.then(settings => {
+ console.log("Settings: ", settings);
const mastodonDomain = settings.domain;
const numPosts = settings.numPosts;
-
-
- browser.runtime.sendMessage({ action: 'searchMastodon', searchTerm: searchTerm })
+ const dropWords = settings.dropWords;
+ const dateType = settings.dateType;
+
+ console.log("All settings: ", settings);
+ console.log("Mastodon Domain: ", mastodonDomain);
+ console.log("Number of Posts: ", numPosts);
+ console.log("Drop Words: ", dropWords);
+ console.log("Date Type: ", dateType);
+
+ // Parse dropWords as a list, then drop words from the search term that match any of the words in the list
+ const dropWordsList = dropWords.split(',').map(word => word.trim());
+
+ // Using Regex instead of a simple split in case someone wants to include the space or have phrases dropped
+ const replacedSearchTerm = searchTerm.replace(new RegExp(dropWordsList.join('|'), 'gi'), '');
+ console.log("Replaced Search Term: ", replacedSearchTerm);
+ browser.runtime.sendMessage({ action: 'searchMastodon', searchTerm: replacedSearchTerm })
.then(response => {
console.log(response);
if (response.success) {
@@ -24,10 +38,17 @@ if (searchTerm) {
var postDiv = document.createElement('div');
postDiv.className = 'duckducksocial-post';
+ var usernameWrapper = document.createElement('div');
+ usernameWrapper.className = 'duckducksocial-username-wrapper';
+
var username = document.createElement('p');
username.textContent = `@${post.account.acct}`;
- username.className = 'duckducksocial-username';
-
+ username.className = 'duckducksocial-username';
+ // Add
separator between username and content
+ var hr = document.createElement('hr');
+ hr.className = 'duckducksocial-hr';
+ usernameWrapper.appendChild(username);
+ usernameWrapper.appendChild(hr);
var sanitizedContent = DOMPurify.sanitize(post.content);
var content = document.createElement('p');
@@ -37,14 +58,40 @@ if (searchTerm) {
// Datetime
var datetime = document.createElement('p');
- datetime.textContent = new Date(post.created_at).toLocaleString();
+ // If the dateType is set to 'relative', use a relative date otherwise use an absolute date
+ if (dateType === 'relative') {
+ // Calculate time since post was created
+ var now = new Date();
+ var created = new Date(post.created_at);
+ var diff = now - created;
+ var seconds = Math.floor(diff / 1000);
+ var minutes = Math.floor(seconds / 60);
+ var hours = Math.floor(minutes / 60);
+ var days = Math.floor(hours / 24);
+ var months = Math.floor(days / 30);
+ var years = Math.floor(months / 12);
+ // Adjust how date is displayed depending on how long ago the post was created
+ if (years > 0) {
+ datetime.textContent = years + 'y';
+ } else if (days > 0) {
+ datetime.textContent = days + 'd';
+ } else if (hours > 0) {
+ datetime.textContent = hours + 'h';
+ } else if (minutes > 0) {
+ datetime.textContent = minutes + 'm';
+ } else {
+ datetime.textContent = seconds + 's';
+ }
+ } else {
+ datetime.textContent = new Date(post.created_at).toLocaleString();
+ }
datetime.className = 'duckducksocial-datetime';
var postHref = document.createElement('a');
postHref.href = `https://${mastodonDomain}/@${post.account.acct}/${post.id}`;
postHref.appendChild(postDiv);
- postDiv.appendChild(username);
+ postDiv.appendChild(usernameWrapper);
postDiv.appendChild(content);
if (post.media_attachments && post.media_attachments.length > 0) {
var mediaContainer = document.createElement('div');
diff --git a/manifest.json b/manifest.json
index d4c5245..2e9fd82 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "DuckDuckSocial",
- "version": "0.0.5",
+ "version": "0.0.6",
"description": "Adds a scrollable list of posts from the social web that match your DuckDuckGo search. Mobile support requires API key",
"icons": {
"512": "icons/border-512.png"
diff --git a/options.js b/options.js
index 050f2d6..07ed678 100644
--- a/options.js
+++ b/options.js
@@ -7,6 +7,12 @@ document.addEventListener('DOMContentLoaded', function() {
const toggleButton = document.getElementById('toggleAdvancedSettings');
const advancedSettings = document.querySelector('.advanced-settings');
+ // Advanced settings
+ dropWordsInput = document.getElementById('dropWords');
+
+ const dateConfig = document.getElementById('dateConfig');
+
+
// Load saved settings
loadSettings();
@@ -29,6 +35,7 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
+ console.log('Connecting to Mastodon: ', domain);
browser.runtime.sendMessage({
action: 'authorize',
domain: domain
@@ -48,11 +55,15 @@ document.addEventListener('DOMContentLoaded', function() {
const domain = domainInput.value;
const numPosts = numPostsInput.value;
const apiKey = apiKeyInput.value;
+ const dropWords = dropWordsInput.value;
+ const dateType = dateConfig.value;
browser.storage.local.set({
domain: domain,
numPosts: numPosts,
- apiKey: apiKey // Save the API key as well
+ apiKey: apiKey,
+ dropWords: dropWords,
+ dateType: dateType
}).then(() => {
updateMessage('Settings saved successfully!');
}).catch(error => {
@@ -62,14 +73,16 @@ document.addEventListener('DOMContentLoaded', function() {
}
function loadSettings() {
- browser.storage.local.get(['domain', 'numPosts', 'apiKey'])
- .then(({ domain, numPosts = 5, apiKey }) => {
+ browser.storage.local.get(['domain', 'numPosts', 'apiKey', 'dropWords', 'dateType'])
+ .then(({ domain, numPosts = 5, apiKey, dropWords, dateType }) => {
if (domain) domainInput.value = domain;
if (numPosts) numPostsInput.value = numPosts;
if (apiKey) {
apiKeyInput.value = apiKey;
advancedSettingsDiv.style.display = 'block';
}
+ if (dropWords) dropWordsInput.value = dropWords;
+ if (dateType) dateConfig.value = dateType;
})
.catch(error => {
console.error('Failed to load settings:', error);
diff --git a/settings.html b/settings.html
index 52d3409..723d992 100644
--- a/settings.html
+++ b/settings.html
@@ -20,6 +20,21 @@
.advanced-settings {
display: none;
}
+ button {
+ padding: 10px 20px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ cursor: pointer;
+ }
+
+ .dateConfigGroup {
+ display: flex;
+ justify-content: space-between;
+ }
+
+
+
@@ -30,11 +45,22 @@ DuckDuckSocial
-
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
index 6c82b0e..14be44f 100644
--- a/styles.css
+++ b/styles.css
@@ -36,12 +36,12 @@
border-radius: 8px;
box-shadow: 0 2px 8px var(--shadow-light);
height: 15em;
- width: 12.5em;
+ width: 12.5em;
background-color: var(--post-background-light);
transition: transform 0.3s ease, box-shadow 0.3s ease;
display: flex;
flex-direction: column;
- justify-content: space-between;
+ justify-content: flex-start;
padding: 5px;
overflow: hidden;
}
@@ -51,6 +51,11 @@
box-shadow: 0 4px 12px rgba(150, 150, 150, 0.5);
}
+.duckducksocial-username-wrapper {
+ position: relative;
+ height: 20px;
+}
+
.duckducksocial-username {
font-weight: bold;
margin: 0;
@@ -59,6 +64,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ line-height: 15px;
}
.duckducksocial-content {
@@ -68,9 +74,16 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
- -webkit-line-clamp: 3;
+ -webkit-line-clamp: 3; /* Limits the content to 3 lines when there's media */
margin: 3px 0;
- flex-grow: 1;
+ flex-grow: 1; /* Ensures the content grows to fill available space */
+ max-height: 4.5em; /* Adjust this to prevent cut-off */
+}
+
+.duckducksocial-post:not(.has-media) .duckducksocial-content {
+ -webkit-line-clamp: 10; /* Remove the line clamp if there's no media */
+ max-height: none;
+ flex-grow: 0.3;
}
.duckducksocial-datetime {
@@ -79,8 +92,10 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ position: absolute;
+ bottom: 5px;
+ right: 5px;
margin: 0;
- align-self: flex-end;
}
.duckducksocial-media-container {
@@ -91,7 +106,7 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
- margin: 5px 0;
+ margin: 18px 0;
}
.duckducksocial-media-image,
@@ -102,10 +117,17 @@
display: block;
}
+.duckducksocial-hr {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: 0;
+ height: 0.5px;
+}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
-
.duckducksocial-results {
border: 1px solid var(--border-color-dark);
background-color: var(--background-color-dark);
@@ -128,6 +150,10 @@
.duckducksocial-datetime {
color: var(--datetime-text-color-dark);
}
+
+ .duckducksocial-hr {
+ background-color: #555;
+ }
}
/* Remove underline from all links and nested elements */
@@ -135,7 +161,7 @@
text-decoration: none;
}
-.content a {
+.duckducksocial-content a {
color: lightblue;
text-decoration-line: underline;
}