summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2025-03-26 16:59:39 +0100
committerLibravatar GitHub <noreply@github.com>2025-03-26 15:59:39 +0000
commitb6e481d63eec15191f2717957682c13ee8a68308 (patch)
tree03cb9fc8bcb5f9eefddee754ad64b9de10c44c39
parent[chore] bumps our spf13/viper version (#3943) (diff)
downloadgotosocial-b6e481d63eec15191f2717957682c13ee8a68308.tar.xz
[feature] Allow user to choose "gallery" style layout for web view of profile (#3917)
* [feature] Allow user to choose "gallery" style web layout * find a bug and squish it up and all day long you'll have good luck * just a sec * [performance] reindex public timeline + tinker with query a bit * fiddling * should be good now * last bit of finagling, i'm done now i prommy * panic normally
-rw-r--r--cmd/process-media/main.go111
-rw-r--r--docs/api/swagger.yaml14
-rw-r--r--docs/overrides/public/user-settings-layout-gallery.pngbin0 -> 969869 bytes
-rw-r--r--docs/overrides/public/user-settings-layout-microblog.pngbin0 -> 811730 bytes
-rw-r--r--docs/user_guide/settings.md24
-rw-r--r--internal/api/client/accounts/accountupdate.go11
-rw-r--r--internal/api/client/accounts/search_test.go8
-rw-r--r--internal/api/client/admin/accountsgetv2_test.go127
-rw-r--r--internal/api/client/instance/instancepatch_test.go24
-rw-r--r--internal/api/client/search/searchget_test.go4
-rw-r--r--internal/api/model/account.go7
-rw-r--r--internal/api/model/attachment.go4
-rw-r--r--internal/api/model/source.go4
-rw-r--r--internal/db/account.go2
-rw-r--r--internal/db/bundb/account.go67
-rw-r--r--internal/db/bundb/account_test.go10
-rw-r--r--internal/db/bundb/basic_test.go2
-rw-r--r--internal/db/bundb/instance_test.go4
-rw-r--r--internal/db/bundb/migrations/20250314120945_add_gallery_web_layout.go85
-rw-r--r--internal/gtsmodel/accountsettings.go43
-rw-r--r--internal/processing/account/rss.go16
-rw-r--r--internal/processing/account/statuses.go15
-rw-r--r--internal/processing/account/update.go12
-rw-r--r--internal/router/template.go23
-rw-r--r--internal/timeline/prune_test.go8
-rw-r--r--internal/typeutils/internaltoas_test.go14
-rw-r--r--internal/typeutils/internaltofrontend.go18
-rw-r--r--internal/typeutils/internaltofrontend_test.go15
-rw-r--r--internal/typeutils/wrap_test.go2
-rw-r--r--internal/web/profile.go260
-rw-r--r--internal/web/web.go19
-rw-r--r--testrig/media/bunny-original.webmbin0 -> 2165608 bytes
-rw-r--r--testrig/media/bunny-small.webpbin0 -> 324 bytes
-rw-r--r--testrig/media/buscemi-original.jpegbin0 -> 42899 bytes
-rw-r--r--testrig/media/buscemi-small.jpegbin0 -> 17341 bytes
-rw-r--r--testrig/media/butt-original.gifbin0 -> 636 bytes
-rw-r--r--testrig/media/butt-small.webpbin0 -> 406 bytes
-rw-r--r--testrig/media/computerbye-original.gifbin0 -> 1911633 bytes
-rw-r--r--testrig/media/computerbye-small.webpbin0 -> 10056 bytes
-rw-r--r--testrig/media/diarrhea-original.gifbin0 -> 779296 bytes
-rw-r--r--testrig/media/diarrhea-small.webpbin0 -> 10238 bytes
-rw-r--r--testrig/media/dollar-original.jpegbin0 -> 291230 bytes
-rw-r--r--testrig/media/dollar-small.jpegbin0 -> 24486 bytes
-rw-r--r--testrig/media/dollar2-original.pngbin0 -> 405238 bytes
-rw-r--r--testrig/media/dollar2-small.webpbin0 -> 26478 bytes
-rw-r--r--testrig/media/ffmpreg-original.jpegbin0 -> 137328 bytes
-rw-r--r--testrig/media/ffmpreg-small.jpegbin0 -> 19775 bytes
-rw-r--r--testrig/media/marge-original.pngbin0 -> 380878 bytes
-rw-r--r--testrig/media/marge-small.webpbin0 -> 51882 bytes
-rw-r--r--testrig/media/notabug-original.jpegbin0 -> 106636 bytes
-rw-r--r--testrig/media/notabug-small.jpegbin0 -> 27483 bytes
-rw-r--r--testrig/media/sickos-original.jpegbin0 -> 513277 bytes
-rw-r--r--testrig/media/sickos-small.jpegbin0 -> 23550 bytes
-rw-r--r--testrig/media/sloth-gear-original.webpbin0 -> 366592 bytes
-rw-r--r--testrig/media/sloth-gear-small.jpegbin0 -> 15461 bytes
-rw-r--r--testrig/media/you-posted-original.webpbin0 -> 80917 bytes
-rw-r--r--testrig/media/you-posted-small.webpbin0 -> 5344 bytes
-rw-r--r--testrig/testmodels.go819
-rw-r--r--web/assets/themes/brutalist-dark.css4
-rw-r--r--web/assets/themes/brutalist.css4
-rw-r--r--web/source/css/_media-wrapper.css207
-rw-r--r--web/source/css/_prism.css (renamed from web/source/css/prism.css)0
-rw-r--r--web/source/css/_profile-header.css343
-rw-r--r--web/source/css/_status-media.css44
-rw-r--r--web/source/css/base.css2
-rw-r--r--web/source/css/profile-gallery.css108
-rw-r--r--web/source/css/profile.css318
-rw-r--r--web/source/css/status.css210
-rw-r--r--web/source/css/tag.css8
-rw-r--r--web/source/frontend/index.js65
-rw-r--r--web/source/package.json2
-rw-r--r--web/source/settings/lib/types/account.ts2
-rw-r--r--web/source/settings/views/user/profile.tsx47
-rw-r--r--web/source/yarn.lock8
-rw-r--r--web/template/profile-gallery.tmpl87
-rw-r--r--web/template/profile.tmpl197
-rw-r--r--web/template/profile_about_user.tmpl56
-rw-r--r--web/template/profile_header.tmpl185
-rw-r--r--web/template/status.tmpl20
-rw-r--r--web/template/status_attachment.tmpl179
-rw-r--r--web/template/status_attachments.tmpl195
-rw-r--r--web/template/status_info.tmpl29
82 files changed, 2921 insertions, 1171 deletions
diff --git a/cmd/process-media/main.go b/cmd/process-media/main.go
index 9fc4983ec..27d1de201 100644
--- a/cmd/process-media/main.go
+++ b/cmd/process-media/main.go
@@ -19,14 +19,18 @@ package main
import (
"context"
+ "fmt"
"io"
"os"
"os/signal"
+ "strings"
"syscall"
+ "time"
"codeberg.org/gruf/go-storage/memory"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/media/ffmpeg"
@@ -39,7 +43,7 @@ func main() {
ctx, cncl := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
defer cncl()
- log.SetLevel(log.INFO)
+ log.SetLevel(log.ERROR)
if len(os.Args) != 4 {
log.Panic(ctx, "Usage: go run ./cmd/process-media <input-file> <output-processed> <output-thumbnail>")
@@ -63,7 +67,8 @@ func main() {
var err error
- config.SetHost("example.com")
+ config.SetProtocol("http")
+ config.SetHost("localhost:8080")
config.SetStorageBackend("disk")
config.SetStorageLocalBasePath("/tmp/gotosocial")
config.SetDbType("sqlite")
@@ -109,6 +114,7 @@ func main() {
log.Panic(ctx, err)
}
+ outputCopyable(media)
copyFile(ctx, &st, media.File.Path, os.Args[2])
copyFile(ctx, &st, media.Thumbnail.Path, os.Args[3])
}
@@ -136,3 +142,104 @@ func copyFile(ctx context.Context, st *storage.Driver, key string, path string)
log.Panic(ctx, err)
}
}
+
+func outputCopyable(media *gtsmodel.MediaAttachment) {
+ var (
+ now = time.Now()
+ nowStr = now.Format(time.RFC3339)
+ mediaType string
+ fileMetaExtra string
+ )
+
+ switch media.Type {
+ case gtsmodel.FileTypeImage:
+ mediaType = "gtsmodel.FileTypeImage"
+ case gtsmodel.FileTypeVideo:
+ mediaType = "gtsmodel.FileTypeVideo"
+ case gtsmodel.FileTypeGifv:
+ mediaType = "gtsmodel.FileTypeGifv"
+ case gtsmodel.FileTypeAudio:
+ mediaType = "gtsmodel.FileTypeAudio"
+ case gtsmodel.FileTypeUnknown:
+ mediaType = "gtsmodel.FileTypeUnknown"
+ }
+
+ if media.FileMeta.Original.Duration != nil {
+ fileMetaExtra += fmt.Sprintf("\n\t\t\tDuration: util.Ptr[float32](%f),", *media.FileMeta.Original.Duration)
+ }
+ if media.FileMeta.Original.Framerate != nil {
+ fileMetaExtra += fmt.Sprintf("\n\t\t\tFramerate: util.Ptr[float32](%f),", *media.FileMeta.Original.Framerate)
+ }
+ if media.FileMeta.Original.Bitrate != nil {
+ fileMetaExtra += fmt.Sprintf("\n\t\t\tBitrate: util.Ptr[uint64](%d),", *media.FileMeta.Original.Bitrate)
+ }
+
+ fmt.Printf(`{
+ ID: "%s",
+ StatusID: "STATUS_ID_GOES_HERE",
+ URL: "%s",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("%s"),
+ Type: %s,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: %d,
+ Height: %d,
+ Size: %d,
+ Aspect: %f,%s
+ },
+ Small: gtsmodel.Small{
+ Width: %d,
+ Height: %d,
+ Size: %d,
+ Aspect: %f,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "ACCOUNT_ID_GOES_HERE",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "%s",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "%s",
+ ContentType: "%s",
+ FileSize: %d,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "%s",
+ ContentType: "%s",
+ FileSize: %d,
+ URL: "%s",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+}`+"\n",
+ media.ID,
+ strings.ReplaceAll(media.URL, media.AccountID, "ACCOUNT_ID_GOES_HERE"),
+ nowStr,
+ mediaType,
+ media.FileMeta.Original.Width,
+ media.FileMeta.Original.Height,
+ media.FileMeta.Original.Size,
+ media.FileMeta.Original.Aspect,
+ fileMetaExtra,
+ media.FileMeta.Small.Width,
+ media.FileMeta.Small.Height,
+ media.FileMeta.Small.Size,
+ media.FileMeta.Small.Aspect,
+ media.Blurhash,
+ strings.ReplaceAll(media.File.Path, media.AccountID, "ACCOUNT_ID_GOES_HERE"),
+ media.File.ContentType,
+ media.File.FileSize,
+ strings.ReplaceAll(media.Thumbnail.Path, media.AccountID, "ACCOUNT_ID_GOES_HERE"),
+ media.Thumbnail.ContentType,
+ media.Thumbnail.FileSize,
+ strings.ReplaceAll(media.Thumbnail.URL, media.AccountID, "ACCOUNT_ID_GOES_HERE"),
+ )
+}
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index ce415784a..1a96b0f65 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -179,6 +179,13 @@ definitions:
description: The default posting content type for new statuses.
type: string
x-go-name: StatusContentType
+ web_layout:
+ description: |-
+ Layout to use for the web view of the account.
+ "microblog": default, classic microblog layout.
+ "gallery": gallery layout with media only.
+ type: string
+ x-go-name: WebLayout
web_visibility:
description: |-
Visibility level(s) of posts to show for this account via the web api.
@@ -4902,6 +4909,13 @@ paths:
in: formData
name: web_visibility
type: string
+ - description: |-
+ Layout to use for the web view of the account.
+ "microblog": default, classic microblog layout.
+ "gallery": gallery layout with media only.
+ in: formData
+ name: web_layout
+ type: string
- description: Name of 1st profile field to be added to this account's profile. (The index may be any string; add more indexes to send more fields.)
in: formData
name: fields_attributes[0][name]
diff --git a/docs/overrides/public/user-settings-layout-gallery.png b/docs/overrides/public/user-settings-layout-gallery.png
new file mode 100644
index 000000000..75a57f8e5
--- /dev/null
+++ b/docs/overrides/public/user-settings-layout-gallery.png
Binary files differ
diff --git a/docs/overrides/public/user-settings-layout-microblog.png b/docs/overrides/public/user-settings-layout-microblog.png
new file mode 100644
index 000000000..a573e327b
--- /dev/null
+++ b/docs/overrides/public/user-settings-layout-microblog.png
Binary files differ
diff --git a/docs/user_guide/settings.md b/docs/user_guide/settings.md
index 590b04b42..3d7bd811c 100644
--- a/docs/user_guide/settings.md
+++ b/docs/user_guide/settings.md
@@ -31,6 +31,30 @@ To choose a theme, just select it from the profile settings page, and click/tap
!!! tip "Adding more themes"
Instance admins can add more themes by dropping css files into the `web/assets/themes` folder. See the [themes](../admin/themes.md) part of the admin docs for more information.
+### Select Layout
+
+GoToSocial lets you choose from two different layouts for the web view of your profile.
+
+The setting does not affect how the API behaves or how client applications look or work, it's purely a cosmetic change for the web view.
+
+In both cases, only top-level posts (or media from top-level posts) is shown, not replies or boosts, and the [Visibility Level of Posts to Show on Your Profile](#visibility-level-of-posts-to-show-on-your-profile) setting is respected.
+
+#### Microblog
+
+The GtS classic microblog layout. Your profile is split into two columns with your bio and recent/pinned posts.
+
+This is a good choice if you primarily post text, or a mixture of text and media.
+
+![Microblog layout](../public/user-settings-layout-microblog.png)
+
+#### Gallery
+
+'Gram-style layout. Posts are not shown directly on your profile. Instead, your recent/pinned media is shown in a gallery grid view. Posts (with their replies) can still be accessed via link.
+
+This is a good choice if you primarily post media.
+
+![Gallery layout](../public/user-settings-layout-gallery.png)
+
### Basic Information
#### Display Name
diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go
index 617031d79..50e6632f4 100644
--- a/internal/api/client/accounts/accountupdate.go
+++ b/internal/api/client/accounts/accountupdate.go
@@ -153,6 +153,14 @@ import (
// "none": show no posts on the web, not even Public ones.
// type: string
// -
+// name: web_layout
+// in: formData
+// description: |-
+// Layout to use for the web view of the account.
+// "microblog": default, classic microblog layout.
+// "gallery": gallery layout with media only.
+// type: string
+// -
// name: fields_attributes[0][name]
// in: formData
// description: Name of 1st profile field to be added to this account's profile.
@@ -351,7 +359,8 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
form.CustomCSS == nil &&
form.EnableRSS == nil &&
form.HideCollections == nil &&
- form.WebVisibility == nil) {
+ form.WebVisibility == nil &&
+ form.WebLayout == nil) {
return nil, errors.New("empty form submitted")
}
diff --git a/internal/api/client/accounts/search_test.go b/internal/api/client/accounts/search_test.go
index 119900331..f5216d5b9 100644
--- a/internal/api/client/accounts/search_test.go
+++ b/internal/api/client/accounts/search_test.go
@@ -369,16 +369,16 @@ func (suite *AccountSearchTestSuite) TestSearchAFollowing() {
suite.FailNow(err.Error())
}
- if l := len(accounts); l != 5 {
- suite.FailNow("", "expected length %d got %d", 5, l)
+ if l := len(accounts); l != 6 {
+ suite.FailNow("", "expected length %d got %d", 6, l)
}
- usernames := make([]string, 0, 5)
+ usernames := make([]string, 0, 6)
for _, account := range accounts {
usernames = append(usernames, account.Username)
}
- suite.EqualValues([]string{"her_fuckin_maj", "foss_satan", "1happyturtle", "the_mighty_zork", "admin"}, usernames)
+ suite.EqualValues([]string{"her_fuckin_maj", "media_mogul", "foss_satan", "1happyturtle", "the_mighty_zork", "admin"}, usernames)
}
func (suite *AccountSearchTestSuite) TestSearchANotFollowing() {
diff --git a/internal/api/client/admin/accountsgetv2_test.go b/internal/api/client/admin/accountsgetv2_test.go
index 0e3eb95e1..339c97431 100644
--- a/internal/api/client/admin/accountsgetv2_test.go
+++ b/internal/api/client/admin/accountsgetv2_test.go
@@ -223,6 +223,69 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
}
},
{
+ "id": "01JPCMD83Y4WR901094YES3QC5",
+ "username": "media_mogul",
+ "domain": null,
+ "created_at": "2025-03-15T11:08:00.000Z",
+ "email": "media.mogul@example.org",
+ "ip": null,
+ "ips": [],
+ "locale": "en",
+ "invite_request": null,
+ "role": {
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
+ },
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01JPCMD83Y4WR901094YES3QC5",
+ "username": "media_mogul",
+ "acct": "media_mogul",
+ "display_name": "",
+ "locked": false,
+ "discoverable": false,
+ "bot": false,
+ "created_at": "2025-03-15T11:08:00.000Z",
+ "note": "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
+ "url": "http://localhost:8080/@media_mogul",
+ "avatar": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ "avatar_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ "avatar_description": "DESCRIPTION_GOES_HERE",
+ "avatar_media_id": "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
+ "header": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
+ "header_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
+ "header_description": "DESCRIPTION_GOES_HERE",
+ "header_media_id": "01JPHRB7F2RXPTEQFRYC85EPD9",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 2,
+ "last_status_at": "2025-03-15",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "I'm going to post a lot of",
+ "value": "media!",
+ "verified_at": null
+ },
+ {
+ "name": "and there's nothing",
+ "value": "you can do about it",
+ "verified_at": null
+ }
+ ],
+ "enable_rss": true,
+ "group": false
+ },
+ "created_by_application_id": "01HT5P2YHDMPAAD500NDAY8JW1"
+ },
+ {
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"domain": null,
@@ -547,18 +610,18 @@ func (suite *AccountsGetTestSuite) TestAccountsMinID() {
}
link := recorder.Header().Get("Link")
- suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=1&max_id=%2F%40localhost%3A8080>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=1&min_id=%2F%40localhost%3A8080>; rel="prev"`, link)
+ suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=1&max_id=%2F%40media_mogul>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=1&min_id=%2F%40media_mogul>; rel="prev"`, link)
suite.Equal(`[
{
- "id": "01AY6P665V14JJR0AFVRT7311Y",
- "username": "localhost:8080",
+ "id": "01JPCMD83Y4WR901094YES3QC5",
+ "username": "media_mogul",
"domain": null,
- "created_at": "2020-05-17T13:10:59.000Z",
- "email": "",
+ "created_at": "2025-03-15T11:08:00.000Z",
+ "email": "media.mogul@example.org",
"ip": null,
"ips": [],
- "locale": "",
+ "locale": "en",
"invite_request": null,
"role": {
"id": "user",
@@ -567,35 +630,51 @@ func (suite *AccountsGetTestSuite) TestAccountsMinID() {
"permissions": "0",
"highlighted": false
},
- "confirmed": false,
- "approved": false,
+ "confirmed": true,
+ "approved": true,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
- "id": "01AY6P665V14JJR0AFVRT7311Y",
- "username": "localhost:8080",
- "acct": "localhost:8080",
+ "id": "01JPCMD83Y4WR901094YES3QC5",
+ "username": "media_mogul",
+ "acct": "media_mogul",
"display_name": "",
"locked": false,
- "discoverable": true,
+ "discoverable": false,
"bot": false,
- "created_at": "2020-05-17T13:10:59.000Z",
- "note": "",
- "url": "http://localhost:8080/@localhost:8080",
- "avatar": "",
- "avatar_static": "",
- "header": "http://localhost:8080/assets/default_header.webp",
- "header_static": "http://localhost:8080/assets/default_header.webp",
- "header_description": "Flat gray background (default header).",
+ "created_at": "2025-03-15T11:08:00.000Z",
+ "note": "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
+ "url": "http://localhost:8080/@media_mogul",
+ "avatar": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ "avatar_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ "avatar_description": "DESCRIPTION_GOES_HERE",
+ "avatar_media_id": "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
+ "header": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
+ "header_static": "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
+ "header_description": "DESCRIPTION_GOES_HERE",
+ "header_media_id": "01JPHRB7F2RXPTEQFRYC85EPD9",
"followers_count": 0,
"following_count": 0,
- "statuses_count": 0,
- "last_status_at": null,
+ "statuses_count": 2,
+ "last_status_at": "2025-03-15",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "I'm going to post a lot of",
+ "value": "media!",
+ "verified_at": null
+ },
+ {
+ "name": "and there's nothing",
+ "value": "you can do about it",
+ "verified_at": null
+ }
+ ],
+ "enable_rss": true,
"group": false
- }
+ },
+ "created_by_application_id": "01HT5P2YHDMPAAD500NDAY8JW1"
}
]`, dst.String())
}
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index a63ca9e11..b0ce795f0 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -158,8 +158,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
@@ -301,8 +301,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
@@ -444,8 +444,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
@@ -638,8 +638,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
@@ -803,8 +803,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
"thumbnail_type": "image/gif",
@@ -987,8 +987,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go
index 318010387..53f0a993c 100644
--- a/internal/api/client/search/searchget_test.go
+++ b/internal/api/client/search/searchget_test.go
@@ -915,7 +915,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
suite.FailNow(err.Error())
}
- suite.Len(searchResult.Accounts, 5)
+ suite.Len(searchResult.Accounts, 6)
suite.Len(searchResult.Statuses, 9)
suite.Len(searchResult.Hashtags, 0)
}
@@ -1130,7 +1130,7 @@ func (suite *SearchGetTestSuite) TestSearchAAccounts() {
suite.FailNow(err.Error())
}
- suite.Len(searchResult.Accounts, 5)
+ suite.Len(searchResult.Accounts, 6)
suite.Len(searchResult.Statuses, 0)
suite.Len(searchResult.Hashtags, 0)
}
diff --git a/internal/api/model/account.go b/internal/api/model/account.go
index d14ef9047..34d8f5958 100644
--- a/internal/api/model/account.go
+++ b/internal/api/model/account.go
@@ -149,6 +149,9 @@ type WebAccount struct {
// Only set if this account had a header set
// (and not just the default "blank" image.)
HeaderAttachment *WebAttachment `json:"-"`
+
+ // Layout for this account (microblog, gallery).
+ WebLayout string `json:"-"`
}
// MutedAccount extends Account with a field used only by the muted user list.
@@ -240,6 +243,10 @@ type UpdateCredentialsRequest struct {
// Visibility of statuses to show via the web view.
// "none", "public" (default), or "unlisted" (which includes public as well).
WebVisibility *string `form:"web_visibility" json:"web_visibility"`
+ // Layout to use for the web view of the account.
+ // "microblog": default, classic microblog layout.
+ // "gallery": gallery layout with media only.
+ WebLayout *string `form:"web_layout" json:"web_layout"`
}
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go
index 1d910343c..63bfc52a6 100644
--- a/internal/api/model/attachment.go
+++ b/internal/api/model/attachment.go
@@ -136,6 +136,10 @@ type WebAttachment struct {
// MIME type of
// the thumbnail.
PreviewMIMEType string
+
+ // Link to the URL of the parent
+ // status of this attachment.
+ ParentStatusLink string
}
// MediaMeta models media metadata.
diff --git a/internal/api/model/source.go b/internal/api/model/source.go
index cc3eb78ee..ff0b25424 100644
--- a/internal/api/model/source.go
+++ b/internal/api/model/source.go
@@ -31,6 +31,10 @@ type Source struct {
// "unlisted" = show Public *and* Unlisted visibility posts on the web.
// "none" = show no posts on the web, not even Public ones.
WebVisibility Visibility `json:"web_visibility"`
+ // Layout to use for the web view of the account.
+ // "microblog": default, classic microblog layout.
+ // "gallery": gallery layout with media only.
+ WebLayout string `json:"web_layout"`
// Whether new statuses should be marked sensitive by default.
Sensitive bool `json:"sensitive"`
// The default posting language for new statuses.
diff --git a/internal/db/account.go b/internal/db/account.go
index aa0dfd985..cfb81308f 100644
--- a/internal/db/account.go
+++ b/internal/db/account.go
@@ -121,7 +121,7 @@ type Account interface {
// returning statuses that should be visible via the web view of a *LOCAL* account.
//
// In the case of no statuses, this function will return db.ErrNoEntries.
- GetAccountWebStatuses(ctx context.Context, account *gtsmodel.Account, limit int, maxID string) ([]*gtsmodel.Status, error)
+ GetAccountWebStatuses(ctx context.Context, account *gtsmodel.Account, mediaOnly bool, limit int, maxID string) ([]*gtsmodel.Status, error)
// GetInstanceAccount returns the instance account for the given domain.
// If domain is empty, this instance account will be returned.
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index f905101e4..aacfcd247 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -878,6 +878,29 @@ func (a *accountDB) GetAccountFaves(ctx context.Context, accountID string) ([]*g
return *faves, nil
}
+func qMediaOnly(q *bun.SelectQuery) *bun.SelectQuery {
+ // Attachments are stored as a json object; this
+ // implementation differs between SQLite and Postgres,
+ // so we have to be thorough to cover all eventualities
+ return q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
+ switch d := q.Dialect().Name(); d {
+ case dialect.PG:
+ return q.
+ Where("? IS NOT NULL", bun.Ident("status.attachments")).
+ Where("? != '{}'", bun.Ident("status.attachments"))
+
+ case dialect.SQLite:
+ return q.
+ Where("? IS NOT NULL", bun.Ident("status.attachments")).
+ Where("? != 'null'", bun.Ident("status.attachments")).
+ Where("? != '[]'", bun.Ident("status.attachments"))
+
+ default:
+ panic("dialect " + d.String() + " was neither pg nor sqlite")
+ }
+ })
+}
+
func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, error) {
// Ensure reasonable
if limit < 0 {
@@ -918,28 +941,9 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
q = q.Where("? IS NULL", bun.Ident("status.boost_of_id"))
}
+ // Respect media-only preference.
if mediaOnly {
- // Attachments are stored as a json object; this
- // implementation differs between SQLite and Postgres,
- // so we have to be thorough to cover all eventualities
- q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
- switch a.db.Dialect().Name() {
- case dialect.PG:
- return q.
- Where("? IS NOT NULL", bun.Ident("status.attachments")).
- Where("? != '{}'", bun.Ident("status.attachments"))
- case dialect.SQLite:
- return q.
- Where("? IS NOT NULL", bun.Ident("status.attachments")).
- Where("? != ''", bun.Ident("status.attachments")).
- Where("? != 'null'", bun.Ident("status.attachments")).
- Where("? != '{}'", bun.Ident("status.attachments")).
- Where("? != '[]'", bun.Ident("status.attachments"))
- default:
- log.Panic(ctx, "db dialect was neither pg nor sqlite")
- return q
- }
- })
+ q = qMediaOnly(q)
}
if publicOnly {
@@ -1018,6 +1022,7 @@ func (a *accountDB) GetAccountPinnedStatuses(ctx context.Context, accountID stri
func (a *accountDB) GetAccountWebStatuses(
ctx context.Context,
account *gtsmodel.Account,
+ mediaOnly bool,
limit int,
maxID string,
) ([]*gtsmodel.Status, error) {
@@ -1046,10 +1051,7 @@ func (a *accountDB) GetAccountWebStatuses(
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
// Select only IDs from table
Column("status.id").
- Where("? = ?", bun.Ident("status.account_id"), account.ID).
- // Don't show replies or boosts.
- Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
- Where("? IS NULL", bun.Ident("status.boost_of_id"))
+ Where("? = ?", bun.Ident("status.account_id"), account.ID)
// Select statuses for this account according
// to their web visibility preference.
@@ -1074,10 +1076,19 @@ func (a *accountDB) GetAccountWebStatuses(
)
}
- // Don't show local-only statuses on the web view.
- q = q.Where("? = ?", bun.Ident("status.federated"), true)
+ // Don't show replies, boosts, or
+ // local-only statuses on the web view.
+ q = q.
+ Where("? IS NULL", bun.Ident("status.in_reply_to_uri")).
+ Where("? IS NULL", bun.Ident("status.boost_of_id")).
+ Where("? = ?", bun.Ident("status.federated"), true)
+
+ // Respect media-only preference.
+ if mediaOnly {
+ q = qMediaOnly(q)
+ }
- // return only statuses LOWER (ie., older) than maxID
+ // Return only statuses LOWER (ie., older) than maxID
if maxID == "" {
maxID = id.Highest
}
diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go
index 879250408..e3d36855e 100644
--- a/internal/db/bundb/account_test.go
+++ b/internal/db/bundb/account_test.go
@@ -49,6 +49,12 @@ func (suite *AccountTestSuite) TestGetAccountStatuses() {
suite.Len(statuses, 9)
}
+func (suite *AccountTestSuite) TestGetAccountWebStatusesMediaOnly() {
+ statuses, err := suite.db.GetAccountWebStatuses(context.Background(), suite.testAccounts["local_account_3"], true, 20, "")
+ suite.NoError(err)
+ suite.Len(statuses, 2)
+}
+
func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
// get the first page
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, "", "", false, false)
@@ -490,7 +496,7 @@ func (suite *AccountTestSuite) TestGetAccountsAll() {
suite.FailNow(err.Error())
}
- suite.Len(accounts, 9)
+ suite.Len(accounts, 10)
}
func (suite *AccountTestSuite) TestGetAccountsMaxID() {
@@ -564,7 +570,7 @@ func (suite *AccountTestSuite) TestGetAccountsMinID() {
suite.FailNow(err.Error())
}
- suite.Len(accounts, 3)
+ suite.Len(accounts, 4)
}
func (suite *AccountTestSuite) TestGetAccountsModsOnly() {
diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go
index e20aab765..1f2d1ac48 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, 28)
+ suite.Len(s, 30)
}
func (suite *BasicTestSuite) TestGetAllNotNull() {
diff --git a/internal/db/bundb/instance_test.go b/internal/db/bundb/instance_test.go
index 1364bacc2..c0d63003d 100644
--- a/internal/db/bundb/instance_test.go
+++ b/internal/db/bundb/instance_test.go
@@ -35,7 +35,7 @@ type InstanceTestSuite struct {
func (suite *InstanceTestSuite) TestCountInstanceUsers() {
count, err := suite.db.CountInstanceUsers(context.Background(), config.GetHost())
suite.NoError(err)
- suite.Equal(4, count)
+ suite.Equal(5, count)
}
func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
@@ -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(21, count)
+ suite.Equal(23, count)
}
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
diff --git a/internal/db/bundb/migrations/20250314120945_add_gallery_web_layout.go b/internal/db/bundb/migrations/20250314120945_add_gallery_web_layout.go
new file mode 100644
index 000000000..64b133cd5
--- /dev/null
+++ b/internal/db/bundb/migrations/20250314120945_add_gallery_web_layout.go
@@ -0,0 +1,85 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ // Add new column to settings.
+ if _, err := tx.
+ NewAddColumn().
+ Table("account_settings").
+ ColumnExpr(
+ "? SMALLINT NOT NULL DEFAULT ?",
+ bun.Ident("web_layout"), 1,
+ ).
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Drop existing statuses web index as it's out of date.
+ log.Info(ctx, "updating statuses_profile_web_view_idx, this may take a while, please wait!")
+ if _, err := tx.
+ NewDropIndex().
+ Index("statuses_profile_web_view_idx").
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Note: "attachments" field is not included in
+ // the index below as SQLite is fussy about using it,
+ // and it prevents this index from being used
+ // properly in non media-only queries.
+ if _, err := tx.
+ NewCreateIndex().
+ Table("statuses").
+ Index("statuses_profile_web_view_idx").
+ Column(
+ "account_id",
+ "visibility",
+ "in_reply_to_uri",
+ "boost_of_id",
+ "federated",
+ ).
+ ColumnExpr("? DESC", bun.Ident("id")).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return nil
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go
index 4624aa0b1..30fb7e5df 100644
--- a/internal/gtsmodel/accountsettings.go
+++ b/internal/gtsmodel/accountsettings.go
@@ -18,6 +18,7 @@
package gtsmodel
import (
+ "strings"
"time"
)
@@ -35,9 +36,51 @@ type AccountSettings struct {
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile.
+ WebLayout WebLayout `bun:",nullzero,notnull,default:1"` // Layout to use when showing this profile via the web.
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
InteractionPolicyMutualsOnly *InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy.
InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
InteractionPolicyUnlocked *InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy.
InteractionPolicyPublic *InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy.
}
+
+// WebLayout represents an account owner's
+// choice for how they want their profile to be
+// laid out via the web view, by default.
+type WebLayout enumType
+
+const (
+ WebLayoutUnknown WebLayout = 0
+
+ // "Classic" / default GtS microblog view.
+ WebLayoutMicroblog WebLayout = 1
+
+ // 'gram-style gallery view with media only.
+ WebLayoutGallery WebLayout = 2
+)
+
+// String returns a stringified, frontend
+// API compatible form of WebLayout.
+func (wrm WebLayout) String() string {
+ switch wrm {
+ case WebLayoutMicroblog:
+ return "microblog"
+ case WebLayoutGallery:
+ return "gallery"
+ default:
+ panic("invalid web layout")
+ }
+}
+
+// ParseWebLayout returns a web
+// layout from the given value.
+func ParseWebLayout(in string) WebLayout {
+ switch strings.ToLower(in) {
+ case "microblog":
+ return WebLayoutMicroblog
+ case "gallery":
+ return WebLayoutGallery
+ default:
+ return WebLayoutUnknown
+ }
+}
diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go
index 22ba0fe42..b0debcc91 100644
--- a/internal/processing/account/rss.go
+++ b/internal/processing/account/rss.go
@@ -115,8 +115,20 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// Reuse the lastPostAt value for feed.Updated.
feed.Updated = lastPostAt
- // Retrieve latest statuses as they'd be shown on the web view of the account profile.
- statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, rssFeedLength, "")
+ // Retrieve latest statuses as they'd be shown
+ // on the web view of the account profile.
+ //
+ // Take into account whether the user wants
+ // their web view laid out in gallery mode.
+ mediaOnly := account.Settings != nil &&
+ account.Settings.WebLayout == gtsmodel.WebLayoutGallery
+ statuses, err := p.state.DB.GetAccountWebStatuses(
+ ctx,
+ account,
+ mediaOnly,
+ rssFeedLength,
+ "", // Latest posts from the top.
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("db error getting account web statuses: %w", err)
return "", gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go
index 8029a460b..701fe44ae 100644
--- a/internal/processing/account/statuses.go
+++ b/internal/processing/account/statuses.go
@@ -143,6 +143,7 @@ func (p *Processor) StatusesGet(
func (p *Processor) WebStatusesGet(
ctx context.Context,
targetAccountID string,
+ mediaOnly bool,
maxID string,
) (*apimodel.PageableResponse, gtserror.WithCode) {
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
@@ -159,7 +160,13 @@ func (p *Processor) WebStatusesGet(
return nil, gtserror.NewErrorNotFound(err)
}
- statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, 10, maxID)
+ statuses, err := p.state.DB.GetAccountWebStatuses(
+ ctx,
+ account,
+ mediaOnly,
+ 20,
+ maxID,
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -198,6 +205,7 @@ func (p *Processor) WebStatusesGet(
func (p *Processor) WebStatusesGetPinned(
ctx context.Context,
targetAccountID string,
+ mediaOnly bool,
) ([]*apimodel.WebStatus, gtserror.WithCode) {
statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, targetAccountID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
@@ -206,6 +214,11 @@ func (p *Processor) WebStatusesGetPinned(
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
for _, status := range statuses {
+ if mediaOnly && len(status.Attachments) == 0 {
+ // No media, skip.
+ continue
+ }
+
// Ensure visible via the web.
visible, err := p.visFilter.StatusVisible(ctx, nil, status)
if err != nil {
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index 3a59dbdf3..a833d72c1 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -294,6 +294,18 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
settingsColumns = append(settingsColumns, "web_visibility")
}
+ if form.WebLayout != nil {
+ webLayout := gtsmodel.ParseWebLayout(*form.WebLayout)
+ if webLayout == gtsmodel.WebLayoutUnknown {
+ const text = "web_layout must be one of microblog or gallery"
+ err := errors.New(text)
+ return nil, gtserror.NewErrorBadRequest(err, text)
+ }
+
+ account.Settings.WebLayout = webLayout
+ settingsColumns = append(settingsColumns, "web_layout")
+ }
+
// We've parsed + set everything, do
// necessary database updates now.
diff --git a/internal/router/template.go b/internal/router/template.go
index 70d87add1..51c0c4960 100644
--- a/internal/router/template.go
+++ b/internal/router/template.go
@@ -76,6 +76,10 @@ func LoadTemplates(engine *gin.Engine) error {
// Set additional "include" functions to render
// provided template name using the base template.
+
+ // Include renders the given template with the given data.
+ // Unlike `template`, `include` can be chained with `indent`
+ // to produce nicely-indented HTML.
funcMap["include"] = func(name string, data any) (template.HTML, error) {
var buf strings.Builder
err := tmpl.ExecuteTemplate(&buf, name, data)
@@ -85,6 +89,25 @@ func LoadTemplates(engine *gin.Engine) error {
return noescape(buf.String()), err
}
+ // includeIndex is like `include` but an index can be specified at
+ // `.Index` and data will be nested at `.Item`. Useful when ranging.
+ funcMap["includeIndex"] = func(name string, data any, index int) (template.HTML, error) {
+ var buf strings.Builder
+ withIndex := struct {
+ Item any
+ Index int
+ }{
+ Item: data,
+ Index: index,
+ }
+ err := tmpl.ExecuteTemplate(&buf, name, withIndex)
+
+ // Template was already escaped by
+ // ExecuteTemplate so we can trust it.
+ return noescape(buf.String()), err
+ }
+
+ // includeAttr is like `include` but for element attributes.
funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) {
var buf strings.Builder
err := tmpl.ExecuteTemplate(&buf, name, data)
diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go
index 4b909540c..6ff67d505 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(23, pruned)
+ suite.Equal(25, 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(23, pruned)
+ suite.Equal(25, 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(28, pruned)
+ suite.Equal(30, 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(28, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
+ suite.Equal(30, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
func TestPruneTestSuite(t *testing.T) {
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 7a2428f42..32f835da1 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -85,7 +85,7 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
@@ -151,7 +151,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
@@ -216,7 +216,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
"publicKey": {
"id": "http://localhost:8080/users/1happyturtle#main-key",
"owner": "http://localhost:8080/users/1happyturtle",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-06-04T13:12:00Z",
"summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
@@ -298,7 +298,7 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
@@ -360,7 +360,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
"publicKey": {
"id": "http://localhost:8080/users/1happyturtle#main-key",
"owner": "http://localhost:8080/users/1happyturtle",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-06-04T13:12:00Z",
"summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
@@ -422,7 +422,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
@@ -497,7 +497,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index b0e137f75..b0f5d12fa 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -137,6 +137,7 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
apiAccount.Source = &apimodel.Source{
Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy),
WebVisibility: c.VisToAPIVis(ctx, a.Settings.WebVisibility),
+ WebLayout: a.Settings.WebLayout.String(),
Sensitive: *a.Settings.Sensitive,
Language: a.Settings.Language,
StatusContentType: statusContentType,
@@ -222,6 +223,14 @@ func (c *Converter) AccountToWebAccount(
}
}
+ // Check for presence of settings before
+ // populating settings-specific thingies,
+ // as instance account doesn't store a
+ // settings struct.
+ if a.Settings != nil {
+ webAccount.WebLayout = a.Settings.WebLayout.String()
+ }
+
return webAccount, nil
}
@@ -1227,10 +1236,11 @@ func (c *Converter) StatusToWebStatus(
for i, apiAttachment := range apiStatus.MediaAttachments {
ogAttachment := ogAttachments[apiAttachment.ID]
webStatus.MediaAttachments[i] = &apimodel.WebAttachment{
- Attachment: apiAttachment,
- Sensitive: apiStatus.Sensitive,
- MIMEType: ogAttachment.File.ContentType,
- PreviewMIMEType: ogAttachment.Thumbnail.ContentType,
+ Attachment: apiAttachment,
+ Sensitive: apiStatus.Sensitive,
+ MIMEType: ogAttachment.File.ContentType,
+ PreviewMIMEType: ogAttachment.Thumbnail.ContentType,
+ ParentStatusLink: apiStatus.URL,
}
}
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index c94d4481a..d70c210f3 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -128,6 +128,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
"source": {
"privacy": "public",
"web_visibility": "unlisted",
+ "web_layout": "microblog",
"sensitive": false,
"language": "en",
"status_content_type": "text/plain",
@@ -324,6 +325,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
"source": {
"privacy": "public",
"web_visibility": "unlisted",
+ "web_layout": "microblog",
"sensitive": false,
"language": "en",
"status_content_type": "text/plain",
@@ -1815,7 +1817,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"blurhash": "LKE3VIw}0KD%a2o{M|t7NFWps:t7",
"Sensitive": true,
"MIMEType": "image/jpg",
- "PreviewMIMEType": "image/webp"
+ "PreviewMIMEType": "image/webp",
+ "ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
},
{
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
@@ -1830,7 +1833,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
"Sensitive": true,
"MIMEType": "",
- "PreviewMIMEType": ""
+ "PreviewMIMEType": "",
+ "ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
},
{
"id": "01HE88YG74PVAB81PX2XA9F3FG",
@@ -1845,7 +1849,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"blurhash": null,
"Sensitive": true,
"MIMEType": "",
- "PreviewMIMEType": ""
+ "PreviewMIMEType": "",
+ "ParentStatusLink": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"
}
],
"LanguageTag": "en",
@@ -2364,8 +2369,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
},
"stats": {
"domain_count": 2,
- "status_count": 21,
- "user_count": 4
+ "status_count": 23,
+ "user_count": 5
},
"thumbnail": "http://localhost:8080/assets/logo.webp",
"contact_account": {
diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go
index 9076c6cbc..dde4f18b5 100644
--- a/internal/typeutils/wrap_test.go
+++ b/internal/typeutils/wrap_test.go
@@ -206,7 +206,7 @@ func (suite *WrapTestSuite) TestWrapAccountableInUpdate() {
"publicKey": {
"id": "http://localhost:8080/users/the_mighty_zork/main-key",
"owner": "http://localhost:8080/users/the_mighty_zork",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtQQjwFLHPez+7uF9AX7\nuvLFHm3SyNIozhhVmGhxHIs0xdgRnZKmzmZkFdrFuXddBTAglU4C2u3dw10jJO1a\nWIFQF8bGkRHZG7Pd25/XmWWBRPmOJxNLeWBqpj0G+2zTMgnAV72hALSDFY2/QDsx\nUthenKw0Srpj1LUwvRbyVQQ8fGu4v0HACFnlOX2hCQwhfAnGrb0V70Y2IJu++MP7\n6i49md0vR0Mv3WbsEJUNp1fTIUzkgWB31icvfrNmaaAxw5FkAE+KfkkylhRxi5i5\nRR1XQUINWc2Kj2Kro+CJarKG+9zasMyN7+D230gpESi8rXv1SwRu865FR3gANdDS\nMwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2022-05-20T11:09:18Z",
"summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
diff --git a/internal/web/profile.go b/internal/web/profile.go
index cf12ca33a..52d918b48 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -19,7 +19,6 @@ package web
import (
"context"
- "encoding/json"
"fmt"
"net/http"
"strings"
@@ -28,9 +27,24 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
)
-func (m *Module) profileGETHandler(c *gin.Context) {
+type profile struct {
+ instance *apimodel.InstanceV1
+ account *apimodel.WebAccount
+ rssFeed string
+ robotsMeta string
+ pinnedStatuses []*apimodel.WebStatus
+ statusResp *apimodel.PageableResponse
+ paging bool
+}
+
+// prepareProfile does content type checks, fetches the
+// targeted account from the db, and converts it to its
+// web representation, along with other data needed to
+// render the web view of the account.
+func (m *Module) prepareProfile(c *gin.Context) *profile {
ctx := c.Request.Context()
// We'll need the instance later, and we can also use it
@@ -38,7 +52,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
instance, errWithCode := m.processor.InstanceGetV1(ctx)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
+ return nil
}
// Return instance we already got from the db,
@@ -47,90 +61,142 @@ func (m *Module) profileGETHandler(c *gin.Context) {
return instance, nil
}
- // Parse account targetUsername from the URL.
- targetUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
+ // Parse + normalize account username from the URL.
+ requestedUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
- return
+ return nil
}
+ requestedUsername = strings.ToLower(requestedUsername)
- // Normalize requested username:
- //
- // - Usernames on our instance are (currently) always lowercase.
- //
- // todo: Update this logic when different username patterns
- // are allowed, and/or when status slugs are introduced.
- targetUsername = strings.ToLower(targetUsername)
-
- // Check what type of content is being requested. If we're getting an AP
- // request on this endpoint we should render the AP representation instead.
- accept, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
+ // Check what type of content is being requested.
+ // If we're getting an AP request on this endpoint
+ // we should render the AP representation instead.
+ contentType, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
if err != nil {
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
- return
+ return nil
}
- if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) {
- // AP account representation has been requested.
- m.returnAPAccount(c, targetUsername, accept, instanceGet)
- return
+ if contentType == string(apiutil.AppActivityJSON) ||
+ contentType == string(apiutil.AppActivityLDJSON) {
+ // AP account representation has
+ // been requested, return that.
+ m.returnAPAccount(c, requestedUsername, contentType)
+ return nil
}
- // text/html has been requested. Proceed with getting the web view of the account.
-
- // Fetch the target account so we can do some checks on it.
- targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername)
+ // text/html has been requested.
+ //
+ // Proceed with getting the web
+ // representation of the account.
+ account, errWithCode := m.processor.Account().GetWeb(ctx, requestedUsername)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
- return
+ return nil
}
- // If target account is suspended, this page should not be visible.
+ // If target account is suspended,
+ // this page should not be visible.
+ //
// TODO: change this to 410?
- if targetAccount.Suspended {
- err := fmt.Errorf("target account %s is suspended", targetUsername)
+ if account.Suspended {
+ err := fmt.Errorf("target account %s is suspended", requestedUsername)
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
- return
+ return nil
}
- // Only generate RSS link if account has RSS enabled.
+ // Only generate RSS link if
+ // account has RSS enabled.
var rssFeed string
- if targetAccount.EnableRSS {
- rssFeed = "/@" + targetAccount.Username + "/feed.rss"
+ if account.EnableRSS {
+ rssFeed = "/@" + account.Username + "/feed.rss"
}
- // Only allow search engines / robots to
- // index if account is discoverable.
+ // Only allow search robots
+ // if account is discoverable.
var robotsMeta string
- if targetAccount.Discoverable {
+ if account.Discoverable {
robotsMeta = apiutil.RobotsDirectivesAllowSome
}
- // We need to change our response slightly if the
- // profile visitor is paging through statuses.
+ // Check if paging.
+ maxStatusID := apiutil.ParseMaxID(c.Query(apiutil.MaxIDKey), "")
+ paging := maxStatusID != ""
+
+ // If not paging, load pinned statuses.
var (
- maxStatusID = apiutil.ParseMaxID(c.Query(apiutil.MaxIDKey), "")
- paging = maxStatusID != ""
+ mediaOnly = account.WebLayout == "gallery"
pinnedStatuses []*apimodel.WebStatus
)
-
if !paging {
- // Client opened bare profile (from the top)
- // so load + display pinned statuses.
- pinnedStatuses, errWithCode = m.processor.Account().WebStatusesGetPinned(ctx, targetAccount.ID)
+ var errWithCode gtserror.WithCode
+ pinnedStatuses, errWithCode = m.processor.Account().WebStatusesGetPinned(
+ ctx,
+ account.ID,
+ mediaOnly,
+ )
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
- return
+ return nil
}
}
// Get statuses from maxStatusID onwards (or from top if empty string).
- statusResp, errWithCode := m.processor.Account().WebStatusesGet(ctx, targetAccount.ID, maxStatusID)
+ statusResp, errWithCode := m.processor.Account().WebStatusesGet(
+ ctx,
+ account.ID,
+ mediaOnly,
+ maxStatusID,
+ )
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
+ return nil
+ }
+
+ return &profile{
+ instance: instance,
+ account: account,
+ rssFeed: rssFeed,
+ robotsMeta: robotsMeta,
+ pinnedStatuses: pinnedStatuses,
+ statusResp: statusResp,
+ paging: paging,
+ }
+}
+
+// profileGETHandler selects the appropriate rendering
+// mode for the target account profile, and serves that.
+func (m *Module) profileGETHandler(c *gin.Context) {
+ p := m.prepareProfile(c)
+ if p == nil {
+ // Something went wrong,
+ // error already written.
return
}
+ // Choose desired web renderer for this acct.
+ switch wrm := p.account.WebLayout; wrm {
+
+ // El classico.
+ case "", "microblog":
+ m.profileMicroblog(c, p)
+
+ // 'gram style media gallery.
+ case "gallery":
+ m.profileGallery(c, p)
+
+ default:
+ log.Panicf(
+ c.Request.Context(),
+ "unknown webrenderingmode %s", wrm,
+ )
+ }
+}
+
+// profileMicroblog serves the profile
+// in classic GtS "microblog" view.
+func (m *Module) profileMicroblog(c *gin.Context, p *profile) {
// Prepare stylesheets for profile.
stylesheets := make([]string, 0, 7)
@@ -146,7 +212,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
)
// User-selected theme if set.
- if theme := targetAccount.Theme; theme != "" {
+ if theme := p.account.Theme; theme != "" {
stylesheets = append(
stylesheets,
themesPathPrefix+"/"+theme,
@@ -156,23 +222,89 @@ func (m *Module) profileGETHandler(c *gin.Context) {
// Custom CSS for this user last in cascade.
stylesheets = append(
stylesheets,
- "/@"+targetAccount.Username+"/custom.css",
+ "/@"+p.account.Username+"/custom.css",
)
page := apiutil.WebPage{
Template: "profile.tmpl",
- Instance: instance,
- OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
+ Instance: p.instance,
+ OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
+ Stylesheets: stylesheets,
+ Javascript: []string{jsFrontend},
+ Extra: map[string]any{
+ "account": p.account,
+ "rssFeed": p.rssFeed,
+ "robotsMeta": p.robotsMeta,
+ "statuses": p.statusResp.Items,
+ "statuses_next": p.statusResp.NextLink,
+ "pinned_statuses": p.pinnedStatuses,
+ "show_back_to_top": p.paging,
+ },
+ }
+
+ apiutil.TemplateWebPage(c, page)
+}
+
+// profileMicroblog serves the profile
+// in media-only 'gram-style gallery view.
+func (m *Module) profileGallery(c *gin.Context, p *profile) {
+ // Get just attachments from pinned,
+ // making a rough guess for slice size.
+ pinnedGalleryItems := make([]*apimodel.WebAttachment, 0, len(p.pinnedStatuses)*4)
+ for _, status := range p.pinnedStatuses {
+ pinnedGalleryItems = append(pinnedGalleryItems, status.MediaAttachments...)
+ }
+
+ // Get just attachments from statuses,
+ // making a rough guess for slice size.
+ galleryItems := make([]*apimodel.WebAttachment, 0, len(p.statusResp.Items)*4)
+ for _, statusI := range p.statusResp.Items {
+ status := statusI.(*apimodel.WebStatus)
+ galleryItems = append(galleryItems, status.MediaAttachments...)
+ }
+
+ // Prepare stylesheets for profile.
+ stylesheets := make([]string, 0, 4)
+
+ // Profile gallery stylesheets.
+ stylesheets = append(
+ stylesheets,
+ []string{
+ cssFA,
+ cssProfileGallery,
+ }...)
+
+ // User-selected theme if set.
+ if theme := p.account.Theme; theme != "" {
+ stylesheets = append(
+ stylesheets,
+ themesPathPrefix+"/"+theme,
+ )
+ }
+
+ // Custom CSS for this
+ // user last in cascade.
+ stylesheets = append(
+ stylesheets,
+ "/@"+p.account.Username+"/custom.css",
+ )
+
+ page := apiutil.WebPage{
+ Template: "profile-gallery.tmpl",
+ Instance: p.instance,
+ OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
Stylesheets: stylesheets,
Javascript: []string{jsFrontend},
Extra: map[string]any{
- "account": targetAccount,
- "rssFeed": rssFeed,
- "robotsMeta": robotsMeta,
- "statuses": statusResp.Items,
- "statuses_next": statusResp.NextLink,
- "pinned_statuses": pinnedStatuses,
- "show_back_to_top": paging,
+ "account": p.account,
+ "rssFeed": p.rssFeed,
+ "robotsMeta": p.robotsMeta,
+ "pinnedGalleryItems": pinnedGalleryItems,
+ "galleryItems": galleryItems,
+ "statuses": p.statusResp.Items,
+ "statuses_next": p.statusResp.NextLink,
+ "pinned_statuses": p.pinnedStatuses,
+ "show_back_to_top": p.paging,
},
}
@@ -184,8 +316,7 @@ func (m *Module) profileGETHandler(c *gin.Context) {
func (m *Module) returnAPAccount(
c *gin.Context,
targetUsername string,
- accept string,
- instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode),
+ contentType string,
) {
user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), targetUsername, c.Request.URL)
if errWithCode != nil {
@@ -193,12 +324,5 @@ func (m *Module) returnAPAccount(
return
}
- b, err := json.Marshal(user)
- if err != nil {
- err := gtserror.Newf("could not marshal json: %w", err)
- apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
- return
- }
-
- c.Data(http.StatusOK, accept, b)
+ apiutil.JSONType(c, http.StatusOK, contentType, user)
}
diff --git a/internal/web/web.go b/internal/web/web.go
index e5d4db4c4..dbfc2a3b5 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -56,15 +56,16 @@ const (
eTagHeader = "ETag" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
- cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
- cssAbout = distPathPrefix + "/about.css"
- cssIndex = distPathPrefix + "/index.css"
- cssLoginInfo = distPathPrefix + "/login-info.css"
- cssStatus = distPathPrefix + "/status.css"
- cssThread = distPathPrefix + "/thread.css"
- cssProfile = distPathPrefix + "/profile.css"
- cssSettings = distPathPrefix + "/settings-style.css"
- cssTag = distPathPrefix + "/tag.css"
+ cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
+ cssAbout = distPathPrefix + "/about.css"
+ cssIndex = distPathPrefix + "/index.css"
+ cssLoginInfo = distPathPrefix + "/login-info.css"
+ cssStatus = distPathPrefix + "/status.css"
+ cssThread = distPathPrefix + "/thread.css"
+ cssProfile = distPathPrefix + "/profile.css"
+ cssProfileGallery = distPathPrefix + "/profile-gallery.css"
+ cssSettings = distPathPrefix + "/settings-style.css"
+ cssTag = distPathPrefix + "/tag.css"
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
diff --git a/testrig/media/bunny-original.webm b/testrig/media/bunny-original.webm
new file mode 100644
index 000000000..6716c0066
--- /dev/null
+++ b/testrig/media/bunny-original.webm
Binary files differ
diff --git a/testrig/media/bunny-small.webp b/testrig/media/bunny-small.webp
new file mode 100644
index 000000000..09ff1b8ed
--- /dev/null
+++ b/testrig/media/bunny-small.webp
Binary files differ
diff --git a/testrig/media/buscemi-original.jpeg b/testrig/media/buscemi-original.jpeg
new file mode 100644
index 000000000..0a0a0db2a
--- /dev/null
+++ b/testrig/media/buscemi-original.jpeg
Binary files differ
diff --git a/testrig/media/buscemi-small.jpeg b/testrig/media/buscemi-small.jpeg
new file mode 100644
index 000000000..60f933bf7
--- /dev/null
+++ b/testrig/media/buscemi-small.jpeg
Binary files differ
diff --git a/testrig/media/butt-original.gif b/testrig/media/butt-original.gif
new file mode 100644
index 000000000..078b75561
--- /dev/null
+++ b/testrig/media/butt-original.gif
Binary files differ
diff --git a/testrig/media/butt-small.webp b/testrig/media/butt-small.webp
new file mode 100644
index 000000000..9cc638d19
--- /dev/null
+++ b/testrig/media/butt-small.webp
Binary files differ
diff --git a/testrig/media/computerbye-original.gif b/testrig/media/computerbye-original.gif
new file mode 100644
index 000000000..b13410dc7
--- /dev/null
+++ b/testrig/media/computerbye-original.gif
Binary files differ
diff --git a/testrig/media/computerbye-small.webp b/testrig/media/computerbye-small.webp
new file mode 100644
index 000000000..55a46c38a
--- /dev/null
+++ b/testrig/media/computerbye-small.webp
Binary files differ
diff --git a/testrig/media/diarrhea-original.gif b/testrig/media/diarrhea-original.gif
new file mode 100644
index 000000000..364743a9e
--- /dev/null
+++ b/testrig/media/diarrhea-original.gif
Binary files differ
diff --git a/testrig/media/diarrhea-small.webp b/testrig/media/diarrhea-small.webp
new file mode 100644
index 000000000..7b910e3e6
--- /dev/null
+++ b/testrig/media/diarrhea-small.webp
Binary files differ
diff --git a/testrig/media/dollar-original.jpeg b/testrig/media/dollar-original.jpeg
new file mode 100644
index 000000000..48b4caf70
--- /dev/null
+++ b/testrig/media/dollar-original.jpeg
Binary files differ
diff --git a/testrig/media/dollar-small.jpeg b/testrig/media/dollar-small.jpeg
new file mode 100644
index 000000000..91c2a8d54
--- /dev/null
+++ b/testrig/media/dollar-small.jpeg
Binary files differ
diff --git a/testrig/media/dollar2-original.png b/testrig/media/dollar2-original.png
new file mode 100644
index 000000000..a5b99b191
--- /dev/null
+++ b/testrig/media/dollar2-original.png
Binary files differ
diff --git a/testrig/media/dollar2-small.webp b/testrig/media/dollar2-small.webp
new file mode 100644
index 000000000..c6e50d6b1
--- /dev/null
+++ b/testrig/media/dollar2-small.webp
Binary files differ
diff --git a/testrig/media/ffmpreg-original.jpeg b/testrig/media/ffmpreg-original.jpeg
new file mode 100644
index 000000000..c0d08a9a5
--- /dev/null
+++ b/testrig/media/ffmpreg-original.jpeg
Binary files differ
diff --git a/testrig/media/ffmpreg-small.jpeg b/testrig/media/ffmpreg-small.jpeg
new file mode 100644
index 000000000..9d5724bdf
--- /dev/null
+++ b/testrig/media/ffmpreg-small.jpeg
Binary files differ
diff --git a/testrig/media/marge-original.png b/testrig/media/marge-original.png
new file mode 100644
index 000000000..c3a1c6c3d
--- /dev/null
+++ b/testrig/media/marge-original.png
Binary files differ
diff --git a/testrig/media/marge-small.webp b/testrig/media/marge-small.webp
new file mode 100644
index 000000000..4dc49c60c
--- /dev/null
+++ b/testrig/media/marge-small.webp
Binary files differ
diff --git a/testrig/media/notabug-original.jpeg b/testrig/media/notabug-original.jpeg
new file mode 100644
index 000000000..1086fff63
--- /dev/null
+++ b/testrig/media/notabug-original.jpeg
Binary files differ
diff --git a/testrig/media/notabug-small.jpeg b/testrig/media/notabug-small.jpeg
new file mode 100644
index 000000000..a3ee0db48
--- /dev/null
+++ b/testrig/media/notabug-small.jpeg
Binary files differ
diff --git a/testrig/media/sickos-original.jpeg b/testrig/media/sickos-original.jpeg
new file mode 100644
index 000000000..1a0fb37ec
--- /dev/null
+++ b/testrig/media/sickos-original.jpeg
Binary files differ
diff --git a/testrig/media/sickos-small.jpeg b/testrig/media/sickos-small.jpeg
new file mode 100644
index 000000000..69c8a9f94
--- /dev/null
+++ b/testrig/media/sickos-small.jpeg
Binary files differ
diff --git a/testrig/media/sloth-gear-original.webp b/testrig/media/sloth-gear-original.webp
new file mode 100644
index 000000000..d3b2b0426
--- /dev/null
+++ b/testrig/media/sloth-gear-original.webp
Binary files differ
diff --git a/testrig/media/sloth-gear-small.jpeg b/testrig/media/sloth-gear-small.jpeg
new file mode 100644
index 000000000..28d48cc7a
--- /dev/null
+++ b/testrig/media/sloth-gear-small.jpeg
Binary files differ
diff --git a/testrig/media/you-posted-original.webp b/testrig/media/you-posted-original.webp
new file mode 100644
index 000000000..ed26e34a2
--- /dev/null
+++ b/testrig/media/you-posted-original.webp
Binary files differ
diff --git a/testrig/media/you-posted-small.webp b/testrig/media/you-posted-small.webp
new file mode 100644
index 000000000..e6f669543
--- /dev/null
+++ b/testrig/media/you-posted-small.webp
Binary files differ
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index d8c91b611..08ca3b943 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -95,6 +95,15 @@ func NewTestTokens() map[string]*gtsmodel.Token {
AccessCreateAt: TimeMustParse("2022-06-10T15:22:08Z"),
AccessExpiresAt: TimeMustParse("2050-01-01T15:22:08Z"),
},
+ "local_account_3": {
+ ID: "01JPCMGR09M8VGARPSBABXNZFQ",
+ ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70",
+ UserID: "01JPCMFRTQ0B6R8SXPM7RS80Q4",
+ RedirectURI: "http://localhost:8080",
+ Scope: "read write push",
+ Access: "01JPCMK0YQ24FFVZ98PYZGJCC901JPCMK32ZKZMM737HGSWMW",
+ AccessCreateAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ },
"admin_account": {
ID: "01FS4TP8ANA5VE92EAPA9E0M7Q",
ClientID: "01F8MGWSJCND9BWBD4WGJXBM93",
@@ -249,6 +258,29 @@ func NewTestUsers() map[string]*gtsmodel.User {
ResetPasswordToken: "",
ResetPasswordSentAt: time.Time{},
},
+ "local_account_3": {
+ ID: "01JPCMFRTQ0B6R8SXPM7RS80Q4",
+ Email: "media.mogul@example.org",
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
+ CreatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ SignUpIP: nil,
+ UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ InviteID: "",
+ Locale: "en",
+ CreatedByApplicationID: "01HT5P2YHDMPAAD500NDAY8JW1",
+ LastEmailedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ ConfirmationToken: "",
+ ConfirmedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ ConfirmationSentAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ UnconfirmedEmail: "",
+ Moderator: util.Ptr(false),
+ Admin: util.Ptr(false),
+ Disabled: util.Ptr(false),
+ Approved: util.Ptr(true),
+ ResetPasswordToken: "",
+ ResetPasswordSentAt: time.Time{},
+ },
}
return users
@@ -446,6 +478,59 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SuspensionOrigin: "",
Settings: settings["local_account_2"],
},
+ "local_account_3": {
+ ID: "01JPCMD83Y4WR901094YES3QC5",
+ Username: "media_mogul",
+ AvatarMediaAttachmentID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
+ HeaderMediaAttachmentID: "01JPHRB7F2RXPTEQFRYC85EPD9",
+ DisplayName: "",
+ Fields: []*gtsmodel.Field{
+ {
+ Name: "I'm going to post a lot of",
+ Value: "media!",
+ },
+ {
+ Name: "and there's nothing",
+ Value: "you can do about it",
+ },
+ },
+ FieldsRaw: []*gtsmodel.Field{
+ {
+ Name: "I'm going to post a lot of",
+ Value: "media!",
+ },
+ {
+ Name: "and there's nothing",
+ Value: "you can do about it",
+ },
+ },
+ Note: "<p>I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode</p>",
+ NoteRaw: "I'm a test account that posts a shitload of media and I have my account rendered in \"gallery\" mode",
+ Memorial: util.Ptr(false),
+ MovedToURI: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ Bot: util.Ptr(false),
+ Locked: util.Ptr(false),
+ Discoverable: util.Ptr(false),
+ URI: "http://localhost:8080/users/media_mogul",
+ URL: "http://localhost:8080/@media_mogul",
+ FetchedAt: time.Time{},
+ InboxURI: "http://localhost:8080/users/media_mogul/inbox",
+ OutboxURI: "http://localhost:8080/users/media_mogul/outbox",
+ FollowersURI: "http://localhost:8080/users/media_mogul/followers",
+ FollowingURI: "http://localhost:8080/users/media_mogul/following",
+ FeaturedCollectionURI: "http://localhost:8080/users/media_mogul/collections/featured",
+ ActorType: ap.ActorPerson,
+ PrivateKey: &rsa.PrivateKey{},
+ PublicKey: &rsa.PublicKey{},
+ PublicKeyURI: "http://localhost:8080/users/media_mogul#main-key",
+ SensitizedAt: time.Time{},
+ SilencedAt: time.Time{},
+ SuspendedAt: time.Time{},
+ SuspensionOrigin: "",
+ Settings: settings["local_account_3"],
+ },
"remote_account_1": {
ID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
Username: "foss_satan",
@@ -596,6 +681,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCq1BCPAUsc97P7u4X0Bfu68sUebdLI0ijOGFWYaHEcizTF2BGdkqbOZmQV2sW5d10FMCCVTgLa7d3DXSMk7VpYgVAXxsaREdkbs93bn9eZZYFE+Y4nE0t5YGqmPQb7bNMyCcBXvaEAtIMVjb9AOzFS2F6crDRKumPUtTC9FvJVBDx8a7i/QcAIWeU5faEJDCF8CcatvRXvRjYgm774w/vqLj2Z3S9HQy/dZuwQlQ2nV9MhTOSBYHfWJy9+s2ZpoDHDkWQAT4p+STKWFHGLmLlFHVdBQg1ZzYqPYquj4Ilqsob73NqwzI3v4PbfSCkRKLyte/VLBG7zrkVHeAA10NIzAgMBAAECggEAJQLTH5ihJIKKTTUAvbD6LDPi/0e+DmJyEsz05pNiRlPmuCKrFl+qojdO4elHQ3qX/cLCnHaNac91Z5lrPtnp5BkIOE6JwO6EAluC6s2D0alLS51h7hdhF8gK8z9vntOiIko4kQn1swhpCidu00S/1/om7Xzly3b8oB4tlBo/oKlyrhoZr9r3VDPwJVY1Z9r1feyjNtUVblDRRLBXBGyeCqUhPgESM+huNIVl8QM7zXMs0ie2QrjWSevF6Hzcdxqf05/UwVj0tfMrWf9kTz6aUR1ZUYuzuVxEn96xmrsnvAXI9BTYpRKdZzTfL5gItxdvfF6uPrK0W9QNS9ZIk7EUgQKBgQDOzP82IsZhywEr0D4bOm6GIspk05LGEi6AVVp1YaP9ZxGGTXwIXpXPbWhoZh8o3smnVgW89kD4xIA+2AXJRS/ZSA+XCqlIzGSfekd8UfLM6o6zDiC0YGgce4xMhcHXabKrGquEp64a4hrs3JcrQCM0EqhFlpOWrX3On4JJI/QlwQKBgQDTeDQizbn/wygAn1kccSBeOx45Pc8Bkpcq8KxVYsYpwpKcz4m7hqPIcz8kOofWGFqjV2AHEIoDm5OB5DwejutKJQIJhGln/boS5fOJDhvOwSaV8Lo7ehcqGqD1tbvZfDQJWjEf6acj2owIBNU5ni0GlHo/zqyu+ibaABPH36f88wKBgA8e/io/MLJF3bgOafwjsaEtOg9VSQ4iljPcCdk7YnpM5wMi90bFY77fCRtZHD4ozCXoLFM8zlNiSt5NfV7SKEWC92Db7rTb/R+MGV4Fv/Mr03NUPR/zTKmIfyG5RgsyN1Y7hP8WI6zji4R2PLd04R4Vnyg3cmM6HFDXaPdgIaIBAoGAKOYPl0eYmImi+/PVpTWP4Amo/8MffRtf1zMy8VSoJL1345IT/ku883CunpAfY13UcdDdRqCBQM9fCPkeU36qrO1ZZoPQawdcbHlCz5gF8sfScZ9cNVKYllEOHldmnFp0Kfbil1x2Me37tTVSE9GuvZ4LwrlzFmhVCUaIjNiJwdcCgYBnR7lp+rnJpXPkvllArmrKEvhcyCbcDIEGaV8aPUsXfXoVMUaiVEybdUrL3IuLtNgiab3qNZ/knYSsuAW+0tnoaOhRCUFzK47x+uLFFKCMw4FOOOJJzVu8E/5Lu0d6FpU7MuVXMa0UUGIqfOYNGywuo3XOIfWHh3iSHUg1X6/+1A==",
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSIsx0TsUCeSHXDYPzViqRwB/wZhBkj5f0Mrc+Q0yogUmiTcubYQcf/xj9LOvtArJ+8/rori0j8aFX17jZqtFyDDINyhICT+i5bk1ZKPt/uH/H5oFpjtsL+bCoOF8F4AUeELExH0dO3uwl8v9fPZZ3AZEGj6UB6Ru13LON7fKHt+JT6s9jNtUIUpHUDg2GZYv9gLFGDDm9H91Yervl8yF6VWbK+7pcVyhlz5wqHR/qNUiyUXhiie+veiJc9ipCU7RriNEuehvF12d3rRIOK/wRsFAG4LxufJS8Shu8VJrOBlKzsufqjDZtnZb8SrTY0EjLJpslMf67zRDD1kEDpq4jAgMBAAECggEBAMeKxe2YMxpjHpBRRECZTTk0YN/ue5iShrAcTMeyLqRAiUS3bSXyIErw+bDIrIxXKFrHoja71x+vvw9kSSNhQxxymkFf5nQNn6geJxMIiLJC6AxSRgeP4U/g3jEPvqQck592KFzGH/e0Vji/JGMzX6NIeIfrdbx3uJmcp2CaWNkoOs7UYV5VbNDaIWYcgptQS9hJpCQ+cuMov7scXE88uKtwAl+0VVopNr/XA7vV+npsESBCt3dfnp6poA13ldfqReLdPTmDWH7Z8QrTIagrfPi5mKpxksTYyC0/quKyk4yTj8Ge5GWmsXCHtyf19NX7reeJa8MjEWonYDCdnqReDoECgYEA8R5OHNIGC6yw6ZyTuyEt2epXwUj0h2Z9d+JAT9ndRGK9xdMqJt4acjxfcEck2wjv9BuNLr5YvLc4CYiOgyqJHNt5c5Ys5rJEOgBZ2IFoaoXZNom2LEtr583T4RFXp/Id8ix85D6EZj8Hp6OvZygQFwEYQexY383hZZh5enkorUECgYEA3xr3u/SbttM86ib1RP1uuON9ZURfzpmrr2ubSWiRDqwift0T2HesdhWi6xDGjzGyeT5e7irf1BsBKUq2dp/wFX6+15A6eV12C7PvC4N8u3NJwGBdvCmufh5wZ19rerelaB7+vG9c+Nbw9h1BbDi8MlGs06oVSawvwUzp2oVKLmMCgYEAq1RFXOU/tnv3GYhQ0N86nWWPBaC5YJzK+qyh1huQxk8DWdY6VXPshs+vYTCsV5d6KZKKN3S5yR7Hir6lxT4sP30UR7WmIib5o90r+lO5xjdlqQMhl0fgXM48h+iyyHuaG8LQ274whhazccM1l683/6Cfg/hVDnJUfsRhTU1aQgECgYBrZPTZcf6+u+I3qHcqNYBl2YPUCly/+7LsJzVB2ebxlCSqwsq5yamn0fRxiMq7xSVvPXm+1b6WwEUH1mIMqiKMhk1hQJkVMMsRCRVJioqxROa8hua4G6xWI1riN8lp8hraCwl+NXEgi37ESgLjEFBvPGegH+BNbWgzeU2clcrGlwKBgHBxlFLf6AjDxjR8Z5dnZVPyvLOUjejs5nsLdOfONJ8F/MU0PoKFWdBavhbnwXwium6NvcearnhbWL758sKooZviQL6m/sKDGWMq3O8SCnX+TKTEOw+kLLFn4L3sT02WaHYg+C5iVEDdGlsXSehhI2e7hBoTulE/zbUkbA3+wlmv",
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6LR5HNVS8rwA6P8U9TGOwEQ1Z8bVTCfWXJ+SjzPNYaTh/YWHA9bg+0TIKbXB9yxPVETKbEBYaP953OcIXJjGFtHNi4snhOP2/F61XoGkLltSDE2tOaGQJ0gQ5uhkGjmK2jfptBcESAZ2W4UzQkV6mGej194leGLjtxdk0A9b/Rk0MPMDrurnHH818pU2XsWfEabUGFAQlU4SuZmLHPqnxMDkOXjnOQdyXweSeMtQVYgiUOy8xkY+ecAbm7f+HGuZM5uSaAg/6z7xOpvVJeACI2PVme6pGV46o5yJUO56tt/ioCmrvgun7LqDDU0VxPuiX5WuwGeNUFrHi0boz3XivAgMBAAECggEAdWgYjQ1rx6WQvisTBooS36iRQ+Ry1dAVCWLGBCouV9XbJDFURSxwKWUhaoQDicC0XAyBXloxphIbCBLrfE/AsTHQBk9AwoB/PLAAx57IP9+5WoO3ivW4CJ1hvsnGGGVYiQlWIMSdMe7E465nE6xpBNSYHe0huq5aiM/ZHr1BKy+l5T2z2k0437+3d8RhSfwlW8T7WYWK2rQZ3hPq9Cl+gDvyvcMNt2Wo9AGonwB+XtrF13tF3nqnPx8jomj4pbmFXMzKR5RsgWNX2Fec064e53OQzkYhqQ6mByUPA//UxfOO1BtNwhFQUjNEZCYMKWcD3EoR17dcosX/GlHt+MZGuQKBgQDWBdDKqV3zZSjeUJwnkd3ykdNdVggqJiNfLww3owUG1E/VUHZuvYzsJbyWp0g+rLESqa+sPp8cKP93q1ve4Dw9Dqp4ejR8hqYUEzq2Adrcgb30WDj5IZRnku34CGsq/wUP9IOyA7chZYONzllY07m/W9ZZcSwG6ziXFeyPj4XzbQKBgQDesR4jMSEys2b5PA4MO+rQYgbKj+lVzHn4uYX0ghhuoYwZYEZ0yJKyDztbgD2x7/DP8bYAZTuksqRk4Ss/bS6iRDZlGQQaXVNeEJMiIMbLCDxx69I312nYHgZ0/ETyk/5eOdJkObshkTrFA0UO13c9t4jRQfNdjTepQj56mTcvCwKBgQCQXaXkPnCoULFLnNZofqVXDXSkvfaN7+HmP8ce9HDclXQwcLEiq+uWEzJt8PLzi+t5qkpchnUvOpxwbX9wDJO1n+HvmIc1BGKcogf1Y7TtDvtCCgyMSFFhuCObLpqTiygwBgCboJP0DBS8H9f26gKeiOVCues304z9pQVIJUj21QKBgBsUDGcZFUFWAUJzI/4m1wGpucutviC5sWcmH/zASPpC2IdJZqfSr8vJAF269UWKuIyAhrH7nUoEkurVWm3m99GxW6/lX9NY38dDWrC+rY2Indj4ZOJ3Zh5qYDyfZD7e8gJBI60eO/vz7eKA6EfKuWwewhs32sDYaBlDvdcohEZLAoGBAIoWjKNJg02dKQUU4df1BjhvEw5pSEh4hGDBR12cD52ibqGPLF36TBwVnNL284BXipjBWejzvVnCUAzflym4UgMUidhJxpVrVJSx0Tdclr0+70Lz6emtNA4e+A9ttJLwuiZrmct7G9FWJ6GgBa/1z7a+/qRLM4SMxgbMufQcIl+r",
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXvT0UjYZ7vIXSnlAtCH/FurOW4V7YKp3KsXkI3p3kqpwUkwojD6a16npHw+oN6FOS0ZPli5++KpCmXPw4WDkFXC9ldi82ZxYBQL0Gu3xeRfuizvRjN3pNfw80/ph/QV9ZCc4iYr2EuMHmC352ga36tvrt89UvZeS0+UweRNlKiEJG320Mu5zUpSKiWER2d2GDfWDIKmaoF7dlG745kkL+gYBM9g6Umq67oMLVZou0FMhXsFDbeFuir/VstT8eHwlUuKdK9w8dtJJlDoYg5EXMKCckrBXADwUWBEIfVxPHwOWRrYe2Xv5Nf326se993vuSEufzBDU4hN/4nuM6pOdFAgMBAAECggEAZp04JEJ8qPYuoNN0Rzc3rxDywt1Hg4Ihs3temn1olI8h1hdqRur23Kg+qUviU+MhfT/6HMCgpo8QZlDsFtC/rnD+ikAAjNvTd50XS9B5g02+Nt5BF8AXiCzbStWeK0ko1Oz5Axn8EtjeQVFOQYfE/O9zwyKrT/QjKIE7V1pgEDaHtm4TmmTgC7238zkzvaCXSUckyi6ShsFoU2NcJvomMNeD5XgWZqbwO6rHig6BQhIizi0NsLXvIvIPXsawYV1AQFIap76c2biCgdPODMTtA/rgkGlpdu/PhST+gsx0CbA5iaIHY4nmKavrpbLzF2TG6GjomH4n4+1C/5HVqarbAQKBgQDiQUt0/RirbGr+9B4LOOLKEmoJoOrdNXoydKssTqUvOtMNTmDnJNoVQ0zYH5waydgZSN7Ce3pGztFwZ6gHyxQ80utjF4ttb5CmZCpoWyMqOyEbiV70lWjxcdfGnTtm0b2XJPTFFCXI+JemWoy+c7B+1AViYlHX/IMB/jWH+Y/q8QKBgQD0GgdjHYcyk5MZha5bWTRdzrX/IyWtmsqY1vvKwwb8e2W/AFLljL91elb6eKPhfLhbWoGRSLzgGJ1LGSv4e15bIPk6ZXkxl+PDlCvlAMLmV5LiH3ky5xlC7/zBFhKvLVztb66JGbielilVV4zTqS04VsYhZOKVuCNRNYjh4Km5lQKBgQCRdPLi6lgy1QfQkvbBtjevO7lqKUb1Ig1GZNUrLgBqZcILmukXkQyXgOXlSCUe38cLMlrr42BQJ2RkhG91WyzOkbb8xMVBfOkc3+aXoofv/YWiY2VljqyiFNNo/+qRhqQBiKPIE9Ta6F7uduZnBo9gakRv5M/DMLa00E5v9ZR9sQKBgD3KsQAII4dMEDqvunlpVXZBs5SIgys1OgACu+6R/BzB5/m3zURKotTMSWRSUbns5oZJnO74KMfZs0elcZoPMM2ExVJhCZLiTkfeJFZuIOhKVuZi7T1TfvOQ6LzAJ66snw+D6/zMxA1xGbl+1ilmdAoE/VbKwQkBef8+vA3h31UZAoGAUzlh0nGH59pZ7pRH5XHCXCSqnwFn9l9Dnfoin2tsjSLQVqANAqUySaNfZ6CxHlP/J5Cg6PMebZGr0I3KIXl3iXfth1Jnf8kPtBc5/OLOtN2njleILVlrqHwnWA757OsE+BKpqI9wOKn/B9iY3SgBSlosSIbOQKd/V2vZVUGf37U=",
}
if diff := len(accountsSorted) - len(preserializedKeys); diff > 0 {
@@ -640,6 +726,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
EnableRSS: util.Ptr(false),
HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic,
+ WebLayout: gtsmodel.WebLayoutMicroblog,
},
"admin_account": {
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
@@ -651,6 +738,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
EnableRSS: util.Ptr(true),
HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic,
+ WebLayout: gtsmodel.WebLayoutMicroblog,
},
"local_account_1": {
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
@@ -662,6 +750,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
EnableRSS: util.Ptr(true),
HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityUnlocked,
+ WebLayout: gtsmodel.WebLayoutMicroblog,
},
"local_account_2": {
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
@@ -673,6 +762,19 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
EnableRSS: util.Ptr(false),
HideCollections: util.Ptr(true),
WebVisibility: gtsmodel.VisibilityPublic,
+ WebLayout: gtsmodel.WebLayoutMicroblog,
+ },
+ "local_account_3": {
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ CreatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ UpdatedAt: TimeMustParse("2025-03-15T11:08:00Z"),
+ Privacy: gtsmodel.VisibilityPublic,
+ Sensitive: util.Ptr(true),
+ Language: "en",
+ EnableRSS: util.Ptr(true),
+ HideCollections: util.Ptr(false),
+ WebVisibility: gtsmodel.VisibilityUnlocked,
+ WebLayout: gtsmodel.WebLayoutGallery,
},
}
}
@@ -1035,6 +1137,623 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Header: util.Ptr(false),
Cached: util.Ptr(false),
},
+ "local_account_3_avatar": {
+ ID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
+ StatusID: "", // this attachment isn't connected to a status
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T10:46:37+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 1280,
+ Height: 720,
+ Size: 921600,
+ Aspect: 1.777778,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 288,
+ Size: 147456,
+ Aspect: 1.777778,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LRF~2LIU0esp-qRjR*aeJ$s;iwW.",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 291230,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 24486,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/avatar/small/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(true),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ "local_account_3_header": {
+ ID: "01JPHRB7F2RXPTEQFRYC85EPD9",
+ StatusID: "", // this attachment isn't connected to a status
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T10:53:17+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 725,
+ Height: 307,
+ Size: 222575,
+ Aspect: 2.361563,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 216,
+ Size: 110592,
+ Aspect: 2.361563,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "L9I5h:%M%M?a~os:D*bFMybFM{jI",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
+ ContentType: "image/png",
+ FileSize: 405238,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
+ ContentType: "image/webp",
+ FileSize: 26478,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/header/small/01JPHRB7F2RXPTEQFRYC85EPD9.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(true),
+ Cached: util.Ptr(true),
+ },
+ // sickos
+ "local_account_3_status_1_attachment_1": {
+ ID: "01JPCPRMPPGWKBCAE7X81XA0PK",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPRMPPGWKBCAE7X81XA0PK.jpeg",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:49:28+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 1920,
+ Height: 1200,
+ Size: 2304000,
+ Aspect: 1.600000,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 320,
+ Size: 163840,
+ Aspect: 1.600000,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "L~EqXWX5t6og%jW=owa~N1WFjYWC",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPRMPPGWKBCAE7X81XA0PK.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 513277,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPRMPPGWKBCAE7X81XA0PK.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 23550,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPRMPPGWKBCAE7X81XA0PK.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // marge
+ "local_account_3_status_1_attachment_2": {
+ ID: "01JPCPTSFNQDAGTHP49DXSD0BM",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPTSFNQDAGTHP49DXSD0BM.png",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:50:38+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 976,
+ Height: 741,
+ Size: 723216,
+ Aspect: 1.317139,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 388,
+ Size: 198656,
+ Aspect: 1.317139,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LGH1i6RpD;-,0DoZaIogA2N3xZI]",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPTSFNQDAGTHP49DXSD0BM.png",
+ ContentType: "image/png",
+ FileSize: 380878,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPTSFNQDAGTHP49DXSD0BM.webp",
+ ContentType: "image/webp",
+ FileSize: 51882,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPTSFNQDAGTHP49DXSD0BM.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // sloth-gear
+ "local_account_3_status_1_attachment_3": {
+ ID: "01JPCPYJ6N2E2R7GAJ1XECXNV5",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPYJ6N2E2R7GAJ1XECXNV5.webp",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:52:42+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 2830,
+ Height: 1472,
+ Size: 4165760,
+ Aspect: 1.922554,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 266,
+ Size: 136192,
+ Aspect: 1.922554,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LOE.|bxZx]j[~pt7WWWW%Lj@%Mj[",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPYJ6N2E2R7GAJ1XECXNV5.webp",
+ ContentType: "image/webp",
+ FileSize: 366592,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPYJ6N2E2R7GAJ1XECXNV5.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 15461,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCPYJ6N2E2R7GAJ1XECXNV5.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // you-posted
+ "local_account_3_status_1_attachment_4": {
+ ID: "01JPCQ4WXEA52VVR9V1HN7E0RS",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ4WXEA52VVR9V1HN7E0RS.png",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:56:09+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 1920,
+ Height: 1080,
+ Size: 2073600,
+ Aspect: 1.777778,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 288,
+ Size: 147456,
+ Aspect: 1.777778,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "L00+zhoLNubHj[fQa|fQ9tWVw{jZ",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ4WXEA52VVR9V1HN7E0RS.png",
+ ContentType: "image/png",
+ FileSize: 80917,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCQ4WXEA52VVR9V1HN7E0RS.webp",
+ ContentType: "image/webp",
+ FileSize: 5344,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCQ4WXEA52VVR9V1HN7E0RS.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // buscemi
+ "local_account_3_status_1_attachment_5": {
+ ID: "01JPCQ9VBZBMSTVN56QN3R5188",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ9VBZBMSTVN56QN3R5188.jpeg",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-15T11:58:51+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 1077,
+ Height: 525,
+ Size: 565425,
+ Aspect: 2.051429,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 249,
+ Size: 127488,
+ Aspect: 2.051429,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "L5A9A=}?J*5m56Rk={$%O?Nb$M$i",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ9VBZBMSTVN56QN3R5188.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 42899,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCQ9VBZBMSTVN56QN3R5188.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 17341,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPCQ9VBZBMSTVN56QN3R5188.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // butt
+ "local_account_3_status_1_attachment_6": {
+ ID: "01JPG1RZPRH3Y00VSA3RQ2SJWP",
+ StatusID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPG1RZPRH3Y00VSA3RQ2SJWP.gif",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-16T18:59:36+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 31,
+ Height: 25,
+ Size: 775,
+ Aspect: 1.240000,
+ },
+ Small: gtsmodel.Small{
+ Width: 31,
+ Height: 25,
+ Size: 775,
+ Aspect: 1.240000,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LWLN.4~q00ofxuxu-;%M9F-;-;xu",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPG1RZPRH3Y00VSA3RQ2SJWP.gif",
+ ContentType: "image/gif",
+ FileSize: 636,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPG1RZPRH3Y00VSA3RQ2SJWP.webp",
+ ContentType: "image/webp",
+ FileSize: 406,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPG1RZPRH3Y00VSA3RQ2SJWP.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // bunny
+ "local_account_3_status_2_attachment_1": {
+ ID: "01JPHFKQ86GT9W76SWPHE9P8JB",
+ StatusID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFKQ86GT9W76SWPHE9P8JB.webm",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T08:20:38+01:00"),
+ Type: gtsmodel.FileTypeVideo,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 640,
+ Height: 360,
+ Size: 230400,
+ Aspect: 1.777778,
+ Duration: util.Ptr[float32](32.480000),
+ Bitrate: util.Ptr[uint64](533294),
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 288,
+ Size: 147456,
+ Aspect: 1.777778,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LEQcn{?bfQ?b~qoffQoffQfQfQfQ",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFKQ86GT9W76SWPHE9P8JB.webm",
+ ContentType: "video/webm",
+ FileSize: 2165608,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFKQ86GT9W76SWPHE9P8JB.webp",
+ ContentType: "image/webp",
+ FileSize: 324,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFKQ86GT9W76SWPHE9P8JB.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // computerbye
+ "local_account_3_status_2_attachment_2": {
+ ID: "01JPHFSCVGGH02FX9VJMXGXN45",
+ StatusID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFSCVGGH02FX9VJMXGXN45.gif",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T08:23:44+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 442,
+ Height: 332,
+ Size: 146744,
+ Aspect: 1.331325,
+ Duration: util.Ptr[float32](3.750000),
+ Framerate: util.Ptr[float32](20.000000),
+ Bitrate: util.Ptr[uint64](4078150),
+ },
+ Small: gtsmodel.Small{
+ Width: 442,
+ Height: 332,
+ Size: 146744,
+ Aspect: 1.331325,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LLHUzr-;o#_2~q-:IV%Mxu%MM{M{",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFSCVGGH02FX9VJMXGXN45.gif",
+ ContentType: "image/gif",
+ FileSize: 1911633,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFSCVGGH02FX9VJMXGXN45.webp",
+ ContentType: "image/webp",
+ FileSize: 10056,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFSCVGGH02FX9VJMXGXN45.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // diarrhea
+ "local_account_3_status_2_attachment_3": {
+ ID: "01JPHFW5HKFWQNQ954P5KNXWSR",
+ StatusID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFW5HKFWQNQ954P5KNXWSR.gif",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T08:25:15+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 320,
+ Height: 214,
+ Size: 68480,
+ Aspect: 1.495327,
+ Duration: util.Ptr[float32](3.100000),
+ Framerate: util.Ptr[float32](10.000000),
+ Bitrate: util.Ptr[uint64](2011086),
+ },
+ Small: gtsmodel.Small{
+ Width: 320,
+ Height: 214,
+ Size: 68480,
+ Aspect: 1.495327,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "L78qTmNG00xZkWxsIURQ01s;?aR*",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFW5HKFWQNQ954P5KNXWSR.gif",
+ ContentType: "image/gif",
+ FileSize: 779296,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFW5HKFWQNQ954P5KNXWSR.webp",
+ ContentType: "image/webp",
+ FileSize: 10238,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFW5HKFWQNQ954P5KNXWSR.webp",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // ffmpreg
+ "local_account_3_status_2_attachment_4": {
+ ID: "01JPHFZP2VNS1M2RQ646BXBZQG",
+ StatusID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFZP2VNS1M2RQ646BXBZQG.jpeg",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T08:27:10+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 1280,
+ Height: 720,
+ Size: 921600,
+ Aspect: 1.777778,
+ },
+ Small: gtsmodel.Small{
+ Width: 512,
+ Height: 288,
+ Size: 147456,
+ Aspect: 1.777778,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LOCX.y}rIpE3,?w{S4W;9vENX8t6",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFZP2VNS1M2RQ646BXBZQG.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 137328,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFZP2VNS1M2RQ646BXBZQG.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 19775,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHFZP2VNS1M2RQ646BXBZQG.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
+ // notabug
+ "local_account_3_status_2_attachment_5": {
+ ID: "01JPHG32F7M6F084WKEGAYJ40X",
+ StatusID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHG32F7M6F084WKEGAYJ40X.jpeg",
+ RemoteURL: "",
+ CreatedAt: TimeMustParse("2025-03-17T08:29:01+01:00"),
+ Type: gtsmodel.FileTypeImage,
+ FileMeta: gtsmodel.FileMeta{
+ Original: gtsmodel.Original{
+ Width: 500,
+ Height: 739,
+ Size: 369500,
+ Aspect: 0.676590,
+ },
+ Small: gtsmodel.Small{
+ Width: 346,
+ Height: 512,
+ Size: 177152,
+ Aspect: 0.676590,
+ },
+ Focus: gtsmodel.Focus{
+ X: 0,
+ Y: 0,
+ },
+ },
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Description: "DESCRIPTION_GOES_HERE",
+ ScheduledStatusID: "",
+ Blurhash: "LTGbrRxAE1og0OR:xve-OFs6kCWY",
+ Processing: 2,
+ File: gtsmodel.File{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHG32F7M6F084WKEGAYJ40X.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 106636,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ Path: "01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHG32F7M6F084WKEGAYJ40X.jpeg",
+ ContentType: "image/jpeg",
+ FileSize: 27483,
+ URL: "http://localhost:8080/fileserver/01JPCMD83Y4WR901094YES3QC5/attachment/small/01JPHG32F7M6F084WKEGAYJ40X.jpeg",
+ RemoteURL: "",
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(true),
+ },
"remote_account_1_status_1_attachment_1": {
ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
@@ -1372,6 +2091,58 @@ func newTestStoredAttachments() map[string]filenames {
Original: "ghosts-original.mp3",
Small: "ghosts-small.webp",
},
+ "local_account_3_status_1_attachment_1": {
+ Original: "sickos-original.jpeg",
+ Small: "sickos-small.jpeg",
+ },
+ "local_account_3_status_1_attachment_2": {
+ Original: "marge-original.png",
+ Small: "marge-small.webp",
+ },
+ "local_account_3_status_1_attachment_3": {
+ Original: "sloth-gear-original.webp",
+ Small: "sloth-gear-small.jpeg",
+ },
+ "local_account_3_status_1_attachment_4": {
+ Original: "you-posted-original.webp",
+ Small: "you-posted-small.webp",
+ },
+ "local_account_3_status_1_attachment_5": {
+ Original: "buscemi-original.jpeg",
+ Small: "buscemi-small.jpeg",
+ },
+ "local_account_3_avatar": {
+ Original: "dollar-original.jpeg",
+ Small: "dollar-small.jpeg",
+ },
+ "local_account_3_header": {
+ Original: "dollar2-original.png",
+ Small: "dollar2-small.webp",
+ },
+ "local_account_3_status_1_attachment_6": {
+ Original: "butt-original.gif",
+ Small: "butt-small.webp",
+ },
+ "local_account_3_status_2_attachment_1": {
+ Original: "bunny-original.webm",
+ Small: "bunny-small.webp",
+ },
+ "local_account_3_status_2_attachment_2": {
+ Original: "computerbye-original.gif",
+ Small: "computerbye-small.webp",
+ },
+ "local_account_3_status_2_attachment_3": {
+ Original: "diarrhea-original.gif",
+ Small: "diarrhea-small.webp",
+ },
+ "local_account_3_status_2_attachment_4": {
+ Original: "ffmpreg-original.jpeg",
+ Small: "ffmpreg-small.jpeg",
+ },
+ "local_account_3_status_2_attachment_5": {
+ Original: "notabug-original.jpeg",
+ Small: "notabug-small.jpeg",
+ },
"remote_account_1_status_1_attachment_1": {
Original: "thoughtsofdog-original.jpg",
Small: "thoughtsofdog-small.jpeg",
@@ -1941,6 +2712,54 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
+ "local_account_3_status_1": {
+ ID: "01JPCNB4417JG3XHHP0WS60RM3",
+ URI: "http://localhost:8080/users/media_mogul/statuses/01JPCNB4417JG3XHHP0WS60RM3",
+ URL: "http://localhost:8080/@media_mogul/statuses/01JPCNB4417JG3XHHP0WS60RM3",
+ AttachmentIDs: []string{
+ "01JPCPRMPPGWKBCAE7X81XA0PK",
+ "01JPCPTSFNQDAGTHP49DXSD0BM",
+ "01JPCPYJ6N2E2R7GAJ1XECXNV5",
+ "01JPCQ4WXEA52VVR9V1HN7E0RS",
+ "01JPCQ9VBZBMSTVN56QN3R5188",
+ "01JPG1RZPRH3Y00VSA3RQ2SJWP",
+ },
+ ContentType: gtsmodel.StatusContentTypePlain,
+ CreatedAt: TimeMustParse("2025-03-15T11:26:17Z"),
+ Local: util.Ptr(true),
+ AccountURI: "http://localhost:8080/users/media_mogul",
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Visibility: gtsmodel.VisibilityUnlocked,
+ Sensitive: util.Ptr(false),
+ Language: "en",
+ CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
+ Federated: util.Ptr(true),
+ ActivityStreamsType: ap.ObjectNote,
+ PinnedAt: TimeMustParse("2025-03-15T11:27:00Z"),
+ },
+ "local_account_3_status_2": {
+ ID: "01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URI: "http://localhost:8080/users/media_mogul/statuses/01JPCNJAPHJKJC4EXWA6N9BXDD",
+ URL: "http://localhost:8080/@media_mogul/statuses/01JPCNJAPHJKJC4EXWA6N9BXDD",
+ AttachmentIDs: []string{
+ "01JPHFKQ86GT9W76SWPHE9P8JB",
+ "01JPHFSCVGGH02FX9VJMXGXN45",
+ "01JPHFW5HKFWQNQ954P5KNXWSR",
+ "01JPHFZP2VNS1M2RQ646BXBZQG",
+ "01JPHG32F7M6F084WKEGAYJ40X",
+ },
+ ContentType: gtsmodel.StatusContentTypePlain,
+ CreatedAt: TimeMustParse("2025-03-15T11:28:42Z"),
+ Local: util.Ptr(true),
+ AccountURI: "http://localhost:8080/users/media_mogul",
+ AccountID: "01JPCMD83Y4WR901094YES3QC5",
+ Visibility: gtsmodel.VisibilityUnlocked,
+ Sensitive: util.Ptr(false),
+ Language: "en",
+ CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
+ Federated: util.Ptr(true),
+ ActivityStreamsType: ap.ObjectNote,
+ },
"remote_account_1_status_1": {
ID: "01FVW7JHQFSFK166WWKR8CBA6M",
URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
diff --git a/web/assets/themes/brutalist-dark.css b/web/assets/themes/brutalist-dark.css
index 4cf8cd655..9be12ba92 100644
--- a/web/assets/themes/brutalist-dark.css
+++ b/web/assets/themes/brutalist-dark.css
@@ -135,12 +135,12 @@ html, body {
}
/* Make show more/less buttons more legible */
-.status button, .status .button {
+.button {
background-color: var(--almost-white);
color: var(--almost-black);
border: var(--dashed-border);
}
-.status button:hover, .status .button:hover {
+.button:hover {
background-color: var(--almost-black);
color: var(--almost-white);
border: var(--dashed-border);
diff --git a/web/assets/themes/brutalist.css b/web/assets/themes/brutalist.css
index e29509c21..e183ee50c 100644
--- a/web/assets/themes/brutalist.css
+++ b/web/assets/themes/brutalist.css
@@ -130,12 +130,12 @@ html, body {
}
/* Make show more/less buttons more legible */
-.status button, .status .button {
+.button {
background-color: var(--almost-black);
color: var(--almost-white);
border: var(--dashed-border);
}
-.status button:hover, .status .button:hover {
+.button:hover {
background-color: var(--almost-white);
color: var(--almost-black);
border: var(--dashed-border);
diff --git a/web/source/css/_media-wrapper.css b/web/source/css/_media-wrapper.css
new file mode 100644
index 000000000..1c2ae1503
--- /dev/null
+++ b/web/source/css/_media-wrapper.css
@@ -0,0 +1,207 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import "photoswipe/dist/photoswipe.css";
+@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
+@import "plyr/dist/plyr.css";
+
+.media-wrapper {
+ height: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ border: 0.15rem solid $gray1;
+ border-radius: $br;
+ position: relative;
+ overflow: hidden;
+ z-index: 2;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ details {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+
+ &[open] summary {
+ height: auto;
+ width: auto;
+ margin: 1rem;
+ padding: 0;
+
+ .show, video, img {
+ display: none;
+ }
+
+ .eye.button .hide {
+ display: inline-block;
+ grid-column: 1 / span 3;
+ grid-row: 1 / span 2;
+ }
+ }
+
+ summary {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ z-index: 3;
+ overflow: hidden;
+
+ display: grid;
+ padding: 1rem;
+ grid-template-columns: 1fr auto 1fr;
+ grid-template-rows: 1fr 1fr;
+ grid-template-areas:
+ "eye sensitive ."
+ ". sensitive .";
+
+ &::-webkit-details-marker {
+ display: none; /* Safari */
+ }
+
+ .eye.button {
+ grid-area: eye;
+ align-self: start;
+ justify-self: start;
+ margin: 0;
+ padding: 0.4rem;
+
+ .fa-fw {
+ line-height: $fa-fw;
+ }
+
+ .hide {
+ display: none;
+ }
+ }
+
+ .show.sensitive {
+ grid-area: sensitive;
+ align-self: center;
+
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+
+ .button {
+ cursor: pointer;
+ align-self: center;
+ }
+ }
+
+ video, img {
+ z-index: -1;
+ position: absolute;
+ height: calc(100% + 1.2rem);
+ width: calc(100% + 1.2rem);
+ top: -0.6rem;
+ left: -0.6rem;
+ filter: blur(1.2rem);
+ }
+ }
+
+ video.plyr-video, .plyr {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ background: $gray1;
+ }
+
+ .unknown-attachment {
+ .placeholder {
+ width: 100%;
+ height: 100%;
+ padding: 0.8rem;
+ border: 0.2rem dashed $white2;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.25rem;
+
+ color: $white2;
+
+ .placeholder-external-link {
+ align-self: end;
+ font-size: 2.5rem;
+ }
+
+ .placeholder-icon {
+ width: 100%;
+ font-size: 3.5rem;
+ text-align: center;
+ margin-top: auto;
+ }
+
+ .placeholder-link-to {
+ width: 100%;
+ text-align: center;
+ margin-bottom: auto;
+ }
+ }
+ }
+ }
+}
+
+.pswp__button--open-post-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ span > i {
+ background: $status-bg;
+ color: $fg;
+ border-radius: 25%;
+ }
+}
+
+.plyr--video {
+ flex-direction: column-reverse;
+
+ .plyr__video-wrapper {
+ position: relative;
+ }
+
+ .plyr__controls {
+ align-self: stretch;
+ position: initial;
+ padding: 0.1rem;
+ padding-top: 0.2rem;
+ }
+
+ .plyr__control {
+ box-shadow: none;
+ }
+
+ .plyr__control--overlaid {
+ top: calc(50% - 18px);
+ }
+}
+
+.pswp__content {
+ padding: 2rem;
+
+ .plyr {
+ max-height: 100%;
+ }
+}
diff --git a/web/source/css/prism.css b/web/source/css/_prism.css
index c1d369a9c..c1d369a9c 100644
--- a/web/source/css/prism.css
+++ b/web/source/css/_prism.css
diff --git a/web/source/css/_profile-header.css b/web/source/css/_profile-header.css
new file mode 100644
index 000000000..b4ebadf8d
--- /dev/null
+++ b/web/source/css/_profile-header.css
@@ -0,0 +1,343 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+.profile .profile-header {
+ background: $profile-bg;
+ border-radius: $br;
+ overflow: hidden;
+ margin-bottom: 1rem;
+
+ .moved-to {
+ padding: 1rem;
+ text-align: center;
+ }
+
+ .header-image-wrapper {
+ position: relative;
+ padding-top: 33.33%; /* aspect-ratio 1/3 */
+
+ img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ /*
+ Basic info container has the user's avatar, display- and username, and role
+ It's partially overlapped over the header image, by a negative margin-top.
+ */
+ $avatar-size: 8.5rem;
+ $name-size: 3rem;
+ $username-size: 2rem;
+ $overlap: calc($avatar-size - $name-size - $username-size);
+
+ .basic-info {
+ position: relative;
+ display: grid;
+ box-sizing: border-box;
+ grid-template-columns: $avatar-size auto 1fr;
+ grid-template-rows: $overlap $name-size auto;
+ grid-template-areas:
+ "avatar . ."
+ "avatar namerole namerole"
+ "avatar namerole namerole";
+
+ margin: 1rem;
+ margin-top: calc(-1 * $overlap);
+ gap: 0 1rem;
+
+ .avatar-image-wrapper {
+ grid-area: avatar;
+
+ border: 0.2rem solid $avatar-border;
+ border-radius: $br;
+
+ /*
+ Wrapper always same
+ size + proportions no
+ matter image inside.
+ */
+ height: $avatar-size;
+ width: $avatar-size;
+
+ .avatar {
+ /*
+ Fit 100% of the wrapper.
+ */
+ height: 100%;
+ width: 100%;
+
+ /*
+ Normalize non-square images.
+ */
+ object-fit: cover;
+
+ /*
+ Prevent image extending
+ beyond rounded borders.
+ */
+ border-radius: $br-inner;
+ }
+ }
+
+ .namerole {
+ grid-area: namerole;
+
+ display: grid;
+ gap: 0 1rem;
+ box-sizing: border-box;
+ grid-template-columns: 1fr auto;
+ grid-template-rows: $name-size auto;
+ grid-template-areas:
+ "displayname displayname"
+ "username role";
+
+ .displayname {
+ grid-area: displayname;
+ line-height: $name-size;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+
+ .bot-username-wrapper {
+ display: flex;
+ gap: 0.5rem;
+ grid-area: username;
+ align-items: center;
+
+ .bot-legend-wrapper {
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+
+ background: $bg;
+ color: $fg;
+
+ border-radius: $br;
+ padding: 0.1rem 0.4rem 0.2rem 0.4rem;
+
+ font-variant: small-caps;
+ font-weight: bold;
+
+ cursor: default;
+
+ .bot-icon {
+ /*
+ FA icon is weirdly
+ aligned so tweak it
+ */
+ margin-top: 0.25rem;
+ }
+ }
+
+ .username {
+ min-width: 0;
+ line-height: $username-size;
+
+ font-size: 1rem;
+ font-weight: bold;
+ color: $fg-accent;
+ user-select: all;
+ }
+ }
+
+ .role {
+ background: $bg;
+ color: $fg;
+ border: 0.13rem solid $bg;
+
+ grid-area: role;
+ align-self: center;
+ justify-self: start;
+ border-radius: $br;
+ padding: 0.3rem;
+
+ line-height: 1.1rem;
+ font-size: 0.9rem;
+ font-variant: small-caps;
+ font-weight: bold;
+
+ &.admin {
+ color: $role-admin;
+ border-color: $role-admin;
+ }
+
+ &.moderator {
+ color: $role-mod;
+ border-color: $role-mod;
+ }
+ }
+ }
+ }
+}
+
+.profile .about-user {
+ flex: 35 14rem;
+ border-radius: $br;
+ overflow: hidden;
+
+ .col-header {
+ margin-bottom: -0.25rem;
+ }
+
+ dt {
+ font-weight: bold;
+ }
+
+ .fields {
+ background: $profile-bg;
+ display: flex;
+ flex-direction: column;
+ padding: 0 0.5rem;
+ padding-top: 0.25rem;
+
+ .field {
+ padding: 0.25rem;
+ display: flex;
+ flex-direction: column;
+ border-bottom: 0.1rem solid $gray2;
+
+ > dt, > dd {
+ word-break: break-word;
+ }
+
+ &:first-child {
+ border-top: 0.1rem solid $gray2;
+ }
+ }
+ }
+
+ .bio {
+ background: $profile-bg;
+ padding: 1rem 0.75rem;
+ padding-bottom: 1.25rem;
+ }
+
+ .accountstats {
+ background: $bg-accent;
+ padding: 0.75rem;
+
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+
+ .stats-item {
+ display: flex;
+ dt {
+ width: 7rem;
+ }
+ }
+ }
+}
+
+/*
+ RSS icon isn't really part of the profile header exactly,
+ but also it sort of is, and we want it styled the same for
+ both microblog and gallery view anyway, so include it here.
+*/
+.rss-icon {
+ display: block;
+ margin: -0.25rem 0;
+
+ .fa {
+ font-size: 2rem;
+ object-fit: contain;
+ vertical-align: middle;
+ color: $orange2;
+ /*
+ Can't size a single-color background, so we use
+ a linear-gradient that's effectively white.
+ */
+ background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
+ background-size: 1.2rem 1.4rem;
+ /* light mode */
+ @media (prefers-color-scheme: light) {
+ background: linear-gradient(to right, $white 100%, transparent 0) no-repeat center center;
+ background-size: 1.2rem 1.4rem;
+ }
+ }
+}
+
+/*
+ Tablet-ish-kinda size.
+*/
+@media screen and (max-width: 750px) {
+ .profile .profile-header {
+ .basic-info {
+ grid-template-columns: auto 1fr;
+ grid-template-rows: $avatar-size $name-size auto;
+ grid-template-areas:
+ "avatar avatar"
+ "namerole namerole"
+ "namerole namerole";
+
+ /*
+ Make display name a bit smaller
+ so there's more chance of being
+ able to read everything.
+ */
+ .namerole {
+ .displayname {
+ font-size: 1.2rem;
+ line-height: 2rem;
+ margin-top: 0.5rem;
+ }
+ }
+ }
+ }
+}
+
+/*
+ Phone-ish-kinda size.
+*/
+@media screen and (max-width: 500px) {
+ .profile
+ .profile-header
+ .basic-info
+ .namerole {
+ /*
+ Line up in smallest possible
+ horizontal space to avoid overflow.
+ */
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ /*
+ Don't hug the right anymore
+ (good life advice in general).
+ */
+ .role {
+ align-self: flex-start;
+ }
+
+ /*
+ Allow this to wrap in case
+ of a really skinny screen.
+ */
+ .bot-username-wrapper {
+ flex-wrap: wrap;
+ }
+ }
+}
diff --git a/web/source/css/_status-media.css b/web/source/css/_status-media.css
new file mode 100644
index 000000000..e8386c87a
--- /dev/null
+++ b/web/source/css/_status-media.css
@@ -0,0 +1,44 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import "./_media-wrapper.css";
+
+.media {
+ grid-column: span 3;
+ display: grid;
+ grid-template-columns: 50% 50%;
+ grid-auto-rows: 10rem;
+ overflow: hidden;
+
+ &.single .media-wrapper {
+ grid-column: span 2;
+ }
+
+ &.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;
+ }
+ }
+}
diff --git a/web/source/css/base.css b/web/source/css/base.css
index 5f2f75802..765453ac2 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -22,7 +22,7 @@
****************************************/
@import "modern-normalize/modern-normalize.css";
-@import "./prism.css";
+@import "./_prism.css";
/* noto-sans-regular - latin */
@font-face {
diff --git a/web/source/css/profile-gallery.css b/web/source/css/profile-gallery.css
new file mode 100644
index 000000000..cb70eff22
--- /dev/null
+++ b/web/source/css/profile-gallery.css
@@ -0,0 +1,108 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+@import "./_profile-header.css";
+@import "./_media-wrapper.css";
+
+.page {
+ /*
+ Profile gallery can be wider than default.
+ */
+ grid-template-columns: 1fr min(95%, 65rem) 1fr;
+}
+
+.profile {
+ .about-user {
+ margin-bottom: 1rem;
+
+ .accountstats {
+ flex-direction: row;
+ justify-content: space-between;
+
+ .stats-item {
+ gap: 0.5rem;
+ width: 25%;
+ justify-content: space-around;
+
+ dt {
+ width: fit-content;
+ margin-left: auto;
+ }
+
+ dd {
+ margin-right: auto;
+ }
+ }
+
+ @media screen and (max-width: 750px) {
+ flex-direction: column;
+ .stats-item {
+ width: fit-content;
+ dt {
+ width: 7rem;
+ }
+ }
+ }
+ }
+ }
+
+ .media-galleries-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ min-width: 0%;
+
+ .media-gallery {
+ margin-top: 0.15rem;
+ margin-bottom: 0.15rem;
+
+ display: grid;
+ gap: 0.15rem;
+
+ /* Desktop-ish width, show 3 cols of media */
+ grid-template-columns: repeat(3, 1fr);
+
+ @media screen and (max-width: 55rem) {
+ /* Tablet-ish width, switch to 2 cols */
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media screen and (max-width: 36rem) {
+ /* Mobile-ish width, switch to 1 col */
+ grid-template-columns: repeat(1, 1fr);
+ }
+
+ .media-wrapper {
+ aspect-ratio: 4/3;
+ border: 0;
+ border-radius: 0;
+ background: $bg;
+ }
+ }
+
+ .backnextlinks {
+ display: flex;
+ justify-content: space-between;
+
+ .next {
+ margin-left: auto;
+ }
+ }
+ }
+}
diff --git a/web/source/css/profile.css b/web/source/css/profile.css
index 1296b8927..5b9f2188e 100644
--- a/web/source/css/profile.css
+++ b/web/source/css/profile.css
@@ -17,13 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+@import "./_profile-header.css";
+
.page {
/*
Profile page can be a little wider than default
page, since we're using a side-by-side column view.
*/
- grid-template-columns: 1fr minmax(auto, 60rem) 1fr;
- grid-template-columns: 1fr min(92%, 65rem) 1fr;
+ grid-template-columns: 1fr min(95%, 65rem) 1fr;
}
.profile .column-split {
@@ -32,244 +33,6 @@
gap: 1rem;
}
-.profile .profile-header {
- background: $profile-bg;
- border-radius: $br;
- overflow: hidden;
- margin-bottom: 1rem;
-
- .moved-to {
- padding: 1rem;
- text-align: center;
- }
-
- .header-image-wrapper {
- position: relative;
- padding-top: 33.33%; /* aspect-ratio 1/3 */
-
- img {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
-
- /*
- Basic info container has the user's avatar, display- and username, and role
- It's partially overlapped over the header image, by a negative margin-top.
- */
- $avatar-size: 8.5rem;
- $name-size: 3rem;
- $username-size: 2rem;
- $overlap: calc($avatar-size - $name-size - $username-size);
-
- .basic-info {
- position: relative;
- display: grid;
- box-sizing: border-box;
- grid-template-columns: $avatar-size auto 1fr;
- grid-template-rows: $overlap $name-size auto;
- grid-template-areas:
- "avatar . ."
- "avatar namerole namerole"
- "avatar namerole namerole";
-
- margin: 1rem;
- margin-top: calc(-1 * $overlap);
- gap: 0 1rem;
-
- .avatar-image-wrapper {
- grid-area: avatar;
-
- border: 0.2rem solid $avatar-border;
- border-radius: $br;
-
- /*
- Wrapper always same
- size + proportions no
- matter image inside.
- */
- height: $avatar-size;
- width: $avatar-size;
-
- .avatar {
- /*
- Fit 100% of the wrapper.
- */
- height: 100%;
- width: 100%;
-
- /*
- Normalize non-square images.
- */
- object-fit: cover;
-
- /*
- Prevent image extending
- beyond rounded borders.
- */
- border-radius: $br-inner;
- }
- }
-
- .namerole {
- grid-area: namerole;
-
- display: grid;
- gap: 0 1rem;
- box-sizing: border-box;
- grid-template-columns: 1fr auto;
- grid-template-rows: $name-size auto;
- grid-template-areas:
- "displayname displayname"
- "username role";
-
- .displayname {
- grid-area: displayname;
- line-height: $name-size;
- font-size: 1.5rem;
- font-weight: bold;
- }
-
- .bot-username-wrapper {
- display: flex;
- gap: 0.5rem;
- grid-area: username;
- align-items: center;
-
- .bot-legend-wrapper {
- display: flex;
- gap: 0.25rem;
- align-items: center;
-
- background: $bg;
- color: $fg;
-
- border-radius: $br;
- padding: 0.1rem 0.4rem 0.2rem 0.4rem;
-
- font-variant: small-caps;
- font-weight: bold;
-
- cursor: default;
-
- .bot-icon {
- /*
- FA icon is weirdly
- aligned so tweak it
- */
- margin-top: 0.25rem;
- }
- }
-
- .username {
- min-width: 0;
- line-height: $username-size;
-
- font-size: 1rem;
- font-weight: bold;
- color: $fg-accent;
- user-select: all;
- }
- }
-
- .role {
- background: $bg;
- color: $fg;
- border: 0.13rem solid $bg;
-
- grid-area: role;
- align-self: center;
- justify-self: start;
- border-radius: $br;
- padding: 0.3rem;
-
- line-height: 1.1rem;
- font-size: 0.9rem;
- font-variant: small-caps;
- font-weight: bold;
-
- &.admin {
- color: $role-admin;
- border-color: $role-admin;
- }
-
- &.moderator {
- color: $role-mod;
- border-color: $role-mod;
- }
- }
- }
- }
-}
-
-/*
- Tablet-ish-kinda size.
-*/
-@media screen and (max-width: 750px) {
- .profile .profile-header {
- .basic-info {
- grid-template-columns: auto 1fr;
- grid-template-rows: $avatar-size $name-size auto;
- grid-template-areas:
- "avatar avatar"
- "namerole namerole"
- "namerole namerole";
-
- /*
- Make display name a bit smaller
- so there's more chance of being
- able to read everything.
- */
- .namerole {
- .displayname {
- font-size: 1.2rem;
- line-height: 2rem;
- margin-top: 0.5rem;
- }
- }
- }
- }
-}
-
-/*
- Phone-ish-kinda size.
-*/
-@media screen and (max-width: 500px) {
- .profile
- .profile-header
- .basic-info
- .namerole {
- /*
- Line up in smallest possible
- horizontal space to avoid overflow.
- */
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-
- /*
- Don't hug the right anymore
- (good life advice in general).
- */
- .role {
- align-self: flex-start;
- }
-
- /*
- Allow this to wrap in case
- of a really skinny screen.
- */
- .bot-username-wrapper {
- flex-wrap: wrap;
- }
- }
-}
-
.profile .statuses-wrapper {
flex: 65 25rem;
display: flex;
@@ -283,29 +46,6 @@
flex-direction: column;
gap: 0.4rem;
- .rss-icon {
- display: block;
- margin: -0.25rem 0;
-
- .fa {
- font-size: 2rem;
- object-fit: contain;
- vertical-align: middle;
- color: $orange2;
- /*
- Can't size a single-color background, so we use
- a linear-gradient that's effectively white.
- */
- background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
- background-size: 1.2rem 1.4rem;
- /* light mode */
- @media (prefers-color-scheme: light) {
- background: linear-gradient(to right, $white 100%, transparent 0) no-repeat center center;
- background-size: 1.2rem 1.4rem;
- }
- }
- }
-
.backnextlinks {
display: flex;
justify-content: space-between;
@@ -315,55 +55,3 @@
}
}
}
-
-.profile .about-user {
- flex: 35 14rem;
- border-radius: $br;
- overflow: hidden;
-
- .col-header {
- margin-bottom: -0.25rem;
- }
-
- dt {
- font-weight: bold;
- }
-
- .fields {
- background: $profile-bg;
- display: flex;
- flex-direction: column;
- padding: 0 0.5rem;
- padding-top: 0.25rem;
-
- .field {
- padding: 0.25rem;
- display: flex;
- flex-direction: column;
- border-bottom: 0.1rem solid $gray2;
-
- > dt, > dd {
- word-break: break-word;
- }
-
- &:first-child {
- border-top: 0.1rem solid $gray2;
- }
- }
- }
-
- .bio {
- background: $profile-bg;
- padding: 1rem 0.75rem;
- padding-bottom: 1.25rem;
- }
-
- .accountstats {
- background: $bg-accent;
- padding: 0.75rem;
-
- display: grid;
- grid-template-columns: auto 1fr;
- gap: 0.25rem 1rem;
- }
-} \ No newline at end of file
diff --git a/web/source/css/status.css b/web/source/css/status.css
index 4b2d7e2a7..ec6cac3e5 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -17,14 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-@import "photoswipe/dist/photoswipe.css";
-@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
-@import "plyr/dist/plyr.css";
-
-main {
- background: transparent;
- grid-auto-rows: auto;
-}
+@import "./_status-media.css";
.status {
background: $status-bg;
@@ -257,172 +250,6 @@ main {
}
}
- .media {
- grid-column: span 3;
- display: grid;
- grid-template-columns: 50% 50%;
- grid-auto-rows: 10rem;
- overflow: hidden;
-
- .media-wrapper {
- height: 100%;
- width: 100%;
- box-sizing: border-box;
- border: 0.15rem solid $gray1;
- border-radius: $br;
- position: relative;
- overflow: hidden;
- z-index: 2;
-
- details {
- position: absolute;
- height: 100%;
- width: 100%;
-
- &[open] summary {
- height: auto;
- width: auto;
- margin: 1rem;
- padding: 0;
-
- .show, video, img {
- display: none;
- }
-
- .eye.button .hide {
- display: inline-block;
- grid-column: 1 / span 3;
- grid-row: 1 / span 2;
- }
- }
-
- summary {
- position: absolute;
- height: 100%;
- width: 100%;
- z-index: 3;
- overflow: hidden;
-
- display: grid;
- padding: 1rem;
- grid-template-columns: 1fr auto 1fr;
- grid-template-rows: 1fr 1fr;
- grid-template-areas:
- "eye sensitive ."
- ". sensitive .";
-
- &::-webkit-details-marker {
- display: none; /* Safari */
- }
-
- .eye.button {
- grid-area: eye;
- align-self: start;
- justify-self: start;
- margin: 0;
- padding: 0.4rem;
-
- .fa-fw {
- line-height: $fa-fw;
- }
-
- .hide {
- display: none;
- }
- }
-
- .show.sensitive {
- grid-area: sensitive;
- align-self: center;
-
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-
- .button {
- cursor: pointer;
- align-self: center;
- }
- }
-
- video, img {
- z-index: -1;
- position: absolute;
- height: calc(100% + 1.2rem);
- width: calc(100% + 1.2rem);
- top: -0.6rem;
- left: -0.6rem;
- filter: blur(1.2rem);
- }
- }
-
- video.plyr-video, .plyr {
- position: absolute;
- height: 100%;
- width: 100%;
- object-fit: contain;
- background: $gray1;
- }
-
- .unknown-attachment {
- .placeholder {
- width: 100%;
- height: 100%;
- padding: 0.8rem;
- border: 0.2rem dashed $white2;
-
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
-
- color: $white2;
-
- .placeholder-external-link {
- align-self: end;
- font-size: 2.5rem;
- }
-
- .placeholder-icon {
- width: 100%;
- font-size: 3.5rem;
- text-align: center;
- margin-top: auto;
- }
-
- .placeholder-link-to {
- width: 100%;
- text-align: center;
- margin-bottom: auto;
- }
- }
- }
- }
- }
-
- &.single .media-wrapper {
- grid-column: span 2;
- }
-
- &.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%;
- object-fit: cover;
- }
- }
-
.status-info {
background: $status-info-bg;
color: $fg-reduced;
@@ -448,10 +275,6 @@ main {
gap: 0.4rem;
}
- .stats-item.published-at {
- text-decoration: underline;
- }
-
.stats-item:not(.published-at):not(.edited-at) {
z-index: 1;
user-select: none;
@@ -497,34 +320,3 @@ main {
}
}
}
-
-.plyr--video {
- flex-direction: column-reverse;
-
- .plyr__video-wrapper {
- position: relative;
- }
-
- .plyr__controls {
- align-self: stretch;
- position: initial;
- padding: 0.1rem;
- padding-top: 0.2rem;
- }
-
- .plyr__control {
- box-shadow: none;
- }
-
- .plyr__control--overlaid {
- top: calc(50% - 18px);
- }
-}
-
-.pswp__content {
- padding: 2rem;
-
- .plyr {
- max-height: 100%;
- }
-} \ No newline at end of file
diff --git a/web/source/css/tag.css b/web/source/css/tag.css
index 2f1c9db98..99dbd6f61 100644
--- a/web/source/css/tag.css
+++ b/web/source/css/tag.css
@@ -18,8 +18,8 @@
*/
.thread {
- #tag-name {
- /* Ensure ridiculous length tags get wrapped */
- word-wrap: anywhere;
- }
+ #tag-name {
+ /* Ensure ridiculous length tags get wrapped */
+ word-wrap: anywhere;
+ }
}
diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js
index 310d149ed..d45420255 100644
--- a/web/source/frontend/index.js
+++ b/web/source/frontend/index.js
@@ -40,6 +40,9 @@ const lightbox = new PhotoswipeLightbox({
gallery: '.photoswipe-gallery',
children: '.photoswipe-slide',
pswpModule: Photoswipe,
+ // Bit darker than default 0.8.
+ bgOpacity: 0.9,
+ loop: false,
});
new PhotoswipeCaptionPlugin(lightbox, {
@@ -71,7 +74,9 @@ lightbox.addFilter('itemData', (item) => {
}
},
width: parseInt(el.dataset.pswpWidth),
- height: parseInt(el.dataset.pswpHeight)
+ height: parseInt(el.dataset.pswpHeight),
+ parentStatus: el.dataset.pswpParentStatus,
+ attachmentId: el.dataset.pswpAttachmentId,
};
}
return item;
@@ -98,6 +103,26 @@ lightbox.on("close", function () {
}
});
+lightbox.on('uiRegister', function() {
+ lightbox.pswp.ui.registerElement({
+ name: 'open-post-link',
+ ariaLabel: 'Open post',
+ order: 8,
+ isButton: true,
+ tagName: "a",
+ html: '<span title="Open post"><span class="sr-only">Open post</span><i class="fa fa-lg fa-external-link-square" aria-hidden="true"></i></span>',
+ onInit: (el, pswp) => {
+ el.setAttribute('target', '_blank');
+ el.setAttribute('rel', 'noopener');
+ pswp.on('change', () => {
+ el.href = pswp.currSlide.data.parentStatus
+ ? pswp.currSlide.data.parentStatus
+ : pswp.currSlide.data.element.dataset.pswpParentStatus;
+ });
+ }
+ });
+});
+
lightbox.init();
function dynamicSpoiler(className, updateFunc) {
@@ -156,22 +181,40 @@ Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
let player = new Plyr(video, {
title: video.title,
- settings: ["loop"],
+ settings: [],
+ controls: ['play-large', 'play', 'progress', 'current-time', 'volume', 'mute', 'fullscreen'],
disableContextMenu: false,
hideControls: false,
- tooltips: { contrors: true, seek: true },
+ tooltips: { controls: true, seek: true },
iconUrl: "/assets/plyr.svg",
+ invertTime: false,
listeners: {
fullscreen: () => {
- if (player.playing) {
- setTimeout(() => {
- player.play();
- }, 1);
+ // Check if the photoswipe lightbox is
+ // open with this as the current slide.
+ const alreadyInLightbox = (
+ lightbox.pswp !== undefined &&
+ video.dataset.pswpAttachmentId === lightbox.pswp.currSlide.data.attachmentId
+ );
+
+ if (alreadyInLightbox) {
+ // If this video is already open as the
+ // current photoswipe slide, the fullscreen
+ // button toggles proper fullscreen.
+ player.fullscreen.toggle();
+ } else {
+ // Otherwise the fullscreen button opens
+ // the video as current photoswipe slide.
+ //
+ // (Don't pause the video while it's
+ // being transitioned to a slide.)
+ if (player.playing) {
+ setTimeout(() => player.play(), 1);
+ }
+ lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
+ gallery: video.closest(".photoswipe-gallery")
+ });
}
- lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
- gallery: video.closest(".photoswipe-gallery")
- });
-
return false;
}
}
diff --git a/web/source/package.json b/web/source/package.json
index ea90137c8..7b11d7eb2 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -24,7 +24,7 @@
"object-to-formdata": "^4.4.2",
"papaparse": "^5.3.2",
"parse-link-header": "^2.0.0",
- "photoswipe": "^5.3.3",
+ "photoswipe": "^5.4.4",
"photoswipe-dynamic-caption-plugin": "^1.2.7",
"plyr": "^3.7.8",
"psl": "^1.9.0",
diff --git a/web/source/settings/lib/types/account.ts b/web/source/settings/lib/types/account.ts
index 76055ba53..f5238a7d3 100644
--- a/web/source/settings/lib/types/account.ts
+++ b/web/source/settings/lib/types/account.ts
@@ -79,6 +79,8 @@ export interface AccountSource {
privacy: string;
sensitive: boolean;
status_content_type: string;
+ web_visibility: string;
+ web_layout: string;
}
export interface SearchAccountParams {
diff --git a/web/source/settings/views/user/profile.tsx b/web/source/settings/views/user/profile.tsx
index 80be3c878..9ea948337 100644
--- a/web/source/settings/views/user/profile.tsx
+++ b/web/source/settings/views/user/profile.tsx
@@ -61,20 +61,6 @@ interface UserProfileFormProps {
}
function UserProfileForm({ data: profile }: UserProfileFormProps) {
- /*
- User profile update form keys
- - bool bot
- - bool locked
- - string display_name
- - string note
- - file avatar
- - file header
- - bool enable_rss
- - bool hide_collections
- - string custom_css (if enabled)
- - string theme
- */
-
const { data: instance } = useInstanceV1Query();
const instanceConfig = React.useMemo(() => {
return {
@@ -120,7 +106,8 @@ function UserProfileForm({ data: profile }: UserProfileFormProps) {
discoverable: useBoolInput("discoverable", { source: profile}),
enableRSS: useBoolInput("enable_rss", { source: profile }),
hideCollections: useBoolInput("hide_collections", { source: profile }),
- webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p) => p.source?.web_visibility }),
+ webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p: Account) => p.source?.web_visibility }),
+ webLayout: useTextInput("web_layout", { source: profile, valueSelector: (p: Account) => p.source?.web_layout }),
fields: useFieldArrayInput("fields_attributes", {
defaultValue: profile?.source?.fields,
length: instanceConfig.maxPinnedFields
@@ -185,18 +172,24 @@ function UserProfileForm({ data: profile }: UserProfileFormProps) {
/>
</fieldset>
- <div className="theme">
- <div>
- <b id="theme-label">Theme</b>
- <br/>
- <span>After choosing theme and saving, <a href={profile.url} target="_blank">open your profile</a> and refresh to see changes.</span>
- </div>
- <Select
- aria-labelledby="theme-label"
- field={form.theme}
- options={<>{themeOptions}</>}
- />
- </div>
+ <span>After choosing theme or layout and saving, <a href={profile.url} target="_blank">open your profile</a> and refresh to see changes.</span>
+
+ <Select
+ label="Theme for the web view of your profile"
+ field={form.theme}
+ options={<>{themeOptions}</>}
+ />
+
+ <Select
+ field={form.webLayout}
+ label="Layout for the web view of your profile"
+ options={
+ <>
+ <option value="microblog">Classic microblog layout (show recent + pinned posts; media shown alongside its parent post)</option>
+ <option value="gallery">'Gram-style gallery layout (show recent + pinned media; parent posts still accessible by link)</option>
+ </>
+ }
+ />
</div>
<div className="form-section-docs">
diff --git a/web/source/yarn.lock b/web/source/yarn.lock
index 826aaaed0..f3f3a5752 100644
--- a/web/source/yarn.lock
+++ b/web/source/yarn.lock
@@ -5399,10 +5399,10 @@ photoswipe-dynamic-caption-plugin@^1.2.7:
resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2"
integrity sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q==
-photoswipe@^5.3.3:
- version "5.4.2"
- resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.2.tgz#bed976c27f876bd9c86085a022701a8cea484f7e"
- integrity sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==
+photoswipe@^5.4.4:
+ version "5.4.4"
+ resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
+ integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==
picocolors@^1.0.0:
version "1.0.0"
diff --git a/web/template/profile-gallery.tmpl b/web/template/profile-gallery.tmpl
new file mode 100644
index 000000000..badb04615
--- /dev/null
+++ b/web/template/profile-gallery.tmpl
@@ -0,0 +1,87 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with . }}
+<main class="profile h-card">
+ {{- with . }}
+ {{- include "profile_header.tmpl" . | indent 1 }}
+ {{- end }}
+ {{- with . }}
+ {{- include "profile_about_user.tmpl" . | indent 1 }}
+ {{- end }}
+ <div
+ class="media-galleries-wrapper"
+ role="region"
+ aria-label="Media by {{ .account.Username -}}"
+ >
+ {{- if .pinned_statuses }}
+ <section class="pinned h-feed" aria-labelledby="pinned">
+ <div class="col-header">
+ <h3 class="p-name" id="pinned">Pinned media</h3>
+ <a href="#recent">jump to recent</a>
+ </div>
+ <div
+ class="media-gallery photoswipe-gallery"
+ role="group"
+ >
+ {{- range $index, $attachment := .pinnedGalleryItems }}
+ {{- includeIndex "status_attachment.tmpl" $attachment $index | indent 4 }}
+ {{- end }}
+ </div>
+ </section>
+ {{- end }}
+ <section class="recent h-feed" aria-labelledby="recent">
+ <div class="col-header">
+ <h3 id="recent p-name" tabindex="-1">Recent media</h3>
+ {{- if .rssFeed }}
+ <a href="{{- .rssFeed -}}" class="rss-icon" aria-label="RSS feed">
+ <i class="fa fa-rss-square" aria-hidden="true"></i>
+ </a>
+ {{- end }}
+ </div>
+ {{- if not .galleryItems }}
+ <div data-nosnippet class="nothinghere">
+ {{- if .show_back_to_top }}
+ Reached the end of visible media!
+ {{- else }}
+ Nothing to see here! {{ .account.Username }} has either not posted any public media yet, or has opted not to make posts visible via the World Wide Web.
+ {{- end }}
+ </div>
+ {{- else }}
+ <div
+ class="media-gallery photoswipe-gallery"
+ role="group"
+ >
+ {{- range $index, $attachment := .galleryItems }}
+ {{- includeIndex "status_attachment.tmpl" $attachment $index | indent 4 }}
+ {{- end }}
+ </div>
+ {{- end }}
+ <nav class="backnextlinks">
+ {{- if .show_back_to_top }}
+ <a href="/@{{- .account.Username -}}">Back to top</a>
+ {{- end }}
+ {{- if .statuses_next }}
+ <a href="{{- .statuses_next -}}" class="next">Show older</a>
+ {{- end }}
+ </nav>
+ </section>
+ </div>
+</main>
+{{- end }} \ No newline at end of file
diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl
index 6b486eb74..d7d24cb64 100644
--- a/web/template/profile.tmpl
+++ b/web/template/profile.tmpl
@@ -17,200 +17,15 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
-{{- define "profileMovedTo" -}}
-{{- with .account.Moved }}
-<div class="moved-to">
- <b>
- ℹ️ This account has permanently moved to
- <a
- href="{{ .URL }}"
- class="nounderline"
- rel="nofollow noreferrer noopener"
- target="_blank"
- >
- @{{ .Username }}
- </a>
- </b>
-</div>
-{{- end }}
-{{- end -}}
-
-{{- define "defaultAvatarDimension" -}}
-{{- /* 136 is the default width/height for 8.5rem avatars, double it to get a good look when expanded. */ -}}
-272
-{{- end -}}
-
-{{- define "avatarWidth" -}}
-{{- with .account }}
- {{- if isNil .AvatarAttachment -}}
- {{- template "defaultAvatarDimension" . -}}
- {{- else -}}
- {{- /* Use the avatar's proper dimensions. */ -}}
- {{- .AvatarAttachment.Meta.Original.Width -}}
- {{- end -}}
-{{- end }}
-{{- end -}}
-
-{{- define "avatarHeight" -}}
-{{- with .account }}
- {{- if isNil .AvatarAttachment -}}
- {{- template "defaultAvatarDimension" . -}}
- {{- else -}}
- {{- /* Use the avatar's proper dimensions. */ -}}
- {{- .AvatarAttachment.Meta.Original.Height -}}
- {{- end -}}
-{{- end }}
-{{- end -}}
-
-{{- define "avatarAlt" -}}
- Avatar for {{ .account.Username -}}
- {{- if .account.AvatarDescription }}
- {{- /* Add the avatar's image description. */ -}}
- : {{ .account.AvatarDescription -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "headerAlt" -}}
- Header for {{ .account.Username -}}
- {{- if .account.HeaderDescription }}
- {{- /* Add the header's image description. */ -}}
- : {{ .account.HeaderDescription -}}
- {{- end -}}
-{{- end -}}
-
-{{- define "avatar" -}}
-{{- with . }}
-<div
- class="media photoswipe-gallery odd single avatar-image-wrapper"
- role="group"
->
- <a
- class="photoswipe-slide"
- href="{{- .account.Avatar -}}"
- target="_blank"
- data-pswp-width="{{- template "avatarWidth" . -}}px"
- data-pswp-height="{{- template "avatarHeight" . -}}px"
- data-cropped="true"
- alt="{{- template "avatarAlt" . -}}"
- title="{{- template "avatarAlt" . -}}"
- >
- <picture
- aria-hidden="true"
- >
- {{- if .account.AvatarAttachment }}
- <source
- class="avatar"
- srcset="{{- .account.AvatarStatic -}}"
- type="{{- .account.AvatarAttachment.PreviewMIMEType -}}"
- media="(prefers-reduced-motion: reduce)"
- />
- {{- end }}
- <img
- class="avatar u-photo"
- src="{{- .account.Avatar -}}"
- alt="{{- template "avatarAlt" . -}}"
- title="{{- template "avatarAlt" . -}}"
- width="{{- template "avatarWidth" . -}}"
- height="{{- template "avatarHeight" . -}}"
- />
- </picture>
- </a>
-</div>
-{{- end }}
-{{- end -}}
-
{{- with . }}
<main class="profile h-card">
- <h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
- <section class="profile-header" role="region" aria-label="Basic info">
- {{- if .account.Moved }}
- {{- include "profileMovedTo" . | indent 2 }}
- {{- end }}
- <div class="header-image-wrapper">
- <picture>
- {{- if .account.HeaderAttachment }}
- <source
- srcset="{{- .account.HeaderStatic -}}"
- type="{{- .account.HeaderAttachment.PreviewMIMEType -}}"
- media="(prefers-reduced-motion: reduce)"
- />
- {{- end }}
- <img
- src="{{- .account.Header -}}"
- alt="{{- template "headerAlt" . -}}"
- title="{{- template "headerAlt" . -}}"
- />
- </picture>
- </div>
- <div class="basic-info">
- {{- with . }}
- {{- include "avatar" . | indent 3 }}
- {{- end }}
- <dl class="namerole">
- <dt class="sr-only">Display name</dt>
- <dd class="displayname text-cutoff p-name">
- {{- if .account.DisplayName -}}
- {{- emojify .account.Emojis (escape .account.DisplayName) -}}
- {{- else -}}
- {{- .account.Username -}}
- {{- end -}}
- </dd>
- <div class="bot-username-wrapper">
- {{- if .account.Bot }}
- <dt class="sr-only">Bot account</dt>
- <dd>
- <span class="sr-only">true</span>
- <div
- class="bot-legend-wrapper"
- aria-hidden="true"
- title="This is a bot account."
- >
- <i class="bot-icon fa fa-microchip"></i>
- <span class="bot-legend">bot</span>
- </div>
- </dd>
- {{- end }}
- <dt class="sr-only">Username</dt>
- <dd class="username text-cutoff p-nickname">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
- </div>
- {{- if .account.Roles }}
- <dt class="sr-only">Role</dt>
- {{- range .account.Roles }}
- <dd class="role {{ .Name -}}">{{- .Name -}}</dd>
- {{- end }}
- {{- end }}
- </dl>
- <a class="u-url u-uid hidden" rel="me" href="/@{{- .account.Username -}}"></a>
- </div>
- </section>
+ {{- with . }}
+ {{- include "profile_header.tmpl" . | indent 1 }}
+ {{- end }}
<div class="column-split">
- <section class="about-user" role="region" aria-labelledby="about-header">
- <div class="col-header">
- <h3 id="about-header">About<span class="sr-only">&nbsp;{{- .account.Username -}}</span></h3>
- </div>
- {{- if .account.Fields }}
- {{- include "profile_fields.tmpl" . | indent 3 }}
- {{- end }}
- <h4 class="sr-only">Bio</h4>
- <div class="bio p-note">
- {{- if .account.Note }}
- {{ emojify .account.Emojis (noescape .account.Note) }}
- {{- else }}
- <p>This GoToSocial user hasn't written a bio yet!</p>
- {{- end }}
- </div>
- <h4 class="sr-only">Stats</h4>
- <dl class="accountstats">
- <dt>Joined</dt>
- <dd><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
- <dt>Posts</dt>
- <dd>{{- .account.StatusesCount -}}</dd>
- <dt>Followed by</dt>
- <dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowersCount -}}{{- end -}}</dd>
- <dt>Following</dt>
- <dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowingCount -}}{{- end -}}</dd>
- </dl>
- </section>
+ {{- with . }}
+ {{- include "profile_about_user.tmpl" . | indent 2 }}
+ {{- end }}
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
{{- if .pinned_statuses }}
<section class="pinned statuses h-feed" aria-labelledby="pinned">
diff --git a/web/template/profile_about_user.tmpl b/web/template/profile_about_user.tmpl
new file mode 100644
index 000000000..7f3ce0e97
--- /dev/null
+++ b/web/template/profile_about_user.tmpl
@@ -0,0 +1,56 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- with . }}
+<section class="about-user" role="region" aria-labelledby="about-header">
+ <div class="col-header">
+ <h3 id="about-header">About<span class="sr-only">&nbsp;{{- .account.Username -}}</span></h3>
+ </div>
+ {{- if .account.Fields }}
+ {{- include "profile_fields.tmpl" . | indent 1 }}
+ {{- end }}
+ <h4 class="sr-only">Bio</h4>
+ <div class="bio p-note">
+ {{- if .account.Note }}
+ {{ emojify .account.Emojis (noescape .account.Note) }}
+ {{- else }}
+ <p>This GoToSocial user hasn't written a bio yet!</p>
+ {{- end }}
+ </div>
+ <h4 class="sr-only">Stats</h4>
+ <dl class="accountstats">
+ <div class="stats-item">
+ <dt class="joineddt text-cutoff">Joined</dt>
+ <dd class="joineddd text-cutoff"><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
+ </div>
+ <div class="stats-item">
+ <dt class="postsdt text-cutoff">Posts</dt>
+ <dd class="postsdd text-cutoff">{{- .account.StatusesCount -}}</dd>
+ </div>
+ <div class="stats-item">
+ <dt class="followeddt text-cutoff">Followed by</dt>
+ <dd class="followeddd text-cutoff">{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowersCount -}}{{- end -}}</dd>
+ </div>
+ <div class="stats-item">
+ <dt class="followingdt text-cutoff">Following</dt>
+ <dd class="followingdd text-cutoff">{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowingCount -}}{{- end -}}</dd>
+ </div>
+ </dl>
+</section>
+{{- end }} \ No newline at end of file
diff --git a/web/template/profile_header.tmpl b/web/template/profile_header.tmpl
new file mode 100644
index 000000000..4be900287
--- /dev/null
+++ b/web/template/profile_header.tmpl
@@ -0,0 +1,185 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- define "profileMovedTo" -}}
+{{- with .account.Moved }}
+<div class="moved-to">
+ <b>
+ ℹ️ This account has permanently moved to
+ <a
+ href="{{ .URL }}"
+ class="nounderline"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ >
+ @{{ .Username }}
+ </a>
+ </b>
+</div>
+{{- end }}
+{{- end -}}
+
+{{- define "defaultAvatarDimension" -}}
+{{- /* 136 is the default width/height for 8.5rem avatars, double it to get a good look when expanded. */ -}}
+272
+{{- end -}}
+
+{{- define "avatarWidth" -}}
+{{- with .account }}
+ {{- if isNil .AvatarAttachment -}}
+ {{- template "defaultAvatarDimension" . -}}
+ {{- else -}}
+ {{- /* Use the avatar's proper dimensions. */ -}}
+ {{- .AvatarAttachment.Meta.Original.Width -}}
+ {{- end -}}
+{{- end }}
+{{- end -}}
+
+{{- define "avatarHeight" -}}
+{{- with .account }}
+ {{- if isNil .AvatarAttachment -}}
+ {{- template "defaultAvatarDimension" . -}}
+ {{- else -}}
+ {{- /* Use the avatar's proper dimensions. */ -}}
+ {{- .AvatarAttachment.Meta.Original.Height -}}
+ {{- end -}}
+{{- end }}
+{{- end -}}
+
+{{- define "avatarAlt" -}}
+ Avatar for {{ .account.Username -}}
+ {{- if .account.AvatarDescription }}
+ {{- /* Add the avatar's image description. */ -}}
+ : {{ .account.AvatarDescription -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "headerAlt" -}}
+ Header for {{ .account.Username -}}
+ {{- if .account.HeaderDescription }}
+ {{- /* Add the header's image description. */ -}}
+ : {{ .account.HeaderDescription -}}
+ {{- end -}}
+{{- end -}}
+
+{{- define "avatar" -}}
+{{- with . }}
+<div
+ class="photoswipe-gallery odd single avatar-image-wrapper"
+ role="group"
+>
+ <a
+ class="photoswipe-slide"
+ href="{{- .account.Avatar -}}"
+ target="_blank"
+ data-pswp-width="{{- template "avatarWidth" . -}}px"
+ data-pswp-height="{{- template "avatarHeight" . -}}px"
+ data-cropped="true"
+ alt="{{- template "avatarAlt" . -}}"
+ title="{{- template "avatarAlt" . -}}"
+ >
+ <picture
+ aria-hidden="true"
+ >
+ {{- if .account.AvatarAttachment }}
+ <source
+ class="avatar"
+ srcset="{{- .account.AvatarStatic -}}"
+ type="{{- .account.AvatarAttachment.PreviewMIMEType -}}"
+ media="(prefers-reduced-motion: reduce)"
+ />
+ {{- end }}
+ <img
+ class="avatar u-photo"
+ src="{{- .account.Avatar -}}"
+ alt="{{- template "avatarAlt" . -}}"
+ title="{{- template "avatarAlt" . -}}"
+ width="{{- template "avatarWidth" . -}}"
+ height="{{- template "avatarHeight" . -}}"
+ />
+ </picture>
+ </a>
+</div>
+{{- end }}
+{{- end -}}
+
+{{- with . }}
+<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
+<section class="profile-header" role="region" aria-label="Basic info">
+ {{- if .account.Moved }}
+ {{- include "profileMovedTo" . | indent 2 }}
+ {{- end }}
+ <div class="header-image-wrapper">
+ <picture>
+ {{- if .account.HeaderAttachment }}
+ <source
+ srcset="{{- .account.HeaderStatic -}}"
+ type="{{- .account.HeaderAttachment.PreviewMIMEType -}}"
+ media="(prefers-reduced-motion: reduce)"
+ />
+ {{- end }}
+ <img
+ src="{{- .account.Header -}}"
+ alt="{{- template "headerAlt" . -}}"
+ title="{{- template "headerAlt" . -}}"
+ />
+ </picture>
+ </div>
+ <div class="basic-info">
+ {{- with . }}
+ {{- include "avatar" . | indent 3 }}
+ {{- end }}
+ <dl class="namerole">
+ <dt class="sr-only">Display name</dt>
+ <dd class="displayname text-cutoff p-name">
+ {{- if .account.DisplayName -}}
+ {{- emojify .account.Emojis (escape .account.DisplayName) -}}
+ {{- else -}}
+ {{- .account.Username -}}
+ {{- end -}}
+ </dd>
+ <div class="bot-username-wrapper">
+ {{- if .account.Bot }}
+ <dt class="sr-only">Bot account</dt>
+ <dd>
+ <span class="sr-only">true</span>
+ <div
+ class="bot-legend-wrapper"
+ aria-hidden="true"
+ title="This is a bot account."
+ >
+ <i class="bot-icon fa fa-microchip"></i>
+ <span class="bot-legend">bot</span>
+ </div>
+ </dd>
+ {{- end }}
+ <dt class="sr-only">Username</dt>
+ <dd class="username text-cutoff p-nickname">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
+ </div>
+ {{- if .account.Roles }}
+ <dt class="sr-only">Role</dt>
+ {{- range .account.Roles }}
+ <dd class="role {{ .Name -}}">{{- .Name -}}</dd>
+ {{- end }}
+ {{- end }}
+ </dl>
+ <a class="u-url u-uid hidden" rel="me" href="/@{{- .account.Username -}}"></a>
+ </div>
+</section>
+{{- end }} \ No newline at end of file
diff --git a/web/template/status.tmpl b/web/template/status.tmpl
index 85000fd72..872b784ed 100644
--- a/web/template/status.tmpl
+++ b/web/template/status.tmpl
@@ -30,6 +30,16 @@
it in an appropriate <article></article>!
*/ -}}
+{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
+{{- define "attachmentsLength" -}}
+{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
+{{- end -}}
+
+{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
+{{- define "galleryClass" -}}
+media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
+{{- end -}}
+
{{- with . }}
<header class="status-header">
{{- include "status_header.tmpl" . | indent 1 }}
@@ -63,7 +73,15 @@
</div>
{{- end }}
{{- if .MediaAttachments }}
- {{- include "status_attachments.tmpl" . | indent 1 }}
+ <div
+ class="{{- template "galleryClass" .MediaAttachments -}}"
+ role="group"
+ aria-label="{{- template "attachmentsLength" .MediaAttachments -}}"
+ >
+ {{- range $index, $media := .MediaAttachments }}
+ {{- includeIndex "status_attachment.tmpl" $media $index | indent 2 }}
+ {{- end }}
+ </div>
{{- end }}
</div>
<aside class="status-info" aria-hidden="true">
diff --git a/web/template/status_attachment.tmpl b/web/template/status_attachment.tmpl
new file mode 100644
index 000000000..bdfafa96f
--- /dev/null
+++ b/web/template/status_attachment.tmpl
@@ -0,0 +1,179 @@
+{{- /*
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/ -}}
+
+{{- define "imagePreview" }}
+<img
+ src="{{- .PreviewURL -}}"
+ loading="lazy"
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="{{- .Meta.Original.Width -}}"
+ height="{{- .Meta.Original.Height -}}"
+/>
+{{- end }}
+
+{{- define "videoPreview" }}
+<img
+ src="{{- .PreviewURL -}}"
+ loading="lazy"
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="{{- .Meta.Small.Width -}}"
+ height="{{- .Meta.Small.Height -}}"
+/>
+{{- end }}
+
+{{- define "audioPreview" }}
+{{- if and .PreviewURL .Meta.Small.Width }}
+<img
+ src="{{- .PreviewURL -}}"
+ loading="lazy"
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="{{- .Meta.Small.Width -}}"
+ height="{{- .Meta.Small.Height -}}"
+/>
+{{- else }}
+<img
+ src="/assets/logo.webp"
+ loading="lazy"
+ {{- if .Description }}
+ alt="{{- .Description -}}"
+ title="{{- .Description -}}"
+ {{- end }}
+ width="518"
+ height="460"
+/>
+{{- end }}
+{{- end }}
+
+{{- with . }}
+<div class="media-wrapper">
+ <details class="{{- .Item.Type -}}-spoiler media-spoiler" {{- if not .Item.Sensitive }} open{{- end -}}>
+ <summary>
+ <div class="show sensitive button" aria-hidden="true">Show sensitive</div>
+ <span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
+ <i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
+ <i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
+ </span>
+ {{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
+ {{- include "videoPreview" .Item | indent 3 }}
+ {{- else if eq .Item.Type "image" }}
+ {{- include "imagePreview" .Item | indent 3 }}
+ {{- else if eq .Item.Type "audio" }}
+ {{- include "audioPreview" .Item | indent 3 }}
+ {{- end }}
+ </summary>
+ {{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
+ <video
+ {{- if eq .Item.Type "video" }}
+ preload="none"
+ {{- else }}
+ preload="auto"
+ muted
+ {{- end }}
+ class="plyr-video photoswipe-slide{{- if eq .Item.Type "gifv" }} gifv{{ end }}"
+ controls
+ playsinline
+ data-pswp-index="{{- .Index -}}"
+ data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
+ data-pswp-attachment-id="{{- .Item.ID -}}"
+ poster="{{- .Item.PreviewURL -}}"
+ data-pswp-width="{{- .Item.Meta.Original.Width -}}px"
+ data-pswp-height="{{- .Item.Meta.Original.Height -}}px"
+ {{- if .Item.Description }}
+ alt="{{- .Item.Description -}}"
+ title="{{- .Item.Description -}}"
+ {{- end }}
+ >
+ <source type="{{- .Item.MIMEType -}}" src="{{- .Item.URL -}}"/>
+ </video>
+ {{- else if eq .Item.Type "audio" }}
+ <video
+ preload="none"
+ class="plyr-video photoswipe-slide"
+ controls
+ playsinline
+ data-pswp-index="{{- .Index -}}"
+ data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
+ data-pswp-attachment-id="{{- .Item.ID -}}"
+ {{- if and .Item.PreviewURL .Item.Meta.Small.Width }}
+ poster="{{- .Item.PreviewURL -}}"
+ data-pswp-width="{{- .Item.Meta.Small.Width -}}px"
+ data-pswp-height="{{- .Item.Meta.Small.Height -}}px"
+ {{- else }}
+ poster="/assets/logo.webp"
+ width="518px"
+ height="460px"
+ {{- end }}
+ {{- if .Item.Description }}
+ alt="{{- .Item.Description -}}"
+ title="{{- .Item.Description -}}"
+ {{- end }}
+ >
+ <source type="{{- .Item.MIMEType -}}" src="{{- .Item.URL -}}"/>
+ </video>
+ {{- else if eq .Item.Type "image" }}
+ <a
+ class="photoswipe-slide"
+ data-pswp-index="{{- .Index -}}"
+ data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
+ data-pswp-attachment-id="{{- .Item.ID -}}"
+ href="{{- .Item.URL -}}"
+ target="_blank"
+ data-pswp-width="{{- .Item.Meta.Original.Width -}}px"
+ data-pswp-height="{{- .Item.Meta.Original.Height -}}px"
+ data-cropped="true"
+ {{- if .Item.Description }}
+ alt="{{- .Item.Description -}}"
+ title="{{- .Item.Description -}}"
+ {{- end }}
+ >
+ {{- with .Item }}
+ {{- include "imagePreview" . | indent 3 }}
+ {{- end }}
+ </a>
+ {{- else }}
+ <a
+ class="unknown-attachment"
+ href="{{- .Item.RemoteURL -}}"
+ rel="nofollow noreferrer noopener"
+ target="_blank"
+ {{- if .Item.Description }}
+ title="Open external media: {{ .Item.Description -}}&#10;&#13;{{- .Item.RemoteURL -}}"
+ {{- else }}
+ title="Open external media.&#10;&#13;{{- .Item.RemoteURL -}}"
+ {{- end }}
+ >
+ <div class="placeholder" aria-hidden="true">
+ <i class="placeholder-external-link fa fa-external-link"></i>
+ <i class="placeholder-icon fa fa-file-text"></i>
+ <div class="placeholder-link-to">External media</div>
+ </div>
+ </a>
+ {{- end }}
+ </details>
+</div>
+{{- end }} \ No newline at end of file
diff --git a/web/template/status_attachments.tmpl b/web/template/status_attachments.tmpl
deleted file mode 100644
index ee564d934..000000000
--- a/web/template/status_attachments.tmpl
+++ /dev/null
@@ -1,195 +0,0 @@
-{{- /*
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/ -}}
-
-{{- /*
- Template for rendering a gallery of status media attachments.
- To use this template, pass a web view status into it.
-*/ -}}
-
-{{- define "imagePreview" }}
-<img
- src="{{- .PreviewURL -}}"
- loading="lazy"
- {{- if .Description }}
- alt="{{- .Description -}}"
- title="{{- .Description -}}"
- {{- end }}
- width="{{- .Meta.Original.Width -}}"
- height="{{- .Meta.Original.Height -}}"
-/>
-{{- end }}
-
-{{- define "videoPreview" }}
-<img
- src="{{- .PreviewURL -}}"
- loading="lazy"
- {{- if .Description }}
- alt="{{- .Description -}}"
- title="{{- .Description -}}"
- {{- end }}
- width="{{- .Meta.Small.Width -}}"
- height="{{- .Meta.Small.Height -}}"
-/>
-{{- end }}
-
-{{- define "audioPreview" }}
-{{- if and .PreviewURL .Meta.Small.Width }}
-<img
- src="{{- .PreviewURL -}}"
- loading="lazy"
- {{- if .Description }}
- alt="{{- .Description -}}"
- title="{{- .Description -}}"
- {{- end }}
- width="{{- .Meta.Small.Width -}}"
- height="{{- .Meta.Small.Height -}}"
-/>
-{{- else }}
-<img
- src="/assets/logo.webp"
- loading="lazy"
- {{- if .Description }}
- alt="{{- .Description -}}"
- title="{{- .Description -}}"
- {{- end }}
- width="518"
- height="460"
-/>
-{{- end }}
-{{- end }}
-
-{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
-{{- define "attachmentsLength" -}}
-{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
-{{- end -}}
-
-{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
-{{- define "galleryClass" -}}
-media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
-{{- end -}}
-
-{{- with .MediaAttachments }}
-<div
- class="{{- template "galleryClass" . -}}"
- role="group"
- aria-label="{{- template "attachmentsLength" . -}}"
->
- {{- range $index, $media := . }}
- <div class="media-wrapper">
- <details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{- end -}}>
- <summary>
- <div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
- <span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
- <i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
- <i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
- </span>
- {{- if or (eq .Type "video") (eq .Type "gifv") }}
- {{- 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 }}
- </summary>
- {{- if or (eq .Type "video") (eq .Type "gifv") }}
- <video
- {{- if eq .Type "video" }}
- preload="none"
- {{- else }}
- preload="auto"
- muted
- {{- end }}
- class="plyr-video photoswipe-slide{{- if eq .Type "gifv" }} gifv{{ end }}"
- controls
- playsinline
- data-pswp-index="{{- $index -}}"
- poster="{{- .PreviewURL -}}"
- data-pswp-width="{{- $media.Meta.Small.Width -}}px"
- data-pswp-height="{{- $media.Meta.Small.Height -}}px"
- {{- if .Description }}
- alt="{{- $media.Description -}}"
- title="{{- $media.Description -}}"
- {{- end }}
- >
- <source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
- </video>
- {{- else if eq .Type "audio" }}
- <video
- preload="none"
- class="plyr-video photoswipe-slide"
- controls
- playsinline
- data-pswp-index="{{- $index -}}"
- {{- if and $media.PreviewURL $media.Meta.Small.Width }}
- poster="{{- .PreviewURL -}}"
- data-pswp-width="{{- $media.Meta.Small.Width -}}px"
- data-pswp-height="{{- $media.Meta.Small.Height -}}px"
- {{- else }}
- poster="/assets/logo.webp"
- width="518px"
- height="460px"
- {{- end }}
- {{- if .Description }}
- alt="{{- $media.Description -}}"
- title="{{- $media.Description -}}"
- {{- end }}
- >
- <source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
- </video>
- {{- else if eq .Type "image" }}
- <a
- class="photoswipe-slide"
- href="{{- $media.URL -}}"
- target="_blank"
- data-pswp-width="{{- $media.Meta.Original.Width -}}px"
- data-pswp-height="{{- $media.Meta.Original.Height -}}px"
- data-cropped="true"
- {{- if .Description }}
- alt="{{- $media.Description -}}"
- title="{{- $media.Description -}}"
- {{- end }}
- >
- {{- with $media }}
- {{- include "imagePreview" . | indent 4 }}
- {{- end }}
- </a>
- {{- else }}
- <a
- class="unknown-attachment"
- href="{{- $media.RemoteURL -}}"
- rel="nofollow noreferrer noopener"
- target="_blank"
- {{- if .Description }}
- title="Open external media: {{ $media.Description -}}&#10;&#13;{{- $media.RemoteURL -}}"
- {{- else }}
- title="Open external media.&#10;&#13;{{- $media.RemoteURL -}}"
- {{- end }}
- >
- <div class="placeholder" aria-hidden="true">
- <i class="placeholder-external-link fa fa-external-link"></i>
- <i class="placeholder-icon fa fa-file-text"></i>
- <div class="placeholder-link-to">External media</div>
- </div>
- </a>
- {{- end }}
- </details>
- </div>
- {{- end }}
-</div>
-{{- end }} \ No newline at end of file
diff --git a/web/template/status_info.tmpl b/web/template/status_info.tmpl
index 5d26811d7..194a799f5 100644
--- a/web/template/status_info.tmpl
+++ b/web/template/status_info.tmpl
@@ -17,36 +17,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
-{{- define "visibility_icon" -}}
- {{- if eq .Visibility "public" -}}
- globe
- {{- else if eq .Visibility "unlisted" -}}
- unlock
- {{- else -}}
- question
- {{- end -}}
-{{- end -}}
-
-{{- define "visibility_title" -}}
- {{- if eq .Visibility "public" -}}
- Public
- {{- else if eq .Visibility "unlisted" -}}
- Unlisted
- {{- else -}}
- Unknown
- {{- end -}}
-{{- end -}}
-
{{- with . }}
<dl class="status-stats">
<div class="stats-grouping">
- <div class="stats-item visibility-level" title="{{- template "visibility_title" . -}}">
- <dt class="sr-only">Visibility</dt>
- <dd>
- <i class="fa fa-{{- template "visibility_icon" . -}}" aria-hidden="true"></i>
- <span class="sr-only">{{- template "visibility_title" . -}}</span>
- </dd>
- </div>
<div class="stats-item published-at text-cutoff">
<dt class="sr-only">Published</dt>
<dd>
@@ -57,7 +30,7 @@
<div class="stats-item edited-at text-cutoff">
<dt class="sr-only">Edited</dt>
<dd>
- edited <time class="dt-updated" datetime="{{- .EditedAt -}}">{{- .EditedAt | timestampPrecise -}}</time>
+ (edited <time class="dt-updated" datetime="{{- .EditedAt -}}">{{- .EditedAt | timestampPrecise -}}</time>)
</dd>
</div>
{{ end }}