diff --git a/build/windows/setup.iss b/build/windows/setup.iss
index 65f51fc..18a2634 100644
--- a/build/windows/setup.iss
+++ b/build/windows/setup.iss
@@ -1,10 +1,11 @@
#define NAME "Offline Twitter"
#define EXE_NAME "twitter.exe"
+; The `version` macro should be passed from command line using `/Dversion=[...]`
[Setup]
AppName={#NAME}
-AppVersion={#VERSION}
+AppVersion={#version}
WizardStyle=modern
DefaultDirName={autopf}/offline-twitter
DefaultGroupName={#NAME}
diff --git a/internal/webserver/handler_user_feed.go b/internal/webserver/handler_user_feed.go
index 3a671ca..efced83 100644
--- a/internal/webserver/handler_user_feed.go
+++ b/internal/webserver/handler_user_feed.go
@@ -91,7 +91,8 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
data := struct {
persistence.Feed
scraper.UserID
- FeedType string
+ PinnedTweet scraper.Tweet
+ FeedType string
}{Feed: feed, UserID: user.ID}
if len(parts) == 2 {
@@ -100,6 +101,16 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
data.FeedType = ""
}
+ // Add a pinned tweet if there is one and it's in the DB; otherwise skip
+ if user.PinnedTweetID != scraper.TweetID(0) && len(parts) <= 1 || parts[1] == "without_replies" {
+ data.PinnedTweet, err = app.Profile.GetTweetById(user.PinnedTweetID)
+ if err == nil {
+ feed.TweetTrove.Tweets[data.PinnedTweet.ID] = data.PinnedTweet
+ } else if !errors.Is(err, persistence.ErrNotInDB) {
+ panic(err)
+ }
+ }
+
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
// It's a Show More request
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, data)
diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go
index 4711382..9551ac7 100644
--- a/internal/webserver/server_test.go
+++ b/internal/webserver/server_test.go
@@ -81,10 +81,9 @@ func TestUserFeed(t *testing.T) {
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter")
- tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
- assert.Len(tweet_nodes, 7)
- including_quote_tweets := cascadia.QueryAll(root, selector(".tweet"))
- assert.Len(including_quote_tweets, 10)
+ assert.Len(cascadia.QueryAll(root, selector(".timeline > .tweet")), 8)
+ assert.Len(cascadia.QueryAll(root, selector(".timeline > .pinned-tweet")), 1)
+ assert.Len(cascadia.QueryAll(root, selector(".tweet")), 12) // Pinned tweet appears again
}
func TestUserFeedWithEntityInBio(t *testing.T) {
@@ -224,7 +223,7 @@ func TestTimeline(t *testing.T) {
assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
- assert.Len(tweet_nodes, 18)
+ assert.Len(tweet_nodes, 19)
}
func TestTimelineWithCursor(t *testing.T) {
diff --git a/internal/webserver/static/styles.css b/internal/webserver/static/styles.css
index e3f632a..3ce478e 100644
--- a/internal/webserver/static/styles.css
+++ b/internal/webserver/static/styles.css
@@ -343,6 +343,18 @@ h3 {
color: var(--color-twitter-blue);
}
+.pinned-tweet__pin-container {
+ margin: 0.5em 0em -1em 3em;
+ z-index: 1;
+ position: relative; /* z-index is ignored if `position` is "static" */
+ gap: 0.2em;
+}
+img.svg-icon.pinned-tweet__pin-icon {
+ filter: invert(43%) saturate(30%);
+ width: 1em;
+ height: auto;
+}
+
.row {
display: flex;
flex-direction: row;
@@ -474,7 +486,7 @@ ul.quick-links {
padding-top: 0.8em;
padding-bottom: 0.8em;
}
-.timeline > .tweet {
+.timeline > .tweet, .timeline > .pinned-tweet {
/* not for nested (i.e., quoted) tweets */
border-bottom: 1px solid var(--color-twitter-off-white-dark);
}
diff --git a/internal/webserver/tpl/user_feed.tpl b/internal/webserver/tpl/user_feed.tpl
index ebbe633..9f6e1e8 100644
--- a/internal/webserver/tpl/user_feed.tpl
+++ b/internal/webserver/tpl/user_feed.tpl
@@ -22,6 +22,15 @@
+ {{if .PinnedTweet.ID}}
+
+ {{end}}
{{template "timeline" .}}
{{end}}
diff --git a/pkg/persistence/compound_ssf_queries_test.go b/pkg/persistence/compound_ssf_queries_test.go
index 020d51e..a042d49 100644
--- a/pkg/persistence/compound_ssf_queries_test.go
+++ b/pkg/persistence/compound_ssf_queries_test.go
@@ -207,10 +207,11 @@ func TestSearchDateFilters(t *testing.T) {
c.UntilTimestamp.Time = time.Date(2021, 10, 1, 0, 0, 0, 0, time.UTC)
feed, err = profile.NextPage(c, UserID(0))
require.NoError(err)
- assert.Len(feed.Items, 3)
- assert.Equal(feed.Items[0].TweetID, TweetID(1439027915404939265))
- assert.Equal(feed.Items[1].TweetID, TweetID(1439068749336748043))
- assert.Equal(feed.Items[2].TweetID, TweetID(1439067163508150272))
+ assert.Len(feed.Items, 4)
+ assert.Equal(feed.Items[0].TweetID, TweetID(1439747634277740546))
+ assert.Equal(feed.Items[1].TweetID, TweetID(1439027915404939265))
+ assert.Equal(feed.Items[2].TweetID, TweetID(1439068749336748043))
+ assert.Equal(feed.Items[3].TweetID, TweetID(1439067163508150272))
}
func TestSearchMediaFilters(t *testing.T) {
diff --git a/sample_data/seed_data.sql b/sample_data/seed_data.sql
index 5d40655..3698a8e 100644
--- a/sample_data/seed_data.sql
+++ b/sample_data/seed_data.sql
@@ -175,6 +175,7 @@ INSERT INTO tweets VALUES
(2857357,1489944024278523906,96906231,'According to @gofundme it was "as a result of multiple discussions with locals law enforcement and *police reports of violence and other unlawful activity*". ABSOLUTE LIES! I asked police officers live and they CONFIRMED there was no violence. Pure censorship. #BankruptGoFundMe',1644065311000,5753,2127,219,110,0,0,'gofundme','','BankruptGoFundMe',NULL,NULL,0,1,0,0,0),
(121936,1513313535480287235,1178839081222115328,'Smh wish I could RT',1649637037000,4,0,1,0,1513312559981551619,0,'PublicAnthony','PublicAnthony','',NULL,NULL,0,1,0,0,0),
(869468,1624833173514293249,1240784920831762433,'',1676225391000,1,0,0,0,0,0,'','','','1OwGWwnoleRGQ',NULL,0,1,0,0,0),
+ (2090918,1439747634277740546,358545917,'Explain why staff but not talent at these events have to wear masks. Using science.',1632097559000,29832,4145,783,163,0,0,'','','',NULL,NULL,0,1,1,1710818767264,0),
(2857431,1695110851324256692,19370504,replace('My dad was a doctor, he retired this past year \n\nHe’s been healthy his whole life, and he saw the titanic shift (no pun intended) in obesity being normalized in real time \n\nIt used to be a 300lb person was uncommon \n\nThen it was 400lbs\n\nThen 500lbs\n\nHospitals had to upgrade their scales to veterinary scales they use in zoos,\nThat’s how fat people became \n\nObese patients would be OFFENDED if you suggested they lose weight \n\nThey would complain if you told them their back pain was because their BMI was 45 \n\nThey’d ignore all suggestions of exercise or diet and complain why can’t they just take a pill \n\nThis wasn’t outliers, this is at least 50% of the population \n\nUntil you work with general public, you cannot fully conceive the existent of people’s sloth and apathy towards their own quality of life','\n',char(10)),1692980895000,1894,224,137,25,0,0,'','','',NULL,NULL,0,1,1,1693055764000,1),
(1405789,1698426460061487546,1458284524761075714,'Zig''s "comptime" leads to the most elegant reflection code I''ve ever seen. It''s much cleaner and more expressive than, e.g., Python''s various __methods__, or worse, the deranged "metaclasses" nonsense; but it also has no runtime cost!',1693771397000,6,0,1,1,0,1692962678824648811,'','','',NULL,NULL,0,1,0,0,0),
(1408662,1698762403163304110,1458284524761075714,replace('Another very cool use of Zig''s "comptime" is it lets you write real, compiled mini-languages in strings; e.g.:\n\n- SQL prepared statements\n- "printf" style format strings\n- regexps\n\nEvery language uses these, but they''re interpreted at runtime, even in compiled languages.','\n',char(10)),1693851493000,7,2,3,0,0,1698426460061487546,'','','',NULL,NULL,0,1,0,0,0),