aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-12 23:58:57 +0900
committernsfisis <nsfisis@gmail.com>2025-07-12 23:58:57 +0900
commit756b66b31fd02215fc2d8a30ae263a3bf08a90a6 (patch)
tree245cc37a1d81728260246ae5241eeb8225ec0ddc
parentfbe4bff7e8b6a5239c490601436fb3638dc8e13b (diff)
downloadfeedaka-756b66b31fd02215fc2d8a30ae263a3bf08a90a6.tar.gz
feedaka-756b66b31fd02215fc2d8a30ae263a3bf08a90a6.tar.zst
feedaka-756b66b31fd02215fc2d8a30ae263a3bf08a90a6.zip
feat(backend,frontend): add feature to unsubscribe feed
-rw-r--r--backend/db/articles.sql.go70
-rw-r--r--backend/db/feeds.sql.go25
-rw-r--r--backend/db/models.go9
-rw-r--r--backend/db/queries/articles.sql10
-rw-r--r--backend/db/queries/feeds.sql15
-rw-r--r--backend/db/schema.sql9
-rw-r--r--backend/graphql/generated.go120
-rw-r--r--backend/graphql/model/generated.go2
-rw-r--r--backend/graphql/resolver/schema.resolvers.go73
-rw-r--r--common/graphql/schema.graphql9
-rw-r--r--frontend/src/components/AddFeedForm.tsx8
-rw-r--r--frontend/src/components/FeedList.tsx20
-rw-r--r--frontend/src/graphql/generated/gql.ts12
-rw-r--r--frontend/src/graphql/generated/graphql.ts34
-rw-r--r--frontend/src/graphql/mutations.graphql4
-rw-r--r--frontend/src/graphql/queries.graphql5
-rw-r--r--frontend/src/pages/Settings.tsx24
-rw-r--r--justfile2
18 files changed, 277 insertions, 174 deletions
diff --git a/backend/db/articles.sql.go b/backend/db/articles.sql.go
index 9e60cb4..7492598 100644
--- a/backend/db/articles.sql.go
+++ b/backend/db/articles.sql.go
@@ -75,22 +75,23 @@ func (q *Queries) DeleteArticlesByFeed(ctx context.Context, feedID int64) error
const getArticle = `-- name: GetArticle :one
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
WHERE a.id = ?
`
type GetArticleRow struct {
- ID int64
- FeedID int64
- Guid string
- Title string
- Url string
- IsRead int64
- FeedID2 int64
- FeedUrl string
- FeedTitle string
+ ID int64
+ FeedID int64
+ Guid string
+ Title string
+ Url string
+ IsRead int64
+ FeedID2 int64
+ FeedUrl string
+ FeedTitle string
+ FeedIsSubscribed int64
}
func (q *Queries) GetArticle(ctx context.Context, id int64) (GetArticleRow, error) {
@@ -106,6 +107,7 @@ func (q *Queries) GetArticle(ctx context.Context, id int64) (GetArticleRow, erro
&i.FeedID2,
&i.FeedUrl,
&i.FeedTitle,
+ &i.FeedIsSubscribed,
)
return i, err
}
@@ -179,24 +181,25 @@ func (q *Queries) GetArticlesByFeed(ctx context.Context, feedID int64) ([]Articl
const getReadArticles = `-- name: GetReadArticles :many
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE a.is_read = 1
+WHERE a.is_read = 1 AND f.is_subscribed = 1
ORDER BY a.id DESC
LIMIT 100
`
type GetReadArticlesRow struct {
- ID int64
- FeedID int64
- Guid string
- Title string
- Url string
- IsRead int64
- FeedID2 int64
- FeedUrl string
- FeedTitle string
+ ID int64
+ FeedID int64
+ Guid string
+ Title string
+ Url string
+ IsRead int64
+ FeedID2 int64
+ FeedUrl string
+ FeedTitle string
+ FeedIsSubscribed int64
}
func (q *Queries) GetReadArticles(ctx context.Context) ([]GetReadArticlesRow, error) {
@@ -218,6 +221,7 @@ func (q *Queries) GetReadArticles(ctx context.Context) ([]GetReadArticlesRow, er
&i.FeedID2,
&i.FeedUrl,
&i.FeedTitle,
+ &i.FeedIsSubscribed,
); err != nil {
return nil, err
}
@@ -235,24 +239,25 @@ func (q *Queries) GetReadArticles(ctx context.Context) ([]GetReadArticlesRow, er
const getUnreadArticles = `-- name: GetUnreadArticles :many
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE a.is_read = 0
+WHERE a.is_read = 0 AND f.is_subscribed = 1
ORDER BY a.id DESC
LIMIT 100
`
type GetUnreadArticlesRow struct {
- ID int64
- FeedID int64
- Guid string
- Title string
- Url string
- IsRead int64
- FeedID2 int64
- FeedUrl string
- FeedTitle string
+ ID int64
+ FeedID int64
+ Guid string
+ Title string
+ Url string
+ IsRead int64
+ FeedID2 int64
+ FeedUrl string
+ FeedTitle string
+ FeedIsSubscribed int64
}
func (q *Queries) GetUnreadArticles(ctx context.Context) ([]GetUnreadArticlesRow, error) {
@@ -274,6 +279,7 @@ func (q *Queries) GetUnreadArticles(ctx context.Context) ([]GetUnreadArticlesRow
&i.FeedID2,
&i.FeedUrl,
&i.FeedTitle,
+ &i.FeedIsSubscribed,
); err != nil {
return nil, err
}
diff --git a/backend/db/feeds.sql.go b/backend/db/feeds.sql.go
index 4db84af..29b26ca 100644
--- a/backend/db/feeds.sql.go
+++ b/backend/db/feeds.sql.go
@@ -12,7 +12,7 @@ import (
const createFeed = `-- name: CreateFeed :one
INSERT INTO feeds (url, title, fetched_at)
VALUES (?, ?, ?)
-RETURNING id, url, title, fetched_at
+RETURNING id, url, title, fetched_at, is_subscribed
`
type CreateFeedParams struct {
@@ -29,6 +29,7 @@ func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, e
&i.Url,
&i.Title,
&i.FetchedAt,
+ &i.IsSubscribed,
)
return i, err
}
@@ -44,7 +45,7 @@ func (q *Queries) DeleteFeed(ctx context.Context, id int64) error {
}
const getFeed = `-- name: GetFeed :one
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
WHERE id = ?
`
@@ -57,12 +58,13 @@ func (q *Queries) GetFeed(ctx context.Context, id int64) (Feed, error) {
&i.Url,
&i.Title,
&i.FetchedAt,
+ &i.IsSubscribed,
)
return i, err
}
const getFeedByURL = `-- name: GetFeedByURL :one
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
WHERE url = ?
`
@@ -75,13 +77,15 @@ func (q *Queries) GetFeedByURL(ctx context.Context, url string) (Feed, error) {
&i.Url,
&i.Title,
&i.FetchedAt,
+ &i.IsSubscribed,
)
return i, err
}
const getFeeds = `-- name: GetFeeds :many
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
+WHERE is_subscribed = 1
ORDER BY id
`
@@ -99,6 +103,7 @@ func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) {
&i.Url,
&i.Title,
&i.FetchedAt,
+ &i.IsSubscribed,
); err != nil {
return nil, err
}
@@ -116,6 +121,7 @@ func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) {
const getFeedsToFetch = `-- name: GetFeedsToFetch :many
SELECT id, url, fetched_at
FROM feeds
+WHERE is_subscribed = 1
`
type GetFeedsToFetchRow struct {
@@ -147,6 +153,17 @@ func (q *Queries) GetFeedsToFetch(ctx context.Context) ([]GetFeedsToFetchRow, er
return items, nil
}
+const unsubscribeFeed = `-- name: UnsubscribeFeed :exec
+UPDATE feeds
+SET is_subscribed = 0
+WHERE id = ?
+`
+
+func (q *Queries) UnsubscribeFeed(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, unsubscribeFeed, id)
+ return err
+}
+
const updateFeedMetadata = `-- name: UpdateFeedMetadata :exec
UPDATE feeds
SET title = ?, fetched_at = ?
diff --git a/backend/db/models.go b/backend/db/models.go
index 2f36cb4..94ab9c0 100644
--- a/backend/db/models.go
+++ b/backend/db/models.go
@@ -14,8 +14,9 @@ type Article struct {
}
type Feed struct {
- ID int64
- Url string
- Title string
- FetchedAt string
+ ID int64
+ Url string
+ Title string
+ FetchedAt string
+ IsSubscribed int64
}
diff --git a/backend/db/queries/articles.sql b/backend/db/queries/articles.sql
index 3f1590a..c1feaae 100644
--- a/backend/db/queries/articles.sql
+++ b/backend/db/queries/articles.sql
@@ -1,7 +1,7 @@
-- name: GetArticle :one
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
WHERE a.id = ?;
@@ -9,20 +9,20 @@ WHERE a.id = ?;
-- name: GetUnreadArticles :many
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE a.is_read = 0
+WHERE a.is_read = 0 AND f.is_subscribed = 1
ORDER BY a.id DESC
LIMIT 100;
-- name: GetReadArticles :many
SELECT
a.id, a.feed_id, a.guid, a.title, a.url, a.is_read,
- f.id as feed_id_2, f.url as feed_url, f.title as feed_title
+ f.id as feed_id_2, f.url as feed_url, f.title as feed_title, f.is_subscribed as feed_is_subscribed
FROM articles AS a
INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE a.is_read = 1
+WHERE a.is_read = 1 AND f.is_subscribed = 1
ORDER BY a.id DESC
LIMIT 100;
diff --git a/backend/db/queries/feeds.sql b/backend/db/queries/feeds.sql
index 6d4d172..8445532 100644
--- a/backend/db/queries/feeds.sql
+++ b/backend/db/queries/feeds.sql
@@ -1,11 +1,12 @@
-- name: GetFeed :one
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
WHERE id = ?;
-- name: GetFeeds :many
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
+WHERE is_subscribed = 1
ORDER BY id;
-- name: CreateFeed :one
@@ -23,10 +24,16 @@ DELETE FROM feeds
WHERE id = ?;
-- name: GetFeedByURL :one
-SELECT id, url, title, fetched_at
+SELECT id, url, title, fetched_at, is_subscribed
FROM feeds
WHERE url = ?;
-- name: GetFeedsToFetch :many
SELECT id, url, fetched_at
-FROM feeds;
+FROM feeds
+WHERE is_subscribed = 1;
+
+-- name: UnsubscribeFeed :exec
+UPDATE feeds
+SET is_subscribed = 0
+WHERE id = ?;
diff --git a/backend/db/schema.sql b/backend/db/schema.sql
index 5c2bf48..eb40dea 100644
--- a/backend/db/schema.sql
+++ b/backend/db/schema.sql
@@ -1,9 +1,10 @@
-- Feeds
CREATE TABLE IF NOT EXISTS feeds (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- url TEXT NOT NULL,
- title TEXT NOT NULL,
- fetched_at TEXT NOT NULL
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ url TEXT NOT NULL,
+ title TEXT NOT NULL,
+ fetched_at TEXT NOT NULL,
+ is_subscribed INTEGER NOT NULL DEFAULT 1
);
-- Articles
diff --git a/backend/graphql/generated.go b/backend/graphql/generated.go
index eec1a8d..b09d4d8 100644
--- a/backend/graphql/generated.go
+++ b/backend/graphql/generated.go
@@ -57,11 +57,12 @@ type ComplexityRoot struct {
}
Feed struct {
- Articles func(childComplexity int) int
- FetchedAt func(childComplexity int) int
- ID func(childComplexity int) int
- Title func(childComplexity int) int
- URL func(childComplexity int) int
+ Articles func(childComplexity int) int
+ FetchedAt func(childComplexity int) int
+ ID func(childComplexity int) int
+ IsSubscribed func(childComplexity int) int
+ Title func(childComplexity int) int
+ URL func(childComplexity int) int
}
Mutation struct {
@@ -70,7 +71,7 @@ type ComplexityRoot struct {
MarkArticleUnread func(childComplexity int, id string) int
MarkFeedRead func(childComplexity int, id string) int
MarkFeedUnread func(childComplexity int, id string) int
- RemoveFeed func(childComplexity int, id string) int
+ UnsubscribeFeed func(childComplexity int, id string) int
}
Query struct {
@@ -84,7 +85,7 @@ type ComplexityRoot struct {
type MutationResolver interface {
AddFeed(ctx context.Context, url string) (*model.Feed, error)
- RemoveFeed(ctx context.Context, id string) (bool, error)
+ UnsubscribeFeed(ctx context.Context, id string) (bool, error)
MarkArticleRead(ctx context.Context, id string) (*model.Article, error)
MarkArticleUnread(ctx context.Context, id string) (*model.Article, error)
MarkFeedRead(ctx context.Context, id string) (*model.Feed, error)
@@ -187,6 +188,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Feed.ID(childComplexity), true
+ case "Feed.isSubscribed":
+ if e.complexity.Feed.IsSubscribed == nil {
+ break
+ }
+
+ return e.complexity.Feed.IsSubscribed(childComplexity), true
+
case "Feed.title":
if e.complexity.Feed.Title == nil {
break
@@ -261,17 +269,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.MarkFeedUnread(childComplexity, args["id"].(string)), true
- case "Mutation.removeFeed":
- if e.complexity.Mutation.RemoveFeed == nil {
+ case "Mutation.unsubscribeFeed":
+ if e.complexity.Mutation.UnsubscribeFeed == nil {
break
}
- args, err := ec.field_Mutation_removeFeed_args(ctx, rawArgs)
+ args, err := ec.field_Mutation_unsubscribeFeed_args(ctx, rawArgs)
if err != nil {
return 0, false
}
- return e.complexity.Mutation.RemoveFeed(childComplexity, args["id"].(string)), true
+ return e.complexity.Mutation.UnsubscribeFeed(childComplexity, args["id"].(string)), true
case "Query.article":
if e.complexity.Query.Article == nil {
@@ -449,6 +457,11 @@ type Feed {
fetchedAt: DateTime!
"""
+ Whether the user is currently subscribed to this feed
+ """
+ isSubscribed: Boolean!
+
+ """
Articles belonging to this feed
"""
articles: [Article!]!
@@ -534,9 +547,9 @@ type Mutation {
addFeed(url: String!): Feed!
"""
- Remove a feed subscription and all its articles
+ Unsubscribe from a feed (preserves feed and article data)
"""
- removeFeed(id: ID!): Boolean!
+ unsubscribeFeed(id: ID!): Boolean!
"""
Mark an article as read
@@ -706,17 +719,17 @@ func (ec *executionContext) field_Mutation_markFeedUnread_argsID(
return zeroVal, nil
}
-func (ec *executionContext) field_Mutation_removeFeed_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+func (ec *executionContext) field_Mutation_unsubscribeFeed_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
- arg0, err := ec.field_Mutation_removeFeed_argsID(ctx, rawArgs)
+ arg0, err := ec.field_Mutation_unsubscribeFeed_argsID(ctx, rawArgs)
if err != nil {
return nil, err
}
args["id"] = arg0
return args, nil
}
-func (ec *executionContext) field_Mutation_removeFeed_argsID(
+func (ec *executionContext) field_Mutation_unsubscribeFeed_argsID(
ctx context.Context,
rawArgs map[string]any,
) (string, error) {
@@ -1249,6 +1262,8 @@ func (ec *executionContext) fieldContext_Article_feed(_ context.Context, field g
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -1434,6 +1449,50 @@ func (ec *executionContext) fieldContext_Feed_fetchedAt(_ context.Context, field
return fc, nil
}
+func (ec *executionContext) _Feed_isSubscribed(ctx context.Context, field graphql.CollectedField, obj *model.Feed) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Feed_isSubscribed(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IsSubscribed, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(bool)
+ fc.Result = res
+ return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Feed_isSubscribed(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Feed",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Feed_articles(ctx context.Context, field graphql.CollectedField, obj *model.Feed) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Feed_articles(ctx, field)
if err != nil {
@@ -1541,6 +1600,8 @@ func (ec *executionContext) fieldContext_Mutation_addFeed(ctx context.Context, f
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -1561,8 +1622,8 @@ func (ec *executionContext) fieldContext_Mutation_addFeed(ctx context.Context, f
return fc, nil
}
-func (ec *executionContext) _Mutation_removeFeed(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
- fc, err := ec.fieldContext_Mutation_removeFeed(ctx, field)
+func (ec *executionContext) _Mutation_unsubscribeFeed(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_unsubscribeFeed(ctx, field)
if err != nil {
return graphql.Null
}
@@ -1575,7 +1636,7 @@ func (ec *executionContext) _Mutation_removeFeed(ctx context.Context, field grap
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
- return ec.resolvers.Mutation().RemoveFeed(rctx, fc.Args["id"].(string))
+ return ec.resolvers.Mutation().UnsubscribeFeed(rctx, fc.Args["id"].(string))
})
if err != nil {
ec.Error(ctx, err)
@@ -1592,7 +1653,7 @@ func (ec *executionContext) _Mutation_removeFeed(ctx context.Context, field grap
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
-func (ec *executionContext) fieldContext_Mutation_removeFeed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+func (ec *executionContext) fieldContext_Mutation_unsubscribeFeed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
@@ -1609,7 +1670,7 @@ func (ec *executionContext) fieldContext_Mutation_removeFeed(ctx context.Context
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
- if fc.Args, err = ec.field_Mutation_removeFeed_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ if fc.Args, err = ec.field_Mutation_unsubscribeFeed_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
@@ -1805,6 +1866,8 @@ func (ec *executionContext) fieldContext_Mutation_markFeedRead(ctx context.Conte
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -1872,6 +1935,8 @@ func (ec *executionContext) fieldContext_Mutation_markFeedUnread(ctx context.Con
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -1939,6 +2004,8 @@ func (ec *executionContext) fieldContext_Query_feeds(_ context.Context, field gr
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -2112,6 +2179,8 @@ func (ec *executionContext) fieldContext_Query_feed(ctx context.Context, field g
return ec.fieldContext_Feed_title(ctx, field)
case "fetchedAt":
return ec.fieldContext_Feed_fetchedAt(ctx, field)
+ case "isSubscribed":
+ return ec.fieldContext_Feed_isSubscribed(ctx, field)
case "articles":
return ec.fieldContext_Feed_articles(ctx, field)
}
@@ -4390,6 +4459,11 @@ func (ec *executionContext) _Feed(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
out.Invalids++
}
+ case "isSubscribed":
+ out.Values[i] = ec._Feed_isSubscribed(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
case "articles":
out.Values[i] = ec._Feed_articles(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -4444,9 +4518,9 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
- case "removeFeed":
+ case "unsubscribeFeed":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
- return ec._Mutation_removeFeed(ctx, field)
+ return ec._Mutation_unsubscribeFeed(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
diff --git a/backend/graphql/model/generated.go b/backend/graphql/model/generated.go
index bd5dcca..25ed8d8 100644
--- a/backend/graphql/model/generated.go
+++ b/backend/graphql/model/generated.go
@@ -30,6 +30,8 @@ type Feed struct {
Title string `json:"title"`
// Timestamp when the feed was last fetched
FetchedAt string `json:"fetchedAt"`
+ // Whether the user is currently subscribed to this feed
+ IsSubscribed bool `json:"isSubscribed"`
// Articles belonging to this feed
Articles []*Article `json:"articles"`
}
diff --git a/backend/graphql/resolver/schema.resolvers.go b/backend/graphql/resolver/schema.resolvers.go
index 0ee771b..cadcd33 100644
--- a/backend/graphql/resolver/schema.resolvers.go
+++ b/backend/graphql/resolver/schema.resolvers.go
@@ -52,47 +52,24 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed
}
return &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
+ ID: strconv.FormatInt(dbFeed.ID, 10),
+ URL: dbFeed.Url,
+ Title: dbFeed.Title,
+ FetchedAt: dbFeed.FetchedAt,
+ IsSubscribed: dbFeed.IsSubscribed == 1,
}, nil
}
-// RemoveFeed is the resolver for the removeFeed field.
-func (r *mutationResolver) RemoveFeed(ctx context.Context, id string) (bool, error) {
+// UnsubscribeFeed is the resolver for the unsubscribeFeed field.
+func (r *mutationResolver) UnsubscribeFeed(ctx context.Context, id string) (bool, error) {
feedID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return false, fmt.Errorf("invalid feed ID: %w", err)
}
- // Start a transaction
- tx, err := r.DB.Begin()
+ err = r.Queries.UnsubscribeFeed(ctx, feedID)
if err != nil {
- return false, fmt.Errorf("failed to begin transaction: %w", err)
- }
- defer tx.Rollback()
-
- qtx := r.Queries.WithTx(tx)
-
- // Delete articles first (foreign key constraint)
- err = qtx.DeleteArticlesByFeed(ctx, feedID)
- if err != nil {
- return false, fmt.Errorf("failed to delete articles: %w", err)
- }
-
- // Delete the feed
- err = qtx.DeleteFeed(ctx, feedID)
- if err != nil {
- if err == sql.ErrNoRows {
- return false, fmt.Errorf("feed not found")
- }
- return false, fmt.Errorf("failed to delete feed: %w", err)
- }
-
- err = tx.Commit()
- if err != nil {
- return false, fmt.Errorf("failed to commit transaction: %w", err)
+ return false, fmt.Errorf("failed to unsubscribe from feed: %w", err)
}
return true, nil
@@ -182,10 +159,11 @@ func (r *queryResolver) Feeds(ctx context.Context) ([]*model.Feed, error) {
var feeds []*model.Feed
for _, dbFeed := range dbFeeds {
feeds = append(feeds, &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
+ ID: strconv.FormatInt(dbFeed.ID, 10),
+ URL: dbFeed.Url,
+ Title: dbFeed.Title,
+ FetchedAt: dbFeed.FetchedAt,
+ IsSubscribed: dbFeed.IsSubscribed == 1,
})
}
@@ -209,9 +187,10 @@ func (r *queryResolver) UnreadArticles(ctx context.Context) ([]*model.Article, e
URL: row.Url,
IsRead: row.IsRead == 1,
Feed: &model.Feed{
- ID: strconv.FormatInt(row.FeedID2, 10),
- URL: row.FeedUrl,
- Title: row.FeedTitle,
+ ID: strconv.FormatInt(row.FeedID2, 10),
+ URL: row.FeedUrl,
+ Title: row.FeedTitle,
+ IsSubscribed: row.FeedIsSubscribed == 1,
},
})
}
@@ -236,9 +215,10 @@ func (r *queryResolver) ReadArticles(ctx context.Context) ([]*model.Article, err
URL: row.Url,
IsRead: row.IsRead == 1,
Feed: &model.Feed{
- ID: strconv.FormatInt(row.FeedID2, 10),
- URL: row.FeedUrl,
- Title: row.FeedTitle,
+ ID: strconv.FormatInt(row.FeedID2, 10),
+ URL: row.FeedUrl,
+ Title: row.FeedTitle,
+ IsSubscribed: row.FeedIsSubscribed == 1,
},
})
}
@@ -262,10 +242,11 @@ func (r *queryResolver) Feed(ctx context.Context, id string) (*model.Feed, error
}
return &model.Feed{
- ID: strconv.FormatInt(dbFeed.ID, 10),
- URL: dbFeed.Url,
- Title: dbFeed.Title,
- FetchedAt: dbFeed.FetchedAt,
+ ID: strconv.FormatInt(dbFeed.ID, 10),
+ URL: dbFeed.Url,
+ Title: dbFeed.Title,
+ FetchedAt: dbFeed.FetchedAt,
+ IsSubscribed: dbFeed.IsSubscribed == 1,
}, nil
}
diff --git a/common/graphql/schema.graphql b/common/graphql/schema.graphql
index 2ee1365..75d4d77 100644
--- a/common/graphql/schema.graphql
+++ b/common/graphql/schema.graphql
@@ -25,6 +25,11 @@ type Feed {
fetchedAt: DateTime!
"""
+ Whether the user is currently subscribed to this feed
+ """
+ isSubscribed: Boolean!
+
+ """
Articles belonging to this feed
"""
articles: [Article!]!
@@ -110,9 +115,9 @@ type Mutation {
addFeed(url: String!): Feed!
"""
- Remove a feed subscription and all its articles
+ Unsubscribe from a feed (preserves feed and article data)
"""
- removeFeed(id: ID!): Boolean!
+ unsubscribeFeed(id: ID!): Boolean!
"""
Mark an article as read
diff --git a/frontend/src/components/AddFeedForm.tsx b/frontend/src/components/AddFeedForm.tsx
index 79e26e3..519f9e3 100644
--- a/frontend/src/components/AddFeedForm.tsx
+++ b/frontend/src/components/AddFeedForm.tsx
@@ -28,7 +28,9 @@ export function AddFeedForm({ onFeedAdded }: Props) {
onFeedAdded?.();
}
} catch (error) {
- setError(error instanceof Error ? error.message : "Failed to add feed");
+ setError(
+ error instanceof Error ? error.message : "Failed to subscribe to feed",
+ );
}
};
@@ -47,7 +49,7 @@ export function AddFeedForm({ onFeedAdded }: Props) {
<form onSubmit={handleSubmit} className="space-y-4 p-4">
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
- Add New Feed
+ Subscribe to New Feed
</h3>
<div className="flex gap-2">
<div className="flex-1">
@@ -80,7 +82,7 @@ export function AddFeedForm({ onFeedAdded }: Props) {
) : (
<FontAwesomeIcon icon={faPlus} className="mr-2" />
)}
- Add Feed
+ Subscribe
</button>
</div>
</div>
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index 7e46e78..e5b6751 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -9,17 +9,17 @@ import {
GetFeedsDocument,
MarkFeedReadDocument,
MarkFeedUnreadDocument,
- RemoveFeedDocument,
+ UnsubscribeFeedDocument,
} from "../graphql/generated/graphql";
interface Props {
- onFeedDeleted?: () => void;
+ onFeedUnsubscribed?: () => void;
selectedFeeds?: Set<string>;
onSelectFeed?: (feedId: string, selected: boolean) => void;
}
export function FeedList({
- onFeedDeleted,
+ onFeedUnsubscribed,
selectedFeeds,
onSelectFeed,
}: Props) {
@@ -29,7 +29,7 @@ export function FeedList({
const [, markFeedRead] = useMutation(MarkFeedReadDocument);
const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
- const [, removeFeed] = useMutation(RemoveFeedDocument);
+ const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument);
const handleMarkAllRead = async (feedId: string) => {
await markFeedRead({ id: feedId });
@@ -39,13 +39,13 @@ export function FeedList({
await markFeedUnread({ id: feedId });
};
- const handleDeleteFeed = async (feedId: string) => {
+ const handleUnsubscribeFeed = async (feedId: string) => {
const confirmed = window.confirm(
- "Are you sure you want to delete this feed?",
+ "Are you sure you want to unsubscribe from this feed?",
);
if (confirmed) {
- await removeFeed({ id: feedId });
- onFeedDeleted?.();
+ await unsubscribeFeed({ id: feedId });
+ onFeedUnsubscribed?.();
}
};
@@ -134,9 +134,9 @@ export function FeedList({
</button>
<button
type="button"
- onClick={() => handleDeleteFeed(feed.id)}
+ onClick={() => handleUnsubscribeFeed(feed.id)}
className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700"
- title="Delete feed"
+ title="Unsubscribe from feed"
>
<FontAwesomeIcon icon={faTrash} />
</button>
diff --git a/frontend/src/graphql/generated/gql.ts b/frontend/src/graphql/generated/gql.ts
index ae8c1e1..b0b965d 100644
--- a/frontend/src/graphql/generated/gql.ts
+++ b/frontend/src/graphql/generated/gql.ts
@@ -14,12 +14,12 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
- "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation RemoveFeed($id: ID!) {\n removeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}": typeof types.AddFeedDocument,
- "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}": typeof types.GetFeedsDocument,
+ "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}": typeof types.AddFeedDocument,
+ "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}": typeof types.GetFeedsDocument,
};
const documents: Documents = {
- "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation RemoveFeed($id: ID!) {\n removeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}": types.AddFeedDocument,
- "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}": types.GetFeedsDocument,
+ "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}": types.AddFeedDocument,
+ "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}": types.GetFeedsDocument,
};
/**
@@ -39,11 +39,11 @@ export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation RemoveFeed($id: ID!) {\n removeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}"): (typeof documents)["mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation RemoveFeed($id: ID!) {\n removeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}"];
+export function graphql(source: "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}"): (typeof documents)["mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}"): (typeof documents)["query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}"];
+export function graphql(source: "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}"): (typeof documents)["query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
diff --git a/frontend/src/graphql/generated/graphql.ts b/frontend/src/graphql/generated/graphql.ts
index a54f1b6..22b34c5 100644
--- a/frontend/src/graphql/generated/graphql.ts
+++ b/frontend/src/graphql/generated/graphql.ts
@@ -43,6 +43,8 @@ export type Feed = {
fetchedAt: Scalars['DateTime']['output'];
/** Unique identifier for the feed */
id: Scalars['ID']['output'];
+ /** Whether the user is currently subscribed to this feed */
+ isSubscribed: Scalars['Boolean']['output'];
/** Title of the feed (extracted from feed metadata) */
title: Scalars['String']['output'];
/** URL of the RSS/Atom feed */
@@ -61,8 +63,8 @@ export type Mutation = {
markFeedRead: Feed;
/** Mark all articles in a feed as unread */
markFeedUnread: Feed;
- /** Remove a feed subscription and all its articles */
- removeFeed: Scalars['Boolean']['output'];
+ /** Unsubscribe from a feed (preserves feed and article data) */
+ unsubscribeFeed: Scalars['Boolean']['output'];
};
@@ -97,7 +99,7 @@ export type MutationMarkFeedUnreadArgs = {
/** Root mutation type for modifying data */
-export type MutationRemoveFeedArgs = {
+export type MutationUnsubscribeFeedArgs = {
id: Scalars['ID']['input'];
};
@@ -134,12 +136,12 @@ export type AddFeedMutationVariables = Exact<{
export type AddFeedMutation = { addFeed: { id: string, url: string, title: string, fetchedAt: string } };
-export type RemoveFeedMutationVariables = Exact<{
+export type UnsubscribeFeedMutationVariables = Exact<{
id: Scalars['ID']['input'];
}>;
-export type RemoveFeedMutation = { removeFeed: boolean };
+export type UnsubscribeFeedMutation = { unsubscribeFeed: boolean };
export type MarkArticleReadMutationVariables = Exact<{
id: Scalars['ID']['input'];
@@ -172,41 +174,41 @@ export type MarkFeedUnreadMutation = { markFeedUnread: { id: string, url: string
export type GetFeedsQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetFeedsQuery = { feeds: Array<{ id: string, url: string, title: string, fetchedAt: string, articles: Array<{ id: string, isRead: boolean }> }> };
+export type GetFeedsQuery = { feeds: Array<{ id: string, url: string, title: string, fetchedAt: string, isSubscribed: boolean, articles: Array<{ id: string, isRead: boolean }> }> };
export type GetUnreadArticlesQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetUnreadArticlesQuery = { unreadArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string } }> };
+export type GetUnreadArticlesQuery = { unreadArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } }> };
export type GetReadArticlesQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetReadArticlesQuery = { readArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string } }> };
+export type GetReadArticlesQuery = { readArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } }> };
export type GetFeedQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
-export type GetFeedQuery = { feed?: { id: string, url: string, title: string, fetchedAt: string, articles: Array<{ id: string, guid: string, title: string, url: string, isRead: boolean }> } | null };
+export type GetFeedQuery = { feed?: { id: string, url: string, title: string, fetchedAt: string, isSubscribed: boolean, articles: Array<{ id: string, guid: string, title: string, url: string, isRead: boolean }> } | null };
export type GetArticleQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
-export type GetArticleQuery = { article?: { id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string } } | null };
+export type GetArticleQuery = { article?: { id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } } | null };
export const AddFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"url"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"url"},"value":{"kind":"Variable","name":{"kind":"Name","value":"url"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<AddFeedMutation, AddFeedMutationVariables>;
-export const RemoveFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<RemoveFeedMutation, RemoveFeedMutationVariables>;
+export const UnsubscribeFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnsubscribeFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unsubscribeFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<UnsubscribeFeedMutation, UnsubscribeFeedMutationVariables>;
export const MarkArticleReadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkArticleRead"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markArticleRead"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]} as unknown as DocumentNode<MarkArticleReadMutation, MarkArticleReadMutationVariables>;
export const MarkArticleUnreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkArticleUnread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markArticleUnread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]} as unknown as DocumentNode<MarkArticleUnreadMutation, MarkArticleUnreadMutationVariables>;
export const MarkFeedReadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkFeedRead"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markFeedRead"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<MarkFeedReadMutation, MarkFeedReadMutationVariables>;
export const MarkFeedUnreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkFeedUnread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markFeedUnread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<MarkFeedUnreadMutation, MarkFeedUnreadMutationVariables>;
-export const GetFeedsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedsQuery, GetFeedsQueryVariables>;
-export const GetUnreadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUnreadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unreadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]} as unknown as DocumentNode<GetUnreadArticlesQuery, GetUnreadArticlesQueryVariables>;
-export const GetReadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]} as unknown as DocumentNode<GetReadArticlesQuery, GetReadArticlesQueryVariables>;
-export const GetFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedQuery, GetFeedQueryVariables>;
-export const GetArticleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetArticle"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"article"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]} as unknown as DocumentNode<GetArticleQuery, GetArticleQueryVariables>; \ No newline at end of file
+export const GetFeedsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}},{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedsQuery, GetFeedsQueryVariables>;
+export const GetUnreadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUnreadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unreadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}}]}}]} as unknown as DocumentNode<GetUnreadArticlesQuery, GetUnreadArticlesQueryVariables>;
+export const GetReadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}}]}}]} as unknown as DocumentNode<GetReadArticlesQuery, GetReadArticlesQueryVariables>;
+export const GetFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}},{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedQuery, GetFeedQueryVariables>;
+export const GetArticleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetArticle"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"article"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}}]}}]} as unknown as DocumentNode<GetArticleQuery, GetArticleQueryVariables>; \ No newline at end of file
diff --git a/frontend/src/graphql/mutations.graphql b/frontend/src/graphql/mutations.graphql
index 8bcbcec..9070118 100644
--- a/frontend/src/graphql/mutations.graphql
+++ b/frontend/src/graphql/mutations.graphql
@@ -7,8 +7,8 @@ mutation AddFeed($url: String!) {
}
}
-mutation RemoveFeed($id: ID!) {
- removeFeed(id: $id)
+mutation UnsubscribeFeed($id: ID!) {
+ unsubscribeFeed(id: $id)
}
mutation MarkArticleRead($id: ID!) {
diff --git a/frontend/src/graphql/queries.graphql b/frontend/src/graphql/queries.graphql
index af4ac01..0e96851 100644
--- a/frontend/src/graphql/queries.graphql
+++ b/frontend/src/graphql/queries.graphql
@@ -4,6 +4,7 @@ query GetFeeds {
url
title
fetchedAt
+ isSubscribed
articles {
id
isRead
@@ -22,6 +23,7 @@ query GetUnreadArticles {
feed {
id
title
+ isSubscribed
}
}
}
@@ -37,6 +39,7 @@ query GetReadArticles {
feed {
id
title
+ isSubscribed
}
}
}
@@ -47,6 +50,7 @@ query GetFeed($id: ID!) {
url
title
fetchedAt
+ isSubscribed
articles {
id
guid
@@ -68,6 +72,7 @@ query GetArticle($id: ID!) {
feed {
id
title
+ isSubscribed
}
}
}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index 78fc306..81c90e0 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -5,7 +5,7 @@ import {
GetFeedsDocument,
MarkFeedReadDocument,
MarkFeedUnreadDocument,
- RemoveFeedDocument,
+ UnsubscribeFeedDocument,
} from "../graphql/generated/graphql";
export function Settings() {
@@ -14,7 +14,7 @@ export function Settings() {
});
const [, markFeedRead] = useMutation(MarkFeedReadDocument);
const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
- const [, removeFeed] = useMutation(RemoveFeedDocument);
+ const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument);
const [selectedFeeds, setSelectedFeeds] = useState<Set<string>>(new Set());
@@ -22,7 +22,7 @@ export function Settings() {
refetchFeeds();
};
- const handleFeedDeleted = () => {
+ const handleFeedUnsubscribed = () => {
refetchFeeds();
setSelectedFeeds(new Set());
};
@@ -62,17 +62,17 @@ export function Settings() {
refetchFeeds();
};
- const handleBulkDelete = async () => {
+ const handleBulkUnsubscribe = async () => {
const confirmed = window.confirm(
- `Are you sure you want to delete ${selectedFeeds.size} selected feeds?`,
+ `Are you sure you want to unsubscribe from ${selectedFeeds.size} selected feeds?`,
);
if (!confirmed) return;
const promises = Array.from(selectedFeeds).map((feedId) =>
- removeFeed({ id: feedId }),
+ unsubscribeFeed({ id: feedId }),
);
await Promise.all(promises);
- handleFeedDeleted();
+ handleFeedUnsubscribed();
};
const hasFeeds = feedsData?.feeds && feedsData.feeds.length > 0;
@@ -82,10 +82,10 @@ export function Settings() {
<div className="mx-auto max-w-4xl">
<h1 className="mb-6 text-2xl font-bold text-gray-900">Feed Settings</h1>
- {/* Add New Feed Section */}
+ {/* Subscribe to New Feed Section */}
<div className="mb-8">
<h2 className="mb-4 text-xl font-semibold text-gray-800">
- Add New Feed
+ Subscribe to New Feed
</h2>
<AddFeedForm onFeedAdded={handleFeedAdded} />
</div>
@@ -134,10 +134,10 @@ export function Settings() {
</button>
<button
type="button"
- onClick={handleBulkDelete}
+ onClick={handleBulkUnsubscribe}
className="rounded px-3 py-1 text-sm font-medium text-red-700 hover:bg-red-100"
>
- Delete Selected
+ Unsubscribe Selected
</button>
</div>
</div>
@@ -145,7 +145,7 @@ export function Settings() {
)}
<FeedList
- onFeedDeleted={handleFeedDeleted}
+ onFeedUnsubscribed={handleFeedUnsubscribed}
selectedFeeds={selectedFeeds}
onSelectFeed={handleSelectFeed}
/>
diff --git a/justfile b/justfile
index 8153d6f..6f0b8d2 100644
--- a/justfile
+++ b/justfile
@@ -1,7 +1,7 @@
list:
@just -l
-serve: build
+serve:
FEEDAKA_BASE_PATH="" FEEDAKA_PORT=8080 ./backend/feedaka
build: