diff --git a/internal/api/activitypub/users/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go index 521af0ff0..cba1ef31d 100644 --- a/internal/api/activitypub/users/outboxget_test.go +++ b/internal/api/activitypub/users/outboxget_test.go @@ -82,7 +82,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() { "@context": "https://www.w3.org/ns/activitystreams", "first": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40", "id": "http://localhost:8080/users/the_mighty_zork/outbox", - "totalItems": 7, + "totalItems": 8, "type": "OrderedCollection" }`, dst.String()) @@ -161,7 +161,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { ], "partOf": "http://localhost:8080/users/the_mighty_zork/outbox", "prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01HH9KYNQPA416TNJ53NSATP40", - "totalItems": 7, + "totalItems": 8, "type": "OrderedCollectionPage" }`, dst.String()) @@ -224,7 +224,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { "id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40&max_id=01F8MHAMCHF6Y650WCRSCP4WMY", "orderedItems": [], "partOf": "http://localhost:8080/users/the_mighty_zork/outbox", - "totalItems": 7, + "totalItems": 8, "type": "OrderedCollectionPage" }`, dst.String()) diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go index eccda8b2e..cedc11916 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(7, apimodelAccount.StatusesCount) + suite.Equal(8, apimodelAccount.StatusesCount) suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy) suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language) suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note) diff --git a/internal/api/client/admin/accountsgetv2_test.go b/internal/api/client/admin/accountsgetv2_test.go index 85d58cce8..ca9573be5 100644 --- a/internal/api/client/admin/accountsgetv2_test.go +++ b/internal/api/client/admin/accountsgetv2_test.go @@ -240,8 +240,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "enable_rss": true, diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 605b056b9..5113e4c57 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -135,7 +135,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", @@ -256,7 +256,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", @@ -377,7 +377,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", @@ -549,7 +549,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", @@ -692,7 +692,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` @@ -850,7 +850,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "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 27e5f782d..ab4f46689 100644 --- a/internal/api/client/search/searchget_test.go +++ b/internal/api/client/search/searchget_test.go @@ -916,7 +916,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() { } suite.Len(searchResult.Accounts, 5) - suite.Len(searchResult.Statuses, 6) + suite.Len(searchResult.Statuses, 7) suite.Len(searchResult.Hashtags, 0) } @@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() { } suite.Len(searchResult.Accounts, 2) - suite.Len(searchResult.Statuses, 6) + suite.Len(searchResult.Statuses, 7) suite.Len(searchResult.Hashtags, 0) } @@ -1002,7 +1002,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() { } suite.Len(searchResult.Accounts, 0) - suite.Len(searchResult.Statuses, 6) + suite.Len(searchResult.Statuses, 7) suite.Len(searchResult.Hashtags, 0) } diff --git a/internal/api/client/statuses/statushistory_test.go b/internal/api/client/statuses/statushistory_test.go index a88abdb8f..0d9f6c979 100644 --- a/internal/api/client/statuses/statushistory_test.go +++ b/internal/api/client/statuses/statushistory_test.go @@ -114,8 +114,8 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "enable_rss": true, diff --git a/internal/api/client/statuses/statusmute_test.go b/internal/api/client/statuses/statusmute_test.go index 83effd0c2..01bea4e5c 100644 --- a/internal/api/client/statuses/statusmute_test.go +++ b/internal/api/client/statuses/statusmute_test.go @@ -132,8 +132,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "enable_rss": true, @@ -197,8 +197,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "enable_rss": true, diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index 7e81759f2..d0b0c81e5 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -90,12 +90,23 @@ type Attachment struct { // A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet. // See https://github.com/woltapp/blurhash Blurhash *string `json:"blurhash"` +} - // Additional fields not exposed via JSON - // (used only internally for templating etc). +// WebAttachment is like Attachment, but with +// additional fields not exposed via JSON; +// used only internally for templating etc. +// +// swagger:ignore +type WebAttachment struct { + *Attachment - // Parent status of this media is sensitive. - Sensitive bool `json:"-"` + // Parent status of this + // media is sensitive. + Sensitive bool + + // MIME type of + // the attachment. + MIMEType string } // MediaMeta models media metadata. diff --git a/internal/api/model/status.go b/internal/api/model/status.go index a9c668565..e469835bd 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -111,6 +111,10 @@ type Status struct { type WebStatus struct { *Status + // Web version of media + // attached to this status. + MediaAttachments []*WebAttachment `json:"media_attachments"` + // Template-ready language tag and // string, based on *status.Language. LanguageTag *language.Language diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index a9554e0d7..116ea19f0 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -46,7 +46,7 @@ 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, 7) + suite.Len(statuses, 8) } func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() { @@ -69,7 +69,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() { if err != nil { suite.FailNow(err.Error()) } - suite.Len(statuses, 1) + suite.Len(statuses, 2) // try to get the last page (should be empty) statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false) @@ -187,7 +187,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesExcludesSelfR func (suite *AccountTestSuite) TestGetAccountStatusesMediaOnly() { statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", true, false) suite.NoError(err) - suite.Len(statuses, 1) + suite.Len(statuses, 2) } func (suite *AccountTestSuite) TestGetAccountBy() { diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go index 6892291d2..f6647c1f5 100644 --- a/internal/db/bundb/basic_test.go +++ b/internal/db/bundb/basic_test.go @@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() { s := []*gtsmodel.Status{} err := suite.db.GetAll(context.Background(), &s) suite.NoError(err) - suite.Len(s, 23) + suite.Len(s, 24) } func (suite *BasicTestSuite) TestGetAllNotNull() { diff --git a/internal/db/bundb/instance_test.go b/internal/db/bundb/instance_test.go index 4b8ec9962..13fd7f61c 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(19, count) + suite.Equal(20, count) } func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() { diff --git a/internal/db/bundb/status_test.go b/internal/db/bundb/status_test.go index 0111dc6e7..b64177c32 100644 --- a/internal/db/bundb/status_test.go +++ b/internal/db/bundb/status_test.go @@ -169,12 +169,7 @@ func (suite *StatusTestSuite) TestGetStatusChildren() { targetStatus := suite.testStatuses["local_account_1_status_1"] children, err := suite.db.GetStatusChildren(context.Background(), targetStatus.ID) suite.NoError(err) - suite.Len(children, 2) - for _, c := range children { - suite.Equal(targetStatus.URI, c.InReplyToURI) - suite.Equal(targetStatus.AccountID, c.InReplyToAccountID) - suite.Equal(targetStatus.ID, c.InReplyToID) - } + suite.Len(children, 3) } func (suite *StatusTestSuite) TestDeleteStatus() { diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go index 0a014d321..b944bd9b4 100644 --- a/internal/db/bundb/timeline_test.go +++ b/internal/db/bundb/timeline_test.go @@ -155,7 +155,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimeline() { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 19) + suite.checkStatuses(s, id.Highest, id.Lowest, 20) } func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() { @@ -187,7 +187,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() { suite.FailNow(err.Error()) } - suite.checkStatuses(s, id.Highest, id.Lowest, 7) + suite.checkStatuses(s, id.Highest, id.Lowest, 8) } func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() { @@ -209,7 +209,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() { } suite.NotContains(s, futureStatus) - suite.checkStatuses(s, id.Highest, id.Lowest, 19) + suite.checkStatuses(s, id.Highest, id.Lowest, 20) } func (suite *TimelineTestSuite) TestGetHomeTimelineBackToFront() { @@ -240,8 +240,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() { } suite.checkStatuses(s, id.Highest, id.Lowest, 5) - suite.Equal("01HH9KYNQPA416TNJ53NSATP40", s[0].ID) - suite.Equal("01G20ZM733MGN8J344T4ZDDFY1", s[len(s)-1].ID) + suite.Equal("01J2M1HPFSS54S60Y0KYV23KJE", s[0].ID) + suite.Equal("01G36SF3V6Y6V5BF9P4R7PQG7G", s[len(s)-1].ID) } func (suite *TimelineTestSuite) TestGetListTimelineNoParams() { diff --git a/internal/media/ffmpeg.go b/internal/media/ffmpeg.go index eb94849f0..b97c8413f 100644 --- a/internal/media/ffmpeg.go +++ b/internal/media/ffmpeg.go @@ -18,6 +18,7 @@ package media import ( + "cmp" "context" "encoding/json" "errors" @@ -198,6 +199,30 @@ func (res *ffprobeResult) ImageMeta() (width int, height int, err error) { return } +// EmbeddedImageMeta extracts embedded image metadata contained within ffprobe'd media result +// streams, should be used for pulling album image (can be animated image) from audio files. +func (res *ffprobeResult) EmbeddedImageMeta() (width int, height int, framerate float32, err error) { + for _, stream := range res.Streams { + if stream.Width > width { + width = stream.Width + } + if stream.Height > height { + height = stream.Height + } + if fr := stream.GetFrameRate(); fr > 0 { + if framerate == 0 || fr < framerate { + framerate = fr + } + } + } + // Need width + height but + // no framerate is fine. + if width == 0 || height == 0 { + err = errors.New("invalid image stream(s)") + } + return +} + // VideoMeta extracts video metadata contained within ffprobe'd media result streams. func (res *ffprobeResult) VideoMeta() (width, height int, framerate float32, err error) { for _, stream := range res.Streams { @@ -222,6 +247,7 @@ func (res *ffprobeResult) VideoMeta() (width, height int, framerate float32, err type ffprobeStream struct { CodecName string `json:"codec_name"` AvgFrameRate string `json:"avg_frame_rate"` + RFrameRate string `json:"r_frame_rate"` Width int `json:"width"` Height int `json:"height"` // + unused fields. @@ -229,7 +255,7 @@ type ffprobeStream struct { // GetFrameRate calculates float32 framerate value from stream json string. func (str *ffprobeStream) GetFrameRate() float32 { - if str.AvgFrameRate != "" { + numDen := func(strFR string) (float32, float32) { var ( // numerator num float32 @@ -239,7 +265,7 @@ func (str *ffprobeStream) GetFrameRate() float32 { ) // Check for a provided inequality, i.e. numerator / denominator. - if p := strings.SplitN(str.AvgFrameRate, "/", 2); len(p) == 2 { + if p := strings.SplitN(strFR, "/", 2); len(p) == 2 { n, _ := strconv.ParseFloat(p[0], 32) d, _ := strconv.ParseFloat(p[1], 32) num, den = float32(n), float32(d) @@ -248,8 +274,26 @@ func (str *ffprobeStream) GetFrameRate() float32 { num = float32(n) } - return num / den + return num, den } + + var num, den float32 + if str.AvgFrameRate != "" { + // Check if we have avg_frame_rate. + num, den = numDen(str.AvgFrameRate) + } + + if num == 0 && str.RFrameRate != "" { + // Check if we have r_frame_rate. + num, den = numDen(str.RFrameRate) + } + + if num != 0 { + // Found it. + // Avoid divide by zero. + return num / cmp.Or(den, 1) + } + return 0 } diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go index 43e153a4d..8ee242749 100644 --- a/internal/media/processingmedia.go +++ b/internal/media/processingmedia.go @@ -299,8 +299,14 @@ func (p *ProcessingMedia) store(ctx context.Context) error { // Extract image metadata from streams (if any), // this will only exist for embedded album art. - width, height, _ := result.ImageMeta() + width, height, framerate, _ := result.EmbeddedImageMeta() if width > 0 && height > 0 { + // Unlikely to need these but masto API includes them. + p.media.FileMeta.Original.Width = width + p.media.FileMeta.Original.Height = height + if framerate != 0 { + p.media.FileMeta.Original.Framerate = &framerate + } // Determine thumbnail dimensions to use. thumbWidth, thumbHeight := thumbSize(width, height) diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go index 226e994cc..40eeaa328 100644 --- a/internal/processing/account/rss_test.go +++ b/internal/processing/account/rss_test.go @@ -41,11 +41,11 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork") suite.NoError(err) - suite.EqualValues(1702200240, lastModified.Unix()) + suite.EqualValues(1704878640, lastModified.Unix()) feed, err := getFeed() suite.NoError(err) - suite.Equal("\n \n Posts from @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n Posts from @the_mighty_zork@localhost:8080\n Sun, 10 Dec 2023 09:24:00 +0000\n Sun, 10 Dec 2023 09:24:00 +0000\n \n http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg\n Avatar for @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n \n \n HTML in post\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n @the_mighty_zork@localhost:8080 made a new post: "Here's a bunch of HTML, read it and weep, weep then! ```html <section class="about-user"> <div class="col-header"> <h2>About</h2> </div> <div class="fields"> <h3 class="sr-only">Fields</h3> <dl> ...\n Here's a bunch of HTML, read it and weep, weep then!

<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>\n        </dl>\n    </div>\n    <div class="bio">\n        <h3 class="sr-only">Bio</h3>\n        <p>i post about things that concern me</p>\n    </div>\n    <div class="sr-only" role="group">\n        <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

There, hope you liked that!

]]>
\n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n Sun, 10 Dec 2023 09:24:00 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n
\n \n introduction post\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n @the_mighty_zork@localhost:8080 made a new post: "hello everyone!"\n \n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n Wed, 20 Oct 2021 10:40:37 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n \n
\n
", feed) + suite.Equal("\n \n Posts from @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n Posts from @the_mighty_zork@localhost:8080\n Wed, 10 Jan 2024 09:24:00 +0000\n Wed, 10 Jan 2024 09:24:00 +0000\n \n http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg\n Avatar for @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n \n \n HTML in post\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n @the_mighty_zork@localhost:8080 made a new post: "Here's a bunch of HTML, read it and weep, weep then! ```html <section class="about-user"> <div class="col-header"> <h2>About</h2> </div> <div class="fields"> <h3 class="sr-only">Fields</h3> <dl> ...\n Here's a bunch of HTML, read it and weep, weep then!

<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>\n        </dl>\n    </div>\n    <div class="bio">\n        <h3 class="sr-only">Bio</h3>\n        <p>i post about things that concern me</p>\n    </div>\n    <div class="sr-only" role="group">\n        <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

There, hope you liked that!

]]>
\n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n Sun, 10 Dec 2023 09:24:00 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n
\n \n introduction post\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n @the_mighty_zork@localhost:8080 made a new post: "hello everyone!"\n \n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n Wed, 20 Oct 2021 10:40:37 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n \n
\n
", feed) } func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() { diff --git a/internal/timeline/get_test.go b/internal/timeline/get_test.go index 499a9f35d..6b01ca812 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, 19) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 20) } 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, 19) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 20) } 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, 7) + suite.checkStatuses(statuses, id.Highest, id.Lowest, 8) for _, s := range statuses { if s.GetAccountID() != testAccount.ID { diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go index 047166e9e..2cc3ef71a 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(18, pruned) + suite.Equal(19, 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(18, pruned) + suite.Equal(19, 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(23, pruned) + suite.Equal(24, 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(23, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) + suite.Equal(24, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID)) } func TestPruneTestSuite(t *testing.T) { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 9d99205f6..d24ae3ea5 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -624,7 +624,7 @@ func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.M Y: a.FileMeta.Focus.Y, } - case gtsmodel.FileTypeVideo: + case gtsmodel.FileTypeVideo, gtsmodel.FileTypeAudio: if i := a.FileMeta.Original.Duration; i != nil { apiAttachment.Meta.Original.Duration = *i } @@ -1062,14 +1062,36 @@ func (c *Converter) StatusToWebStatus( webStatus.PollOptions = PollOptions } + // Mark local. + webStatus.Local = *s.Local + // Set additional templating // variables on media attachments. - for _, a := range webStatus.MediaAttachments { - a.Sensitive = webStatus.Sensitive + + // Get gtsmodel attachments + // into a convenient map. + ogAttachments := make( + map[string]*gtsmodel.MediaAttachment, + len(s.Attachments), + ) + for _, a := range s.Attachments { + ogAttachments[a.ID] = a } - // Mark this as a local status. - webStatus.Local = *s.Local + // Convert each API attachment + // into a web attachment. + webStatus.MediaAttachments = make( + []*apimodel.WebAttachment, + len(apiStatus.MediaAttachments), + ) + for i, apiAttachment := range apiStatus.MediaAttachments { + ogAttachment := ogAttachments[apiAttachment.ID] + webStatus.MediaAttachments[i] = &apimodel.WebAttachment{ + Attachment: apiAttachment, + Sensitive: apiStatus.Sensitive, + MIMEType: ogAttachment.File.ContentType, + } + } return webStatus, nil } diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 7bbca8ae7..6429df4fa 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -63,8 +63,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "enable_rss": true, @@ -116,8 +116,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved() "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "source": { @@ -209,8 +209,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [ { "shortcode": "rainbow", @@ -259,8 +259,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [ { "shortcode": "rainbow", @@ -305,8 +305,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, - "statuses_count": 7, - "last_status_at": "2023-12-10T09:24:00.000Z", + "statuses_count": 8, + "last_status_at": "2024-01-10T09:24:00.000Z", "emojis": [], "fields": [], "source": { @@ -943,6 +943,18 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() { "emojis": [], "fields": [] }, + "mentions": [ + { + "id": "01F8MH17FWEB39HZJ76B6VXSKF", + "username": "admin", + "url": "http://localhost:8080/@admin", + "acct": "admin" + } + ], + "tags": [], + "emojis": [], + "card": null, + "poll": null, "media_attachments": [ { "id": "01HE7Y3C432WRSNS10EZM86SA5", @@ -971,7 +983,9 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() { } }, "description": "Photograph of a sloth, Public Domain.", - "blurhash": "LNEC{|w}0K9GsEtPM|j[NFbHoeof" + "blurhash": "LNEC{|w}0K9GsEtPM|j[NFbHoeof", + "Sensitive": true, + "MIMEType": "image/jpg" }, { "id": "01HE7ZFX9GKA5ZZVD4FACABSS9", @@ -983,7 +997,9 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() { "preview_remote_url": null, "meta": null, "description": "SVG line art of a sloth, public domain", - "blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of" + "blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of", + "Sensitive": true, + "MIMEType": "image/svg" }, { "id": "01HE88YG74PVAB81PX2XA9F3FG", @@ -995,21 +1011,11 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() { "preview_remote_url": null, "meta": null, "description": "Jolly salsa song, public domain.", - "blurhash": null + "blurhash": null, + "Sensitive": true, + "MIMEType": "audio/mpeg" } ], - "mentions": [ - { - "id": "01F8MH17FWEB39HZJ76B6VXSKF", - "username": "admin", - "url": "http://localhost:8080/@admin", - "acct": "admin" - } - ], - "tags": [], - "emojis": [], - "card": null, - "poll": null, "LanguageTag": "en", "PollOptions": null, "Local": false, @@ -1249,7 +1255,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { }, "stats": { "domain_count": 2, - "status_count": 19, + "status_count": 20, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", diff --git a/testrig/media/ghosts-original.mp3 b/testrig/media/ghosts-original.mp3 new file mode 100644 index 000000000..5e5cc8d81 Binary files /dev/null and b/testrig/media/ghosts-original.mp3 differ diff --git a/testrig/media/ghosts-small.jpg b/testrig/media/ghosts-small.jpg new file mode 100644 index 000000000..bd01b91a0 Binary files /dev/null and b/testrig/media/ghosts-small.jpg differ diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 5f41ed190..efd4785a5 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -989,6 +989,53 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { Header: util.Ptr(true), Cached: util.Ptr(true), }, + "local_account_1_status_8_attachment_1": { + ID: "01J2M20K6K9XQC4WSB961YJHV6", + StatusID: "01J2M1HPFSS54S60Y0KYV23KJE", + URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3", + RemoteURL: "", + CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), + UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), + Type: gtsmodel.FileTypeAudio, + FileMeta: gtsmodel.FileMeta{ + Original: gtsmodel.Original{ + Width: 500, + Height: 500, + Size: 0, + Aspect: 0, + }, + Small: gtsmodel.Small{ + Width: 500, + Height: 500, + Size: 250000, + Aspect: 1, + }, + Focus: gtsmodel.Focus{ + X: 0, + Y: 0, + }, + }, + AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + Description: "This is a track from Nine Inch Nail's \"Ghosts I-V\" album. This is the third track from \"Ghosts II\".", + ScheduledStatusID: "", + Blurhash: "LeDvfpayIUof01j[xuayxuayaxj[", + Processing: 2, + File: gtsmodel.File{ + Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3", + ContentType: "audio/mpeg", + FileSize: 7483917, + }, + Thumbnail: gtsmodel.Thumbnail{ + Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01J2M20K6K9XQC4WSB961YJHV6.jpg", + ContentType: "image/jpeg", + FileSize: 6132, + URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01J2M20K6K9XQC4WSB961YJHV6.jpg", + RemoteURL: "", + }, + Avatar: util.Ptr(false), + Header: util.Ptr(false), + Cached: util.Ptr(true), + }, "remote_account_1_status_1_attachment_1": { ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", StatusID: "01FVW7JHQFSFK166WWKR8CBA6M", @@ -1347,6 +1394,10 @@ func newTestStoredAttachments() map[string]filenames { Original: "team-fortress-original.jpg", Small: "team-fortress-small.jpg", }, + "local_account_1_status_8_attachment_1": { + Original: "ghosts-original.mp3", + Small: "ghosts-small.jpg", + }, "remote_account_1_status_1_attachment_1": { Original: "thoughtsofdog-original.jpg", Small: "thoughtsofdog-small.jpg", @@ -1644,6 +1695,31 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, + "local_account_1_status_8": { + ID: "01J2M1HPFSS54S60Y0KYV23KJE", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/01J2M1HPFSS54S60Y0KYV23KJE", + URL: "http://localhost:8080/@the_mighty_zork/statuses/01J2M1HPFSS54S60Y0KYV23KJE", + Content: "

Thanks! Here's a NIN track

", + Text: "Thanks! Here's a NIN track", + AttachmentIDs: []string{"01J2M20K6K9XQC4WSB961YJHV6"}, + CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), + UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"), + Local: util.Ptr(true), + AccountURI: "http://localhost:8080/users/the_mighty_zork", + AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + InReplyToID: "01FF25D5Q0DH7CHD57CTRS6WK0", + InReplyToAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", + InReplyToURI: "http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0", + BoostOfID: "", + ThreadID: "01HCWDKKBWECZJQ93E262N36VN", + ContentWarning: "", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + }, "local_account_2_status_1": { ID: "01F8MHBQCBTDKN6X5VHGMMN4MA", URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA", @@ -2208,6 +2284,10 @@ func NewTestThreadToStatus() []*gtsmodel.ThreadToStatus { ThreadID: "01HCWDKKBWECZJQ93E262N36VN", StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5", }, + { + ThreadID: "01HCWDKKBWECZJQ93E262N36VN", + StatusID: "01J2M1HPFSS54S60Y0KYV23KJE", + }, { ThreadID: "01HCWE71MGRRDSHBKXFD5DDSWR", StatusID: "01FN3VJGFH10KR7S2PB0GFJZYG", diff --git a/web/source/css/status.css b/web/source/css/status.css index 249033e02..5c7400654 100644 --- a/web/source/css/status.css +++ b/web/source/css/status.css @@ -336,6 +336,10 @@ main { grid-area: sensitive; align-self: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + .button { cursor: pointer; align-self: center; @@ -401,10 +405,18 @@ main { grid-column: span 2; } - &.odd .media-wrapper:first-child, &.double .media-wrapper { + &.odd .media-wrapper:first-child, + &.double .media-wrapper { grid-row: span 2; } + @media screen and (max-width: 42rem) { + .media-wrapper { + grid-column: span 2; + grid-row: span 2; + } + } + img { width: 100%; height: 100%; diff --git a/web/template/status_attachments.tmpl b/web/template/status_attachments.tmpl index b257f2211..5df3d1c5c 100644 --- a/web/template/status_attachments.tmpl +++ b/web/template/status_attachments.tmpl @@ -36,16 +36,42 @@ {{- end }} {{- define "videoPreview" }} - + width="{{- .Meta.Small.Width -}}" + height="{{- .Meta.Small.Height -}}" +/> +{{- end }} + +{{- define "audioPreview" }} +{{- if and .PreviewURL .Meta.Small.Width }} +{{- .Description -}} +{{- else }} +{{- .Description -}} +{{- end }} {{- end }} {{- /* Produces something like "1 attachment", "2 attachments", etc */ -}} @@ -77,21 +103,47 @@ media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ {{- include "videoPreview" $media | indent 4 }} {{- else if eq .Type "image" }} {{- include "imagePreview" $media | indent 4 }} + {{- else if eq .Type "audio" }} + {{- include "audioPreview" $media | indent 4 }} {{- end }} {{- if eq .Type "video" }} + {{- else if eq .Type "audio" }} + {{- else if eq .Type "image" }}