summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/settings/_components/subscriptions-settings.tsx
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 02:18:55 -0800
committerFuwn <[email protected]>2026-02-08 02:18:55 -0800
commit64b5eb44a055bca5042163d19d7a330abc84c58a (patch)
tree8f9cad1ce6fdcdde511b71b45c639b5aa4b6ab91 /apps/web/app/reader/settings/_components/subscriptions-settings.tsx
parentchore: update schema dump with authenticated feed trigger changes and new fun... (diff)
downloadasa.news-64b5eb44a055bca5042163d19d7a330abc84c58a.tar.xz
asa.news-64b5eb44a055bca5042163d19d7a330abc84c58a.zip
feat: add feed management features and fix subscribe_to_feed bugs
- Fix subscribe_to_feed overload ambiguity by dropping old 4-param version - Fix vault permission error by using vault.create_secret instead of direct INSERT - Add duplicate subscription check with clear error message - Add unmute confirmation dialog matching unsubscribe pattern - Add feed button in subscriptions settings page - Add inline rename for feeds, folders, and custom feeds from reader header - Add drag and drop feeds between folders in sidebar - Add credential management UI (add/update) for pro/developer tier - Add add_feed_credentials RPC to convert public feeds to authenticated - Enable pgsodium extension for vault crypto operations
Diffstat (limited to 'apps/web/app/reader/settings/_components/subscriptions-settings.tsx')
-rw-r--r--apps/web/app/reader/settings/_components/subscriptions-settings.tsx101
1 files changed, 100 insertions, 1 deletions
diff --git a/apps/web/app/reader/settings/_components/subscriptions-settings.tsx b/apps/web/app/reader/settings/_components/subscriptions-settings.tsx
index e22a005..1e9245d 100644
--- a/apps/web/app/reader/settings/_components/subscriptions-settings.tsx
+++ b/apps/web/app/reader/settings/_components/subscriptions-settings.tsx
@@ -5,11 +5,14 @@ import { useSubscriptions } from "@/lib/queries/use-subscriptions"
import {
useUpdateSubscriptionTitle,
useUpdateFeedUrl,
+ useUpdateFeedCredentials,
+ useAddFeedCredentials,
useMoveSubscriptionToFolder,
useUnsubscribe,
useRequestFeedRefresh,
} from "@/lib/queries/use-subscription-mutations"
import { useUserProfile } from "@/lib/queries/use-user-profile"
+import { useUserInterfaceStore } from "@/lib/stores/user-interface-store"
import { TIER_LIMITS } from "@asa-news/shared"
import type { Subscription } from "@/lib/types/subscription"
@@ -43,8 +46,13 @@ function SubscriptionRow({
const [isEditingUrl, setIsEditingUrl] = useState(false)
const [editedUrl, setEditedUrl] = useState(subscription.feedUrl)
const [showUnsubscribeConfirm, setShowUnsubscribeConfirm] = useState(false)
+ const [isEditingCredentials, setIsEditingCredentials] = useState(false)
+ const [editedAuthType, setEditedAuthType] = useState("bearer")
+ const [editedCredential, setEditedCredential] = useState("")
const updateTitle = useUpdateSubscriptionTitle()
const updateFeedUrl = useUpdateFeedUrl()
+ const updateCredentials = useUpdateFeedCredentials()
+ const addCredentials = useAddFeedCredentials()
const moveToFolder = useMoveSubscriptionToFolder()
const unsubscribe = useUnsubscribe()
const requestRefresh = useRequestFeedRefresh()
@@ -72,6 +80,27 @@ function SubscriptionRow({
setIsEditingUrl(false)
}
+ function handleSaveCredentials() {
+ const trimmedCredential = editedCredential.trim()
+ if (!trimmedCredential) return
+
+ if (subscription.feedVisibility === "authenticated") {
+ updateCredentials.mutate({
+ feedIdentifier: subscription.feedIdentifier,
+ authenticationType: editedAuthType,
+ credential: trimmedCredential,
+ })
+ } else {
+ addCredentials.mutate({
+ subscriptionIdentifier: subscription.subscriptionIdentifier,
+ authenticationType: editedAuthType,
+ credential: trimmedCredential,
+ })
+ }
+ setIsEditingCredentials(false)
+ setEditedCredential("")
+ }
+
function handleFolderChange(folderIdentifier: string) {
const sourceFolder = folderOptions.find(
(folder) => folder.identifier === subscription.folderIdentifier
@@ -197,7 +226,59 @@ function SubscriptionRow({
)}
</div>
</div>
- <div className="flex items-center gap-2">
+ {isEditingCredentials ? (
+ <div className="flex items-center gap-2">
+ <select
+ value={editedAuthType}
+ onChange={(event) => setEditedAuthType(event.target.value)}
+ className="border border-border bg-background-primary px-2 py-1 text-text-secondary outline-none"
+ >
+ <option value="bearer">bearer token</option>
+ <option value="basic">basic (user:pass)</option>
+ <option value="query_param">query parameter</option>
+ </select>
+ <input
+ type="password"
+ value={editedCredential}
+ onChange={(event) => setEditedCredential(event.target.value)}
+ placeholder={
+ editedAuthType === "query_param"
+ ? "api_key=your_token"
+ : editedAuthType === "basic"
+ ? "username:password"
+ : "your token"
+ }
+ className="min-w-0 flex-1 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
+ onKeyDown={(event) => {
+ if (event.key === "Enter" && editedCredential.trim()) {
+ handleSaveCredentials()
+ }
+ if (event.key === "Escape") {
+ setIsEditingCredentials(false)
+ setEditedCredential("")
+ }
+ }}
+ autoFocus
+ />
+ <button
+ onClick={handleSaveCredentials}
+ disabled={!editedCredential.trim()}
+ className="px-2 py-1 text-text-secondary transition-colors hover:text-text-primary disabled:opacity-50"
+ >
+ save
+ </button>
+ <button
+ onClick={() => {
+ setIsEditingCredentials(false)
+ setEditedCredential("")
+ }}
+ className="px-2 py-1 text-text-secondary transition-colors hover:text-text-primary"
+ >
+ cancel
+ </button>
+ </div>
+ ) : null}
+ <div className="flex flex-wrap items-center gap-2">
<select
value={subscription.folderIdentifier ?? ""}
onChange={(event) => handleFolderChange(event.target.value)}
@@ -224,6 +305,14 @@ function SubscriptionRow({
refresh
</button>
)}
+ {(userProfile?.tier === "pro" || userProfile?.tier === "developer") && !isEditingCredentials && (
+ <button
+ onClick={() => setIsEditingCredentials(true)}
+ className="px-2 py-1 text-text-secondary transition-colors hover:text-text-primary"
+ >
+ {subscription.feedVisibility === "authenticated" ? "update credentials" : "add credentials"}
+ </button>
+ )}
{showUnsubscribeConfirm ? (
<div className="flex items-center gap-1">
<span className="text-text-dim">confirm?</span>
@@ -262,6 +351,9 @@ function SubscriptionRow({
export function SubscriptionsSettings() {
const { data: subscriptionsData, isLoading } = useSubscriptions()
const { data: userProfile } = useUserProfile()
+ const setAddFeedDialogOpen = useUserInterfaceStore(
+ (state) => state.setAddFeedDialogOpen
+ )
const [searchQuery, setSearchQuery] = useState("")
const [folderFilter, setFolderFilter] = useState<string>("all")
@@ -325,6 +417,13 @@ export function SubscriptionsSettings() {
<span className="text-text-dim">
{filteredSubscriptions.length} / {TIER_LIMITS[userProfile?.tier ?? "free"].maximumFeeds}
</span>
+ <button
+ type="button"
+ onClick={() => setAddFeedDialogOpen(true)}
+ className="border border-border bg-background-tertiary px-3 py-1.5 text-text-primary transition-colors hover:bg-border"
+ >
+ + add feed
+ </button>
</div>
<div>
{filteredSubscriptions.map((subscription) => (