diff --git a/internal/api/model/account.go b/internal/api/model/account.go index cf39dd08e..c5b629db0 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -110,17 +110,27 @@ type Account struct { // If set, indicates that this account is currently inactive, and has migrated to the given account. // Key/value omitted for accounts that haven't moved, and for suspended accounts. Moved *Account `json:"moved,omitempty"` +} - // Additional fields not exposed via JSON - // (used only internally for templating etc). +// WebAccount is like Account, but with +// additional fields not exposed via JSON; +// used only internally for templating etc. +// +// swagger:ignore +type WebAccount struct { + *Account // Proper attachment model for the avatar. // - // Only set if this model was converted via - // AccountToWebAccount, AND this account had - // an avatar set (and not just the default - // "blank" avatar image.) - AvatarAttachment *Attachment `json:"-"` + // Only set if this account had an avatar set + // (and not just the default "blank" image.) + AvatarAttachment *WebAttachment `json:"-"` + + // Proper attachment model for the header. + // + // Only set if this account had a header set + // (and not just the default "blank" image.) + HeaderAttachment *WebAttachment `json:"-"` } // MutedAccount extends Account with a field used only by the muted user list. diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index d0b0c81e5..21523a58e 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -107,6 +107,10 @@ type WebAttachment struct { // MIME type of // the attachment. MIMEType string + + // MIME type of + // the thumbnail. + PreviewMIMEType string } // MediaMeta models media metadata. diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 7358916ab..b3ac746d7 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -113,6 +113,9 @@ type Status struct { type WebStatus struct { *Status + // Override API account with web account. + Account *WebAccount `json:"account"` + // Web version of media // attached to this status. MediaAttachments []*WebAttachment `json:"media_attachments"` diff --git a/internal/api/util/opengraph.go b/internal/api/util/opengraph.go index 062151836..094c80021 100644 --- a/internal/api/util/opengraph.go +++ b/internal/api/util/opengraph.go @@ -84,7 +84,7 @@ func OGBase(instance *apimodel.InstanceV1) *OGMeta { // WithAccount uses the given account to build an ogMeta // struct specific to that account. It's suitable for serving // at account profile pages. -func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta { +func (og *OGMeta) WithAccount(account *apimodel.WebAccount) *OGMeta { og.Title = AccountTitle(account, og.SiteName) og.Type = "profile" og.URL = account.URL @@ -148,7 +148,7 @@ func (og *OGMeta) WithStatus(status *apimodel.WebStatus) *OGMeta { } // AccountTitle parses a page title from account and accountDomain -func AccountTitle(account *apimodel.Account, accountDomain string) string { +func AccountTitle(account *apimodel.WebAccount, accountDomain string) string { user := "@" + account.Acct + "@" + accountDomain if len(account.DisplayName) == 0 { diff --git a/internal/api/util/opengraph_test.go b/internal/api/util/opengraph_test.go index 2ecd6a740..4e94d78ef 100644 --- a/internal/api/util/opengraph_test.go +++ b/internal/api/util/opengraph_test.go @@ -51,13 +51,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { Languages: []string{"en"}, }) - accountMeta := baseMeta.WithAccount(&apimodel.Account{ + acct := &apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", Note: "

This is my profile, read it and weep! Weep then!

", Username: "example_account", - }) + } + + accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct}) suite.EqualValues(OGMeta{ Title: "example person!!, @example_account@example.org", @@ -84,13 +86,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() { Languages: []string{"en"}, }) - accountMeta := baseMeta.WithAccount(&apimodel.Account{ + acct := &apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", Note: "", // <- empty Username: "example_account", - }) + } + + accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct}) suite.EqualValues(OGMeta{ Title: "example person!!, @example_account@example.org", diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index 32d45054d..eac0f0c3f 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -98,7 +98,7 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account } // GetWeb returns the web model of a local account by username. -func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.Account, gtserror.WithCode) { +func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.WebAccount, gtserror.WithCode) { targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") if err != nil { if errors.Is(err, db.ErrNoEntries) { diff --git a/internal/text/emojify.go b/internal/text/emojify.go index 23730eaf9..00d121d3b 100644 --- a/internal/text/emojify.go +++ b/internal/text/emojify.go @@ -32,20 +32,41 @@ func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML { out := emojify( emojis, string(html), - func(url, code string, buf *bytes.Buffer) { - buf.WriteString(`:`)
-			buf.WriteString(code)
-			buf.WriteString(`:`) + func(url, staticURL, code string, buf *bytes.Buffer) { + // Open a picture tag so we + // can present multiple options. + buf.WriteString(``) + + // Static version. + buf.WriteString(``) + + // Original image source. + buf.WriteString(``) + + // Close the picture tag. + buf.WriteString(``) }, ) @@ -60,17 +81,18 @@ func EmojifyRSS(emojis []apimodel.Emoji, text string) string { return emojify( emojis, text, - func(url, code string, buf *bytes.Buffer) { - buf.WriteString(`:`)
-			buf.WriteString(code)
-			buf.WriteString(`:`) + func(url, staticURL, code string, buf *bytes.Buffer) { + // Original image source. + buf.WriteString(``) }, ) } @@ -85,7 +107,7 @@ func Demojify(text string) string { func emojify( emojis []apimodel.Emoji, input string, - write func(url, code string, buf *bytes.Buffer), + write func(url, staticURL, code string, buf *bytes.Buffer), ) string { // Build map of shortcodes. Normalize each // shortcode by readding closing colons. @@ -107,10 +129,11 @@ func(shortcode string, buf *bytes.Buffer) string { // Escape raw emoji content. url := html.EscapeString(emoji.URL) + staticURL := html.EscapeString(emoji.StaticURL) code := html.EscapeString(emoji.Shortcode) // Write emoji repr to buffer. - write(url, code, buf) + write(url, staticURL, code, buf) return buf.String() }, ) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index f11c4af21..03e09654d 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -170,22 +170,47 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A func (c *Converter) AccountToWebAccount( ctx context.Context, a *gtsmodel.Account, -) (*apimodel.Account, error) { - webAccount, err := c.AccountToAPIAccountPublic(ctx, a) +) (*apimodel.WebAccount, error) { + apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) if err != nil { return nil, err } + webAccount := &apimodel.WebAccount{ + Account: apiAccount, + } + // Set additional avatar information for - // serving the avatar in a nice photobox. - if a.AvatarMediaAttachment != nil { - avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, a.AvatarMediaAttachment) + // serving the avatar in a nice . + if ogAvi := a.AvatarMediaAttachment; ogAvi != nil { + avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, ogAvi) if err != nil { // This is just extra data so just // log but don't return any error. log.Errorf(ctx, "error converting account avatar attachment: %v", err) } else { - webAccount.AvatarAttachment = &avatarAttachment + webAccount.AvatarAttachment = &apimodel.WebAttachment{ + Attachment: &avatarAttachment, + MIMEType: ogAvi.File.ContentType, + PreviewMIMEType: ogAvi.Thumbnail.ContentType, + } + } + } + + // Set additional header information for + // serving the header in a nice . + if ogHeader := a.HeaderMediaAttachment; ogHeader != nil { + headerAttachment, err := c.AttachmentToAPIAttachment(ctx, ogHeader) + if err != nil { + // This is just extra data so just + // log but don't return any error. + log.Errorf(ctx, "error converting account header attachment: %v", err) + } else { + webAccount.HeaderAttachment = &apimodel.WebAttachment{ + Attachment: &headerAttachment, + MIMEType: ogHeader.File.ContentType, + PreviewMIMEType: ogHeader.Thumbnail.ContentType, + } } } @@ -747,7 +772,15 @@ func (c *Converter) StatusToAPIStatus( filters []*gtsmodel.Filter, mutes *usermute.CompiledUserMuteList, ) (*apimodel.Status, error) { - apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters, mutes) + apiStatus, err := c.statusToFrontend( + ctx, + s, + requestingAccount, // Can be nil. + filterContext, // Can be empty. + filters, + mutes, + false, // This is not a web status. + ) if err != nil { return nil, err } @@ -958,20 +991,25 @@ func (c *Converter) StatusToWebStatus( ctx context.Context, s *gtsmodel.Status, ) (*apimodel.WebStatus, error) { - apiStatus, err := c.statusToFrontend( - ctx, - s, - nil, // No authed requester. - statusfilter.FilterContextNone, - nil, // No filters. - nil, // No mutes. + apiStatus, err := c.statusToFrontend(ctx, s, + nil, // No authed requester. + statusfilter.FilterContextNone, // No filters. + nil, // No filters. + nil, // No mutes. + true, // Web status. ) if err != nil { return nil, err } + webAccount, err := c.AccountToWebAccount(ctx, s.Account) + if err != nil { + return nil, err + } + webStatus := &apimodel.WebStatus{ - Status: apiStatus, + Status: apiStatus, + Account: webAccount, } // Whack a newline before and after each "pre" to make it easier to outdent it. @@ -1062,9 +1100,10 @@ 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, + Attachment: apiAttachment, + Sensitive: apiStatus.Sensitive, + MIMEType: ogAttachment.File.ContentType, + PreviewMIMEType: ogAttachment.Thumbnail.ContentType, } } @@ -1097,6 +1136,7 @@ func (c *Converter) statusToFrontend( filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, mutes *usermute.CompiledUserMuteList, + web bool, ) ( *apimodel.Status, error, @@ -1107,6 +1147,7 @@ func (c *Converter) statusToFrontend( filterContext, filters, mutes, + web, ) if err != nil { return nil, err @@ -1119,6 +1160,7 @@ func (c *Converter) statusToFrontend( filterContext, filters, mutes, + web, ) if errors.Is(err, statusfilter.ErrHideStatus) { // If we'd hide the original status, hide the boost. @@ -1149,6 +1191,7 @@ func (c *Converter) baseStatusToFrontend( filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, mutes *usermute.CompiledUserMuteList, + web bool, ) ( *apimodel.Status, error, @@ -1169,9 +1212,21 @@ func (c *Converter) baseStatusToFrontend( } } - apiAuthorAccount, err := c.AccountToAPIAccountPublic(ctx, s.Account) + var ( + apiAuthorAccount *apimodel.Account + err error + ) + + // Only bother converting the author account if + // this is an API status and not a web status. + // + // If it's a web status, the web function will + // convert the account into a web account instead. + if !web { + apiAuthorAccount, err = c.AccountToAPIAccountPublic(ctx, s.Account) if err != nil { return nil, gtserror.Newf("error converting status author: %w", err) + } } repliesCount, err := c.state.DB.CountStatusReplies(ctx, s.ID) diff --git a/internal/web/thread.go b/internal/web/thread.go index de3d1b361..d3ba6ea5e 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -108,7 +108,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { } // Ensure status actually belongs to target account. - if context.Status.GetAccountID() != targetAccount.ID { + if context.Status.Account.ID != targetAccount.ID { err := fmt.Errorf("target account %s does not own status %s", targetUsername, targetStatusID) apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) return diff --git a/web/source/css/base.css b/web/source/css/base.css index 522820f15..630e4a6d2 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -187,18 +187,20 @@ input, select, textarea, .input { margin: -0.2em 0.02em 0; object-fit: contain; vertical-align: middle; - transition: 0.1s; - - /* - Enlarge emojis on hover to give - viewer a good look at them. - */ - &:hover, &:active { - transform: scale(2); - background-color: $bg; - box-shadow: $boxshadow; - border: $boxshadow-border; - border-radius: $br-inner; + + @media (prefers-reduced-motion: no-preference) { + /* + Enlarge emojis on hover to give + viewer a good look at them. + */ + transition: 0.1s; + &:hover, &:active { + transform: scale(2); + background-color: $bg; + box-shadow: $boxshadow; + border: $boxshadow-border; + border-radius: $br-inner; + } } } diff --git a/web/source/css/status.css b/web/source/css/status.css index 5c7400654..21dc3416e 100644 --- a/web/source/css/status.css +++ b/web/source/css/status.css @@ -193,14 +193,6 @@ main { font-size: 1rem; line-height: initial; } - - img { - max-width: 100%; - margin: 5px auto; - } - img[alt~="!center"] { - display: block; - } } .poll { diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl index 256bbdccf..a06c842ab 100644 --- a/web/template/profile.tmpl +++ b/web/template/profile.tmpl @@ -94,14 +94,26 @@ alt="{{- template "avatarAlt" . -}}" title="{{- template "avatarAlt" . -}}" > - {{- template + {{- end }} @@ -115,11 +127,20 @@ {{- include "profileMovedTo" . | indent 2 }} {{- end }}
- {{- template + + {{- if .account.HeaderAttachment }} + + {{- end }} + {{- template +
{{- with . }} diff --git a/web/template/status_header.tmpl b/web/template/status_header.tmpl index 8946a1030..01b73aea0 100644 --- a/web/template/status_header.tmpl +++ b/web/template/status_header.tmpl @@ -32,13 +32,23 @@ title="Open remote profile (opens in a new window)" > {{- end }} - + {{- if .AvatarAttachment }} + + {{- end }} + Avatar for {{ .Username -}} +
{{- if .DisplayName -}}