diff --git a/README.md b/README.md
index 85c029f..0dd75e0 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,6 @@ This is the official monorepo for Feedbomb.
- **Open-source**: Feedbomb thrives from the open-source community.
- **Self-Hostable**: Host Feedbomb on your own server.
- **PWA Support**: Use Feedbomb as a PWA for a native-like experience on all your devices.
-- **Advanced article reader**: Read articles with our advanced, fully-featured article reader for a distraction-free reading experience. Plays nice with YouTube by embedding the video.
- **Modern UI**: Feedbomb has a modern, responsive user interface.
- **Filter out the bad stuff**: Feedbomb lets you create custom rules to filter out unwanted articles.
- **Much more coming soon**
diff --git a/apps/web/package.json b/apps/web/package.json
index 9f05304..5348182 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
+ "dompurify": "^3.1.7",
"iconv-lite": "^0.6.3",
"jschardet": "^3.1.4",
"lucide-react": "^0.447.0",
@@ -34,6 +35,7 @@
"react": "^18",
"react-dom": "^18",
"react-resizable-panels": "^2.1.4",
+ "rss-parser": "^3.13.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
diff --git a/apps/web/src/app/dashboard/page.jsx b/apps/web/src/app/dashboard/page.jsx
deleted file mode 100644
index 5b34acb..0000000
--- a/apps/web/src/app/dashboard/page.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { AppSidebar } from "@/components/app-sidebar"
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
-import { Separator } from "@/components/ui/separator"
-import {
- SidebarInset,
- SidebarProvider,
- SidebarTrigger,
-} from "@/components/ui/sidebar"
-
-export default function Page() {
- return (
- (
-
-
-
-
-
-
-
-
-
- Building Your Application
-
-
-
-
- Data Fetching
-
-
-
-
-
-
- )
- );
-}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 7d4e6e3..b9a9300 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -157,7 +157,7 @@ img {
}
.dark .custom-scrollbar {
scrollbar-width: normal;
- scrollbar-color: #525252 black !important;
+ scrollbar-color: #525252 hsl(var(--background)) !important;
}
.custom-scrollbar::-webkit-scrollbar {
diff --git a/apps/web/src/app/page.jsx b/apps/web/src/app/page.jsx
index fd1439a..715bebd 100644
--- a/apps/web/src/app/page.jsx
+++ b/apps/web/src/app/page.jsx
@@ -1,5 +1,14 @@
"use client";
import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { ModeToggle } from "@/components/ui/dark-toggle";
+import { SettingsIcon, LoaderCircle } from "lucide-react";
+import { CommandPalette } from "@/components/ui/cmd";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
import {
Dialog,
DialogContent,
@@ -9,20 +18,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { ModeToggle } from "@/components/ui/dark-toggle";
-import {
- SettingsIcon,
- LucideHome,
- LucidePlus,
- LoaderCircle,
-} from "lucide-react";
-import { CommandPalette } from "@/components/ui/cmd";
-import {
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
-} from "@/components/ui/resizable";
import { AppSidebar } from "@/components/app-sidebar";
import {
SidebarContent,
@@ -35,6 +30,11 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
+import Parser from "rss-parser";
+import DOMPurify from "dompurify";
+import { ShareOptions } from "@/components/ui/share-options";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
export default function Home() {
const [feedsJSON, setFeedsJSON] = useState([]);
@@ -42,124 +42,28 @@ export default function Home() {
const [newFeedURL, setNewFeedURL] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [selector, setSelector] = useState("all_posts");
- const [currentIndex, setCurrentIndex] = useState(0);
- const [sidebarOpen, setSidebarOpen] = useState(false);
const [pwaCardShowing, setPwaCardShowing] = useState(false);
const [deferredPrompt, setDeferredPrompt] = useState(null);
const [rules, setRules] = useState([]);
const [rendered, setRendered] = useState(false);
const [feeds, setFeeds] = useState([]);
- const [selectedArticleURL, setSelectedArticleURL] = useState(null);
- const [iframeLoaded, setIframeLoaded] = useState(false);
- const [panelBalance, setPanelBalance] = useState(60);
+ const panelBalance = 60;
+ const [selectedArticle, setSelectedArticle] = useState(null);
const openDialog = () => {
setDialogOpen(true);
};
async function parseRSSFeed(xmlString, url) {
- let feed = {};
- let feedItems = [];
- try {
- const parser = new DOMParser();
- const xmlDoc = parser.parseFromString(xmlString, "application/xml");
- console.log(xmlDoc);
- let feedTitle = xmlDoc.querySelector("title")
- ? xmlDoc.querySelector("title").textContent
- : "";
-
- let feedIcon = xmlDoc.querySelector("link")
- ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(
- xmlDoc.querySelector("link")?.getAttribute("href")?.split("/")[2] ||
- url.split("/")[2]
- )}&sz=32`
- : `https://www.google.com/s2/favicons?domain=${encodeURIComponent(
- url.split("/")[2]
- )}&sz=32`;
- feed.title = feedTitle;
- feed.feedURL = url;
- feed.icon = feedIcon;
- feed.type = "feeds";
-
- let items =
- xmlDoc.querySelectorAll("item, entry") ||
- xmlDoc
- .querySelector("rss")
- .querySelector("channel")
- .querySelectorAll("item, entry");
- setFeeds((prev) => [
- ...prev,
- {
- title: feedTitle,
- feedURL: url,
- icon: feedIcon,
- type: "feeds",
- },
- ]);
- items.forEach((item) => {
- const title = item.querySelector("title")
- ? item.querySelector("title").textContent
- : "";
- const linkElement = item.querySelector("link");
- const content =
- item.getElementsByTagName("content:encoded")[0] ||
- item.querySelector("content") ||
- item.querySelector("description");
- const link = linkElement
- ? linkElement.getAttribute("href") || linkElement.textContent
- : "#";
- const description = item.querySelector("description, summary")
- ? item.querySelector("description, summary").textContent
- : "";
-
- let pubDate = item.querySelector("pubDate, published")
- ? item.querySelector("pubDate, published").textContent
- : "";
- let dateObj = new Date(pubDate);
- if (isNaN(dateObj.getTime())) dateObj = new Date();
- function stripHTML(input) {
- return input.replace(/<\/?[^>]+(>|$)/g, "");
- }
- let authorElement = item.querySelector("author");
- let dcCreatorElement = item.getElementsByTagName("dc:creator")[0];
- let author = authorElement
- ? authorElement.textContent ||
- authorElement.querySelector("name")?.textContent ||
- ""
- : dcCreatorElement
- ? dcCreatorElement.textContent
- : "";
-
- feedItems.push({
- title: title,
- link: link,
- description: stripHTML(content.textContent),
- pubDate: dateObj.toISOString(),
- author: author,
- feedURL: url,
- });
- });
- } catch (error) {
- console.error("Error parsing RSS feed:", error);
- }
- feed.items = feedItems;
+ const parser = new Parser();
+ const feed = await parser.parseString(xmlString);
+ feed.feedId = feed.link;
+ feed.items.forEach((item) => {
+ item.feedId = feed.link;
+ });
return feed;
}
- function decodeHtmlEntities(str) {
- const entities = {
- "<": "<",
- ">": ">",
- "&": "&",
- """: '"',
- "'": "'",
- };
- return str.replace(
- /&(lt|gt|amp|quot|#39);/g,
- (match, p1) => entities[match] || match
- );
- }
-
async function fetchFeeds(urls) {
try {
fetch("/api/fetchFeeds", {
@@ -175,6 +79,7 @@ export default function Home() {
let xml = data[i].xml;
if (xml != null) {
let feed = await parseRSSFeed(xml, data[i].url);
+ setFeeds((prev) => [...prev, feed]);
setFeedsJSON((prev) => [...prev, feed]);
}
}
@@ -210,21 +115,6 @@ export default function Home() {
}
}
- function decodeHtmlEntities(str) {
- const entities = {
- "<": "<",
- ">": ">",
- "&": "&",
- """: '"',
- "'": "'",
- };
-
- return str.replace(
- /&(lt|gt|amp|quot|#39);/g,
- (match, p1) => entities[match] || match
- );
- }
-
const handleAddFeed = () => {
let validatedFeedURL = newFeedURL.trim();
if (!validatedFeedURL.startsWith("http")) {
@@ -363,20 +253,58 @@ export default function Home() {
Home
{feeds.map((feed, index) => {
- if (feed.type != "feeds") return null;
return (
setSelector(feed.feedURL)}
+ onClick={() => setSelector(feed.feedId)}
>
{feed.title}
);
})}
+
@@ -432,7 +360,7 @@ export default function Home() {
return check;
} else {
return (
- item.feedURL === selector &&
+ item.feedId === selector &&
checkArticle(item.title, item.author)
);
}
@@ -440,16 +368,11 @@ export default function Home() {
.map((item, index) => (
@@ -494,26 +413,33 @@ export default function Home() {
defaultSize={panelBalance}
className="max-sm:hidden"
>
-
- {selectedArticleURL != null && (
- <>
- {!iframeLoaded ? (
-
- {selectedArticleURL != null ? (
-
-
-
- ) : null}
-
- ) : null}
-