├── .DS_Store ├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── chrome-extension ├── background.js ├── manifest.json ├── popup.html └── popup_assets │ ├── icon16.png │ ├── icon24.png │ ├── icon32.png │ ├── icon32_notification.png │ └── icon_32.png ├── firefox-extension ├── Readability.js ├── manifest.json ├── popup.html └── popup_assets │ ├── icon16.png │ ├── icon24.png │ ├── icon32.png │ ├── icon32_notification.png │ └── icon_32.png ├── package-lock.json ├── package.json ├── shared ├── background.js ├── compromise.js ├── content-fetcher.js ├── content.js ├── lda.js ├── ranking.js ├── react │ ├── .DS_Store │ ├── components │ │ ├── AlgorithmEditor.js │ │ ├── App.js │ │ ├── ContentBlock.js │ │ ├── ContentFeed.js │ │ ├── DomainGraph.js │ │ ├── FeedSettings.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── TopicEditor.js │ │ └── TopicGraph.js │ ├── contentBranding.js │ ├── fonts │ │ ├── .DS_Store │ │ ├── Courgette-Regular.ttf │ │ ├── JosefinSlab.ttf │ │ ├── Merriweather-Regular.otf │ │ ├── Nexa-Bold.otf │ │ ├── Nexa-Light.otf │ │ ├── TheanoOldStyle-Regular.ttf │ │ └── WorkSans.ttf │ ├── images │ │ ├── .DS_Store │ │ ├── chrome_logo.png │ │ ├── chrome_logo_myalgo.png │ │ ├── icon_32.png │ │ ├── loading-spinner.svg │ │ ├── loading-wwt.svg │ │ └── wikipedia.svg │ ├── index.js │ └── styles │ │ ├── .DS_Store │ │ ├── _utils.scss │ │ ├── _variables.scss │ │ ├── algorithm-editor.scss │ │ ├── content-block.scss │ │ ├── content-feed.scss │ │ ├── domain-graph.scss │ │ ├── feed-settings.scss │ │ ├── footer.scss │ │ ├── header.scss │ │ ├── index.scss │ │ ├── topic-editor.scss │ │ └── topic-graph.scss └── storage.js ├── webpack.chrome.config.js └── webpack.firefox.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ 5 | "@babel/preset-react", 6 | { 7 | "runtime": "automatic" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jawerty 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Jared Wright 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # myAlgorithm (beta) 2 | 3 | Your own self hosted recommendation feed based on your browsing habits. 4 | 5 | myAlgorithm is a [chrome extension](https://chrome.google.com/webstore/detail/myalgorithm/imkkppomfljhnaaolbdgffnleejjbpjn?hl=en) ([firefox version now available](https://addons.mozilla.org/en-US/firefox/addon/myalgorithm/)) I made for myself to have more control over my recommendation feeds. It tracks and stores your browsing habits (searches, clicks, content engagements, text input) _locally_ and web scraped various search engines (right now google, duckduckgo, and yandex) with auto-generated search queries. 6 | 7 | As for privacy and security, the only server interactions made in the app are the web scraping routines (using fetch API) to make the search engine queries. Otherwise, all the data (tracking data and settings) is stored locally in browser to avoid any privacy concerns. 8 | 9 | This project is a work in progress and available for anyone to test in the meantime. 10 | 11 | If you like myAlgorithm and would like more tools to escape the Google algorithm donate here ---> https://www.buymeacoffee.com/bjGHFVW355 12 | In the near future, I plan on working on these projects full time. 13 | 14 | Here's the discord if you're interested in getting involved/contacting the developer (me) https://discord.gg/YmVzHUNfYd 15 | 16 | # Version 17 | 18 | 0.5.0 19 | 20 | ## Todo (upcoming features) 21 | 22 | - ~~Firefox version~~ (complete) 23 | - ~~Ability to add your own sources by domain~~ (complete) 24 | - ~~Topic reporting on Content items in feed~~ 25 | - More to come... 26 | 27 | (create GitHub issue or join discord if you have any feature requests) 28 | 29 | # Install 30 | 31 | [Download the Chrome extension here.](https://chrome.google.com/webstore/detail/myalgorithm/imkkppomfljhnaaolbdgffnleejjbpjn?hl=en&authuser=0) 32 | 33 | [Also available on Firefox](https://addons.mozilla.org/en-US/firefox/addon/myalgorithm/) 34 | 35 | ### If you want to run the developer version (most up to date) 36 | 37 | Fork the repo and then follow this tutorial on loading an unpacked extension in Chrome/Firefox 38 | 39 | [https://webkul.com/blog/how-to-install-the-unpacked-extension-in-chrome/](https://webkul.com/blog/how-to-install-the-unpacked-extension-in-chrome/) 40 | 41 | [https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/](https://blog.mozilla.org/addons/2015/12/23/loading-temporary-add-ons/) 42 | 43 | # How to use? 44 | 45 | Simply use your browser like normal. Go to news articles, watch videos, tweet, post. In realtime you'll be able to see topics relating to your usage in the extension popup. 46 | 47 | # The Content Feed 48 | 49 | You will get a daily feed of content (also an option to refresh at will) after 6am everyday. It will have roughly 10 pieces of content from on your allowed sources (youtube videos, twitter posts, reddit posts, etc.) based on your browsing habits 50 | 51 | Screen Shot 2022-07-17 at 11 38 30 AM 52 | 53 | # Algorithm Editor 54 | 55 | The Algorithm Editor is a dashboard to view and edit your recommendation algorithm. You can see two graphs detailing your overall browsing habits. Below you have the option to view, add and remove all of the topics that will be used in the web scraping routines (these are ranked by occurrences, ranking prorities and partially randomized) 56 | 57 | Screen Shot 2022-07-17 at 11 47 44 AM 58 | 59 | # Feed settings 60 | 61 | You are able to switch on and off which content sources you want and don't want. You can also set the ranking priorities you want for the recommendation algorithm. In addition there's a Refresh mode to update the content feed whenever (_warning_ Refresh mode can cause rate limiting in google/yandex if you run it too often) 62 | 63 | Screen Shot 2022-07-31 at 5 09 47 AM 64 | 65 | 66 | # How it works 67 | 68 | The recommendation algorithm collects keywords from your browsing habits and runs an LDA topic model to gather the prioritized terms to use to web scrape for content. The web scraping uses search queries from these topics to parse from major search engines (Google, Yandex, DuckDuckGo) to get content related to your habits. 69 | 70 | # Build Instructions 71 | If you want to build from source follow the instructions below 72 | 73 | Prerequisites 74 | * You must have the latest Node/NPM installed 75 | 76 | In the root of the repository run 77 | ``` 78 | $ npm install 79 | ``` 80 | This downloads all of the packages you need 81 | 82 | Then run the chrome build 83 | ``` 84 | $ npm run build:chrome 85 | ``` 86 | 87 | Run the firefox build 88 | ``` 89 | $ npm run build:firefox 90 | ``` 91 | 92 | And you're done. 93 | 94 | Most of the code is in the shared folder. There are two nearly identical webpack configs (1 for chrome, 1 for firefox) that generate builds for the background/content scripts and the react popup frontend. 95 | 96 | # Privacy 97 | myAlgorithm does *not* use any 3rd party tracking tools. myAlgorithm does not store any user data outside of your browsers local storage. No outbound requests are made with your personal information. 98 | -------------------------------------------------------------------------------- /chrome-extension/background.js: -------------------------------------------------------------------------------- 1 | importScripts('./dist/background.bundle.js') -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyAlgorithm", 3 | "description": "Your personal recommendation feed - be in control of your algorithm", 4 | "version": "0.5.0", 5 | "manifest_version": 3, 6 | "background": { 7 | "service_worker": "background.js" 8 | }, 9 | "host_permissions": [""], 10 | "content_scripts": [ 11 | { 12 | "matches": ["https://*/*"], 13 | "js": ["dist/content.bundle.js"] 14 | } 15 | ], 16 | "permissions": ["tabs", "webRequest", "storage"], 17 | "action": { 18 | "default_icon": { 19 | "32": "popup_assets/icon_32.png" 20 | }, 21 | "default_title": "Click Me", 22 | "default_popup": "popup.html" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chrome-extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MyAlgorithm 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chrome-extension/popup_assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/chrome-extension/popup_assets/icon16.png -------------------------------------------------------------------------------- /chrome-extension/popup_assets/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/chrome-extension/popup_assets/icon24.png -------------------------------------------------------------------------------- /chrome-extension/popup_assets/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/chrome-extension/popup_assets/icon32.png -------------------------------------------------------------------------------- /chrome-extension/popup_assets/icon32_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/chrome-extension/popup_assets/icon32_notification.png -------------------------------------------------------------------------------- /chrome-extension/popup_assets/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/chrome-extension/popup_assets/icon_32.png -------------------------------------------------------------------------------- /firefox-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyAlgorithm", 3 | "description": "Your personal recommendation feed - be in control of your algorithm", 4 | "version": "0.5.0", 5 | "manifest_version": 2, 6 | "background": { 7 | "scripts": ["dist/background.bundle.js"], 8 | "persistent": true 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": ["https://*/*"], 13 | "js": ["dist/content.bundle.js"] 14 | } 15 | ], 16 | "permissions": ["tabs", "webRequest", "storage", ""], 17 | "browser_action": { 18 | "default_icon": { 19 | "32": "popup_assets/icon_32.png" 20 | }, 21 | "default_title": "Click Me", 22 | "default_popup": "popup.html" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /firefox-extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MyAlgorithm 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /firefox-extension/popup_assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/firefox-extension/popup_assets/icon16.png -------------------------------------------------------------------------------- /firefox-extension/popup_assets/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/firefox-extension/popup_assets/icon24.png -------------------------------------------------------------------------------- /firefox-extension/popup_assets/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/firefox-extension/popup_assets/icon32.png -------------------------------------------------------------------------------- /firefox-extension/popup_assets/icon32_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/firefox-extension/popup_assets/icon32_notification.png -------------------------------------------------------------------------------- /firefox-extension/popup_assets/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/firefox-extension/popup_assets/icon_32.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyAlgorithm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:chrome": "webpack --config webpack.chrome.config.js", 8 | "watch:chrome": "webpack -w --config webpack.chrome.config.js", 9 | "build:firefox": "webpack --config webpack.firefox.config.js", 10 | "watch:firefox": "webpack -w --config webpack.firefox.config.js" 11 | }, 12 | "dependencies": { 13 | "@babel/core": "^7.14.6", 14 | "@babel/preset-env": "^7.14.7", 15 | "@babel/preset-react": "^7.14.5", 16 | "@emotion/react": "^11.9.3", 17 | "@emotion/styled": "^11.9.3", 18 | "@mui/material": "^5.8.7", 19 | "babel-core": "^6.26.3", 20 | "babel-loader": "^8.2.2", 21 | "babel-polyfill": "^6.26.0", 22 | "babel-preset-es2015": "^6.24.1", 23 | "babel-preset-stage-0": "^6.24.1", 24 | "chart.js": "^3.8.0", 25 | "css-loader": "^5.2.6", 26 | "file-loader": "^6.2.0", 27 | "node-sass": "^6.0.1", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "react-router-dom": "^5.3.0", 31 | "sass-loader": "^12.3.0", 32 | "style-loader": "^3.1.0", 33 | "uuid": "^8.3.2", 34 | "webpack": "^5.44.0", 35 | "webpack-cli": "^4.7.2" 36 | }, 37 | "author": "", 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /shared/background.js: -------------------------------------------------------------------------------- 1 | import { nlp } from './compromise.js'; 2 | import { 3 | storage, 4 | FeedSettings, 5 | Keyword 6 | } from './storage.js'; 7 | import { generateKeywordTopics, buildSearchQuery, rankKeywords } from './ranking.js'; 8 | 9 | const API = chrome || browser; 10 | 11 | const runtimeObjects = {}; 12 | 13 | const runtimeSave = async (key, value) => { 14 | await storage.save(key, value); 15 | // also save new changes 16 | API.storage.local.get([key], function (result) { 17 | try { 18 | runtimeObjects[key] = JSON.parse(result[key]); 19 | } catch (e) { 20 | runtimeObjects[key] = {}; 21 | } 22 | }); 23 | } 24 | 25 | const initFeedSettings = async () => { 26 | const updateDefaultsFeedSettings = async (feedSettings) => { 27 | const newFeedSettings = new FeedSettings(feedSettings) 28 | await runtimeSave(storage.KEYS.feed_settings, newFeedSettings) 29 | } 30 | 31 | if ( 32 | !runtimeObjects[storage.KEYS.feed_settings] || 33 | Object.keys(runtimeObjects[storage.KEYS.feed_settings]).length === 0 34 | ) { 35 | console.log('init feed settings') 36 | const newFeedSettings = new FeedSettings({}) 37 | await runtimeSave(storage.KEYS.feed_settings, newFeedSettings) 38 | } else { 39 | console.log('updating feed setting with defaults') 40 | updateDefaultsFeedSettings(runtimeObjects[storage.KEYS.feed_settings]) 41 | } 42 | } 43 | 44 | const parseKeywordsFromText = (text) => { 45 | const wordsToIgnore = [ 46 | 'more', 47 | 'an', 48 | 'of', 49 | 'on', 50 | 'heres', 51 | 'set', 52 | 'it', 53 | 'if', 54 | 'my', 55 | 'your', 56 | 'in', 57 | 'everyone', 58 | 'fe', 59 | 'me', 60 | 'us', 61 | 'someone', 62 | 'we', 63 | 'that', 64 | 'i', 65 | 'im', 66 | 'am', 67 | 'he', 68 | 'she', 69 | 'you', 70 | 'them', 71 | 'they', 72 | 'what', 73 | 'the', 74 | 'his', 75 | 'her', 76 | 'hers', 77 | ] 78 | 79 | const doc = nlp(text) 80 | const namedEntities = Object.assign(doc.nouns().data(), doc.topics().data()) 81 | 82 | return namedEntities 83 | .map((entity) => { 84 | if ( 85 | entity.text.indexOf('https://') === 0 || 86 | entity.text.indexOf('https://') === 0 87 | ) { 88 | return '' 89 | } 90 | 91 | const parsedEntity = entity.text 92 | .replace(/[^\w\s]/gi, '') 93 | .replace(/\s+/g, ' ') 94 | console.log('Found:', parsedEntity.toLowerCase()) 95 | 96 | if (parsedEntity == parseInt(parsedEntity)) { 97 | return '' 98 | } 99 | if (wordsToIgnore.includes(parsedEntity.toLowerCase().trim())) { 100 | return '' 101 | } 102 | if (parsedEntity.toLowerCase().length < 2) { 103 | return '' 104 | } 105 | return parsedEntity.toLowerCase().trim() 106 | }) 107 | .filter((entity) => { 108 | return entity.length > 0 109 | }) 110 | } 111 | 112 | const processEngagementText = async (text, newEngagementType, sourceDomain) => { 113 | const keywords = parseKeywordsFromText(text.join(' ')) 114 | console.log('keywords found:', keywords) 115 | if (!runtimeObjects[storage.KEYS.keywords]) { 116 | runtimeObjects[storage.KEYS.keywords] = await storage.get( 117 | storage.KEYS.keywords 118 | ) 119 | } 120 | const keywordsToSave = keywords.map((keyword) => { 121 | let pastKeywordObject 122 | if (keyword in runtimeObjects[storage.KEYS.keywords]) { 123 | pastKeywordObject = runtimeObjects[storage.KEYS.keywords][keyword] 124 | } else { 125 | pastKeywordObject = { 126 | text: keyword, 127 | occurrences: 0, 128 | history: {}, 129 | engagementTypes: { 130 | 'search-query': 0, 131 | 'meta-keywords': 0, 132 | 'meta-title': 0, 133 | 'link-click': 0, 134 | 'content-click': 0, 135 | 'input-type': 0, 136 | custom: 0, 137 | }, 138 | } 139 | } 140 | return JSON.parse( 141 | JSON.stringify( 142 | new Keyword(pastKeywordObject, newEngagementType, sourceDomain) 143 | ) 144 | ) 145 | }) 146 | 147 | try { 148 | const keywordsToSaveMap = {} 149 | for (let keywordToSave of keywordsToSave) { 150 | keywordsToSaveMap[keywordToSave.text] = keywordToSave 151 | } 152 | const newRuntimeKeywords = Object.assign( 153 | runtimeObjects[storage.KEYS.keywords], 154 | keywordsToSaveMap 155 | ) 156 | await runtimeSave(storage.KEYS.keywords, newRuntimeKeywords) 157 | console.log('keywords saved', newRuntimeKeywords) 158 | } catch (e) { 159 | console.log('keyword save BROKE for query:', text, e) 160 | } 161 | } 162 | 163 | const init = () => { 164 | API.webRequest.onHeadersReceived.addListener( 165 | async (data) => { 166 | if (runtimeObjects[storage.KEYS.feed_settings] && runtimeObjects[storage.KEYS.feed_settings].disableAlgorithm) { 167 | return 168 | } 169 | if (data.type === 'main_frame') { 170 | const url = data.url 171 | try { 172 | const urlObj = new URL(url) 173 | if (urlObj.search[0] === '?') { 174 | for (let queryItem of urlObj.search.slice(1).split('&')) { 175 | if (queryItem.split('=')[0] === 'q') { 176 | const searchQuery = queryItem.split('=')[1].split('+') 177 | console.log('searchQuery found:', searchQuery) 178 | processEngagementText( 179 | searchQuery, 180 | 'search-query', 181 | urlObj.hostname 182 | ) 183 | } 184 | } 185 | } 186 | } catch (e) { 187 | console.log(e) 188 | } 189 | } 190 | }, 191 | { urls: [''] } 192 | ) 193 | 194 | API.runtime.onMessage.addListener(function ( 195 | request, 196 | sender, 197 | sendResponse 198 | ) { 199 | console.log('background request made:', request) 200 | 201 | if (request.action === 'myalgorithm-init') { 202 | sendResponse({ 203 | status: true, 204 | }) 205 | } else if (request.action === 'newEngagementText') { 206 | if (runtimeObjects[storage.KEYS.feed_settings] && runtimeObjects[storage.KEYS.feed_settings].disableAlgorithm) { 207 | sendResponse({ 208 | processedEngagementText: false, 209 | }) 210 | } else { 211 | const engagementText = request.text 212 | processEngagementText( 213 | engagementText, 214 | request.type, 215 | request.sourceDomain 216 | ) 217 | 218 | sendResponse({ 219 | processedEngagementText: true, 220 | }) 221 | } 222 | } else if (request.action === 'getContentFeed') { 223 | if (runtimeObjects[storage.KEYS.fetched_ts]) { 224 | const now = new Date() 225 | const resetTime = new Date() 226 | resetTime.setHours(6, 0, 0, 0) 227 | if (now.getTime() < resetTime.getTime()) { 228 | console.log('sending back content 1') 229 | sendResponse({ 230 | contentFeed: runtimeObjects[storage.KEYS.content_feed] || null, 231 | }) 232 | } else { 233 | if (runtimeObjects[storage.KEYS.fetched_ts] >= resetTime.getTime()) { 234 | console.log('sending back content 2') 235 | sendResponse({ 236 | contentFeed: runtimeObjects[storage.KEYS.content_feed] || null, 237 | }) 238 | } else { 239 | sendResponse({ 240 | contentFeed: null, 241 | }) 242 | } 243 | } 244 | } else { 245 | sendResponse({ 246 | contentFeed: null, 247 | }) 248 | } 249 | } else if (request.action === 'saveContentFeed') { 250 | runtimeSave(storage.KEYS.content_feed, request.contentFeed) 251 | runtimeSave(storage.KEYS.fetched_ts, new Date().getTime()) 252 | 253 | sendResponse({ 254 | save: true, 255 | }) 256 | } else if (request.action === 'getTopics') { 257 | const rankedKeywords = rankKeywords( 258 | runtimeObjects[storage.KEYS.keywords], 259 | runtimeObjects[storage.KEYS.feed_settings] 260 | ) 261 | 262 | if (rankedKeywords.length === 0) { 263 | sendResponse({ 264 | topics: null, 265 | }) 266 | } else { 267 | const topics = generateKeywordTopics(rankedKeywords, true) 268 | sendResponse({ 269 | topics, 270 | }) 271 | } 272 | } else if (request.action === 'getSearchQueries') { 273 | const rankedKeywords = rankKeywords( 274 | runtimeObjects[storage.KEYS.keywords], 275 | runtimeObjects[storage.KEYS.feed_settings] 276 | ) 277 | let topics = [] 278 | if (rankedKeywords.length > 0) { 279 | topics = generateKeywordTopics(rankedKeywords) 280 | } 281 | console.log('topics', topics) 282 | const searchQueries = [] 283 | 284 | for ( 285 | let contentIndex = 0; 286 | contentIndex < topics.length; 287 | contentIndex += 1 288 | ) { 289 | const makeRandom = Math.floor(Math.random() * 100) > 66 // 33% random 290 | const searchQueryOutput = buildSearchQuery( 291 | topics, 292 | rankedKeywords, 293 | contentIndex, 294 | makeRandom 295 | ) 296 | searchQueries.push(searchQueryOutput) 297 | } 298 | console.log('searchQueries', searchQueries) 299 | sendResponse({ 300 | searchQueries, 301 | customSources: runtimeObjects[storage.KEYS.custom_sources], // not great structure here but I didn't want to do another request in ContentFeed.js 302 | }) 303 | } else if (request.action === 'getKeywords') { 304 | if (runtimeObjects[storage.KEYS.keywords]) { 305 | sendResponse({ 306 | keywords: Object.values(runtimeObjects[storage.KEYS.keywords]), 307 | }) 308 | } else { 309 | sendResponse({ keywords: null }) 310 | } 311 | } else if (request.action === 'getFeedSettings') { 312 | if (runtimeObjects[storage.KEYS.feed_settings]) { 313 | sendResponse({ 314 | feedSettings: runtimeObjects[storage.KEYS.feed_settings], 315 | }) 316 | } else { 317 | sendResponse({ feedSettings: null }) 318 | } 319 | } else if (request.action === 'saveFeedSettings') { 320 | if (request.newFeedSettings) { 321 | console.log('saved feed settings', request.newFeedSettings) 322 | runtimeObjects[storage.KEYS.feed_settings] = request.newFeedSettings 323 | runtimeSave(storage.KEYS.feed_settings, request.newFeedSettings) 324 | sendResponse({}) 325 | } else { 326 | sendResponse({}) 327 | } 328 | } else if (request.action === 'removeKeyword') { 329 | const keywordToRemove = request.keyword 330 | if (keywordToRemove in runtimeObjects[storage.KEYS.keywords]) { 331 | delete runtimeObjects[storage.KEYS.keywords][keywordToRemove] 332 | runtimeSave( 333 | storage.KEYS.keywords, 334 | runtimeObjects[storage.KEYS.keywords] 335 | ) 336 | sendResponse({}) 337 | } 338 | } else if (request.action === 'clearKeywords') { 339 | runtimeSave(storage.KEYS.keywords, {}) 340 | sendResponse({}) 341 | } else if (request.action === 'addTopic') { 342 | processEngagementText([request.keyword], 'custom', null) 343 | sendResponse({}) 344 | } else if (request.action === 'addCustomSource') { 345 | const customSourceData = request.customSourceData 346 | if ( 347 | Object.keys(runtimeObjects[storage.KEYS.custom_sources]).includes( 348 | customSourceData.domain 349 | ) 350 | ) { 351 | sendResponse({ 352 | success: false, 353 | message: 'Content source already exists', 354 | }) 355 | } else { 356 | runtimeObjects[storage.KEYS.custom_sources][customSourceData.domain] = 357 | customSourceData 358 | runtimeSave( 359 | storage.KEYS.custom_sources, 360 | runtimeObjects[storage.KEYS.custom_sources] 361 | ) 362 | sendResponse({ 363 | success: true, 364 | message: null, 365 | }) 366 | } 367 | } else if (request.action === 'editCustomSource') { 368 | const customSourceDomain = request.customSourceDomain 369 | const enabled = request.enabled 370 | 371 | if ( 372 | Object.keys(runtimeObjects[storage.KEYS.custom_sources]).includes( 373 | customSourceDomain 374 | ) 375 | ) { 376 | runtimeObjects[storage.KEYS.custom_sources][ 377 | customSourceDomain 378 | ].checked = enabled 379 | runtimeSave( 380 | storage.KEYS.custom_sources, 381 | runtimeObjects[storage.KEYS.custom_sources] 382 | ) 383 | sendResponse({ 384 | success: true, 385 | message: null, 386 | }) 387 | } else { 388 | sendResponse({ 389 | success: false, 390 | message: 'Source does not exist', 391 | }) 392 | } 393 | } else if (request.action === 'removeCustomSource') { 394 | const customSourceDomain = request.customSourceDomain 395 | if ( 396 | Object.keys(runtimeObjects[storage.KEYS.custom_sources]).includes( 397 | customSourceDomain 398 | ) 399 | ) { 400 | delete runtimeObjects[storage.KEYS.custom_sources][customSourceDomain] 401 | runtimeSave( 402 | storage.KEYS.custom_sources, 403 | runtimeObjects[storage.KEYS.custom_sources] 404 | ) 405 | sendResponse({ 406 | success: true, 407 | message: null, 408 | }) 409 | } 410 | } else if (request.action === 'getCustomSources') { 411 | if ( 412 | runtimeObjects[storage.KEYS.custom_sources] && 413 | Object.keys(runtimeObjects[storage.KEYS.custom_sources]).length > 0 414 | ) { 415 | sendResponse({ 416 | customSources: runtimeObjects[storage.KEYS.custom_sources], 417 | }) 418 | } else { 419 | sendResponse({ 420 | customSources: null, 421 | }) 422 | } 423 | } else { 424 | sendResponse({}) 425 | } 426 | 427 | return true 428 | }) 429 | } 430 | 431 | (async function () { 432 | init() 433 | 434 | runtimeObjects[storage.KEYS.keywords] = await storage.get( 435 | storage.KEYS.keywords 436 | ) 437 | runtimeObjects[storage.KEYS.feed_settings] = await storage.get( 438 | storage.KEYS.feed_settings 439 | ) 440 | runtimeObjects[storage.KEYS.fetched_ts] = await storage.get( 441 | storage.KEYS.fetched_ts 442 | ) 443 | runtimeObjects[storage.KEYS.content_feed] = await storage.get( 444 | storage.KEYS.content_feed 445 | ) 446 | runtimeObjects[storage.KEYS.custom_sources] = await storage.get( 447 | storage.KEYS.custom_sources 448 | ) 449 | 450 | initFeedSettings() 451 | 452 | console.log('INIT runtime', runtimeObjects) 453 | })() 454 | -------------------------------------------------------------------------------- /shared/content-fetcher.js: -------------------------------------------------------------------------------- 1 | function timeout(ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | 5 | async function ContentPageFetcher(link) { 6 | return new Promise((resolve) => { 7 | fetch(link, { 8 | method: 'GET', 9 | headers: { 10 | 'User-Agent': 11 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 12 | }, 13 | }) 14 | .then(function (response) { 15 | return response.text() 16 | }) 17 | .then(function (text) { 18 | const doc = document.implementation.createHTMLDocument('') 19 | doc.open() 20 | doc.write(text) 21 | doc.close() 22 | let title = doc.querySelector('title').innerText 23 | const metaOGTitle = doc.querySelector('meta[property="og:title"]') 24 | if (metaOGTitle) { 25 | title = metaOGTitle.getAttribute('content') 26 | } else { 27 | const metaTwitterTitle = doc.querySelector( 28 | 'meta[property="twitter:title"]' 29 | ) 30 | if (metaTwitterTitle) { 31 | title = metaTwitterTitle.getAttribute('content') 32 | } 33 | } 34 | 35 | let description 36 | const metaBaseDesc = doc.querySelector('meta[name="description"]') 37 | if (metaBaseDesc) { 38 | // default 39 | description = metaBaseDesc.getAttribute('content') 40 | } 41 | // prioritize below 42 | const metaOGDesc = doc.querySelector('meta[property="og:description"]') 43 | if (metaOGDesc) { 44 | description = metaOGDesc.getAttribute('content') 45 | } else { 46 | const metaTwitterDesc = doc.querySelector( 47 | 'meta[property="twitter:description"]' 48 | ) 49 | if (metaTwitterDesc) { 50 | description = metaTwitterDesc.getAttribute('content') 51 | } 52 | } 53 | 54 | let image 55 | // prioritize below 56 | const metaOGImage = doc.querySelector('meta[property="og:image"]') 57 | if (metaOGImage) { 58 | image = metaOGImage.getAttribute('content') 59 | } else { 60 | const metaTwitterImage = doc.querySelector( 61 | 'meta[property="twitter:image"]' 62 | ) 63 | if (metaTwitterImage) { 64 | image = metaTwitterImage.getAttribute('content') 65 | } 66 | } 67 | 68 | resolve({ 69 | title, 70 | description, 71 | image, 72 | }) 73 | }) 74 | .catch(() => { 75 | resolve(null) 76 | }) 77 | }) 78 | } 79 | 80 | async function GoogleFetcher(searchQuery, site) { 81 | return new Promise((resolve) => { 82 | console.log(`Google Fetcher: ${site} <---> Query: ${searchQuery}`) 83 | const searchUrl = `https://www.google.com/search?q=${encodeURIComponent( 84 | searchQuery 85 | )} site:${site}` 86 | fetch(searchUrl, { 87 | method: 'GET', 88 | credentials: 'omit', 89 | headers: { 90 | 'User-Agent': 91 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 92 | }, 93 | }) 94 | .then(function (response) { 95 | return response.text() 96 | }) 97 | .then(function (text) { 98 | const doc = document.implementation.createHTMLDocument('') 99 | doc.open() 100 | doc.write(text) 101 | doc.close() 102 | const h3s = doc.querySelectorAll('div div > a > h3') 103 | if (!h3s || h3s.length === 0) { 104 | resolve(null) 105 | } else { 106 | for (let h3 of h3s) { 107 | const link = h3.closest('a') 108 | 109 | const container = h3.closest('div[data-sokoban-container]') 110 | let desc 111 | if (container) { 112 | const descEl = container.querySelector( 113 | 'div[data-content-feature="1"]' 114 | ) 115 | if (descEl) { 116 | desc = descEl.innerText 117 | } 118 | } 119 | if (link) { 120 | const href = link.getAttribute('href') 121 | if (href) { 122 | return resolve({ 123 | href, 124 | title: link.innerText, 125 | desc, 126 | }) 127 | } 128 | } 129 | } 130 | } 131 | }) 132 | .catch(() => { 133 | resolve(null) 134 | }) 135 | }) 136 | } 137 | 138 | async function YandexFetcher(searchQuery, site) { 139 | await timeout(1000) 140 | return new Promise((resolve) => { 141 | console.log(`Yandex Fetcher: ${site} <---> Query: ${searchQuery}`) 142 | const searchUrl = `https://yandex.com/search/?text=${encodeURIComponent( 143 | searchQuery 144 | )}+site:${site}+lang:en&lr=103017&redircnt=1657655444.1` 145 | fetch(searchUrl, { 146 | method: 'GET', 147 | headers: { 148 | accept: 149 | ' text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 150 | 'accept-encoding': 'gzip, deflate, br', 151 | 'accept-language': 'en-US,en;q=0.9', 152 | 'device-memory': '8', 153 | downlink: '1.05', 154 | dpr: '2', 155 | ect: '4g', 156 | 'viewport-width': '1440', 157 | referer: 'https://yandex.com/', 158 | rtt: '150', 159 | 'cache-control': 'max-age=0', 160 | 'User-Agent': 161 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 162 | 'sec-ch-ua': 163 | '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"', 164 | 'sec-ch-ua-mobile': '?0', 165 | 'sec-ch-ua-platform': 'macOS', 166 | 'sec-fetch-dest': 'document', 167 | 'sec-fetch-mode': 'navigate', 168 | 'sec-fetch-site': 'same-origin', 169 | 'sec-fetch-user': '?1', 170 | 'upgrade-insecure-requests': '1', 171 | }, 172 | }) 173 | .then(function (response) { 174 | return response.text() 175 | }) 176 | .then(function (text) { 177 | const doc = document.implementation.createHTMLDocument('') 178 | doc.open() 179 | doc.write(text) 180 | doc.close() 181 | console.log(text) 182 | console.log(text.includes('main__content')) 183 | console.log(text.includes('serp-list')) 184 | console.log(text.includes('serp-item')) 185 | console.log(text.includes('OrganicTitle-Link')) 186 | const links = doc.querySelectorAll( 187 | '.main__content .serp-list .serp-item .OrganicTitle-Link' 188 | ) 189 | console.log(links) 190 | if (!links || links.length === 0) { 191 | resolve(null) 192 | } else { 193 | for (let link of links) { 194 | if (link) { 195 | const href = link.getAttribute('href') 196 | if (href) { 197 | return resolve(href) 198 | } 199 | } 200 | } 201 | } 202 | }) 203 | .catch(() => { 204 | resolve(null) 205 | }) 206 | }) 207 | } 208 | 209 | async function DDGFetcher(searchQuery, site) { 210 | return new Promise((resolve) => { 211 | console.log(`DDG Fetcher: ${site} <---> Query: ${searchQuery}`) 212 | const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent( 213 | searchQuery 214 | )}+site:${encodeURIComponent(site)}` 215 | fetch(searchUrl, { 216 | method: 'GET', 217 | credentials: 'omit', 218 | headers: { 219 | 'User-Agent': 220 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 221 | }, 222 | }) 223 | .then(function (response) { 224 | return response.text() 225 | }) 226 | .then(function (text) { 227 | const doc = document.implementation.createHTMLDocument('') 228 | doc.open() 229 | doc.write(text) 230 | doc.close() 231 | console.log(text) 232 | const titles = doc.querySelectorAll('.result__title .result__a') 233 | if (!titles || titles.length === 0) { 234 | resolve(null) 235 | } else { 236 | for (let title of titles) { 237 | if (title) { 238 | let href = title.getAttribute('href') 239 | let url = 'https:' + href 240 | try { 241 | url = new URL(url) 242 | const queryParams = url.search.slice(1).split('&') 243 | for (let queryParam of queryParams) { 244 | const queryParamSplit = queryParam.split('=') 245 | if (queryParamSplit[0] === 'uddg') { 246 | href = decodeURIComponent(queryParamSplit[1]) 247 | break 248 | } 249 | } 250 | } catch (e) { 251 | continue 252 | } 253 | if (href) { 254 | return resolve(href) 255 | } 256 | } 257 | } 258 | } 259 | }) 260 | .catch(() => { 261 | resolve(null) 262 | }) 263 | }) 264 | } 265 | 266 | async function ContentFetcher(sourcingSettings, customSources, searchQueries) { 267 | console.log('sourcingSettings', sourcingSettings) 268 | 269 | const sourcingSites = Object.assign({ 270 | youtube: 'youtube.com/watch', 271 | twitter: 'twitter.com/*', 272 | reddit: 'reddit.com/r', 273 | quora: 'quora.com', 274 | wikipedia: 'wikipedia.com/wiki', 275 | odysee: 'odysee.com/*/*', 276 | stackoverflow: 'stackoverflow.com/questions', 277 | gab: 'gab.com', 278 | bitchute: 'bitchute.com/video', 279 | }) 280 | 281 | let sitesToSource = Object.keys(sourcingSettings) 282 | .filter((source) => { 283 | return sourcingSettings[source] 284 | }) 285 | .map((source) => { 286 | return sourcingSites[source] 287 | }) 288 | 289 | if (Object.keys(customSources).length > 0) { 290 | const enabledCustomSources = Object.values(customSources) 291 | .filter((customSource) => { 292 | return customSource.checked 293 | }) 294 | .map((customSource) => { 295 | return customSource.domain 296 | }) 297 | 298 | if (enabledCustomSources.length > 0) { 299 | sitesToSource = sitesToSource.concat(enabledCustomSources) 300 | } 301 | } 302 | 303 | const contentFeed = [] 304 | console.log('Search Query count', searchQueries.length) 305 | for (let searchQueryArray of searchQueries) { 306 | const searchQuery = searchQueryArray[0]; 307 | const topics = searchQueryArray[1]; 308 | console.log('searchQuery', searchQuery) 309 | 310 | const site = sitesToSource[Math.floor(Math.random() * sitesToSource.length)] 311 | 312 | const fetchers = [ 313 | YandexFetcher, 314 | DDGFetcher, 315 | GoogleFetcher, 316 | GoogleFetcher, 317 | GoogleFetcher, 318 | GoogleFetcher, 319 | ] // randomness for the fetcher (avoid rate limiting) 320 | let fetcher 321 | 322 | // use google for all twitter sources (for now) 323 | if (site.includes('twitter.com')) { 324 | fetcher = GoogleFetcher 325 | } else { 326 | fetcher = fetchers[Math.floor(Math.random() * fetchers.length)] 327 | } 328 | 329 | const content = await fetcher(searchQuery, site) 330 | if (content) { 331 | let contentLink 332 | let isGoogleFetch = false 333 | if (typeof content === 'object' && Object.keys(content).length > 1) { 334 | isGoogleFetch = true 335 | contentLink = content.href 336 | } else { 337 | contentLink = content 338 | } 339 | const contentInfo = await ContentPageFetcher(contentLink) 340 | let contentFeedData = { 341 | link: contentLink, 342 | source: site, 343 | topics, 344 | } 345 | if (isGoogleFetch) { 346 | contentFeedData = Object.assign(contentFeedData, { 347 | title: content.title, 348 | description: content.desc, 349 | }) 350 | } 351 | if (contentInfo && Object.keys(contentInfo).length > 0) { 352 | contentFeedData = Object.assign(contentFeedData, contentInfo) 353 | } 354 | 355 | contentFeed.push(contentFeedData) 356 | } 357 | } 358 | return contentFeed 359 | } 360 | 361 | const fetchContentSourceDetails = (customSourceUrl, customSource) => { 362 | return new Promise((resolve) => { 363 | let domain = customSource 364 | let urlToFetch = customSourceUrl 365 | try { 366 | const url = new URL(customSourceUrl) 367 | domain = url.hostname 368 | urlToFetch = url.origin 369 | } catch {} 370 | 371 | console.log(`Fetching content source details: ${customSource}`) 372 | fetch(urlToFetch, { 373 | method: 'GET', 374 | credentials: 'omit', 375 | headers: { 376 | 'User-Agent': 377 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36', 378 | }, 379 | }) 380 | .then(function (response) { 381 | return response.text() 382 | }) 383 | .then(function (text) { 384 | const doc = document.implementation.createHTMLDocument('') 385 | doc.open() 386 | doc.write(text) 387 | doc.close() 388 | 389 | let image 390 | let name = domain 391 | 392 | const metaOGName = doc.querySelector('meta[property="og:site_name"]') 393 | if (metaOGName) { 394 | name = metaOGName.getAttribute('content') 395 | } 396 | 397 | const metaOGImage = doc.querySelector('meta[property="og:image"]') 398 | if (metaOGImage) { 399 | image = metaOGImage.getAttribute('content') 400 | } 401 | 402 | resolve({ 403 | name, 404 | image, 405 | }) 406 | }) 407 | }) 408 | } 409 | 410 | export { ContentFetcher, fetchContentSourceDetails } 411 | -------------------------------------------------------------------------------- /shared/content.js: -------------------------------------------------------------------------------- 1 | const API = chrome || browser; 2 | (function (history) { 3 | var pushState = history.pushState 4 | history.pushState = function (state) { 5 | if (typeof history.onpushstate == 'function') { 6 | history.onpushstate({ state: state }) 7 | } 8 | return pushState.apply(history, arguments) 9 | } 10 | })(window.history) 11 | ;(function () { 12 | const myAlgorithmRoutine = function () { 13 | const sourceDomain = window.location.hostname 14 | const getMetaTags = function () { 15 | const messages = [] 16 | if ( 17 | window.location.pathname !== '/' && 18 | !['www.youtube.com'].includes(window.location.hostname) 19 | ) { 20 | const metaKeywords = document.querySelector('meta[name="keywords"]') 21 | if (metaKeywords) { 22 | messages.push({ 23 | text: metaKeywords.getAttribute('content'), 24 | type: 'meta-keywords', 25 | }) 26 | } 27 | } 28 | 29 | const metaOGTitle = document.querySelector('meta[property="og:title"]') 30 | if (metaOGTitle) { 31 | messages.push({ 32 | text: metaOGTitle.getAttribute('content'), 33 | type: 'meta-title', 34 | }) 35 | } else { 36 | const metaTwitterTitle = document.querySelector( 37 | 'meta[property="twitter:title"]' 38 | ) 39 | if (metaTwitterTitle) { 40 | messages.push({ 41 | text: metaTwitterTitle.getAttribute('content'), 42 | type: 'meta-title', 43 | }) 44 | } 45 | } 46 | 47 | for (let message of messages) { 48 | message.text = message.text.split(' ') 49 | API.runtime.sendMessage( 50 | Object.assign( 51 | { 52 | action: 'newEngagementText', 53 | sourceDomain, 54 | }, 55 | message 56 | ) 57 | ) 58 | } 59 | } 60 | 61 | getMetaTags() 62 | 63 | if (window.location.hostname === 'www.youtube.com') { 64 | const As = document.querySelectorAll('a') 65 | 66 | document.body.addEventListener( 67 | 'click', 68 | (e) => { 69 | const element = e.target 70 | 71 | if (element.getAttribute('id') === 'video-title') { 72 | API.runtime.sendMessage({ 73 | action: 'newEngagementText', 74 | text: element.innerText.split(' '), 75 | type: 'link-click', 76 | sourceDomain, 77 | }) 78 | } 79 | }, 80 | true 81 | ) 82 | } else { 83 | document.onclick = function (e) { 84 | const origin = e.target.closest(`a`) 85 | console.log(origin) 86 | if (origin) { 87 | console.log(origin.innerText.split(' ')) 88 | API.runtime.sendMessage({ 89 | action: 'newEngagementText', 90 | text: origin.innerText.split(' '), 91 | type: 'link-click', 92 | sourceDomain, 93 | }) 94 | } 95 | } 96 | if ( 97 | ['www.twitter.com', 'twitter.com'].includes(window.location.hostname) && 98 | (window.location.pathname === '/' || 99 | window.location.pathname === '/home') 100 | ) { 101 | document.body.addEventListener( 102 | 'click', 103 | (e) => { 104 | const origin = e.target.closest(`article`) 105 | if (origin) { 106 | const elements = origin.querySelectorAll('div > span') 107 | if (elements && elements.length > 0) { 108 | const textInElement = [...elements].map((el) => { 109 | return { el, length: el.innerText.length } 110 | }) 111 | const elementWithMostText = textInElement.sort((a, b) => { 112 | return b.length - a.length 113 | })[0] 114 | console.log(elementWithMostText.el.innerText.split(' ')) 115 | API.runtime.sendMessage({ 116 | action: 'newEngagementText', 117 | text: elementWithMostText.el.innerText.split(' '), 118 | type: 'content-click', 119 | sourceDomain, 120 | }) 121 | } 122 | } 123 | }, 124 | true 125 | ) 126 | } 127 | } 128 | 129 | //setup before functions 130 | const allTextInputs = document.querySelectorAll( 131 | 'input[type="text"], textarea' 132 | ) 133 | 134 | allTextInputs.forEach(function (textInput) { 135 | let firstKeydown = false 136 | let start 137 | 138 | function debounce(callback, wait) { 139 | let timeout 140 | return (...args) => { 141 | clearTimeout(timeout) 142 | timeout = setTimeout(function () { 143 | callback.apply(this, args) 144 | }, wait) 145 | } 146 | } 147 | 148 | textInput.addEventListener('keydown', function (e) { 149 | if (!firstKeydown) { 150 | firstKeydown = true 151 | start = e.target.selectionStart 152 | console.log('first', start) 153 | } 154 | }) 155 | 156 | textInput.addEventListener( 157 | 'keyup', 158 | debounce((e) => { 159 | if (e.target.value) { 160 | console.log(start, e.target.selectionEnd) 161 | const newText = e.target.value.slice(start, e.target.selectionEnd) 162 | console.log('newText', newText) 163 | API.runtime.sendMessage({ 164 | action: 'newEngagementText', 165 | text: newText.split(' '), 166 | type: 'input-type', 167 | sourceDomain, 168 | }) 169 | } 170 | 171 | firstKeydown = false 172 | }, 1000) 173 | ) 174 | }) 175 | } 176 | window.onload = myAlgorithmRoutine() 177 | 178 | window.onpopstate = history.onpushstate = function (e) { 179 | myAlgorithmRoutine() 180 | } 181 | 182 | const wakeup = () => { 183 | setTimeout(function () { 184 | API.runtime.sendMessage('ping', function () {}) 185 | wakeup() 186 | }, 10000) 187 | } 188 | })() 189 | -------------------------------------------------------------------------------- /shared/lda.js: -------------------------------------------------------------------------------- 1 | const stop_words = [ 2 | 'a', 3 | 'able', 4 | 'about', 5 | 'above', 6 | 'abroad', 7 | 'according', 8 | 'accordingly', 9 | 'across', 10 | 'actually', 11 | 'adj', 12 | 'after', 13 | 'afterwards', 14 | 'again', 15 | 'against', 16 | 'ago', 17 | 'ahead', 18 | 'aint', 19 | 'all', 20 | 'allow', 21 | 'allows', 22 | 'almost', 23 | 'alone', 24 | 'along', 25 | 'alongside', 26 | 'already', 27 | 'also', 28 | 'although', 29 | 'always', 30 | 'am', 31 | 'amid', 32 | 'amidst', 33 | 'among', 34 | 'amongst', 35 | 'an', 36 | 'and', 37 | 'another', 38 | 'any', 39 | 'anybody', 40 | 'anyhow', 41 | 'anyone', 42 | 'anything', 43 | 'anyway', 44 | 'anyways', 45 | 'anywhere', 46 | 'apart', 47 | 'appear', 48 | 'appreciate', 49 | 'appropriate', 50 | 'are', 51 | 'arent', 52 | 'around', 53 | 'as', 54 | 'as', 55 | 'aside', 56 | 'ask', 57 | 'asking', 58 | 'associated', 59 | 'at', 60 | 'available', 61 | 'away', 62 | 'awfully', 63 | 'b', 64 | 'back', 65 | 'backward', 66 | 'backwards', 67 | 'be', 68 | 'became', 69 | 'because', 70 | 'become', 71 | 'becomes', 72 | 'becoming', 73 | 'been', 74 | 'before', 75 | 'beforehand', 76 | 'begin', 77 | 'behind', 78 | 'being', 79 | 'believe', 80 | 'below', 81 | 'beside', 82 | 'besides', 83 | 'best', 84 | 'better', 85 | 'between', 86 | 'beyond', 87 | 'both', 88 | 'brief', 89 | 'but', 90 | 'by', 91 | 'c', 92 | 'came', 93 | 'can', 94 | 'cannot', 95 | 'cant', 96 | 'cant', 97 | 'caption', 98 | 'cause', 99 | 'causes', 100 | 'certain', 101 | 'certainly', 102 | 'changes', 103 | 'clearly', 104 | 'cmon', 105 | 'co', 106 | 'co.', 107 | 'com', 108 | 'come', 109 | 'comes', 110 | 'concerning', 111 | 'consequently', 112 | 'consider', 113 | 'considering', 114 | 'constructor', 115 | 'contain', 116 | 'containing', 117 | 'contains', 118 | 'corresponding', 119 | 'could', 120 | 'couldnt', 121 | 'course', 122 | 'cs', 123 | 'currently', 124 | 'd', 125 | 'dare', 126 | 'darent', 127 | 'definitely', 128 | 'described', 129 | 'despite', 130 | 'did', 131 | 'didnt', 132 | 'different', 133 | 'directly', 134 | 'do', 135 | 'does', 136 | 'doesnt', 137 | 'doing', 138 | 'done', 139 | 'dont', 140 | 'down', 141 | 'downwards', 142 | 'during', 143 | 'e', 144 | 'each', 145 | 'edu', 146 | 'eg', 147 | 'eight', 148 | 'eighty', 149 | 'either', 150 | 'else', 151 | 'elsewhere', 152 | 'end', 153 | 'ending', 154 | 'enough', 155 | 'entirely', 156 | 'especially', 157 | 'et', 158 | 'etc', 159 | 'even', 160 | 'ever', 161 | 'evermore', 162 | 'every', 163 | 'everybody', 164 | 'everyone', 165 | 'everything', 166 | 'everywhere', 167 | 'ex', 168 | 'exactly', 169 | 'example', 170 | 'except', 171 | 'f', 172 | 'fairly', 173 | 'far', 174 | 'farther', 175 | 'few', 176 | 'fewer', 177 | 'fifth', 178 | 'first', 179 | 'five', 180 | 'followed', 181 | 'following', 182 | 'follows', 183 | 'for', 184 | 'forever', 185 | 'former', 186 | 'formerly', 187 | 'forth', 188 | 'forward', 189 | 'found', 190 | 'four', 191 | 'from', 192 | 'further', 193 | 'furthermore', 194 | 'g', 195 | 'get', 196 | 'gets', 197 | 'getting', 198 | 'given', 199 | 'gives', 200 | 'go', 201 | 'goes', 202 | 'going', 203 | 'gone', 204 | 'got', 205 | 'gotten', 206 | 'greetings', 207 | 'h', 208 | 'had', 209 | 'hadnt', 210 | 'half', 211 | 'happens', 212 | 'hardly', 213 | 'has', 214 | 'hasnt', 215 | 'have', 216 | 'havent', 217 | 'having', 218 | 'he', 219 | 'hed', 220 | 'hell', 221 | 'hello', 222 | 'help', 223 | 'hence', 224 | 'her', 225 | 'here', 226 | 'hereafter', 227 | 'hereby', 228 | 'herein', 229 | 'heres', 230 | 'hereupon', 231 | 'hers', 232 | 'herself', 233 | 'hes', 234 | 'hi', 235 | 'him', 236 | 'himself', 237 | 'his', 238 | 'hither', 239 | 'hopefully', 240 | 'how', 241 | 'howbeit', 242 | 'however', 243 | 'hundred', 244 | 'i', 245 | 'id', 246 | 'ie', 247 | 'if', 248 | 'ignored', 249 | 'ill', 250 | 'im', 251 | 'immediate', 252 | 'in', 253 | 'inasmuch', 254 | 'inc', 255 | 'inc.', 256 | 'indeed', 257 | 'indicate', 258 | 'indicated', 259 | 'indicates', 260 | 'inner', 261 | 'inside', 262 | 'insofar', 263 | 'instead', 264 | 'into', 265 | 'inward', 266 | 'is', 267 | 'isnt', 268 | 'it', 269 | 'itd', 270 | 'itll', 271 | 'its', 272 | 'its', 273 | 'itself', 274 | 'ive', 275 | 'j', 276 | 'just', 277 | 'k', 278 | 'keep', 279 | 'keeps', 280 | 'kept', 281 | 'know', 282 | 'known', 283 | 'knows', 284 | 'l', 285 | 'last', 286 | 'lately', 287 | 'later', 288 | 'latter', 289 | 'latterly', 290 | 'least', 291 | 'less', 292 | 'lest', 293 | 'let', 294 | 'lets', 295 | 'like', 296 | 'liked', 297 | 'likely', 298 | 'likewise', 299 | 'little', 300 | 'look', 301 | 'looking', 302 | 'looks', 303 | 'low', 304 | 'lower', 305 | 'ltd', 306 | 'm', 307 | 'made', 308 | 'mainly', 309 | 'make', 310 | 'makes', 311 | 'many', 312 | 'may', 313 | 'maybe', 314 | 'maynt', 315 | 'me', 316 | 'mean', 317 | 'meantime', 318 | 'meanwhile', 319 | 'merely', 320 | 'might', 321 | 'mightnt', 322 | 'mine', 323 | 'minus', 324 | 'miss', 325 | 'more', 326 | 'moreover', 327 | 'most', 328 | 'mostly', 329 | 'mr', 330 | 'mrs', 331 | 'much', 332 | 'must', 333 | 'mustnt', 334 | 'my', 335 | 'myself', 336 | 'n', 337 | 'name', 338 | 'namely', 339 | 'nd', 340 | 'near', 341 | 'nearly', 342 | 'necessary', 343 | 'need', 344 | 'neednt', 345 | 'needs', 346 | 'neither', 347 | 'never', 348 | 'neverf', 349 | 'neverless', 350 | 'nevertheless', 351 | 'new', 352 | 'next', 353 | 'nine', 354 | 'ninety', 355 | 'no', 356 | 'nobody', 357 | 'non', 358 | 'none', 359 | 'nonetheless', 360 | 'noone', 361 | 'no-one', 362 | 'nor', 363 | 'normally', 364 | 'not', 365 | 'nothing', 366 | 'notwithstanding', 367 | 'novel', 368 | 'now', 369 | 'nowhere', 370 | 'o', 371 | 'obviously', 372 | 'of', 373 | 'off', 374 | 'often', 375 | 'oh', 376 | 'ok', 377 | 'okay', 378 | 'old', 379 | 'on', 380 | 'once', 381 | 'one', 382 | 'ones', 383 | 'ones', 384 | 'only', 385 | 'onto', 386 | 'opposite', 387 | 'or', 388 | 'other', 389 | 'others', 390 | 'otherwise', 391 | 'ought', 392 | 'oughtnt', 393 | 'our', 394 | 'ours', 395 | 'ourselves', 396 | 'out', 397 | 'outside', 398 | 'over', 399 | 'overall', 400 | 'own', 401 | 'p', 402 | 'particular', 403 | 'particularly', 404 | 'past', 405 | 'per', 406 | 'perhaps', 407 | 'placed', 408 | 'please', 409 | 'plus', 410 | 'possible', 411 | 'presumably', 412 | 'probably', 413 | 'provided', 414 | 'provides', 415 | 'q', 416 | 'que', 417 | 'quite', 418 | 'qv', 419 | 'r', 420 | 'rather', 421 | 'rd', 422 | 're', 423 | 'really', 424 | 'reasonably', 425 | 'recent', 426 | 'recently', 427 | 'regarding', 428 | 'regardless', 429 | 'regards', 430 | 'relatively', 431 | 'respectively', 432 | 'right', 433 | 'round', 434 | 's', 435 | 'said', 436 | 'same', 437 | 'saw', 438 | 'say', 439 | 'saying', 440 | 'says', 441 | 'second', 442 | 'secondly', 443 | 'see', 444 | 'seeing', 445 | 'seem', 446 | 'seemed', 447 | 'seeming', 448 | 'seems', 449 | 'seen', 450 | 'self', 451 | 'selves', 452 | 'sensible', 453 | 'sent', 454 | 'serious', 455 | 'seriously', 456 | 'seven', 457 | 'several', 458 | 'shall', 459 | 'shant', 460 | 'she', 461 | 'shed', 462 | 'shell', 463 | 'shes', 464 | 'should', 465 | 'shouldnt', 466 | 'since', 467 | 'six', 468 | 'so', 469 | 'some', 470 | 'somebody', 471 | 'someday', 472 | 'somehow', 473 | 'someone', 474 | 'something', 475 | 'sometime', 476 | 'sometimes', 477 | 'somewhat', 478 | 'somewhere', 479 | 'soon', 480 | 'sorry', 481 | 'specified', 482 | 'specify', 483 | 'specifying', 484 | 'still', 485 | 'sub', 486 | 'such', 487 | 'sup', 488 | 'sure', 489 | 't', 490 | 'take', 491 | 'taken', 492 | 'taking', 493 | 'tell', 494 | 'tends', 495 | 'th', 496 | 'than', 497 | 'thank', 498 | 'thanks', 499 | 'thanx', 500 | 'that', 501 | 'thatll', 502 | 'thats', 503 | 'thats', 504 | 'thatve', 505 | 'the', 506 | 'their', 507 | 'theirs', 508 | 'them', 509 | 'themselves', 510 | 'then', 511 | 'thence', 512 | 'there', 513 | 'thereafter', 514 | 'thereby', 515 | 'thered', 516 | 'therefore', 517 | 'therein', 518 | 'therell', 519 | 'therere', 520 | 'theres', 521 | 'theres', 522 | 'thereupon', 523 | 'thereve', 524 | 'these', 525 | 'they', 526 | 'theyd', 527 | 'theyll', 528 | 'theyre', 529 | 'theyve', 530 | 'thing', 531 | 'things', 532 | 'think', 533 | 'third', 534 | 'thirty', 535 | 'this', 536 | 'thorough', 537 | 'thoroughly', 538 | 'those', 539 | 'though', 540 | 'three', 541 | 'through', 542 | 'throughout', 543 | 'thru', 544 | 'thus', 545 | 'till', 546 | 'to', 547 | 'together', 548 | 'too', 549 | 'took', 550 | 'toward', 551 | 'towards', 552 | 'tried', 553 | 'tries', 554 | 'truly', 555 | 'try', 556 | 'trying', 557 | 'ts', 558 | 'twice', 559 | 'two', 560 | 'u', 561 | 'un', 562 | 'under', 563 | 'underneath', 564 | 'undoing', 565 | 'unfortunately', 566 | 'unless', 567 | 'unlike', 568 | 'unlikely', 569 | 'until', 570 | 'unto', 571 | 'up', 572 | 'upon', 573 | 'upwards', 574 | 'us', 575 | 'use', 576 | 'used', 577 | 'useful', 578 | 'uses', 579 | 'using', 580 | 'usually', 581 | 'v', 582 | 'value', 583 | 'various', 584 | 'versus', 585 | 'very', 586 | 'via', 587 | 'viz', 588 | 'vs', 589 | 'w', 590 | 'want', 591 | 'wants', 592 | 'was', 593 | 'wasnt', 594 | 'way', 595 | 'we', 596 | 'wed', 597 | 'welcome', 598 | 'well', 599 | 'well', 600 | 'went', 601 | 'were', 602 | 'were', 603 | 'werent', 604 | 'weve', 605 | 'what', 606 | 'whatever', 607 | 'whatll', 608 | 'whats', 609 | 'whatve', 610 | 'when', 611 | 'whence', 612 | 'whenever', 613 | 'where', 614 | 'whereafter', 615 | 'whereas', 616 | 'whereby', 617 | 'wherein', 618 | 'wheres', 619 | 'whereupon', 620 | 'wherever', 621 | 'whether', 622 | 'which', 623 | 'whichever', 624 | 'while', 625 | 'whilst', 626 | 'whither', 627 | 'who', 628 | 'whod', 629 | 'whoever', 630 | 'whole', 631 | 'wholl', 632 | 'whom', 633 | 'whomever', 634 | 'whos', 635 | 'whose', 636 | 'why', 637 | 'will', 638 | 'willing', 639 | 'wish', 640 | 'with', 641 | 'within', 642 | 'without', 643 | 'wonder', 644 | 'wont', 645 | 'would', 646 | 'wouldnt', 647 | 'x', 648 | 'y', 649 | 'yes', 650 | 'yet', 651 | 'you', 652 | 'youd', 653 | 'youll', 654 | 'your', 655 | 'youre', 656 | 'yours', 657 | 'yourself', 658 | 'yourselves', 659 | 'youve', 660 | 'z', 661 | 'zero', 662 | ] 663 | 664 | var stemmer = {}, 665 | cache = {} 666 | 667 | stemmer.except = function (word, exceptions) { 668 | if (exceptions instanceof Array) { 669 | if (~exceptions.indexOf(word)) return word 670 | } else { 671 | for (var k in exceptions) { 672 | if (k === word) return exceptions[k] 673 | } 674 | } 675 | return false 676 | } 677 | 678 | // word - String 679 | // offset - Integer (optional) 680 | // replace - Key/Value Array of pattern, string, and function. 681 | stemmer.among = function among(word, offset, replace) { 682 | if (replace == null) return among(word, 0, offset) 683 | 684 | // Store the intial value of the word. 685 | var initial = word.slice(), 686 | pattern, 687 | replacement 688 | 689 | for (var i = 0; i < replace.length; i += 2) { 690 | pattern = replace[i] 691 | pattern = cache[pattern] || (cache[pattern] = new RegExp(replace[i] + '$')) 692 | replacement = replace[i + 1] 693 | 694 | if (typeof replacement === 'function') { 695 | word = word.replace(pattern, function (m) { 696 | var off = arguments['' + (arguments.length - 2)] 697 | if (off >= offset) { 698 | return replacement.apply(null, arguments) 699 | } else { 700 | return m + ' ' 701 | } 702 | }) 703 | } else { 704 | word = word.replace(pattern, function (m) { 705 | var off = arguments['' + (arguments.length - 2)] 706 | return off >= offset ? replacement : m + ' ' 707 | }) 708 | } 709 | 710 | if (word !== initial) break 711 | } 712 | 713 | return word.replace(/ /g, '') 714 | } 715 | 716 | let alphabet = 'abcdefghijklmnopqrstuvwxyz', 717 | vowels = 'aeiouy', 718 | v_wxy = vowels + 'wxY', 719 | valid_li = 'cdeghkmnrt', 720 | r1_re = RegExp('^.*?([' + vowels + '][^' + vowels + ']|$)'), 721 | r1_spec = /^(gener|commun|arsen)/, 722 | doubles = /(bb|dd|ff|gg|mm|nn|pp|rr|tt)$/, 723 | y_cons = RegExp('([' + vowels + '])y', 'g'), 724 | y_suff = RegExp('(.[^' + vowels + '])[yY]$'), 725 | exceptions1 = { 726 | skis: 'ski', 727 | skies: 'sky', 728 | dying: 'die', 729 | lying: 'lie', 730 | tying: 'tie', 731 | 732 | idly: 'idl', 733 | gently: 'gentl', 734 | ugly: 'ugli', 735 | early: 'earli', 736 | only: 'onli', 737 | singly: 'singl', 738 | 739 | sky: 'sky', 740 | news: 'news', 741 | howe: 'howe', 742 | 743 | atlas: 'atlas', 744 | cosmos: 'cosmos', 745 | bias: 'bias', 746 | andes: 'andes', 747 | }, 748 | exceptions2 = [ 749 | 'inning', 750 | 'outing', 751 | 'canning', 752 | 'herring', 753 | 'earring', 754 | 'proceed', 755 | 'exceed', 756 | 'succeed', 757 | ] 758 | 759 | function stem(word) { 760 | // Exceptions 1 761 | var stop = stemmer.except(word, exceptions1) 762 | if (stop) return stop 763 | 764 | // No stemming for short words. 765 | if (word.length < 3) return word 766 | 767 | // Y = "y" as a consonant. 768 | if (word[0] === 'y') word = 'Y' + word.substr(1) 769 | word = word.replace(y_cons, '$1Y') 770 | 771 | // Identify the regions of the word. 772 | var r1, m 773 | if ((m = r1_spec.exec(word))) { 774 | r1 = m[0].length 775 | } else { 776 | r1 = r1_re.exec(word)[0].length 777 | } 778 | 779 | var r2 = r1 + r1_re.exec(word.substr(r1))[0].length 780 | 781 | // Step 0 782 | word = word.replace(/^'/, '') 783 | word = word.replace(/'(s'?)?$/, '') 784 | 785 | // Step 1a 786 | word = stemmer.among(word, [ 787 | 'sses', 788 | 'ss', 789 | '(ied|ies)', 790 | function (match, _, offset) { 791 | return offset > 1 ? 'i' : 'ie' 792 | }, 793 | '([' + vowels + '].*?[^us])s', 794 | function (match, m1) { 795 | return m1 796 | }, 797 | ]) 798 | 799 | stop = stemmer.except(word, exceptions2) 800 | if (stop) return stop 801 | 802 | // Step 1b 803 | word = stemmer.among(word, [ 804 | '(eed|eedly)', 805 | function (match, _, offset) { 806 | return offset >= r1 ? 'ee' : match + ' ' 807 | }, 808 | '([' + vowels + '].*?)(ed|edly|ing|ingly)', 809 | function (match, prefix, suffix, off) { 810 | if (/(?:at|bl|iz)$/.test(prefix)) { 811 | return prefix + 'e' 812 | } else if (doubles.test(prefix)) { 813 | return prefix.substr(0, prefix.length - 1) 814 | } else if ( 815 | shortv(word.substr(0, off + prefix.length)) && 816 | off + prefix.length <= r1 817 | ) { 818 | return prefix + 'e' 819 | } else { 820 | return prefix 821 | } 822 | }, 823 | ]) 824 | 825 | // Step 1c 826 | word = word.replace(y_suff, '$1i') 827 | 828 | // Step 2 829 | word = stemmer.among(word, r1, [ 830 | '(izer|ization)', 831 | 'ize', 832 | '(ational|ation|ator)', 833 | 'ate', 834 | 'enci', 835 | 'ence', 836 | 'anci', 837 | 'ance', 838 | 'abli', 839 | 'able', 840 | 'entli', 841 | 'ent', 842 | 'tional', 843 | 'tion', 844 | '(alism|aliti|alli)', 845 | 'al', 846 | 'fulness', 847 | 'ful', 848 | '(ousli|ousness)', 849 | 'ous', 850 | '(iveness|iviti)', 851 | 'ive', 852 | '(biliti|bli)', 853 | 'ble', 854 | 'ogi', 855 | function (m, off) { 856 | return word[off - 1] === 'l' ? 'og' : 'ogi' 857 | }, 858 | 'fulli', 859 | 'ful', 860 | 'lessli', 861 | 'less', 862 | 'li', 863 | function (m, off) { 864 | return ~valid_li.indexOf(word[off - 1]) ? '' : 'li' 865 | }, 866 | ]) 867 | 868 | // Step 3 869 | word = stemmer.among(word, r1, [ 870 | 'ational', 871 | 'ate', 872 | 'tional', 873 | 'tion', 874 | 'alize', 875 | 'al', 876 | '(icate|iciti|ical)', 877 | 'ic', 878 | '(ful|ness)', 879 | '', 880 | 'ative', 881 | function (m, off) { 882 | return off >= r2 ? '' : 'ative' 883 | }, 884 | ]) 885 | 886 | // Step 4 887 | word = stemmer.among(word, r2, [ 888 | '(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ism|ate|iti|ous|ive|ize)', 889 | '', 890 | 'ion', 891 | function (m, off) { 892 | return ~'st'.indexOf(word[off - 1]) ? '' : m 893 | }, 894 | ]) 895 | 896 | // Step 5 897 | word = stemmer.among(word, r1, [ 898 | 'e', 899 | function (m, off) { 900 | return off >= r2 || !shortv(word, off - 2) ? '' : 'e' 901 | }, 902 | 'l', 903 | function (m, off) { 904 | return word[off - 1] === 'l' && off >= r2 ? '' : 'l' 905 | }, 906 | ]) 907 | 908 | word = word.replace(/Y/g, 'y') 909 | 910 | return word 911 | } 912 | 913 | function shortv(word, i) { 914 | if (i == null) i = word.length - 2 915 | if (word.length < 3) i = 0 //return true 916 | return !!( 917 | (!~vowels.indexOf(word[i - 1]) && 918 | ~vowels.indexOf(word[i]) && 919 | !~v_wxy.indexOf(word[i + 1])) || 920 | (i === 0 && ~vowels.indexOf(word[i]) && !~vowels.indexOf(word[i + 1])) 921 | ) 922 | } 923 | 924 | const _LDA = function ( 925 | sentences, 926 | numberOfTopics, 927 | numberOfTermsPerTopic, 928 | languages, 929 | alphaValue, 930 | betaValue, 931 | randomSeed 932 | ) { 933 | // The result will consist of topics and their included terms [[{"term":"word1", "probability":0.065}, {"term":"word2", "probability":0.047}, ... ], [{"term":"word1", "probability":0.085}, {"term":"word2", "probability":0.024}, ... ]]. 934 | var result = [] 935 | // Index-encoded array of sentences, with each row containing the indices of the words in the vocabulary. 936 | var documents = new Array() 937 | // Hash of vocabulary words and the count of how many times each word has been seen. 938 | var f = {} 939 | // Vocabulary of unique words (porter stemmed). 940 | var vocab = new Array() 941 | // Vocabulary of unique words in their original form. 942 | var vocabOrig = {} 943 | // Array of stop words 944 | languages = languages || Array('en') 945 | 946 | if (sentences && sentences.length > 0) { 947 | var stopwords = new Array() 948 | 949 | languages.forEach(function (value) { 950 | stopwords = stopwords.concat(stop_words) 951 | }) 952 | 953 | for (var i = 0; i < sentences.length; i++) { 954 | if (sentences[i] == '') continue 955 | documents[i] = new Array() 956 | 957 | var words = sentences[i] ? sentences[i].split(/[\s,\"]+/) : null 958 | 959 | if (!words) continue 960 | for (var wc = 0; wc < words.length; wc++) { 961 | var w = words[wc] 962 | .toLowerCase() 963 | .replace(/[^a-z\'A-Z0-9\u00C0-\u00ff ]+/g, '') 964 | var wStemmed = stem(w) 965 | if ( 966 | w == '' || 967 | !wStemmed || 968 | w.length == 1 || 969 | stopwords.indexOf(w.replace("'", '')) > -1 || 970 | stopwords.indexOf(wStemmed) > -1 || 971 | w.indexOf('http') == 0 972 | ) 973 | continue 974 | if (f[wStemmed]) { 975 | f[wStemmed] = f[wStemmed] + 1 976 | } else if (wStemmed) { 977 | f[wStemmed] = 1 978 | vocab.push(wStemmed) 979 | vocabOrig[wStemmed] = w 980 | } 981 | 982 | documents[i].push(vocab.indexOf(wStemmed)) 983 | } 984 | } 985 | 986 | var V = vocab.length 987 | var M = documents.length 988 | var K = parseInt(numberOfTopics) 989 | var alpha = alphaValue || 0.1 // per-document distributions over topics 990 | var beta = betaValue || 0.01 // per-topic distributions over words 991 | documents = documents.filter((doc) => { 992 | return doc.length 993 | }) // filter empty documents 994 | 995 | lda.configure(documents, V, 10000, 2000, 100, 10, randomSeed) 996 | lda.gibbs(K, alpha, beta) 997 | 998 | var theta = lda.getTheta() 999 | var phi = lda.getPhi() 1000 | 1001 | var text = '' 1002 | 1003 | //topics 1004 | var topTerms = numberOfTermsPerTopic 1005 | for (var k = 0; k < phi.length; k++) { 1006 | var things = new Array() 1007 | for (var w = 0; w < phi[k].length; w++) { 1008 | things.push( 1009 | '' + 1010 | phi[k][w].toPrecision(2) + 1011 | '_' + 1012 | vocab[w] + 1013 | '_' + 1014 | vocabOrig[vocab[w]] 1015 | ) 1016 | } 1017 | things.sort().reverse() 1018 | //console.log(things); 1019 | if (topTerms > vocab.length) topTerms = vocab.length 1020 | 1021 | //console.log('Topic ' + (k + 1)); 1022 | var row = [] 1023 | 1024 | for (var t = 0; t < topTerms; t++) { 1025 | var topicTerm = things[t].split('_')[2] 1026 | var prob = parseInt(things[t].split('_')[0] * 100) 1027 | if (prob < 2) continue 1028 | 1029 | //console.log('Top Term: ' + topicTerm + ' (' + prob + '%)'); 1030 | 1031 | var term = {} 1032 | term.term = topicTerm 1033 | term.probability = parseFloat(things[t].split('_')[0]) 1034 | row.push(term) 1035 | } 1036 | 1037 | result.push(row) 1038 | } 1039 | } 1040 | 1041 | return result 1042 | } 1043 | 1044 | function makeArray(x) { 1045 | var a = new Array() 1046 | for (var i = 0; i < x; i++) { 1047 | a[i] = 0 1048 | } 1049 | return a 1050 | } 1051 | 1052 | function make2DArray(x, y) { 1053 | var a = new Array() 1054 | for (var i = 0; i < x; i++) { 1055 | a[i] = new Array() 1056 | for (var j = 0; j < y; j++) a[i][j] = 0 1057 | } 1058 | return a 1059 | } 1060 | 1061 | var lda = new (function () { 1062 | var documents, z, nw, nd, nwsum, ndsum, thetasum, phisum, V, K, alpha, beta 1063 | var THIN_INTERVAL = 20 1064 | var BURN_IN = 100 1065 | var ITERATIONS = 1000 1066 | var SAMPLE_LAG 1067 | var RANDOM_SEED 1068 | var dispcol = 0 1069 | var numstats = 0 1070 | this.configure = function ( 1071 | docs, 1072 | v, 1073 | iterations, 1074 | burnIn, 1075 | thinInterval, 1076 | sampleLag, 1077 | randomSeed 1078 | ) { 1079 | this.ITERATIONS = iterations 1080 | this.BURN_IN = burnIn 1081 | this.THIN_INTERVAL = thinInterval 1082 | this.SAMPLE_LAG = sampleLag 1083 | this.RANDOM_SEED = randomSeed 1084 | this.documents = docs 1085 | this.V = v 1086 | this.dispcol = 0 1087 | this.numstats = 0 1088 | } 1089 | this.initialState = function (K) { 1090 | var i 1091 | var M = this.documents.length 1092 | this.nw = make2DArray(this.V, K) 1093 | this.nd = make2DArray(M, K) 1094 | this.nwsum = makeArray(K) 1095 | this.ndsum = makeArray(M) 1096 | this.z = new Array() 1097 | for (i = 0; i < M; i++) this.z[i] = new Array() 1098 | for (var m = 0; m < M; m++) { 1099 | var N = this.documents[m].length 1100 | this.z[m] = new Array() 1101 | for (var n = 0; n < N; n++) { 1102 | var topic = parseInt('' + this.getRandom() * K) 1103 | this.z[m][n] = topic 1104 | this.nw[this.documents[m][n]][topic]++ 1105 | this.nd[m][topic]++ 1106 | this.nwsum[topic]++ 1107 | } 1108 | this.ndsum[m] = N 1109 | } 1110 | } 1111 | 1112 | this.gibbs = function (K, alpha, beta) { 1113 | var i 1114 | this.K = K 1115 | this.alpha = alpha 1116 | this.beta = beta 1117 | if (this.SAMPLE_LAG > 0) { 1118 | this.thetasum = make2DArray(this.documents.length, this.K) 1119 | this.phisum = make2DArray(this.K, this.V) 1120 | this.numstats = 0 1121 | } 1122 | this.initialState(K) 1123 | //document.write("Sampling " + this.ITERATIONS 1124 | // + " iterations with burn-in of " + this.BURN_IN + " (B/S=" 1125 | // + this.THIN_INTERVAL + ").
"); 1126 | for (i = 0; i < this.ITERATIONS; i++) { 1127 | for (var m = 0; m < this.z.length; m++) { 1128 | for (var n = 0; n < this.z[m].length; n++) { 1129 | var topic = this.sampleFullConditional(m, n) 1130 | this.z[m][n] = topic 1131 | } 1132 | } 1133 | if (i < this.BURN_IN && i % this.THIN_INTERVAL == 0) { 1134 | //document.write("B"); 1135 | this.dispcol++ 1136 | } 1137 | if (i > this.BURN_IN && i % this.THIN_INTERVAL == 0) { 1138 | //document.write("S"); 1139 | this.dispcol++ 1140 | } 1141 | if (i > this.BURN_IN && this.SAMPLE_LAG > 0 && i % this.SAMPLE_LAG == 0) { 1142 | this.updateParams() 1143 | //document.write("|"); 1144 | if (i % this.THIN_INTERVAL != 0) this.dispcol++ 1145 | } 1146 | if (this.dispcol >= 100) { 1147 | //document.write("*
"); 1148 | this.dispcol = 0 1149 | } 1150 | } 1151 | } 1152 | 1153 | this.sampleFullConditional = function (m, n) { 1154 | var topic = this.z[m][n] 1155 | this.nw[this.documents[m][n]][topic]-- 1156 | this.nd[m][topic]-- 1157 | this.nwsum[topic]-- 1158 | this.ndsum[m]-- 1159 | var p = makeArray(this.K) 1160 | for (var k = 0; k < this.K; k++) { 1161 | p[k] = 1162 | (((this.nw[this.documents[m][n]][k] + this.beta) / 1163 | (this.nwsum[k] + this.V * this.beta)) * 1164 | (this.nd[m][k] + this.alpha)) / 1165 | (this.ndsum[m] + this.K * this.alpha) 1166 | } 1167 | for (var k = 1; k < p.length; k++) { 1168 | p[k] += p[k - 1] 1169 | } 1170 | var u = this.getRandom() * p[this.K - 1] 1171 | for (topic = 0; topic < p.length; topic++) { 1172 | if (u < p[topic]) break 1173 | } 1174 | this.nw[this.documents[m][n]][topic]++ 1175 | this.nd[m][topic]++ 1176 | this.nwsum[topic]++ 1177 | this.ndsum[m]++ 1178 | return topic 1179 | } 1180 | 1181 | this.updateParams = function () { 1182 | for (var m = 0; m < this.documents.length; m++) { 1183 | for (var k = 0; k < this.K; k++) { 1184 | this.thetasum[m][k] += 1185 | (this.nd[m][k] + this.alpha) / (this.ndsum[m] + this.K * this.alpha) 1186 | } 1187 | } 1188 | for (var k = 0; k < this.K; k++) { 1189 | for (var w = 0; w < this.V; w++) { 1190 | this.phisum[k][w] += 1191 | (this.nw[w][k] + this.beta) / (this.nwsum[k] + this.V * this.beta) 1192 | } 1193 | } 1194 | this.numstats++ 1195 | } 1196 | 1197 | this.getTheta = function () { 1198 | var theta = new Array() 1199 | for (var i = 0; i < this.documents.length; i++) theta[i] = new Array() 1200 | if (this.SAMPLE_LAG > 0) { 1201 | for (var m = 0; m < this.documents.length; m++) { 1202 | for (var k = 0; k < this.K; k++) { 1203 | theta[m][k] = this.thetasum[m][k] / this.numstats 1204 | } 1205 | } 1206 | } else { 1207 | for (var m = 0; m < this.documents.length; m++) { 1208 | for (var k = 0; k < this.K; k++) { 1209 | theta[m][k] = 1210 | (this.nd[m][k] + this.alpha) / (this.ndsum[m] + this.K * this.alpha) 1211 | } 1212 | } 1213 | } 1214 | return theta 1215 | } 1216 | 1217 | this.getPhi = function () { 1218 | var phi = new Array() 1219 | for (var i = 0; i < this.K; i++) phi[i] = new Array() 1220 | if (this.SAMPLE_LAG > 0) { 1221 | for (var k = 0; k < this.K; k++) { 1222 | for (var w = 0; w < this.V; w++) { 1223 | phi[k][w] = this.phisum[k][w] / this.numstats 1224 | } 1225 | } 1226 | } else { 1227 | for (var k = 0; k < this.K; k++) { 1228 | for (var w = 0; w < this.V; w++) { 1229 | phi[k][w] = 1230 | (this.nw[w][k] + this.beta) / (this.nwsum[k] + this.V * this.beta) 1231 | } 1232 | } 1233 | } 1234 | return phi 1235 | } 1236 | 1237 | this.getRandom = function () { 1238 | if (this.RANDOM_SEED) { 1239 | // generate a pseudo-random number using a seed to ensure reproducable results. 1240 | var x = Math.sin(this.RANDOM_SEED++) * 1000000 1241 | return x - Math.floor(x) 1242 | } else { 1243 | // use standard random algorithm. 1244 | return Math.random() 1245 | } 1246 | } 1247 | })() 1248 | 1249 | export { 1250 | _LDA 1251 | } -------------------------------------------------------------------------------- /shared/ranking.js: -------------------------------------------------------------------------------- 1 | import { _LDA } from "./lda.js"; 2 | 3 | function rankKeywords(keywords, feedSettings) { 4 | return Object.keys(keywords) 5 | .map((keywordText) => { 6 | const keyword = keywords[keywordText] 7 | const searchQueryOccurrences = keyword.engagementTypes['search-query'] 8 | const metaOccurrences = 9 | keyword.engagementTypes['meta-keywords'] + 10 | keyword.engagementTypes['meta-title'] 11 | const clicksOccurrences = 12 | keyword.engagementTypes['link-click'] + 13 | keyword.engagementTypes['content-click'] 14 | const generalInputOccurrences = keyword.engagementTypes['input-type'] 15 | const customOccurrences = keyword.engagementTypes['custom'] 16 | 17 | const score = 18 | feedSettings.priorities.metaInfo * metaOccurrences + 19 | feedSettings.priorities.searchQuery * searchQueryOccurrences + 20 | feedSettings.priorities.clicks * clicksOccurrences + 21 | feedSettings.priorities.generalInput * generalInputOccurrences + 22 | 5 * customOccurrences // default to a lot 23 | 24 | keyword.score = score 25 | return keyword 26 | }) 27 | .sort((a, b) => { 28 | return b.score - a.score 29 | }) 30 | } 31 | 32 | function generateKeywordTopics(rankedKeywords, withProbabilities) { 33 | const maxKeywordScore = rankedKeywords[0].score 34 | const normalMax = 10 35 | const documents = rankedKeywords.map((keyword) => { 36 | const normalScore = (normalMax * keyword.score) / maxKeywordScore 37 | return `${keyword.text} `.repeat(Math.round(normalScore)).trim() 38 | }) 39 | 40 | let topicCount 41 | let termCount = 3 42 | 43 | if (rankedKeywords.length <= 10) { 44 | topicCount = 3 45 | termCount = 1 46 | } else if (rankedKeywords.length <= 200) { 47 | topicCount = 5 48 | termCount = 2 49 | } else { 50 | topicCount = Math.round(rankedKeywords.length / 40) 51 | if (topicCount > 10) { 52 | topicCount = 10; 53 | } 54 | } 55 | 56 | const results = _LDA(documents, topicCount, termCount).sort((a, b) => { 57 | return b[0].probability - a[0].probability 58 | }) 59 | if (withProbabilities) { 60 | return results 61 | } else { 62 | return results.map((result) => { 63 | return result[0].term 64 | }) 65 | } 66 | } 67 | 68 | function buildSearchQuery(topics, rankedKeywords, index, random) { 69 | const queryTypes = ['single-sorted', 'double-sorted'] 70 | const randomQueryTypes = ['single-random', 'double-random'] 71 | console.log('random', random) 72 | if (random) { 73 | const randomQueryType = 74 | randomQueryTypes[Math.floor(Math.random() * randomQueryTypes.length)] 75 | if (randomQueryType === 'single-random') { 76 | const randomTopic = 77 | rankedKeywords[Math.floor(Math.random() * rankedKeywords.length)] 78 | return [randomTopic.text, [randomTopic.text]] 79 | } else if (randomQueryType === 'double-random') { 80 | const randomTopic1 = 81 | rankedKeywords[Math.floor(Math.random() * rankedKeywords.length)] 82 | let randomTopic2 83 | while (randomTopic2 != randomTopic1 && !randomTopic2) { 84 | randomTopic2 = 85 | rankedKeywords[Math.floor(Math.random() * rankedKeywords.length)] 86 | } 87 | return [`${randomTopic1.text} ${randomTopic2.text}`, [randomTopic1.text, randomTopic2.text]] 88 | } 89 | } else { 90 | const queryType = queryTypes[Math.floor(Math.random() * queryTypes.length)] 91 | if (queryType === 'single-sorted') { 92 | return [topics[index], [topics[index]]] 93 | } else if (queryType === 'double-sorted') { 94 | const firstKeyword = topics[index] 95 | let randomRankedKeyword 96 | 97 | while (firstKeyword != randomRankedKeyword && !randomRankedKeyword) { 98 | randomRankedKeyword = topics[Math.floor(Math.random() * topics.length)] 99 | } 100 | return [`${firstKeyword} ${randomRankedKeyword}`, [firstKeyword, randomRankedKeyword]] 101 | } 102 | } 103 | } 104 | 105 | export { 106 | rankKeywords, 107 | generateKeywordTopics, 108 | buildSearchQuery 109 | } -------------------------------------------------------------------------------- /shared/react/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/.DS_Store -------------------------------------------------------------------------------- /shared/react/components/AlgorithmEditor.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import '../styles/algorithm-editor.scss' 3 | import TopicEditor from './TopicEditor' 4 | import DomainGraph from './DomainGraph' 5 | import TopicGraph from './TopicGraph' 6 | 7 | function AlgorithmEditor() { 8 | const [keywords, setKeywords] = useState([]) 9 | 10 | const API = chrome || browser; 11 | const getKeywords = () => { 12 | API.runtime.sendMessage( 13 | { 14 | action: 'getKeywords', 15 | }, 16 | (response) => { 17 | console.log('response.keywords', response.keywords) 18 | 19 | if (response.keywords) { 20 | setKeywords( 21 | response.keywords.sort((a, b) => { 22 | return b.occurrences - a.occurrences 23 | }) 24 | ) 25 | } else { 26 | setKeywords([]) 27 | } 28 | } 29 | ) 30 | } 31 | 32 | useEffect(() => { 33 | getKeywords() 34 | }, []) 35 | 36 | return ( 37 |
38 |

Engagement Overview

39 |
40 | 41 | 42 |
43 | 48 |
49 | ) 50 | } 51 | 52 | export default AlgorithmEditor 53 | -------------------------------------------------------------------------------- /shared/react/components/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import ContentFeed from './ContentFeed' 3 | import Footer from './Footer' 4 | import Header from './Header' 5 | import FeedSettings from './FeedSettings' 6 | import AlgorithmEditor from './AlgorithmEditor' 7 | 8 | function App() { 9 | const [currentPage, setCurrentPage] = useState('view-feed') 10 | return ( 11 |
12 |
13 | {currentPage === 'view-feed' && } 14 | {currentPage === 'algorithm-editor' && } 15 | {currentPage === 'feed-settings' && } 16 |
17 |
18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /shared/react/components/ContentBlock.js: -------------------------------------------------------------------------------- 1 | import '../styles/content-block.scss' 2 | 3 | import contentBranding from '../contentBranding' 4 | 5 | function ContentBlock({ content }) { 6 | let branding 7 | Object.keys(contentBranding).forEach((brandingDomain) => { 8 | if (content.source.includes(brandingDomain)) { 9 | branding = contentBranding[brandingDomain] 10 | } 11 | }) 12 | 13 | const renderBrandingImage = (brandingImage) => { 14 | if (content.source.includes('wikipedia.com')) { 15 | return 16 | } else { 17 | return brandingImage 18 | } 19 | } 20 | 21 | return ( 22 |
23 |
24 | {branding?.image && ( 25 |
30 | {renderBrandingImage(branding.image)} 31 |
32 | )} 33 | {content.title && ( 34 | 35 | {content.title} 36 | 37 | )} 38 | {!content.title && ( 39 | 40 | {content.link} 41 | 42 | )} 43 | {content.topics &&
44 | {content.topics.map((topic) => { 45 | return {topic} 46 | })}
} 47 | {content.description &&

{content.description}

} 48 | 49 |
50 | {content.image && ( 51 |
52 | 53 |
54 | )} 55 |
56 | ) 57 | } 58 | 59 | export default ContentBlock 60 | -------------------------------------------------------------------------------- /shared/react/components/ContentFeed.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import '../styles/content-feed.scss' 3 | 4 | import { ContentFetcher } from '../../content-fetcher' 5 | import ContentBlock from './ContentBlock' 6 | 7 | import loadingSvg from '../images/loading-spinner.svg' 8 | 9 | function ContentFeed({}) { 10 | const API = chrome || browser 11 | const [feedSettings, setFeedSettings] = useState() 12 | const [loading, setLoading] = useState(false) 13 | const [contentFeed, setContentFeed] = useState(null) 14 | const [keywords, setKeywords] = useState([]) 15 | const [runtimeAvailable, setRuntimeAvailable] = useState(false) 16 | const [initAttempts, setInitAttempts] = useState(0) 17 | const initattemptMax = 50 18 | 19 | useEffect(() => { 20 | if (!runtimeAvailable && initAttempts <= initattemptMax) { 21 | setInitAttempts(initAttempts + 1) 22 | API.runtime.sendMessage( 23 | { 24 | action: 'myalgorithm-init', 25 | }, 26 | (response) => { 27 | if (response.status === true) { 28 | setRuntimeAvailable(true) 29 | } 30 | } 31 | ) 32 | } 33 | }, [runtimeAvailable]) 34 | 35 | const saveNewContentFeed = (newContentFeed) => { 36 | return new Promise((resolve) => { 37 | API.runtime.sendMessage({ 38 | action: 'saveContentFeed', 39 | contentFeed: newContentFeed, 40 | }) 41 | resolve(true) 42 | }) 43 | } 44 | 45 | const getNewContentFeed = () => { 46 | return new Promise((resolve) => { 47 | API.runtime.sendMessage( 48 | { 49 | action: 'getSearchQueries', 50 | }, 51 | async (response) => { 52 | if (!response.searchQueries) { 53 | resolve(null) 54 | } 55 | const newContentFeed = await ContentFetcher( 56 | feedSettings.sourcing, 57 | response.customSources, 58 | response.searchQueries 59 | ) 60 | if (newContentFeed && newContentFeed.length > 0) { 61 | await saveNewContentFeed(newContentFeed) 62 | resolve(newContentFeed) 63 | } else { 64 | resolve(null) 65 | } 66 | } 67 | ) 68 | }) 69 | } 70 | 71 | useEffect(async () => { 72 | if ( 73 | !loading && 74 | !contentFeed && 75 | keywords && 76 | keywords.length > 0 && 77 | feedSettings && 78 | runtimeAvailable 79 | ) { 80 | setLoading(true) 81 | console.log('fetching content feed') 82 | API.runtime.sendMessage( 83 | { 84 | action: 'getContentFeed', 85 | }, 86 | async (response) => { 87 | console.log('response', response) 88 | if (response.contentFeed && response.contentFeed.length > 0) { 89 | console.log('got content feed', response.contentFeed) 90 | setContentFeed(response.contentFeed) 91 | } else { 92 | console.log('did not get content feed, getting new content feed') 93 | const newContentFeed = await getNewContentFeed() 94 | console.log(newContentFeed) 95 | if (newContentFeed && newContentFeed.length > 0) { 96 | setContentFeed(newContentFeed) 97 | } 98 | } 99 | setLoading(false) 100 | } 101 | ) 102 | } 103 | 104 | return () => { 105 | setContentFeed(null) 106 | } 107 | }, [loading, contentFeed, keywords, feedSettings, runtimeAvailable]) 108 | 109 | useEffect(() => { 110 | if (!feedSettings && runtimeAvailable) { 111 | API.runtime.sendMessage( 112 | { 113 | action: 'getFeedSettings', 114 | }, 115 | async (response) => { 116 | setFeedSettings(response.feedSettings) 117 | } 118 | ) 119 | } 120 | return () => { 121 | setFeedSettings(null) 122 | } 123 | }, [runtimeAvailable]) 124 | 125 | useEffect(() => { 126 | if ((!keywords || keywords.length === 0) && runtimeAvailable) { 127 | API.runtime.sendMessage( 128 | { 129 | action: 'getKeywords', 130 | }, 131 | (response) => { 132 | console.log(response.keywords) 133 | if (response.keywords) { 134 | setKeywords(response.keywords) 135 | } 136 | } 137 | ) 138 | } 139 | 140 | return () => { 141 | setKeywords(null) 142 | } 143 | }, [runtimeAvailable]) 144 | 145 | return ( 146 |
147 | {loading && ( 148 |
149 | 150 | Please wait for your daily feed to load (~10-15 seconds) 151 |
152 | )} 153 | 154 | {!loading && feedSettings?.refreshMode && ( 155 |
{ 158 | setLoading(true) 159 | const newContentFeed = await getNewContentFeed() 160 | console.log(newContentFeed) 161 | if (newContentFeed && newContentFeed.length > 0) { 162 | setContentFeed(newContentFeed) 163 | } 164 | setLoading(false) 165 | }} 166 | > 167 | Refresh 168 |
169 | )} 170 | 171 | {!loading && contentFeed && contentFeed.length > 0 && ( 172 |
173 | {contentFeed.map((content) => { 174 | return 175 | })} 176 |
177 | )} 178 | {!loading && (!contentFeed || contentFeed.length === 0) && ( 179 |
180 | {keywords && keywords.length > 0 && ( 181 | Could not find content 182 | )} 183 | {(!keywords || keywords.length === 0) && ( 184 |
No data yet :( start browsing and build up your algorithm
185 | )} 186 |
187 | )} 188 |
189 | ) 190 | } 191 | 192 | export default ContentFeed 193 | -------------------------------------------------------------------------------- /shared/react/components/DomainGraph.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react' 2 | import '../styles/domain-graph.scss' 3 | import Chart from 'chart.js/auto' 4 | 5 | function DomainGraph({ keywords }) { 6 | const [chartInstance, setChartInstance] = useState() 7 | const chartContainer = useRef() 8 | 9 | useEffect(() => { 10 | if (keywords && chartContainer && chartContainer.current) { 11 | if (chartInstance) { 12 | chartInstance.destroy() 13 | } 14 | let dataMap = {} 15 | 16 | keywords.forEach((keyword) => { 17 | Object.keys(keyword.history).forEach((sourceDomain) => { 18 | if (sourceDomain in dataMap) { 19 | dataMap[sourceDomain] += keyword.history[sourceDomain] 20 | } else { 21 | dataMap[sourceDomain] = keyword.history[sourceDomain] 22 | } 23 | }) 24 | }) 25 | 26 | const data = Object.values(dataMap) 27 | .sort((a, b) => { 28 | return a - b 29 | }) 30 | .reverse() 31 | .slice(0, 5) 32 | 33 | const labels = Object.keys(dataMap) 34 | .sort((sourceDomainA, sourceDomainB) => { 35 | return dataMap[sourceDomainA] - dataMap[sourceDomainB] 36 | }) 37 | .reverse() 38 | .slice(0, 5) 39 | 40 | const dataCopy = [...data] 41 | const chartData = { 42 | type: 'doughnut', 43 | data: { 44 | labels: labels, 45 | datasets: [ 46 | { 47 | label: '', 48 | data: data, 49 | backgroundColor: dataCopy 50 | .map((datum, i) => { 51 | const iteration = 255 / data.length 52 | const val = (i + 1) * iteration 53 | return `rgb(${255 - val}, ${255 - val}, 255)` 54 | }) 55 | .reverse(), 56 | }, 57 | ], 58 | }, 59 | options: { 60 | maintainAspectRatio: false, 61 | plugins: { 62 | legend: { 63 | display: false, 64 | }, 65 | }, 66 | }, 67 | } 68 | chartContainer.current.height = '150px' 69 | chartContainer.current.style.maxHeight = '150px' 70 | chartContainer.current.width = '150px' 71 | setChartInstance(new Chart(chartContainer.current, chartData)) 72 | } 73 | }, [keywords, chartContainer]) 74 | 75 | return ( 76 |
77 | {keywords && keywords.length > 0 && } 78 | {!keywords || (keywords.length === 0 &&
No data to show
)} 79 | {keywords && keywords.length > 0 && ( 80 |

81 | Websites you engage with the most (by occurrence) 82 |

83 | )} 84 |
85 | ) 86 | } 87 | 88 | export default DomainGraph 89 | -------------------------------------------------------------------------------- /shared/react/components/FeedSettings.js: -------------------------------------------------------------------------------- 1 | import '../styles/feed-settings.scss' 2 | import Slider from '@mui/material/Slider' 3 | import Switch from '@mui/material/Switch' 4 | import { useEffect, useState } from 'react' 5 | 6 | import { fetchContentSourceDetails } from '../../content-fetcher' 7 | 8 | import contentBranding from '../contentBranding' 9 | 10 | function FeedSettings() { 11 | const API = chrome || browser; 12 | 13 | const [metaInfo, setMetaInfo] = useState(1) 14 | const [searchQuery, setSearchQuery] = useState(1) 15 | const [linkClicks, setLinkClicks] = useState(1) 16 | const [generalInput, setGeneralInput] = useState(1) 17 | const [refreshMode, setRefreshMode] = useState(false) 18 | const [offMode, setOffMode] = useState(false) 19 | const [saved, setSaved] = useState(false) 20 | const [initFeedSettings, setInitFeedSettings] = useState(1) 21 | const [sourcingYoutube, setSourcingYoutube] = useState(false) 22 | const [sourcingTwitter, setSourcingTwitter] = useState(false) 23 | const [sourcingReddit, setSourcingReddit] = useState(false) 24 | const [sourcingQuora, setSourcingQuora] = useState(false) 25 | const [sourcingWikipedia, setSourcingWikipedia] = useState(false) 26 | const [sourcingOdysee, setSourcingOdysee] = useState(false) 27 | const [sourcingStackoverflow, setSourcingStackoverflow] = useState(false) 28 | const [sourcingGab, setSourcingGab] = useState(false) 29 | const [sourcingBitchute, setSourcingBitchute] = useState(false) 30 | const [showCustomSourceForm, setShowCustomSourceForm] = useState(false) 31 | const [customSourceText, setCustomSourceText] = useState() 32 | const [customSourceFormError, setCustomSourceFormError] = useState(false) 33 | const [savedSource, setSavedSource] = useState(false) 34 | const [loadingNewSource, setLoadingNewSource] = useState(false) 35 | const [customSources, setCustomSources] = useState() 36 | 37 | useEffect(() => { 38 | API.runtime.sendMessage( 39 | { 40 | action: 'getCustomSources', 41 | }, 42 | (response) => { 43 | setCustomSources(response.customSources) 44 | } 45 | ) 46 | }, []) 47 | 48 | useEffect(() => { 49 | API.runtime.sendMessage( 50 | { 51 | action: 'getFeedSettings', 52 | }, 53 | (response) => { 54 | if (response.feedSettings) { 55 | setInitFeedSettings(response.feedSettings) 56 | setMetaInfo(response.feedSettings.priorities.metaInfo) 57 | setLinkClicks(response.feedSettings.priorities.clicks) 58 | setSearchQuery(response.feedSettings.priorities.searchQuery) 59 | setGeneralInput(response.feedSettings.priorities.generalInput) 60 | setRefreshMode(response.feedSettings.refreshMode) 61 | setOffMode(response.feedSettings.disableAlgorithm) 62 | 63 | setSourcingYoutube(response.feedSettings.sourcing.youtube) 64 | setSourcingTwitter(response.feedSettings.sourcing.twitter) 65 | setSourcingReddit(response.feedSettings.sourcing.reddit) 66 | setSourcingQuora(response.feedSettings.sourcing.quora) 67 | setSourcingWikipedia(response.feedSettings.sourcing.wikipedia) 68 | setSourcingOdysee(response.feedSettings.sourcing.odysee) 69 | setSourcingStackoverflow(response.feedSettings.sourcing.stackoverflow) 70 | setSourcingGab(response.feedSettings.sourcing.gab) 71 | setSourcingBitchute(response.feedSettings.sourcing.bitchute) 72 | } 73 | } 74 | ) 75 | }, []) 76 | 77 | useEffect(() => { 78 | if (saved) { 79 | setTimeout(() => { 80 | setSaved(false) 81 | }, 2000) 82 | } 83 | }, [saved]) 84 | 85 | useEffect(() => { 86 | if (savedSource) { 87 | setTimeout(() => { 88 | setSavedSource(false) 89 | }, 2000) 90 | } 91 | }, [savedSource]) 92 | 93 | const saveSettings = () => { 94 | const newFeedSettings = { ...initFeedSettings } 95 | newFeedSettings.priorities.metaInfo = metaInfo 96 | newFeedSettings.priorities.clicks = linkClicks 97 | newFeedSettings.priorities.searchQuery = searchQuery 98 | newFeedSettings.priorities.generalInput = generalInput 99 | 100 | newFeedSettings.refreshMode = refreshMode 101 | newFeedSettings.disableAlgorithm = offMode 102 | 103 | newFeedSettings.sourcing.youtube = sourcingYoutube 104 | newFeedSettings.sourcing.twitter = sourcingTwitter 105 | newFeedSettings.sourcing.reddit = sourcingReddit 106 | newFeedSettings.sourcing.quora = sourcingQuora 107 | newFeedSettings.sourcing.wikipedia = sourcingWikipedia 108 | newFeedSettings.sourcing.odysee = sourcingOdysee 109 | newFeedSettings.sourcing.stackoverflow = sourcingStackoverflow 110 | newFeedSettings.sourcing.gab = sourcingGab 111 | newFeedSettings.sourcing.bitchute = sourcingBitchute 112 | 113 | if (customSources && Object.values(customSources).length > 0) { 114 | for (let customSource of Object.values(customSources)) { 115 | API.runtime.sendMessage({ 116 | action: 'editCustomSource', 117 | customSourceDomain: customSource.domain, 118 | enabled: customSource.checked, 119 | }) 120 | } 121 | } 122 | 123 | 124 | API.runtime.sendMessage( 125 | { 126 | action: 'saveFeedSettings', 127 | newFeedSettings, 128 | }, 129 | () => { 130 | setSaved(true) 131 | } 132 | ) 133 | } 134 | 135 | const addCustomSource = async () => { 136 | setCustomSourceFormError(null) 137 | setLoadingNewSource(true) 138 | console.log('customSourceText', customSourceText) 139 | let customSourceTextTemp = customSourceText 140 | 141 | if (!customSourceText || customSourceText.length === 0) { 142 | setCustomSourceFormError('You must enter a proper domain') 143 | setLoadingNewSource(false) 144 | return 145 | } 146 | 147 | if (customSourceText.split('.').length < 2) { 148 | setCustomSourceFormError('You must enter a proper domain') 149 | setLoadingNewSource(false) 150 | return 151 | } 152 | 153 | if ( 154 | customSourceTextTemp.indexOf('http://') !== 0 || 155 | customSourceTextTemp.indexOf('https://') !== 0 156 | ) { 157 | customSourceTextTemp = `http://${customSourceTextTemp}` 158 | } 159 | 160 | try { 161 | const url = new URL(customSourceTextTemp) 162 | if (!url.hostname || url.hostname.length === 0) { 163 | setCustomSourceFormError('You must enter a proper domain') 164 | setLoadingNewSource(false) 165 | 166 | return 167 | } 168 | } catch (e) { 169 | console.log(e) 170 | setCustomSourceFormError('You must enter a proper domain') 171 | setLoadingNewSource(false) 172 | 173 | return 174 | } 175 | 176 | let contentSourceDetails 177 | try { 178 | contentSourceDetails = await fetchContentSourceDetails( 179 | customSourceTextTemp, 180 | customSourceText 181 | ) 182 | } catch (e) { 183 | console.log(e) 184 | setCustomSourceFormError('Could not add Content Source try again') 185 | setLoadingNewSource(false) 186 | return 187 | } 188 | 189 | if (contentSourceDetails && contentSourceDetails.name) { 190 | API.runtime.sendMessage( 191 | { 192 | action: 'addCustomSource', 193 | customSourceData: { 194 | domain: customSourceText, 195 | sourceName: contentSourceDetails.name, 196 | image: contentSourceDetails.image, 197 | checked: false, 198 | }, 199 | }, 200 | () => { 201 | setSavedSource(true) 202 | setLoadingNewSource(false) 203 | } 204 | ) 205 | } else { 206 | setCustomSourceFormError('Could not add Content Source try again') 207 | setLoadingNewSource(false) 208 | return 209 | } 210 | } 211 | 212 | const checkCustomSource = (checked, customSourceToCheck) => { 213 | const customSourcesCopy = { ...customSources } 214 | customSourcesCopy[customSourceToCheck.domain].checked = checked 215 | setCustomSources(customSourcesCopy) 216 | } 217 | 218 | return ( 219 |
220 | 221 | 222 | What platforms do you want to get content from? 223 | 224 |
225 | 226 | {contentBranding['youtube.com'].image} 227 | 228 | { 231 | setSourcingYoutube(event.target.checked) 232 | }} 233 | /> 234 |
235 |
236 | 237 | {contentBranding['twitter.com'].image} 238 |  Twitter 239 | 240 | { 243 | setSourcingTwitter(event.target.checked) 244 | }} 245 | /> 246 |
247 |
248 | 249 | {contentBranding['reddit.com'].image} 250 | 251 | { 254 | setSourcingReddit(event.target.checked) 255 | }} 256 | /> 257 |
258 |
259 | 260 | {contentBranding['quora.com'].image} 261 | 262 | { 265 | setSourcingQuora(event.target.checked) 266 | }} 267 | /> 268 |
269 |
270 | 271 | 272 |  Wikipedia 273 | 274 | { 277 | setSourcingWikipedia(event.target.checked) 278 | }} 279 | /> 280 |
281 |
282 | 283 | {contentBranding['odysee.com'].image}  Odysee 284 | 285 | { 288 | setSourcingOdysee(event.target.checked) 289 | }} 290 | /> 291 |
292 | 293 |
294 | 295 | {contentBranding['stackoverflow.com'].image} 296 | 297 | { 300 | setSourcingStackoverflow(event.target.checked) 301 | }} 302 | /> 303 |
304 |
305 | 306 | {contentBranding['gab.com'].image} 307 | 308 | { 311 | setSourcingGab(event.target.checked) 312 | }} 313 | /> 314 |
315 |
316 | 317 | {contentBranding['bitchute.com'].image} 318 | 319 | { 322 | setSourcingBitchute(event.target.checked) 323 | }} 324 | /> 325 |
326 | 327 | {customSources && 328 | Object.keys(customSources).length > 0 && 329 | Object.values(customSources).map((customSource) => { 330 | return ( 331 |
332 | 333 | { 335 | API.runtime.sendMessage( 336 | { 337 | action: 'removeCustomSource', 338 | customSourceDomain: customSource.domain, 339 | }, 340 | () => { 341 | setTimeout(() => { 342 | API.runtime.sendMessage( 343 | { 344 | action: 'getCustomSources', 345 | }, 346 | (response) => { 347 | setCustomSources(response.customSources) 348 | } 349 | ) 350 | }, 500) 351 | } 352 | ) 353 | }} 354 | className="feed-settings__remove" 355 | > 356 | X 357 | 358 | {customSource.image && ( 359 | 363 | )} 364 | {customSource.sourceName}  365 | {customSource.sourceName !== customSource.domain 366 | ? ` (${customSource.domain})` 367 | : ''} 368 | 369 | { 372 | checkCustomSource(event.target.checked, customSource) 373 | }} 374 | /> 375 |
376 | ) 377 | })} 378 |
379 | {!showCustomSourceForm && ( 380 |
{ 383 | setShowCustomSourceForm(true) 384 | }} 385 | > 386 | Add a Custom Source 387 |
388 | )} 389 | {showCustomSourceForm && ( 390 |
391 | {customSourceFormError && ( 392 |

{customSourceFormError}

393 | )} 394 |
395 | setCustomSourceText(e.target.value)} 401 | /> 402 |
403 |
{ 408 | try { 409 | await addCustomSource() 410 | setTimeout(() => { 411 | API.runtime.sendMessage( 412 | { 413 | action: 'getCustomSources', 414 | }, 415 | (response) => { 416 | setCustomSources(response.customSources) 417 | } 418 | ) 419 | }, 500) 420 | } catch (e) { 421 | setCustomSourceFormError( 422 | 'Could not add Content Source try another domain' 423 | ) 424 | setLoadingNewSource(false) 425 | } 426 | }} 427 | > 428 | Add Custom Source 429 |
430 | {savedSource && ( 431 | Saved! 432 | )} 433 | {loadingNewSource && ( 434 | Loading... 435 | )} 436 |
437 |
438 | 439 | Regex Patterns are allowed (goodreads.com/*/) 440 | 441 |
442 | )} 443 |
444 |
445 | 446 |
447 | Meta Details 448 | { 453 | setMetaInfo(newValue) 454 | }} 455 | min={1} 456 | max={5} 457 | valueLabelDisplay="auto" 458 | /> 459 |
460 |
461 | Search Queries 462 | { 467 | setSearchQuery(newValue) 468 | }} 469 | min={1} 470 | max={5} 471 | valueLabelDisplay="auto" 472 | /> 473 |
474 |
475 | Link Clicks 476 | { 481 | setLinkClicks(newValue) 482 | }} 483 | min={1} 484 | max={5} 485 | valueLabelDisplay="auto" 486 | /> 487 |
488 |
489 | General Input 490 | { 495 | setGeneralInput(newValue) 496 | }} 497 | min={1} 498 | max={5} 499 | valueLabelDisplay="auto" 500 | /> 501 |
502 |
503 | 504 |
505 | 506 | Enable Refresh (for Today's Feed) 507 | 508 | { 511 | setRefreshMode(event.target.checked) 512 | }} 513 | /> 514 |
515 |
516 | 517 | Turn off myAlgorithm (no data will be collected) 518 | 519 | { 522 | setOffMode(event.target.checked) 523 | }} 524 | /> 525 |
526 |
527 | 528 |
529 |
530 | Save 531 |
532 | {saved && Saved!} 533 |
534 |
535 | ) 536 | } 537 | 538 | export default FeedSettings 539 | -------------------------------------------------------------------------------- /shared/react/components/Footer.js: -------------------------------------------------------------------------------- 1 | import '../styles/footer.scss' 2 | 3 | function Footer() { 4 | return
5 | } 6 | 7 | export default Footer 8 | -------------------------------------------------------------------------------- /shared/react/components/Header.js: -------------------------------------------------------------------------------- 1 | import '../styles/header.scss' 2 | 3 | function Header({ currentPage, setCurrentPage }) { 4 | return ( 5 |
6 |
7 |

myAlgorithm

8 | 16 |
17 |
18 |
setCurrentPage('view-feed')} 23 | > 24 | Today's Feed 25 |
26 |
setCurrentPage('algorithm-editor')} 31 | > 32 | Algorithm Editor 33 |
34 |
setCurrentPage('feed-settings')} 39 | > 40 | Feed Settings 41 |
42 |
43 |
44 | ) 45 | } 46 | 47 | export default Header 48 | -------------------------------------------------------------------------------- /shared/react/components/TopicEditor.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Tooltip, { tooltipClasses } from '@mui/material/Tooltip' 3 | import { styled } from '@mui/material/styles' 4 | import '../styles/topic-editor.scss' 5 | 6 | function TopicEditor({ getKeywords, keywords, setKeywords }) { 7 | const API = chrome || browser; 8 | 9 | const [showAddTopicForm, setShowAddTopicForm] = useState(false) 10 | const [newTopic, setNewTopic] = useState() 11 | const [showCount, setShowCount] = useState(50) 12 | const TopicTooltip = styled(({ className, ...props }) => ( 13 | 19 | ))(({}) => ({ 20 | [`& .${tooltipClasses.arrow}`]: { 21 | color: '#000000', 22 | }, 23 | [`& .${tooltipClasses.tooltip}`]: { 24 | backgroundColor: '#000000', 25 | fontFamily: 'Roobert', 26 | fontSize: '12px', 27 | borderRadius: '6px', 28 | padding: '8px 10px', 29 | }, 30 | })) 31 | 32 | const removeKeyword = (keywordText, index) => { 33 | API.runtime.sendMessage( 34 | { 35 | action: 'removeKeyword', 36 | keyword: keywordText, 37 | }, 38 | () => { 39 | const newKeywords = [...keywords] 40 | newKeywords.splice(index, 1) 41 | setKeywords(newKeywords) 42 | } 43 | ) 44 | } 45 | 46 | const addTopic = () => { 47 | if (newTopic.length === 0) { 48 | alert('Your topic must not be empty') 49 | } else { 50 | API.runtime.sendMessage( 51 | { 52 | action: 'addTopic', 53 | keyword: newTopic, 54 | }, 55 | () => { 56 | setTimeout(() => { 57 | getKeywords() 58 | }, 500) 59 | } 60 | ) 61 | } 62 | } 63 | return ( 64 |
65 |
66 | 67 | Edit your algorithm: (topics are generated from your browsing habits) 68 | 69 | 70 | {keywords && keywords.length > 0 && ( 71 |
72 | {keywords.slice(0, showCount).map((keyword, i) => { 73 | return ( 74 | 82 | Search Queries = ${keyword.engagementTypes['search-query']}
83 | Meta Keywords = ${keyword.engagementTypes['meta-keywords']}
84 | Meta Titles = ${keyword.engagementTypes['meta-title']}
85 | Link Clicks = ${keyword.engagementTypes['link-click']}
86 | General Input = ${keyword.engagementTypes['input-type']}
87 | Custom = ${keyword.engagementTypes['custom']}
88 | Total = ${keyword.occurrences} 89 | `, 90 | }} 91 | > 92 | } 93 | > 94 |
95 | {keyword.text}{' '} 96 | removeKeyword(keyword.text, i)} 99 | > 100 | X 101 | 102 |
103 |
104 | ) 105 | })} 106 | {showCount < keywords.length && ( 107 | 113 | )} 114 |
115 | )} 116 | {!keywords || 117 | (keywords.length === 0 && ( 118 |
119 | No data yet :( start browsing and build up your algorithm 120 |
121 | ))} 122 | 123 |
128 | {keywords && keywords.length > 0 && ( 129 |
{ 132 | API.runtime.sendMessage( 133 | { 134 | action: 'clearKeywords', 135 | }, 136 | () => { 137 | setTimeout(() => { 138 | getKeywords() 139 | }, 500) 140 | } 141 | ) 142 | }} 143 | > 144 | Reset Algorithm 145 |
146 | )} 147 | {!showAddTopicForm && ( 148 | 154 | )} 155 | {showAddTopicForm && ( 156 |
157 | { 160 | setNewTopic(e.target.value) 161 | }} 162 | className="topic-editor__add-topic-text" 163 | type="text" 164 | placeholder="Topic name (e.g. your favorite car name)" 165 | /> 166 |
addTopic()} 169 | > 170 | Add Topic 171 |
172 |
173 | )} 174 |
175 |
176 |
177 | ) 178 | } 179 | 180 | export default TopicEditor 181 | -------------------------------------------------------------------------------- /shared/react/components/TopicGraph.js: -------------------------------------------------------------------------------- 1 | import '../styles/topic-graph.scss' 2 | import { useEffect, useState, useRef } from 'react' 3 | import Chart from 'chart.js/auto' 4 | 5 | function TopicGraph({ keywords }) { 6 | const [topics, setTopics] = useState() 7 | const [chartInstance, setChartInstance] = useState() 8 | 9 | const chartContainer = useRef() 10 | const API = chrome || browser; 11 | useEffect(() => { 12 | API.runtime.sendMessage( 13 | { 14 | action: 'getTopics', 15 | }, 16 | (response) => { 17 | setTopics(response.topics) 18 | } 19 | ) 20 | }, [keywords]) 21 | 22 | useEffect(() => { 23 | if (keywords && chartContainer && chartContainer.current) { 24 | if (chartInstance) { 25 | chartInstance.destroy() 26 | } 27 | let dataMap = {} 28 | 29 | for (let topic of topics) { 30 | dataMap[topic[0].term] = topic[0].probability 31 | } 32 | 33 | const data = Object.values(dataMap) 34 | .sort((a, b) => { 35 | return a - b 36 | }) 37 | .reverse() 38 | .slice(0, 5) 39 | 40 | const labels = Object.keys(dataMap) 41 | .sort((a, b) => { 42 | return dataMap[a] - dataMap[b] 43 | }) 44 | .reverse() 45 | .slice(0, 5) 46 | 47 | const dataCopy = [...data] 48 | const chartData = { 49 | type: 'doughnut', 50 | data: { 51 | labels: labels, 52 | datasets: [ 53 | { 54 | label: '', 55 | data: data, 56 | backgroundColor: dataCopy 57 | .map((datum, i) => { 58 | const iteration = 255 / data.length 59 | const val = (i + 1) * iteration 60 | return `rgb(${255 - val}, 255, ${255 - val})` 61 | }) 62 | .reverse(), 63 | }, 64 | ], 65 | }, 66 | options: { 67 | maintainAspectRatio: false, 68 | plugins: { 69 | legend: { 70 | display: false, 71 | }, 72 | }, 73 | }, 74 | } 75 | chartContainer.current.height = '150px' 76 | chartContainer.current.style.maxHeight = '150px' 77 | chartContainer.current.width = '150px' 78 | setChartInstance(new Chart(chartContainer.current, chartData)) 79 | } 80 | }, [keywords, chartContainer]) 81 | 82 | return ( 83 |
84 | {keywords && keywords.length > 0 && } 85 | {!keywords || (keywords.length === 0 &&
No data to show
)} 86 | {keywords && keywords.length > 0 && ( 87 |

Your top topics

88 | )} 89 |
90 | ) 91 | } 92 | 93 | export default TopicGraph 94 | -------------------------------------------------------------------------------- /shared/react/contentBranding.js: -------------------------------------------------------------------------------- 1 | import wikipediaSVG from './images/wikipedia.svg' 2 | 3 | const contentBranding = { 4 | 'twitter.com': { 5 | image: ( 6 | 25 | ), 26 | color: '#1DA1F2', 27 | }, 28 | 'quora.com': { 29 | image: ( 30 | 35 | 39 | 40 | ), 41 | color: 'rgb(185, 43, 39)', 42 | }, 43 | 'gab.com': { 44 | image: ( 45 | 54 | 55 | 60 | 61 | ), 62 | color: '#00D178', 63 | }, 64 | 'bitchute.com': { 65 | image: ( 66 | 77 | 78 | 79 | 84 | 89 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ), 101 | color: '#8D7B64', 102 | }, 103 | 'reddit.com': { 104 | image: ( 105 | 115 | 116 | 123 | 137 | 138 | 139 | 146 | 153 | 158 | 162 | 168 | 174 | 180 | 181 | 182 | ), 183 | color: '#FF5700', 184 | }, 185 | 'youtube.com': { 186 | image: ( 187 | 193 | 199 | 200 | 205 | 210 | 211 | 212 | 213 | 218 | 223 | 228 | 233 | 238 | 243 | 248 | 249 | 250 | 251 | 252 | ), 253 | color: '#FF0000', 254 | }, 255 | 'wikipedia.com': { 256 | image: wikipediaSVG, 257 | }, 258 | 'odysee.com': { 259 | image: ( 260 | 261 | Odysee 262 | 263 | 264 | ), 265 | }, 266 | 'stackoverflow.com': { 267 | image: ( 268 | 274 | 275 | 280 | 284 | 289 | 290 | 291 | ), 292 | }, 293 | } 294 | 295 | export default contentBranding 296 | -------------------------------------------------------------------------------- /shared/react/fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/.DS_Store -------------------------------------------------------------------------------- /shared/react/fonts/Courgette-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/Courgette-Regular.ttf -------------------------------------------------------------------------------- /shared/react/fonts/JosefinSlab.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/JosefinSlab.ttf -------------------------------------------------------------------------------- /shared/react/fonts/Merriweather-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/Merriweather-Regular.otf -------------------------------------------------------------------------------- /shared/react/fonts/Nexa-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/Nexa-Bold.otf -------------------------------------------------------------------------------- /shared/react/fonts/Nexa-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/Nexa-Light.otf -------------------------------------------------------------------------------- /shared/react/fonts/TheanoOldStyle-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/TheanoOldStyle-Regular.ttf -------------------------------------------------------------------------------- /shared/react/fonts/WorkSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/fonts/WorkSans.ttf -------------------------------------------------------------------------------- /shared/react/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/images/.DS_Store -------------------------------------------------------------------------------- /shared/react/images/chrome_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/images/chrome_logo.png -------------------------------------------------------------------------------- /shared/react/images/chrome_logo_myalgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/images/chrome_logo_myalgo.png -------------------------------------------------------------------------------- /shared/react/images/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/images/icon_32.png -------------------------------------------------------------------------------- /shared/react/images/loading-spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/react/images/loading-wwt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /shared/react/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDom from 'react-dom' 3 | import App from './components/App' 4 | import './styles/index.scss' 5 | 6 | ReactDom.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /shared/react/styles/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/styles/.DS_Store -------------------------------------------------------------------------------- /shared/react/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | @mixin phone { 4 | @media (max-width: $phone-width) { 5 | @content; 6 | } 7 | } 8 | 9 | @mixin not-phone { 10 | @media (min-width: $phone-width) { 11 | @content; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shared/react/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $phone-width: 640px; 2 | -------------------------------------------------------------------------------- /shared/react/styles/algorithm-editor.scss: -------------------------------------------------------------------------------- 1 | .algorithm-editor { 2 | .algorithm-editor__title { 3 | font-size: 14px; 4 | margin: 0 0 15px 0; 5 | padding: 15px 0 0 0; 6 | text-align: center; 7 | font-weight: 700; 8 | font-family: 'Helvetica'; 9 | border-top: 1px solid rgba(0, 0, 0, 0.3); 10 | width: 100%; 11 | } 12 | .algorithm-editor__hint { 13 | width: 100%; 14 | border-bottom: 1px solid rgba(0, 0, 0, 0.3); 15 | padding: 5px 0px 10px 0px; 16 | } 17 | .algorithm-editor__graphs { 18 | margin-bottom: 20px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shared/react/styles/content-block.scss: -------------------------------------------------------------------------------- 1 | .content-block { 2 | width: 100%; 3 | height: 240px; 4 | overflow-y: hidden; 5 | overflow-x: hidden; 6 | min-width: 100%; 7 | max-width: 100%; 8 | border: 1px solid rgba(0, 0, 0, 0.3); 9 | border-radius: 3px; 10 | margin-bottom: 15px; 11 | 12 | a { 13 | font-size: 16px; 14 | max-height: 100px; 15 | margin-bottom: 5px; 16 | overflow-y: auto; 17 | z-index: 3; 18 | position: relative; 19 | word-break: break-word; 20 | } 21 | p { 22 | font-size: 14px; 23 | overflow-y: auto; 24 | height: 100%; 25 | max-height: 200px; 26 | margin-bottom: 0px; 27 | margin-top: 0px; 28 | z-index: 3; 29 | position: relative; 30 | word-break: break-all; 31 | } 32 | .content-block__content-container { 33 | width: 100%; 34 | position: relative; 35 | padding: 10px; 36 | box-sizing: border-box; 37 | max-height: 240px; 38 | .content-block__background-branding { 39 | pointer-events: none; 40 | width: 100%; 41 | height: 240px; 42 | max-height: 240px; 43 | opacity: 0.3; 44 | position: absolute; 45 | top: 0; 46 | background-repeat: no-repeat; 47 | z-index: 0; 48 | &.twitter { 49 | svg { 50 | width: 33%; 51 | } 52 | } 53 | svg, 54 | img { 55 | width: 50%; 56 | height: auto; 57 | } 58 | } 59 | } 60 | .content-block__image-container { 61 | width: 240px; 62 | height: 240px; 63 | max-width: 240px; 64 | max-height: 240px; 65 | 66 | border-left: 1px solid rgba(0, 0, 0, 0.2); 67 | img, 68 | svg { 69 | width: 240px; 70 | height: 240px; 71 | object-fit: cover; 72 | } 73 | svg { 74 | margin: 10px; 75 | } 76 | } 77 | .content-block__topic-wrapper { 78 | margin: 10px 0px; 79 | .content-block__topic-label { 80 | color: black; 81 | font-weight: bold; 82 | font-size: 12px; 83 | margin-right: 7px; 84 | } 85 | .content-block__topic-bubble { 86 | color: black; 87 | padding: 5px 7px; 88 | border-radius: 25px; 89 | background-color: rgb(200,200,200); 90 | margin-right: 10px; 91 | font-size: 12px; 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /shared/react/styles/content-feed.scss: -------------------------------------------------------------------------------- 1 | .content-feed { 2 | .content-feed__refresh { 3 | margin-bottom: 7px; 4 | font-size: 16px; 5 | padding: 10px; 6 | border: 3px; 7 | background-color: rgba(0, 0, 0, 0.9); 8 | border: 1px solid rgba(0, 0, 0, 0.9); 9 | 10 | color: rgb(255, 255, 255); 11 | &:hover { 12 | color: rgba(0, 0, 0, 0.9); 13 | background-color: rgb(255, 255, 255); 14 | border: 1px solid rgba(0, 0, 0, 0.9); 15 | } 16 | cursor: pointer; 17 | width: 100%; 18 | text-align: center; 19 | } 20 | .content-feed__loading-view { 21 | padding: 40px 0px; 22 | img { 23 | height: 150px; 24 | width: 150px; 25 | margin-bottom: 25px; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shared/react/styles/domain-graph.scss: -------------------------------------------------------------------------------- 1 | .domain-graph { 2 | width: 50%; 3 | min-width: 50%; 4 | max-width: 50%; 5 | .domain-graph__title { 6 | font-size: 12px; 7 | margin: 15px 0 0 0; 8 | padding: 0; 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /shared/react/styles/feed-settings.scss: -------------------------------------------------------------------------------- 1 | .feed-settings { 2 | padding: 10px; 3 | .feed-settings__hint { 4 | font-size: 14px; 5 | color: rgba(0, 0, 0, 0.6); 6 | margin-bottom: 10px; 7 | } 8 | .feed-settings__group-label { 9 | font-size: 16px; 10 | font-weight: bold; 11 | display: block; 12 | margin-bottom: 10px; 13 | font-family: 'Helvetica'; 14 | } 15 | .feed-settings__seperator { 16 | border-top: 1px solid rgba(0, 0, 0, 0.2); 17 | margin-bottom: 15px; 18 | } 19 | .feed-settings__row { 20 | margin-bottom: 5px; 21 | .feed-settings__row-label { 22 | width: 40%; 23 | min-width: 40%; 24 | font-size: 14px; 25 | text-align: left; 26 | cursor: default; 27 | svg, 28 | img { 29 | height: 20px; 30 | width: auto; 31 | } 32 | } 33 | } 34 | .feed-settings__save-button { 35 | font-size: 16px; 36 | width: fit-content; 37 | margin-top: 7px; 38 | padding: 7px 10px; 39 | color: rgb(255, 255, 255); 40 | border: 3px; 41 | background-color: rgba(0, 0, 0, 0.6); 42 | border: 1px solid rgba(0, 0, 0, 0.6); 43 | 44 | &:hover { 45 | color: rgba(0, 0, 0, 0.9); 46 | background-color: rgb(255, 255, 255); 47 | border: 1px solid rgba(0, 0, 0, 0.9); 48 | } 49 | cursor: pointer; 50 | } 51 | .feed-settings__saved-complete { 52 | color: rgb(100, 200, 100); 53 | margin-left: 7px; 54 | font-size: 16px; 55 | font-weight: bold; 56 | margin-top: 2px; 57 | } 58 | .feed-settings__remove { 59 | color: rgba(0, 0, 0, 0.6); 60 | margin-right: 10px; 61 | font-weight: bold; 62 | font-size: 16px; 63 | font-family: 'Helvetica'; 64 | cursor: pointer; 65 | &:hover { 66 | color: red; 67 | } 68 | } 69 | .feed-settings__show-form-btn, 70 | .feed-settings__submit-form-btn { 71 | margin-bottom: 10px; 72 | font-size: 14px; 73 | width: fit-content; 74 | margin-top: 7px; 75 | padding: 7px 10px; 76 | color: rgb(255, 255, 255); 77 | border: 3px; 78 | background-color: rgba(0, 0, 0, 0.9); 79 | border: 1px solid rgba(0, 0, 0, 0.9); 80 | 81 | &:hover { 82 | color: rgba(0, 0, 0, 0.9); 83 | background-color: rgb(255, 255, 255); 84 | border: 1px solid rgba(0, 0, 0, 0.9); 85 | } 86 | cursor: pointer; 87 | } 88 | 89 | .feed-settings__form-text-input { 90 | font-size: 14px; 91 | width: 300px; 92 | margin-top: 7px; 93 | padding: 7px 10px; 94 | color: rgba(0, 0, 0, 0.9); 95 | border: 3px; 96 | border: 1px solid rgba(0, 0, 0, 0.6); 97 | margin-right: 7px; 98 | margin-bottom: 10px; 99 | } 100 | 101 | .feed-settings__custom-source-form { 102 | } 103 | 104 | .feed-settings__hint { 105 | color: rgba(0, 0, 0, 0.6); 106 | font-size: 12px; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /shared/react/styles/footer.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawerty/myAlgorithm/03c91b73312adc05013f053c796f969186293b54/shared/react/styles/footer.scss -------------------------------------------------------------------------------- /shared/react/styles/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 0px; 3 | margin-bottom: 10px; 4 | .header__option { 5 | text-align: center; 6 | flex: 1; 7 | padding: 10px; 8 | font-size: 16px; 9 | background-color: rgba(0, 0, 0, 0.2); 10 | color: rgb(0, 0, 0); 11 | cursor: pointer; 12 | font-weight: bold; 13 | &:hover { 14 | background-color: rgba(0, 0, 0, 0.6); 15 | color: rgb(255, 255, 255); 16 | } 17 | &.active { 18 | background-color: rgb(0, 0, 0); 19 | color: rgb(255, 255, 255); 20 | } 21 | } 22 | 23 | .header__title-area { 24 | padding: 10px; 25 | 26 | .header__title { 27 | font-family: 'Courgette-Regular'; 28 | color: rgb(0, 0, 0); 29 | font-size: 20px; 30 | } 31 | 32 | a { 33 | font-size: 14px; 34 | margin-right: 15px; 35 | &:last-child { 36 | margin-right: 0px; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/react/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'utils'; 2 | 3 | @font-face { 4 | font-family: 'Nexa-Light'; 5 | src: url(../fonts/Nexa-Light.otf) format('opentype'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Nexa-Bold'; 10 | src: url(../fonts/Nexa-Bold.otf) format('opentype'); 11 | } 12 | 13 | @font-face { 14 | font-family: 'WorkSans'; 15 | src: url(../fonts/WorkSans.ttf) format('truetype'); 16 | } 17 | 18 | @font-face { 19 | font-family: 'Courgette-Regular'; 20 | src: url(../fonts/Courgette-Regular.ttf) format('truetype'); 21 | } 22 | 23 | * { 24 | box-sizing: border-box; 25 | } 26 | 27 | body, 28 | html { 29 | } 30 | 31 | * { 32 | font-family: 'WorkSans', sans-serif; 33 | } 34 | 35 | #root { 36 | height: 100%; 37 | } 38 | 39 | .h-100 { 40 | height: 100%; 41 | } 42 | 43 | .app { 44 | width: 500px; 45 | height: calc(100% - 50px); 46 | border-radius: 20px; 47 | background-color: #fff; 48 | } 49 | 50 | a { 51 | text-decoration: none !important; 52 | color: #00a4e8 !important; 53 | } 54 | 55 | .disabled { 56 | opacity: 0.4; 57 | pointer-events: none; 58 | } 59 | 60 | .error-text { 61 | color: red; 62 | margin-bottom: 0; 63 | } 64 | 65 | .flex { 66 | display: flex; 67 | } 68 | 69 | .align-center { 70 | align-items: center; 71 | } 72 | 73 | .align-between { 74 | align-content: space-between; 75 | } 76 | 77 | .align-start { 78 | align-items: start; 79 | } 80 | 81 | .align-end { 82 | align-items: end; 83 | } 84 | 85 | .justify-center { 86 | justify-content: center; 87 | } 88 | 89 | .justify-start { 90 | justify-content: start; 91 | } 92 | 93 | .justify-end { 94 | justify-content: end; 95 | } 96 | 97 | .justify-between { 98 | justify-content: space-between; 99 | } 100 | 101 | .flex-column { 102 | flex-direction: column; 103 | } 104 | 105 | .flex-row { 106 | flex-direction: row; 107 | } 108 | 109 | .flex-row-reverse { 110 | flex-direction: row-reverse; 111 | } 112 | 113 | .flex-wrap { 114 | flex-wrap: wrap; 115 | } 116 | 117 | /* 118 | text colors 119 | */ 120 | 121 | .error-text { 122 | color: red; 123 | } 124 | 125 | /* 126 | margins 127 | */ 128 | 129 | .mt-1 { 130 | margin-top: 10px; 131 | } 132 | 133 | .mt-2 { 134 | margin-top: 20px; 135 | } 136 | 137 | .mb-1 { 138 | margin-bottom: 10px; 139 | } 140 | 141 | .mb-2 { 142 | margin-bottom: 20px; 143 | } 144 | 145 | .mb-3 { 146 | margin-bottom: 30px; 147 | } 148 | 149 | .mb-4 { 150 | margin-bottom: 40px; 151 | } 152 | 153 | .mr-1 { 154 | margin-right: 10px; 155 | } 156 | 157 | .mr-2 { 158 | margin-right: 20px; 159 | } 160 | 161 | .mr-3 { 162 | margin-right: 30px; 163 | } 164 | 165 | .ml-1 { 166 | margin-left: 10px; 167 | } 168 | 169 | .ml-2 { 170 | margin-left: 20px; 171 | } 172 | 173 | .ml-3 { 174 | margin-left: 30px; 175 | } 176 | 177 | .pr-1 { 178 | padding-right: 10px; 179 | } 180 | 181 | .pr-2 { 182 | padding-right: 20px; 183 | } 184 | 185 | .pr-3 { 186 | padding-right: 30px; 187 | } 188 | 189 | .pr-4 { 190 | padding-right: 40px; 191 | } 192 | 193 | .full-width { 194 | width: 100%; 195 | } 196 | 197 | .half-width { 198 | width: 50%; 199 | } 200 | 201 | .no-wrap { 202 | white-space: nowrap; 203 | } 204 | 205 | .quarter-width { 206 | width: 25%; 207 | } 208 | 209 | .three-quarters-width { 210 | width: 75%; 211 | } 212 | 213 | .cursor-pointer { 214 | cursor: pointer; 215 | } 216 | 217 | .text-center { 218 | text-align: center; 219 | } 220 | 221 | .default-image { 222 | background-repeat: no-repeat; 223 | background-size: cover; 224 | height: 100px; 225 | width: 100px; 226 | border-radius: 50%; 227 | } 228 | 229 | .avatar { 230 | border-radius: 50%; 231 | object-fit: cover; 232 | } 233 | 234 | .remove-decor { 235 | text-decoration: none; 236 | } 237 | 238 | .clickable { 239 | cursor: pointer; 240 | } 241 | 242 | .content-wrapper { 243 | min-height: 80vh; 244 | } 245 | 246 | .flex-fill { 247 | flex: 1; 248 | } 249 | 250 | .position-absolute { 251 | position: absolute; 252 | } 253 | 254 | .position-relative { 255 | position: relative; 256 | } 257 | 258 | .unscrollable { 259 | max-height: 100vh !important; 260 | overflow: hidden; 261 | } 262 | 263 | .warning-message { 264 | font-family: 'Nexa-Bold'; 265 | font-size: 24px; 266 | } 267 | 268 | .remove-link-look { 269 | text-decoration: none !important; 270 | color: unset !important; 271 | } 272 | 273 | .hide-mobile { 274 | @include phone { 275 | display: none; 276 | } 277 | } 278 | 279 | .hide-desktop { 280 | @include not-phone { 281 | display: none; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /shared/react/styles/topic-editor.scss: -------------------------------------------------------------------------------- 1 | .topic-editor { 2 | width: 100%; 3 | 4 | .topic-editor__forms { 5 | padding-top: 5px; 6 | margin-top: 10px; 7 | border-top: 1px solid rgba(0, 0, 0, 0.3); 8 | width: 100%; 9 | } 10 | .topic-editor__add-topic-btn, 11 | .topic-editor__add-topic-submit, 12 | .topic-editor__reset-btn { 13 | margin-top: 10px; 14 | font-size: 14px; 15 | width: fit-content; 16 | margin-top: 7px; 17 | padding: 7px 10px; 18 | color: rgb(255, 255, 255); 19 | border: 3px; 20 | background-color: rgba(0, 0, 0, 0.9); 21 | border: 1px solid rgba(0, 0, 0, 0.9); 22 | 23 | &:hover { 24 | color: rgba(0, 0, 0, 0.9); 25 | background-color: rgb(255, 255, 255); 26 | border: 1px solid rgba(0, 0, 0, 0.9); 27 | } 28 | cursor: pointer; 29 | } 30 | .topic-editor__add-topic-btn, 31 | .topic-editor__add-topic-submit { 32 | border: 1px solid rgba(0, 0, 0, 0.6); 33 | background-color: rgb(255, 255, 255); 34 | color: rgba(0, 0, 0, 9); 35 | 36 | &:hover { 37 | background-color: rgba(0, 0, 0, 0.6); 38 | color: rgb(255, 255, 255); 39 | } 40 | } 41 | .topic-editor__add-topic-text { 42 | margin-top: 10px; 43 | font-size: 14px; 44 | width: 300px; 45 | margin-top: 7px; 46 | padding: 7px 10px; 47 | color: rgba(0, 0, 0, 0.9); 48 | border: 3px; 49 | border: 1px solid rgba(0, 0, 0, 0.6); 50 | margin-right: 7px; 51 | } 52 | 53 | .topic-editor__title { 54 | font-size: 14px; 55 | margin: 0 0 15px 0; 56 | padding: 15px 0 0 0; 57 | text-align: left; 58 | font-weight: 700; 59 | font-family: 'Helvetica'; 60 | border-top: 1px solid rgba(0, 0, 0, 0.3); 61 | width: 100%; 62 | } 63 | .topic-editor__hint { 64 | font-size: 14px; 65 | color: rgba(0, 0, 0, 0.6); 66 | margin-bottom: 10px; 67 | } 68 | 69 | .topic-editor__topic-tag { 70 | cursor: pointer; 71 | padding: 7px 10px; 72 | font-size: 14px; 73 | color: rgb(0, 0, 0); 74 | background-color: rgb(255, 255, 255); 75 | border: 1px solid rgba(0, 0, 0, 0.6); 76 | margin-right: 7px; 77 | margin-bottom: 7px; 78 | border-radius: 20px; 79 | word-break: break-all; 80 | } 81 | .topic-editor__show-more-label { 82 | font-size: 16px; 83 | cursor: pointer; 84 | color: rgb(100, 100, 255); 85 | font-weight: bold; 86 | } 87 | .topic-editor_x { 88 | font-family: 'Helvetica'; 89 | font-weight: bold; 90 | margin-left: 7px; 91 | color: rgb(255, 100, 100); 92 | font-size: 12px; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /shared/react/styles/topic-graph.scss: -------------------------------------------------------------------------------- 1 | .topic-graph { 2 | width: 50%; 3 | min-width: 50%; 4 | max-width: 50%; 5 | .topic-graph__title { 6 | font-size: 12px; 7 | margin: 15px 0 0 0; 8 | padding: 0; 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /shared/storage.js: -------------------------------------------------------------------------------- 1 | const API = chrome || browser; 2 | 3 | const storage = { 4 | KEYS: { 5 | keywords: 'myalgorithm_keywords', 6 | feed_settings: 'myalgorithm_feed_settings', 7 | fetched_ts: 'myalgorithm_fetched_ts', 8 | content_feed: 'myalgorithm_content_feed', 9 | custom_sources: 'myalgorithm_custom_sources', 10 | }, 11 | save: (key, value) => { 12 | return new Promise((resolve) => { 13 | const data = {} 14 | data[key] = JSON.stringify(value) 15 | API.storage.local.set(data, function () { 16 | resolve(true) 17 | }) 18 | }) 19 | }, 20 | get: (key) => { 21 | return new Promise((resolve) => { 22 | API.storage.local.get([key], function (result) { 23 | try { 24 | resolve(JSON.parse(result[key])) 25 | } catch (e) { 26 | resolve({}) 27 | } 28 | }) 29 | }) 30 | }, 31 | } 32 | 33 | function ContentItem(contentInfo) { 34 | this.title = contentInfo.title 35 | this.description = contentInfo.description 36 | this.image = contentInfo.image 37 | this.link = contentInfo.link 38 | this.source = contentInfo.source 39 | } 40 | 41 | function ContentFeed(contentItems) { 42 | this.contentItems = contentItems 43 | } 44 | 45 | function Keyword(pastKeywordObject, newEngagementType, sourceDomain) { 46 | this.text = pastKeywordObject.text 47 | this.occurrences = pastKeywordObject.occurrences + 1 48 | this.engagementTypes = pastKeywordObject.engagementTypes 49 | this.engagementTypes[newEngagementType] += 1 50 | this.history = pastKeywordObject.history 51 | if (sourceDomain) { 52 | if (sourceDomain in this.history) { 53 | this.history[sourceDomain] += 1 54 | } else { 55 | this.history[sourceDomain] = 1 56 | } 57 | } 58 | 59 | this.lastInteractionTs = Date.now() 60 | } 61 | 62 | function FeedSettings(pastFeedSettings, newOption) { 63 | this.defaultPriorities = { 64 | metaInfo: 3, 65 | searchQuery: 5, 66 | clicks: 2, 67 | generalInput: 4, 68 | } 69 | 70 | this.defaultSourcing = { 71 | youtube: true, 72 | twitter: true, 73 | reddit: true, 74 | quora: true, 75 | wikipedia: true, 76 | odysee: true, 77 | stackoverflow: true, 78 | gab: true, 79 | bitchute: true, 80 | } 81 | 82 | if (!pastFeedSettings.priorities) { 83 | this.priorities = this.defaultPriorities 84 | } else { 85 | for (let defaultKey of Object.keys(this.defaultPriorities)) { 86 | if (typeof pastFeedSettings.priorities[defaultKey] === 'undefined') { 87 | pastFeedSettings.priorities[defaultKey] = 88 | this.defaultPriorities[defaultKey] 89 | } 90 | } 91 | this.priorities = pastFeedSettings.priorities 92 | } 93 | 94 | if (!pastFeedSettings.sourcing) { 95 | this.sourcing = this.defaultSourcing 96 | } else { 97 | for (let defaultKey of Object.keys(this.defaultSourcing)) { 98 | if (typeof pastFeedSettings.sourcing[defaultKey] === 'undefined') { 99 | pastFeedSettings.sourcing[defaultKey] = this.defaultSourcing[defaultKey] 100 | } 101 | } 102 | this.sourcing = pastFeedSettings.sourcing 103 | } 104 | 105 | this.oppositeMode = pastFeedSettings.oppositeMode || false 106 | this.refreshMode = pastFeedSettings.refreshMode || false 107 | this.randomness = pastFeedSettings.randomness || 0 108 | this.disableAlgorithm = pastFeedSettings.disableAlgorithm || false 109 | 110 | if (newOption) { 111 | Object.assign(this, newOption) 112 | } 113 | } 114 | 115 | export { 116 | storage, 117 | FeedSettings, 118 | Keyword, 119 | ContentFeed, 120 | ContentItem 121 | } 122 | -------------------------------------------------------------------------------- /webpack.chrome.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (env, options) => { 4 | return { 5 | cache: true, 6 | optimization: { 7 | minimize: true, 8 | }, 9 | entry: { 10 | index: ['babel-polyfill', './shared/react/index.js'], 11 | background: ['babel-polyfill', './shared/background.js'], 12 | content: ['babel-polyfill', './shared/content.js'], 13 | }, 14 | output: { 15 | path: path.join(__dirname, '/chrome-extension/dist'), 16 | filename: '[name].bundle.js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: /nodeModules/, 23 | use: { 24 | loader: 'babel-loader', 25 | }, 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif|svg)$/i, 29 | use: [ 30 | { 31 | loader: 'file-loader', 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.(woff(2)?|ttf|otf|eot)(\?v=\d+\.\d+\.\d+)?$/, 37 | use: [ 38 | { 39 | loader: 'file-loader', 40 | options: { 41 | name: '[name].[ext]', 42 | outputPath: 'fonts/', 43 | }, 44 | }, 45 | ], 46 | }, 47 | { 48 | test: /\.css$/, 49 | use: ['style-loader', 'css-loader'], 50 | }, 51 | { 52 | test: /\.s[ac]ss$/i, 53 | use: [ 54 | // Creates `style` nodes from JS strings 55 | 'style-loader', 56 | // Translates CSS into CommonJS 57 | 'css-loader', 58 | // Compiles Sass to CSS 59 | 'sass-loader', 60 | ], 61 | }, 62 | ], 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /webpack.firefox.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (env, options) => { 4 | return { 5 | cache: true, 6 | optimization: { 7 | minimize: true, 8 | }, 9 | entry: { 10 | index: ['babel-polyfill', './shared/react/index.js'], 11 | background: ['babel-polyfill', './shared/background.js'], 12 | content: ['babel-polyfill', './shared/content.js'], 13 | }, 14 | output: { 15 | path: path.join(__dirname, '/firefox-extension/dist'), 16 | filename: '[name].bundle.js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: /nodeModules/, 23 | use: { 24 | loader: 'babel-loader', 25 | }, 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif|svg)$/i, 29 | use: [ 30 | { 31 | loader: 'file-loader', 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.(woff(2)?|ttf|otf|eot)(\?v=\d+\.\d+\.\d+)?$/, 37 | use: [ 38 | { 39 | loader: 'file-loader', 40 | options: { 41 | name: '[name].[ext]', 42 | outputPath: 'fonts/', 43 | }, 44 | }, 45 | ], 46 | }, 47 | { 48 | test: /\.css$/, 49 | use: ['style-loader', 'css-loader'], 50 | }, 51 | { 52 | test: /\.s[ac]ss$/i, 53 | use: [ 54 | // Creates `style` nodes from JS strings 55 | 'style-loader', 56 | // Translates CSS into CommonJS 57 | 'css-loader', 58 | // Compiles Sass to CSS 59 | 'sass-loader', 60 | ], 61 | }, 62 | ], 63 | }, 64 | } 65 | } 66 | --------------------------------------------------------------------------------