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
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
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 |