From d60edf7ec61537d70915c41de5526b81dd795a2c Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Sun, 10 Dec 2023 12:36:00 +0100
Subject: [PATCH] [bugfix] Ensure `pre` renders as expected, fix
 orderedCollectionPage (#2434)

---
 internal/ap/serialize.go                      | 19 ++++---
 .../api/activitypub/users/outboxget_test.go   | 56 +++++++++++++------
 .../api/activitypub/users/repliesget_test.go  |  2 +-
 .../api/client/accounts/accountverify_test.go |  2 +-
 .../api/client/instance/instancepatch_test.go | 12 ++--
 internal/api/client/search/searchget_test.go  |  6 +-
 .../api/client/statuses/statusmute_test.go    |  8 +--
 internal/db/bundb/account_test.go             | 24 ++++----
 internal/db/bundb/basic_test.go               |  2 +-
 internal/db/bundb/instance_test.go            |  2 +-
 internal/db/bundb/timeline_test.go            | 10 ++--
 internal/processing/account/rss_test.go       |  4 +-
 internal/timeline/get_test.go                 |  6 +-
 internal/timeline/prune_test.go               |  8 +--
 internal/typeutils/internaltofrontend_test.go | 18 +++---
 testrig/testmodels.go                         | 28 ++++++++++
 web/source/css/status.css                     |  1 +
 17 files changed, 130 insertions(+), 78 deletions(-)

diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go
index 944e67407..774e95f2d 100644
--- a/internal/ap/serialize.go
+++ b/internal/ap/serialize.go
@@ -35,13 +35,15 @@ import (
 // Currently, the following things will be custom serialized:
 //
 //   - OrderedCollection:       'orderedItems' property will always be made into an array.
+//   - OrderedCollectionPage:   'orderedItems' property will always be made into an array.
 //   - Any Accountable type:    'attachment' property will always be made into an array.
 //   - Any Statusable type:     'attachment' property will always be made into an array; 'content' and 'contentMap' will be normalized.
 //   - Any Activityable type:   any 'object's set on an activity will be custom serialized as above.
 func Serialize(t vocab.Type) (m map[string]interface{}, e error) {
 	switch tn := t.GetTypeName(); {
-	case tn == ObjectOrderedCollection:
-		return serializeOrderedCollection(t)
+	case tn == ObjectOrderedCollection ||
+		tn == ObjectOrderedCollectionPage:
+		return serializeWithOrderedItems(t)
 	case IsAccountable(tn):
 		return serializeAccountable(t, true)
 	case IsStatusable(tn):
@@ -54,16 +56,17 @@ func Serialize(t vocab.Type) (m map[string]interface{}, e error) {
 	}
 }
 
-// serializeOrderedCollection is a custom serializer for an ActivityStreamsOrderedCollection.
-// Unlike the standard streams.Serialize function, this serializer normalizes the orderedItems
-// value to always be an array/slice, regardless of how many items are contained therein.
-//
-// TODO: Remove this function if we can fix the underlying issue in Go-Fed.
+// serializeWithOrderedItems is a custom serializer
+// for any type that has an `orderedItems` property.
+// Unlike the standard streams.Serialize function,
+// this serializer normalizes the orderedItems
+// value to always be an array/slice, regardless
+// of how many items are contained therein.
 //
 // See:
 //   - https://github.com/go-fed/activity/issues/139
 //   - https://github.com/mastodon/mastodon/issues/24225
-func serializeOrderedCollection(t vocab.Type) (map[string]interface{}, error) {
+func serializeWithOrderedItems(t vocab.Type) (map[string]interface{}, error) {
 	data, err := streams.Serialize(t)
 	if err != nil {
 		return nil, err
diff --git a/internal/api/activitypub/users/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go
index 31b7e8e9b..55e9f2f78 100644
--- a/internal/api/activitypub/users/outboxget_test.go
+++ b/internal/api/activitypub/users/outboxget_test.go
@@ -140,16 +140,26 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
   "@context": "https://www.w3.org/ns/activitystreams",
   "id": "http://localhost:8080/users/the_mighty_zork/outbox?page=true",
   "next": "http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
-  "orderedItems": {
-    "actor": "http://localhost:8080/users/the_mighty_zork",
-    "cc": "http://localhost:8080/users/the_mighty_zork/followers",
-    "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create",
-    "object": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
-    "to": "https://www.w3.org/ns/activitystreams#Public",
-    "type": "Create"
-  },
+  "orderedItems": [
+    {
+      "actor": "http://localhost:8080/users/the_mighty_zork",
+      "cc": "http://localhost:8080/users/the_mighty_zork/followers",
+      "id": "http://localhost:8080/users/the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40/activity#Create",
+      "object": "http://localhost:8080/users/the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40",
+      "to": "https://www.w3.org/ns/activitystreams#Public",
+      "type": "Create"
+    },
+    {
+      "actor": "http://localhost:8080/users/the_mighty_zork",
+      "cc": "http://localhost:8080/users/the_mighty_zork/followers",
+      "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create",
+      "object": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
+      "to": "https://www.w3.org/ns/activitystreams#Public",
+      "type": "Create"
+    }
+  ],
   "partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
-  "prev": "http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026min_id=01F8MHAMCHF6Y650WCRSCP4WMY",
+  "prev": "http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026min_id=01HH9KYNQPA416TNJ53NSATP40",
   "type": "OrderedCollectionPage"
 }`, dst.String())
 
@@ -237,22 +247,32 @@ func checkDropPublished(t *testing.T, b []byte, at ...string) []byte {
 	if err := json.Unmarshal(b, &m); err != nil {
 		t.Fatalf("error unmarshaling json into map: %v", err)
 	}
-	mm := m
+
+	entries := make([]map[string]any, 0)
 	for _, key := range at {
-		switch vt := mm[key].(type) {
-		case map[string]any:
-			mm = vt
+		switch vt := m[key].(type) {
+		case []interface{}:
+			for _, t := range vt {
+				if entry, ok := t.(map[string]any); ok {
+					entries = append(entries, entry)
+				}
+			}
 		}
 	}
-	if s, ok := mm["published"].(string); !ok {
-		t.Fatal("missing published data on json")
-	} else if _, err := time.Parse(time.RFC3339, s); err != nil {
-		t.Fatalf("error parsing published time: %v", err)
+
+	for _, entry := range entries {
+		if s, ok := entry["published"].(string); !ok {
+			t.Fatal("missing published data on json")
+		} else if _, err := time.Parse(time.RFC3339, s); err != nil {
+			t.Fatalf("error parsing published time: %v", err)
+		}
+		delete(entry, "published")
 	}
-	delete(mm, "published")
+
 	b, err := json.Marshal(m)
 	if err != nil {
 		t.Fatalf("error remarshaling json: %v", err)
 	}
+
 	return b
 }
diff --git a/internal/api/activitypub/users/repliesget_test.go b/internal/api/activitypub/users/repliesget_test.go
index d20d8c6c0..757d0bc83 100644
--- a/internal/api/activitypub/users/repliesget_test.go
+++ b/internal/api/activitypub/users/repliesget_test.go
@@ -163,7 +163,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
 		"partOf":       targetStatus.URI + "/replies?only_other_accounts=false",
 		"next":         "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&min_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
 		"prev":         "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&max_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
-		"orderedItems": "http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
+		"orderedItems": []string{"http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0"},
 		"totalItems":   1,
 	})
 	assert.Equal(suite.T(), expect, string(b))
diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go
index 7827cc732..744488bf3 100644
--- a/internal/api/client/accounts/accountverify_test.go
+++ b/internal/api/client/accounts/accountverify_test.go
@@ -79,7 +79,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
 	suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.HeaderStatic)
 	suite.Equal(2, apimodelAccount.FollowersCount)
 	suite.Equal(2, apimodelAccount.FollowingCount)
-	suite.Equal(6, apimodelAccount.StatusesCount)
+	suite.Equal(7, apimodelAccount.StatusesCount)
 	suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy)
 	suite.Equal(testAccount.Language, apimodelAccount.Source.Language)
 	suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index 0c270de21..420bcd79e 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -133,7 +133,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
@@ -250,7 +250,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
@@ -367,7 +367,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
@@ -535,7 +535,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
@@ -674,7 +674,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
@@ -828,7 +828,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go
index 307b928bd..1b4b92c21 100644
--- a/internal/api/client/search/searchget_test.go
+++ b/internal/api/client/search/searchget_test.go
@@ -877,7 +877,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
 	}
 
 	suite.Len(searchResult.Accounts, 5)
-	suite.Len(searchResult.Statuses, 5)
+	suite.Len(searchResult.Statuses, 6)
 	suite.Len(searchResult.Hashtags, 0)
 }
 
@@ -918,7 +918,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() {
 	}
 
 	suite.Len(searchResult.Accounts, 2)
-	suite.Len(searchResult.Statuses, 5)
+	suite.Len(searchResult.Statuses, 6)
 	suite.Len(searchResult.Hashtags, 0)
 }
 
@@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() {
 	}
 
 	suite.Len(searchResult.Accounts, 0)
-	suite.Len(searchResult.Statuses, 5)
+	suite.Len(searchResult.Statuses, 6)
 	suite.Len(searchResult.Hashtags, 0)
 }
 
diff --git a/internal/api/client/statuses/statusmute_test.go b/internal/api/client/statuses/statusmute_test.go
index 91a361bd6..a83720a20 100644
--- a/internal/api/client/statuses/statusmute_test.go
+++ b/internal/api/client/statuses/statusmute_test.go
@@ -130,8 +130,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
     "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
     "followers_count": 2,
     "following_count": 2,
-    "statuses_count": 6,
-    "last_status_at": "2022-05-20T11:41:10.000Z",
+    "statuses_count": 7,
+    "last_status_at": "2023-12-10T09:24:00.000Z",
     "emojis": [],
     "fields": [],
     "enable_rss": true,
@@ -193,8 +193,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
     "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
     "followers_count": 2,
     "following_count": 2,
-    "statuses_count": 6,
-    "last_status_at": "2022-05-20T11:41:10.000Z",
+    "statuses_count": 7,
+    "last_status_at": "2023-12-10T09:24:00.000Z",
     "emojis": [],
     "fields": [],
     "enable_rss": true,
diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go
index 8c2de5519..e03300700 100644
--- a/internal/db/bundb/account_test.go
+++ b/internal/db/bundb/account_test.go
@@ -42,33 +42,33 @@ type AccountTestSuite struct {
 func (suite *AccountTestSuite) TestGetAccountStatuses() {
 	statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false)
 	suite.NoError(err)
-	suite.Len(statuses, 6)
+	suite.Len(statuses, 7)
 }
 
 func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
 	// get the first page
-	statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 2, false, false, "", "", false, false)
+	statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, "", "", false, false)
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.Len(statuses, 2)
+	suite.Len(statuses, 3)
 
 	// get the second page
-	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 2, false, false, statuses[len(statuses)-1].ID, "", false, false)
+	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.Len(statuses, 2)
+	suite.Len(statuses, 3)
 
 	// get the third page
-	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 2, false, false, statuses[len(statuses)-1].ID, "", false, false)
+	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.Len(statuses, 2)
+	suite.Len(statuses, 1)
 
 	// try to get the last page (should be empty)
-	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 2, false, false, statuses[len(statuses)-1].ID, "", false, false)
+	statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
 	suite.ErrorIs(err, db.ErrNoEntries)
 	suite.Empty(statuses)
 }
@@ -76,13 +76,13 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
 func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs() {
 	statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false)
 	suite.NoError(err)
-	suite.Len(statuses, 6)
+	suite.Len(statuses, 7)
 }
 
 func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() {
 	statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true)
 	suite.NoError(err)
-	suite.Len(statuses, 1)
+	suite.Len(statuses, 2)
 }
 
 func (suite *AccountTestSuite) TestGetAccountStatusesMediaOnly() {
@@ -306,13 +306,13 @@ func (suite *AccountTestSuite) TestUpdateAccount() {
 func (suite *AccountTestSuite) TestGetAccountLastPosted() {
 	lastPosted, err := suite.db.GetAccountLastPosted(context.Background(), suite.testAccounts["local_account_1"].ID, false)
 	suite.NoError(err)
-	suite.EqualValues(1653046870, lastPosted.Unix())
+	suite.EqualValues(1702200240, lastPosted.Unix())
 }
 
 func (suite *AccountTestSuite) TestGetAccountLastPostedWebOnly() {
 	lastPosted, err := suite.db.GetAccountLastPosted(context.Background(), suite.testAccounts["local_account_1"].ID, true)
 	suite.NoError(err)
-	suite.EqualValues(1634726437, lastPosted.Unix())
+	suite.EqualValues(1702200240, lastPosted.Unix())
 }
 
 func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go
index 67973a171..fc601f2a5 100644
--- a/internal/db/bundb/basic_test.go
+++ b/internal/db/bundb/basic_test.go
@@ -121,7 +121,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
 	s := []*gtsmodel.Status{}
 	err := suite.db.GetAll(context.Background(), &s)
 	suite.NoError(err)
-	suite.Len(s, 22)
+	suite.Len(s, 23)
 }
 
 func (suite *BasicTestSuite) TestGetAllNotNull() {
diff --git a/internal/db/bundb/instance_test.go b/internal/db/bundb/instance_test.go
index f2ac202ef..4b8ec9962 100644
--- a/internal/db/bundb/instance_test.go
+++ b/internal/db/bundb/instance_test.go
@@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
 func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
 	count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
 	suite.NoError(err)
-	suite.Equal(18, count)
+	suite.Equal(19, count)
 }
 
 func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go
index b3ce91755..98ae6b20f 100644
--- a/internal/db/bundb/timeline_test.go
+++ b/internal/db/bundb/timeline_test.go
@@ -157,7 +157,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimeline() {
 		suite.FailNow(err.Error())
 	}
 
-	suite.checkStatuses(s, id.Highest, id.Lowest, 18)
+	suite.checkStatuses(s, id.Highest, id.Lowest, 19)
 }
 
 func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
@@ -189,7 +189,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
 		suite.FailNow(err.Error())
 	}
 
-	suite.checkStatuses(s, id.Highest, id.Lowest, 6)
+	suite.checkStatuses(s, id.Highest, id.Lowest, 7)
 }
 
 func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
@@ -211,7 +211,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
 	}
 
 	suite.NotContains(s, futureStatus)
-	suite.checkStatuses(s, id.Highest, id.Lowest, 18)
+	suite.checkStatuses(s, id.Highest, id.Lowest, 19)
 }
 
 func (suite *TimelineTestSuite) TestGetHomeTimelineBackToFront() {
@@ -242,8 +242,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() {
 	}
 
 	suite.checkStatuses(s, id.Highest, id.Lowest, 5)
-	suite.Equal("01HEN2RZ8BG29Y5Z9VJC73HZW7", s[0].ID)
-	suite.Equal("01FN3VJGFH10KR7S2PB0GFJZYG", s[len(s)-1].ID)
+	suite.Equal("01HH9KYNQPA416TNJ53NSATP40", s[0].ID)
+	suite.Equal("01G20ZM733MGN8J344T4ZDDFY1", s[len(s)-1].ID)
 }
 
 func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {
diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go
index 80f86211f..e528c27e0 100644
--- a/internal/processing/account/rss_test.go
+++ b/internal/processing/account/rss_test.go
@@ -45,14 +45,14 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
 func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
 	getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork")
 	suite.NoError(err)
-	suite.EqualValues(1634726437, lastModified.Unix())
+	suite.EqualValues(1702200240, lastModified.Unix())
 
 	feed, err := getFeed()
 	suite.NoError(err)
 
 	fmt.Println(feed)
 
-	suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n  <channel>\n    <title>Posts from @the_mighty_zork@localhost:8080</title>\n    <link>http://localhost:8080/@the_mighty_zork</link>\n    <description>Posts from @the_mighty_zork@localhost:8080</description>\n    <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n    <lastBuildDate>Wed, 20 Oct 2021 10:40:37 +0000</lastBuildDate>\n    <image>\n      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg</url>\n      <title>Avatar for @the_mighty_zork@localhost:8080</title>\n      <link>http://localhost:8080/@the_mighty_zork</link>\n    </image>\n    <item>\n      <title>introduction post</title>\n      <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n      <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>\n      <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n      <author>@the_mighty_zork@localhost:8080</author>\n      <guid>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n      <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n    </item>\n  </channel>\n</rss>", feed)
+	suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n  <channel>\n    <title>Posts from @the_mighty_zork@localhost:8080</title>\n    <link>http://localhost:8080/@the_mighty_zork</link>\n    <description>Posts from @the_mighty_zork@localhost:8080</description>\n    <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n    <lastBuildDate>Sun, 10 Dec 2023 09:24:00 +0000</lastBuildDate>\n    <image>\n      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg</url>\n      <title>Avatar for @the_mighty_zork@localhost:8080</title>\n      <link>http://localhost:8080/@the_mighty_zork</link>\n    </image>\n    <item>\n      <title>HTML in post</title>\n      <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>\n      <description>@the_mighty_zork@localhost:8080 made a new post: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;```html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>\n      <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n    &lt;div class=&#34;col-header&#34;&gt;\n        &lt;h2&gt;About&lt;/h2&gt;\n    &lt;/div&gt;            \n    &lt;div class=&#34;fields&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n        &lt;dl&gt;\n            &lt;div class=&#34;field&#34;&gt;\n                &lt;dt&gt;should you follow me?&lt;/dt&gt;\n                &lt;dd&gt;maybe!&lt;/dd&gt;\n            &lt;/div&gt;\n            &lt;div class=&#34;field&#34;&gt;\n                &lt;dt&gt;age&lt;/dt&gt;\n                &lt;dd&gt;120&lt;/dd&gt;\n            &lt;/div&gt;\n        &lt;/dl&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;bio&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n        &lt;p&gt;i post about things that concern me&lt;/p&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n        &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n        &lt;span&gt;8 posts.&lt;/span&gt;\n        &lt;span&gt;Followed by 1.&lt;/span&gt;\n        &lt;span&gt;Following 1.&lt;/span&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n        &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n        &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n        &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n        &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n    &lt;/div&gt;\n&lt;/section&gt;\n</code></pre><p>There, hope you liked that!</p>]]></content:encoded>\n      <author>@the_mighty_zork@localhost:8080</author>\n      <guid>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>\n      <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n    </item>\n    <item>\n      <title>introduction post</title>\n      <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n      <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>\n      <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n      <author>@the_mighty_zork@localhost:8080</author>\n      <guid>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n      <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n    </item>\n  </channel>\n</rss>", feed)
 }
 
 func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
diff --git a/internal/timeline/get_test.go b/internal/timeline/get_test.go
index 360eac7b6..02256054e 100644
--- a/internal/timeline/get_test.go
+++ b/internal/timeline/get_test.go
@@ -228,7 +228,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() {
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.checkStatuses(statuses, id.Highest, id.Lowest, 18)
+	suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
 }
 
 func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
@@ -255,7 +255,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.checkStatuses(statuses, id.Highest, id.Lowest, 18)
+	suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
 }
 
 func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
@@ -284,7 +284,7 @@ func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
 	if err != nil {
 		suite.FailNow(err.Error())
 	}
-	suite.checkStatuses(statuses, id.Highest, id.Lowest, 6)
+	suite.checkStatuses(statuses, id.Highest, id.Lowest, 7)
 
 	for _, s := range statuses {
 		if s.GetAccountID() != testAccount.ID {
diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go
index 4cd677953..047166e9e 100644
--- a/internal/timeline/prune_test.go
+++ b/internal/timeline/prune_test.go
@@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
 
 	pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
 	suite.NoError(err)
-	suite.Equal(17, pruned)
+	suite.Equal(18, pruned)
 	suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
 }
 
@@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() {
 
 	pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
 	suite.NoError(err)
-	suite.Equal(17, pruned)
+	suite.Equal(18, pruned)
 	suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
 
 	// Prune same again, nothing should be pruned this time.
@@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() {
 
 	pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
 	suite.NoError(err)
-	suite.Equal(22, pruned)
+	suite.Equal(23, pruned)
 	suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
 }
 
@@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() {
 	pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
 	suite.NoError(err)
 	suite.Equal(0, pruned)
-	suite.Equal(22, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
+	suite.Equal(23, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
 }
 
 func TestPruneTestSuite(t *testing.T) {
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 6152bf77f..d65d47bf1 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -58,8 +58,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
   "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
   "followers_count": 2,
   "following_count": 2,
-  "statuses_count": 6,
-  "last_status_at": "2022-05-20T11:41:10.000Z",
+  "statuses_count": 7,
+  "last_status_at": "2023-12-10T09:24:00.000Z",
   "emojis": [],
   "fields": [],
   "enable_rss": true,
@@ -100,8 +100,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
   "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
   "followers_count": 2,
   "following_count": 2,
-  "statuses_count": 6,
-  "last_status_at": "2022-05-20T11:41:10.000Z",
+  "statuses_count": 7,
+  "last_status_at": "2023-12-10T09:24:00.000Z",
   "emojis": [
     {
       "shortcode": "rainbow",
@@ -148,8 +148,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
   "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
   "followers_count": 2,
   "following_count": 2,
-  "statuses_count": 6,
-  "last_status_at": "2022-05-20T11:41:10.000Z",
+  "statuses_count": 7,
+  "last_status_at": "2023-12-10T09:24:00.000Z",
   "emojis": [
     {
       "shortcode": "rainbow",
@@ -192,8 +192,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
   "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
   "followers_count": 2,
   "following_count": 2,
-  "statuses_count": 6,
-  "last_status_at": "2022-05-20T11:41:10.000Z",
+  "statuses_count": 7,
+  "last_status_at": "2023-12-10T09:24:00.000Z",
   "emojis": [],
   "fields": [],
   "source": {
@@ -896,7 +896,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
   },
   "stats": {
     "domain_count": 2,
-    "status_count": 18,
+    "status_count": 19,
     "user_count": 4
   },
   "thumbnail": "http://localhost:8080/assets/logo.png",
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index e039a7c16..e7c1f0c02 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -1676,6 +1676,31 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
 			ActivityStreamsType:      ap.ActivityQuestion,
 			PollID:                   "01HEN2RKT1YTEZ80SA8HGP105F",
 		},
+		"local_account_1_status_7": {
+			ID:                       "01HH9KYNQPA416TNJ53NSATP40",
+			URI:                      "http://localhost:8080/users/the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40",
+			URL:                      "http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40",
+			Content:                  "<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n    &lt;div class=&#34;col-header&#34;&gt;\n        &lt;h2&gt;About&lt;/h2&gt;\n    &lt;/div&gt;            \n    &lt;div class=&#34;fields&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n        &lt;dl&gt;\n            &lt;div class=&#34;field&#34;&gt;\n                &lt;dt&gt;should you follow me?&lt;/dt&gt;\n                &lt;dd&gt;maybe!&lt;/dd&gt;\n            &lt;/div&gt;\n            &lt;div class=&#34;field&#34;&gt;\n                &lt;dt&gt;age&lt;/dt&gt;\n                &lt;dd&gt;120&lt;/dd&gt;\n            &lt;/div&gt;\n        &lt;/dl&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;bio&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n        &lt;p&gt;i post about things that concern me&lt;/p&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n        &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n        &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n        &lt;span&gt;8 posts.&lt;/span&gt;\n        &lt;span&gt;Followed by 1.&lt;/span&gt;\n        &lt;span&gt;Following 1.&lt;/span&gt;\n    &lt;/div&gt;\n    &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n        &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n        &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n        &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n        &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n    &lt;/div&gt;\n&lt;/section&gt;\n</code></pre><p>There, hope you liked that!</p>",
+			Text:                     "Here's a bunch of HTML, read it and weep, weep then!\n\n```html\n<section class=\"about-user\">\n <div class=\"col-header\">\n <h2>About</h2>\n </div> \n <div class=\"fields\">\n <h3 class=\"sr-only\">Fields</h3>\n <dl>\n <div class=\"field\">\n <dt>should you follow me?</dt>\n <dd>maybe!</dd>\n </div>\n <div class=\"field\">\n <dt>age</dt>\n <dd>120</dd>\n </div>… <h3 class=\"sr-only\">Stats</h3>\n <span>Joined in Jun, 2022.</span>\n <span>8 posts.</span>\n <span>Followed by 1.</span>\n <span>Following 1.</span>\n </div>\n <div class=\"accountstats\" aria-hidden=\"true\">\n <b>Joined</b><time datetime=\"2022-06-04T13:12:00.000Z\">Jun, 2022</time>\n <b>Posts</b><span>8</span>\n <b>Followed by</b><span>1</span>\n <b>Following</b><span>1</span>\n </div>\n</section>\n```\n\nThere, hope you liked that!",
+			CreatedAt:                TimeMustParse("2023-12-10T11:24:00+02:00"),
+			UpdatedAt:                TimeMustParse("2023-12-10T11:24:00+02:00"),
+			Local:                    util.Ptr(true),
+			AccountURI:               "http://localhost:8080/users/the_mighty_zork",
+			AccountID:                "01F8MH1H7YV1Z7D2C8K2730QBF",
+			InReplyToID:              "",
+			BoostOfID:                "",
+			ThreadID:                 "01HH9M3FVSF5J7120X9T6PG4GF",
+			ContentWarning:           "HTML in post",
+			Visibility:               gtsmodel.VisibilityPublic,
+			Sensitive:                util.Ptr(true),
+			Language:                 "en",
+			CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
+			Federated:                util.Ptr(true),
+			Boostable:                util.Ptr(true),
+			Replyable:                util.Ptr(true),
+			Likeable:                 util.Ptr(true),
+			ActivityStreamsType:      ap.ObjectNote,
+		},
 		"local_account_2_status_1": {
 			ID:                       "01F8MHBQCBTDKN6X5VHGMMN4MA",
 			URI:                      "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA",
@@ -2169,6 +2194,9 @@ func NewTestThreads() map[string]*gtsmodel.Thread {
 		"local_account_1_status_5": {
 			ID: "01HCWE1ERQSMMVWDD0BE491E2P",
 		},
+		"local_account_1_status_7": {
+			ID: "01HH9M3FVSF5J7120X9T6PG4GF",
+		},
 		"local_account_2_status_1": {
 			ID: "01HCWE2Q24FWCZE41AS77SDFRZ",
 		},
diff --git a/web/source/css/status.css b/web/source/css/status.css
index d2aa658f1..cbe8c04d2 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -155,6 +155,7 @@ main {
 		.content {
 			word-break: break-word;
 			line-height: 1.6rem;
+			width: 100%;
 
 			h1 {
 				margin: 0;