diff --git a/internal/webserver/handler_bookmarks.go b/internal/webserver/handler_bookmarks.go
new file mode 100644
index 0000000..d758448
--- /dev/null
+++ b/internal/webserver/handler_bookmarks.go
@@ -0,0 +1,36 @@
+package webserver
+
+import (
+ "net/http"
+ "errors"
+
+ "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
+)
+
+func (app *Application) Bookmarks(w http.ResponseWriter, r *http.Request) {
+ app.traceLog.Printf("'Bookmarks' handler (path: %q)", r.URL.Path)
+
+ c := persistence.NewUserFeedBookmarksCursor(app.ActiveUser.Handle)
+ err := parse_cursor_value(&c, r)
+ if err != nil {
+ app.error_400_with_message(w, "invalid cursor (must be a number)")
+ return
+ }
+
+ feed, err := app.Profile.NextPage(c, app.ActiveUser.ID)
+ if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) {
+ panic(err)
+ }
+
+ if is_htmx(r) && c.CursorPosition == persistence.CURSOR_MIDDLE {
+ // It's a Show More request
+ app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
+ } else {
+ app.buffered_render_page(
+ w,
+ "tpl/bookmarks.tpl",
+ PageGlobalData{TweetTrove: feed.TweetTrove},
+ TimelineData{Feed: feed},
+ )
+ }
+}
diff --git a/internal/webserver/server.go b/internal/webserver/server.go
index a857039..96ba0f7 100644
--- a/internal/webserver/server.go
+++ b/internal/webserver/server.go
@@ -122,6 +122,8 @@ func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/search", http.HandlerFunc(app.Search)).ServeHTTP(w, r)
case "lists":
http.StripPrefix("/lists", http.HandlerFunc(app.Lists)).ServeHTTP(w, r)
+ case "bookmarks":
+ app.Bookmarks(w, r)
case "messages":
http.StripPrefix("/messages", http.HandlerFunc(app.Messages)).ServeHTTP(w, r)
case "nav-sidebar-poll-updates":
diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go
index 5074305..e8df2dd 100644
--- a/internal/webserver/server_test.go
+++ b/internal/webserver/server_test.go
@@ -180,6 +180,37 @@ func TestUserFeedLikesTab(t *testing.T) {
assert.Len(tweets, 4)
}
+func TestBookmarksTab(t *testing.T) {
+ assert := assert.New(t)
+ require := require.New(t)
+
+ // Boilerplate for setting an active user
+ app := webserver.NewApp(profile)
+ app.IsScrapingDisabled = true
+ app.ActiveUser = scraper.User{ID: 1488963321701171204, Handle: "Offline_Twatter"} // Simulate a login
+
+ recorder := httptest.NewRecorder()
+ app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks", nil))
+ resp := recorder.Result()
+ require.Equal(resp.StatusCode, 200)
+
+ root, err := html.Parse(resp.Body)
+ require.NoError(err)
+ tweets := cascadia.QueryAll(root, selector(".timeline > .tweet"))
+ assert.Len(tweets, 2)
+
+ // Double check pagination works properly
+ recorder = httptest.NewRecorder()
+ app.ServeHTTP(recorder, httptest.NewRequest("GET", "/bookmarks?cursor=1800452344077464795", nil))
+ resp = recorder.Result()
+ require.Equal(resp.StatusCode, 200)
+
+ root, err = html.Parse(resp.Body)
+ require.NoError(err)
+ tweets = cascadia.QueryAll(root, selector(".timeline > .tweet"))
+ assert.Len(tweets, 1)
+}
+
// Followers and followees
// -----------------------
diff --git a/internal/webserver/static/styles.css b/internal/webserver/static/styles.css
index 12eeb96..2780a27 100644
--- a/internal/webserver/static/styles.css
+++ b/internal/webserver/static/styles.css
@@ -1190,6 +1190,15 @@ main {
}
}
+
+/******************************************************
+ * Bookmarks pages
+ ******************************************************/
+
+.bookmarks-feed-header {
+ border-bottom: 1px solid var(--color-outline-gray);
+}
+
.add-users-container {
padding: 1em;
text-align: center;
diff --git a/internal/webserver/tpl/includes/nav_sidebar.tpl b/internal/webserver/tpl/includes/nav_sidebar.tpl
index 1502f49..aa1f46c 100644
--- a/internal/webserver/tpl/includes/nav_sidebar.tpl
+++ b/internal/webserver/tpl/includes/nav_sidebar.tpl
@@ -42,7 +42,7 @@
-
+
@@ -50,7 +50,7 @@
-
+
diff --git a/sample_data/seed_data.sql b/sample_data/seed_data.sql
index d705dcb..ba38b99 100644
--- a/sample_data/seed_data.sql
+++ b/sample_data/seed_data.sql
@@ -347,6 +347,8 @@ create table likes(rowid integer primary key,
foreign key(user_id) references users(id)
foreign key(tweet_id) references tweets(id)
);
+create index if not exists index_likes_user_id on likes (user_id);
+create index if not exists index_likes_tweet_id on likes (tweet_id);
insert into likes values
(1, 1, 1178839081222115328, 1413646595493568516),
@@ -356,6 +358,22 @@ insert into likes values
(5, 5, 1178839081222115328, 1698765208393576891);
+create table bookmarks(rowid integer primary key,
+ sort_order integer not null,
+ user_id integer not null,
+ tweet_id integer not null,
+ unique(user_id, tweet_id)
+ foreign key(tweet_id) references tweets(id)
+ foreign key(user_id) references users(id)
+);
+create index if not exists index_bookmarks_user_id on bookmarks (user_id);
+create index if not exists index_bookmarks_tweet_id on bookmarks (tweet_id);
+insert into bookmarks values
+ (23,1800452344077464795,1488963321701171204,1413647919215906817),
+ (24,1800452337108289740,1488963321701171204,1439747634277740546);
+
+
+
create table chat_rooms (rowid integer primary key,
id text unique not null,
type text not null,
@@ -514,6 +532,6 @@ insert into fake_user_sequence values(0x4000000000000000);
create table database_version(rowid integer primary key,
version_number integer not null unique
);
-insert into database_version(version_number) values (29);
+insert into database_version(version_number) values (30);
COMMIT;