Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Database (only for cloud dbs or local libsql running on a custom port)
# DATABASE_URL='http://127.0.0.1:8080'
# DATABASE_AUTH_TOKEN=
# REQUIRED - Your base domain url (e.g. https://www.example.com).
VITE_PUBLIC_BASE_URL=

# Authentication
# REQUIRED - Authentication
# Generate a secret here: https://www.better-auth.com/docs/installation#set-environment-variables
# Alternatively, run `openssl rand -base64 32` in a terminal and use that value.
BETTER_AUTH_SECRET=

# Database (only for cloud dbs or local libsql running on a custom port)
# DATABASE_URL='http://127.0.0.1:8080'
# DATABASE_AUTH_TOKEN=

# OAuth (optional, for custom SSO login)
# OAUTH_PROVIDER_ID=
# OAUTH_PROVIDER_NAME=
Expand Down
2 changes: 1 addition & 1 deletion .env.test.main
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DATABASE_URL=http://127.0.0.1:8081

# Auth
BETTER_AUTH_SECRET=test-secret-key-for-main-tests
BETTER_AUTH_BASE_URL=http://localhost:3002
VITE_PUBLIC_BASE_URL=http://localhost:3002

# Main instance flag
VITE_PUBLIC_IS_MAIN_INSTANCE=true
Expand Down
2 changes: 1 addition & 1 deletion .env.test.self-hosted
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DATABASE_URL=http://127.0.0.1:8082

# Auth
BETTER_AUTH_SECRET=test-secret-key-for-self-hosted-tests
BETTER_AUTH_BASE_URL=http://localhost:3001
VITE_PUBLIC_BASE_URL=http://localhost:3001

# Self-hosted flag
VITE_PUBLIC_IS_MAIN_INSTANCE=false
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Self hosting Serial is relatively easy. Here are the current step by step platfo

If your preferred platform doesn't have a guide, follow these rough steps:

1. Fork the `hfellerhoff/serial` respository to your own GitHub account.
1. Fork the `megaflorasoftware/serial` respository to your own GitHub account.
2. Use a git-based deployment system to deploy when a new commit happens. This will make it easy to keep your deploment up to date.
3. Set up a custom domain (if desired)
4. Set up your database:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.arm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ services:
DATABASE_URL: http://libsql:8080
KV_STORE: ioredis
REDIS_URL: redis://redis:6379
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.build-arm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
DATABASE_URL: http://libsql:8080
KV_STORE: ioredis
REDIS_URL: redis://redis:6379
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.build-main-instance-ioredis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ services:
DATABASE_URL: ${DATABASE_URL}
DATABASE_AUTH_TOKEN: ${DATABASE_AUTH_TOKEN}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
VITE_PUBLIC_IS_MAIN_INSTANCE: "true"
KV_STORE: ioredis
REDIS_URL: redis://redis:6379
POLAR_ENVIRONMENT: ${POLAR_ENVIRONMENT}
Expand All @@ -41,9 +40,11 @@ services:
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
RESEND_API_KEY: ${RESEND_API_KEY}
SENDGRID_API_KEY: ${SENDGRID_API_KEY}
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
INSTAPAPER_OAUTH_ID: ${INSTAPAPER_OAUTH_ID}
INSTAPAPER_OAUTH_SECRET: ${INSTAPAPER_OAUTH_SECRET}
VITE_PUBLIC_IS_MAIN_INSTANCE: "true"
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
VITE_PUBLIC_IS_MAINTENANCE_MODE: ${VITE_PUBLIC_IS_MAINTENANCE_MODE}
depends_on:
redis:
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.build-main-instance-upstash.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ services:
DATABASE_URL: ${DATABASE_URL}
DATABASE_AUTH_TOKEN: ${DATABASE_AUTH_TOKEN}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
VITE_PUBLIC_IS_MAIN_INSTANCE: "true"
KV_STORE: upstash
UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL}
UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN}
Expand All @@ -29,9 +28,11 @@ services:
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
RESEND_API_KEY: ${RESEND_API_KEY}
SENDGRID_API_KEY: ${SENDGRID_API_KEY}
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
INSTAPAPER_OAUTH_ID: ${INSTAPAPER_OAUTH_ID}
INSTAPAPER_OAUTH_SECRET: ${INSTAPAPER_OAUTH_SECRET}
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "true"
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
VITE_PUBLIC_IS_MAINTENANCE_MODE: ${VITE_PUBLIC_IS_MAINTENANCE_MODE}
ports:
- 3000:3000
1 change: 1 addition & 0 deletions docker-compose.build-standalone.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
DATABASE_URL: ${DATABASE_URL}
DATABASE_AUTH_TOKEN: ${DATABASE_AUTH_TOKEN}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
KV_STORE: ${KV_STORE}
REDIS_URL: ${REDIS_URL}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
DATABASE_URL: http://libsql:8080
KV_STORE: ioredis
REDIS_URL: redis://redis:6379
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.standalone.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
DATABASE_URL: ${DATABASE_URL}
DATABASE_AUTH_TOKEN: ${DATABASE_AUTH_TOKEN}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
KV_STORE: ${KV_STORE}
REDIS_URL: ${REDIS_URL}
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ services:
DATABASE_URL: http://libsql:8080
KV_STORE: ioredis
REDIS_URL: redis://redis:6379
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
FROM_EMAIL_ADDRESS: ${FROM_EMAIL_ADDRESS}
RESEND_API_KEY: ${RESEND_API_KEY}
SENDGRID_API_KEY: ${SENDGRID_API_KEY}
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
INSTAPAPER_OAUTH_ID: ${INSTAPAPER_OAUTH_ID}
INSTAPAPER_OAUTH_SECRET: ${INSTAPAPER_OAUTH_SECRET}
VITE_PUBLIC_BASE_URL: ${VITE_PUBLIC_BASE_URL}
VITE_PUBLIC_IS_MAIN_INSTANCE: "false"
VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS: ${VITE_PUBLIC_SUPPORT_EMAIL_ADDRESS}
VITE_PUBLIC_IS_MAINTENANCE_MODE: ${VITE_PUBLIC_IS_MAINTENANCE_MODE}
depends_on:
libsql:
Expand Down
16 changes: 8 additions & 8 deletions docs/hosting/coolify.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
2. Create or pick a project for Serial to live in.
3. Create a new resource, then choose `Public Repository`.
4. Choose the server to deploy Serial on. If you run Coolify on a single VPS, this will be `localhost`.
5. For the repository, enter `https://github.com/hfellerhoff/serial`.
5. For the repository, enter `https://github.com/megaflorasoftware/serial`.
6. Update `Build Pack` to be `Docker Compose` and choose a compose file:
- Use `docker-compose.yaml` to use a local DB on an x86 architecture (default)
- Use `docker-compose.arm.yaml` to use a local DB on an ARM architecture
Expand All @@ -28,7 +28,7 @@

You can access the app through the domain you added (which can always be found in the `Links` header item). To update the app in the future, hit `Redeploy` in Coolify.

If you'd like to support additional features, [see this section](https://github.com/hfellerhoff/serial#enabling-additional-features)!
If you'd like to support additional features, [see this section](https://github.com/megaflorasoftware/serial#enabling-additional-features)!

## Deploy using Docker Compose file

Expand All @@ -41,9 +41,9 @@ If you'd like to support additional features, [see this section](https://github.
3. Create a new resource, then choose `Docker Compose Empty`.
4. Choose the server to deploy Serial on. If you run Coolify on a single VPS, this will be `localhost`.
5. Determine the best Serial docker compose file for your needs:
- Use [`docker-compose.yaml`](https://raw.githubusercontent.com/hfellerhoff/serial/refs/heads/main/docker-compose.yaml) to use a local DB on an x86 architecture (default)
- Use [`docker-compose.arm.yaml`](https://raw.githubusercontent.com/hfellerhoff/serial/refs/heads/main/docker-compose.arm.yaml) to use a local DB on an ARM architecture
- Use [`docker-compose.standalone.yaml`](https://raw.githubusercontent.com/hfellerhoff/serial/refs/heads/main/docker-compose.standalone.yaml) for standalone deployments (for plugging in your own cloud services or external DB)
- Use [`docker-compose.yaml`](https://raw.githubusercontent.com/megaflorasoftware/serial/refs/heads/main/docker-compose.yaml) to use a local DB on an x86 architecture (default)
- Use [`docker-compose.arm.yaml`](https://raw.githubusercontent.com/megaflorasoftware/serial/refs/heads/main/docker-compose.arm.yaml) to use a local DB on an ARM architecture
- Use [`docker-compose.standalone.yaml`](https://raw.githubusercontent.com/megaflorasoftware/serial/refs/heads/main/docker-compose.standalone.yaml) for standalone deployments (for plugging in your own cloud services or external DB)
6. Paste the file content into Coolify
7. (optional) Add your custom domain:
- Click `Settings` for the `Serial` service
Expand All @@ -56,11 +56,11 @@ If you'd like to support additional features, [see this section](https://github.

You can access the app through the domain you added (which can always be found in the `Links` header item). To update the app in the future, hit `Redeploy` in Coolify.

If you'd like to support additional features, [see this section](https://github.com/hfellerhoff/serial#enabling-additional-features)!
If you'd like to support additional features, [see this section](https://github.com/megaflorasoftware/serial#enabling-additional-features)!

## Deploy using Private Repository (with GitHub App)

1. Fork the `hfellerhoff/serial` respository to your own GitHub account.
1. Fork the `megaflorasoftware/serial` respository to your own GitHub account.
2. If you don't have a [Coolify](https://coolify.io/) instance set up:
1. Set up a locally hosted server, or purchase a VPS.
- The cheapest option (that I know of in January 2026) is through Hetzner, where a VPS based in Germany or Finland will be around $4 a month. An `x86` architecure is recommended, but not required.
Expand All @@ -86,4 +86,4 @@ If you'd like to support additional features, [see this section](https://github.

You can access the app through the domain you added (which can always be found in the `Links` header item). To update the app in the future, sync your branch with the upstream repository and Coolify will redeploy it automatically.

If you'd like to support additional features, [see this section](https://github.com/hfellerhoff/serial#enabling-additional-features)!
If you'd like to support additional features, [see this section](https://github.com/megaflorasoftware/serial#enabling-additional-features)!
4 changes: 2 additions & 2 deletions docs/hosting/vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Note: Only cloud database hosting is available on Vercel deployments.

1. Fork the `hfellerhoff/serial` respository to your own GitHub account.
1. Fork the `megaflorasoftware/serial` respository to your own GitHub account.
2. Login to [Vercel](https://vercel.com/) and follow the onboarding to link your GitHub account.
3. Choose the `serial` repository and hit deploy. Your initial deployment will fail – that's okay.
4. Within your project, navigate to `Settings > Domains`. You have a few options for project domains:
Expand All @@ -18,4 +18,4 @@
6. Navigate to [Better Auth](https://www.better-auth.com/docs/installation#set-environment-variables) and generate an auth secret. Set this as `BETTER_AUTH_SECRET` in your environment variables.
7. That's it! Head on over to `Deployments` in the top navigation bar, choose `Create Deployment` in the top right menu, and head on over to your project URL once it's done!

If you'd like to support additional features, [see this section](https://github.com/hfellerhoff/serial#enabling-additional-features)!
If you'd like to support additional features, [see this section](https://github.com/megaflorasoftware/serial#enabling-additional-features)!
Binary file added public/welcome/screenshot-desktop-dark.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/welcome/screenshot-desktop-light.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/welcome/screenshot-mobile-dark.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/welcome/screenshot-mobile-light.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 64 additions & 10 deletions src/app/_app.import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import {
CheckIcon,
ExternalLinkIcon,
GlobeIcon,
Loader2Icon,
MinusIcon,
PauseIcon,
PlayCircleIcon,
XIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useSetAtom } from "jotai";
import { ImportDropzone } from "../components/feed/import/ImportDropzone";
import { getInitialFeedDataFromFileInputElement } from "../components/feed/import/utils/getInitialFeedDataFromFileInputElement";
import type { ImportFeedDataItem } from "../components/feed/import/utils/shared";
import type {
ImportFeedDataFromFilesError,
ImportFeedDataItem,
} from "../components/feed/import/utils/shared";
import type { CardRadioOption } from "~/components/ui/card-radio-group";
import type { FeedPlatform } from "~/server/db/schema";
import { YoutubeIcon } from "~/components/brand-icons";
import { getBlogUrl } from "~/lib/constants";
import { getGuidesUrl } from "~/lib/constants";
import { ImportLoading } from "~/components/ImportLoading";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
Expand All @@ -35,6 +40,7 @@ import { feedItemsStore } from "~/lib/data/store";
import { useImportResults, useLoadingMode } from "~/lib/data/loading-machine";
import { dataSubscriptionActions } from "~/lib/data/useDataSubscription";
import { useDialogStore } from "~/components/feed/dialogStore";
import { shouldAlwaysKeepSSEConnectionAlive } from "~/lib/data/atoms";

function ImportedFeedStatus({
feedUrl,
Expand Down Expand Up @@ -105,7 +111,12 @@ function EditFeedsPage() {
>(null);
const [hasStartedImport, setHasStartedImport] = useState(false);
const [isImportComplete, setIsImportComplete] = useState(false);
const [isImportPending, setIsImportPending] = useState(false);
const [importMode, setImportMode] = useState<ImportMode>("views");

const [fileInputErrorList, setFileInputErrorList] =
useState<ImportFeedDataFromFilesError | null>(null);

// Signal to Playwright tests that React has hydrated and the onChange handler
// is attached to the file input, so file-chooser interactions are reliable.
useEffect(() => {
Expand All @@ -124,6 +135,30 @@ function EditFeedsPage() {
const { launchDialog } = useDialogStore();

const isPostImportScreen = isImportComplete || hasStartedImport;
const setShouldAlwaysKeepSSEConnectionAlive = useSetAtom(
shouldAlwaysKeepSSEConnectionAlive,
);

// Keep SSE open during import so visibility changes don't disconnect the
// streaming import. Reset when the import loader is hidden.
useEffect(() => {
if (!isFetchingRss && hasStartedImport) {
setShouldAlwaysKeepSSEConnectionAlive(false);
}
}, [isFetchingRss, hasStartedImport, setShouldAlwaysKeepSSEConnectionAlive]);

useEffect(() => {
return () => {
setShouldAlwaysKeepSSEConnectionAlive(false);
};
}, [setShouldAlwaysKeepSSEConnectionAlive]);

useEffect(() => {
if (isImportPending && loading.mode === "importing" && !hasStartedImport) {
const id = requestAnimationFrame(() => setHasStartedImport(true));
return () => cancelAnimationFrame(id);
}
}, [isImportPending, loading.mode, hasStartedImport]);

useEffect(() => {
if (isImportComplete && importDeactivatedCount > 0) {
Expand Down Expand Up @@ -158,15 +193,17 @@ function EditFeedsPage() {
),
}));
setFeedsFoundFromFile(feedsWithImportStatus);
setFileInputErrorList(null);
} else {
setFileInputErrorList(feedResult);
}
};

const onFeedImport = async () => {
if (!feedsFoundFromFile?.length) return;

setTimeout(() => {
setHasStartedImport(true);
}, 500);
setShouldAlwaysKeepSSEConnectionAlive(true);
setIsImportPending(true);

const channelsToImport = feedsFoundFromFile
.filter((channel) => channel.shouldImport)
Expand Down Expand Up @@ -207,12 +244,15 @@ function EditFeedsPage() {
]);

setIsImportComplete(true);
setIsImportPending(false);
};

const onReset = () => {
setFeedsFoundFromFile(null);
setHasStartedImport(false);
setIsImportComplete(false);
setIsImportPending(false);
setShouldAlwaysKeepSSEConnectionAlive(false);
};

if (isFetchingRss) {
Expand All @@ -232,7 +272,7 @@ function EditFeedsPage() {
</code>{" "}
files from{" "}
<a
href={getBlogUrl("/how-to-export-youtube-subscriptions")}
href={getGuidesUrl("/how-to-export-youtube-subscriptions")}
className="underline"
target="_blank"
rel="noopener noreferrer"
Expand Down Expand Up @@ -272,11 +312,18 @@ function EditFeedsPage() {
id="import-file-input"
ref={inputElementRef}
type="file"
accept="text/csv,.opml"
accept="text"
className="sr-only"
multiple
onChange={onSelectFiles}
/>
{!!fileInputErrorList?.errors?.length && (
<div className="text-destructive mt-2">
{fileInputErrorList.errors.map((error) => (
<div key={error}>{error}</div>
))}
</div>
)}
{!!feedsFoundFromFile && (
<>
{!isPostImportScreen &&
Expand Down Expand Up @@ -459,12 +506,19 @@ function EditFeedsPage() {
<div className="fixed inset-x-0 bottom-0">
<div className="mx-auto box-border max-w-2xl p-6">
<Button
className="w-full"
className="w-full gap-2"
size="lg"
onClick={onFeedImport}
disabled={channelImportCount === 0}
disabled={channelImportCount === 0 || isImportPending}
>
Import {channelImportCount} feeds
{isImportPending && !hasStartedImport ? (
<>
Importing...
<Loader2Icon size={16} className="animate-spin" />
</>
) : (
<>Import {channelImportCount} feeds</>
)}
</Button>
</div>
</div>
Expand Down
Loading
Loading