Feat: display boosts on public profile

This commit is contained in:
vdyotte 2024-09-24 00:44:09 -04:00
parent d9e59820ed
commit af5a766f62
No known key found for this signature in database
GPG key ID: B170DEDABC527E66
14 changed files with 112 additions and 34 deletions

View file

@ -447,7 +447,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {
suite.Equal(http.StatusBadRequest, recorder.Code) suite.Equal(http.StatusBadRequest, recorder.Code)
// We should have a helpful error // We should have a helpful error
// message telling us how we screwed up. // message telling us how we screwed up.
suite.Equal(`{ suite.Equal(`{
"error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only" "error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only"
}`, out) }`, out)

View file

@ -118,6 +118,10 @@ type WebStatus struct {
// Override API account with web account. // Override API account with web account.
Account *WebAccount `json:"account"` Account *WebAccount `json:"account"`
// Account that reblogged the status.
// needed to properly render reblogged statuses on profile pages.
ReblogAccount *WebAccount `json:"reblog_account"`
// Web version of media // Web version of media
// attached to this status. // attached to this status.
MediaAttachments []*WebAttachment `json:"media_attachments"` MediaAttachments []*WebAttachment `json:"media_attachments"`

View file

@ -1017,6 +1017,7 @@ func (a *accountDB) GetAccountWebStatuses(
) ([]*gtsmodel.Status, error) { ) ([]*gtsmodel.Status, error) {
// Check for an easy case: account exposes no statuses via the web. // Check for an easy case: account exposes no statuses via the web.
webVisibility := account.Settings.WebVisibility webVisibility := account.Settings.WebVisibility
hideBoosts := *account.Settings.HideBoosts
if webVisibility == gtsmodel.VisibilityNone { if webVisibility == gtsmodel.VisibilityNone {
return nil, db.ErrNoEntries return nil, db.ErrNoEntries
} }
@ -1035,9 +1036,12 @@ func (a *accountDB) GetAccountWebStatuses(
// Select only IDs from table // Select only IDs from table
Column("status.id"). Column("status.id").
Where("? = ?", bun.Ident("status.account_id"), account.ID). Where("? = ?", bun.Ident("status.account_id"), account.ID).
// Don't show replies or boosts. // Don't show replies.
Where("? IS NULL", bun.Ident("status.in_reply_to_uri")). Where("? IS NULL", bun.Ident("status.in_reply_to_uri"))
Where("? IS NULL", bun.Ident("status.boost_of_id"))
if hideBoosts {
q = q.Where("? IS NULL", bun.Ident("status.boost_of_id"))
}
// Select statuses for this account according // Select statuses for this account according
// to their web visibility preference. // to their web visibility preference.

View file

@ -33,7 +33,7 @@ type AccountSettings struct {
Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set). Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
HideBoosts *bool `bun:",nullzero,notnull,default:false"` // Hide boosts from this accounts profile page. HideBoosts *bool `bun:",nullzero,notnull,default:false"` // Hide boosts from this accounts profile page.
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections. HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
WebVisibility Visibility `bun:",nullzero,notnull,default:public"` // Visibility level of statuses that visitors can view via the web profile. WebVisibility Visibility `bun:",nullzero,notnull,default:public"` // Visibility level of statuses that visitors can view via the web profile.
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy. InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.

View file

@ -123,8 +123,8 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
} }
// Add each status to the rss feed. // Add each status to the rss feed.
for _, status := range statuses { for _, s := range statuses {
item, err := p.converter.StatusToRSSItem(ctx, status) item, err := p.converter.StatusToRSSItem(ctx, s)
if err != nil { if err != nil {
err = gtserror.Newf("error converting status to feed item: %w", err) err = gtserror.Newf("error converting status to feed item: %w", err)
return "", gtserror.NewErrorInternalError(err) return "", gtserror.NewErrorInternalError(err)

View file

@ -42,6 +42,16 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
<description>Posts from @admin@localhost:8080</description> <description>Posts from @admin@localhost:8080</description>
<pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate> <pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate>
<lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate> <lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate>
<item>
<title>introduction post</title>
<link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>
<description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>
<content:encoded><![CDATA[hello everyone!]]></content:encoded>
<author>@the_mighty_zork@localhost:8080</author>
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>
<pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
</item>
<item> <item>
<title>open to see some puppies</title> <title>open to see some puppies</title>
<link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link> <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>

View file

@ -184,6 +184,7 @@ func (p *Processor) WebStatusesGet(
log.Errorf(ctx, "error convering to web status: %v", err) log.Errorf(ctx, "error convering to web status: %v", err)
continue continue
} }
items = append(items, item) items = append(items, item)
} }

View file

@ -310,7 +310,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
enableRSS bool enableRSS bool
theme string theme string
customCSS string customCSS string
hideBoosts bool hideBoosts bool
hideCollections bool hideCollections bool
) )
@ -1046,7 +1046,15 @@ func (c *Converter) StatusToWebStatus(
ctx context.Context, ctx context.Context,
s *gtsmodel.Status, s *gtsmodel.Status,
) (*apimodel.WebStatus, error) { ) (*apimodel.WebStatus, error) {
apiStatus, err := c.statusToFrontend(ctx, s,
isBoost := s.BoostOf != nil
status := s
if isBoost {
status = s.BoostOf
}
apiStatus, err := c.statusToFrontend(ctx, status,
nil, // No authed requester. nil, // No authed requester.
statusfilter.FilterContextNone, // No filters. statusfilter.FilterContextNone, // No filters.
nil, // No filters. nil, // No filters.
@ -1057,7 +1065,7 @@ func (c *Converter) StatusToWebStatus(
} }
// Convert status author to web model. // Convert status author to web model.
acct, err := c.AccountToWebAccount(ctx, s.Account) acct, err := c.AccountToWebAccount(ctx, status.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1067,6 +1075,14 @@ func (c *Converter) StatusToWebStatus(
Account: acct, Account: acct,
} }
if isBoost {
reblogAcct, err := c.AccountToWebAccount(ctx, s.Account)
if err != nil {
return nil, err
}
webStatus.ReblogAccount = reblogAcct
}
// Whack a newline before and after each "pre" to make it easier to outdent it. // Whack a newline before and after each "pre" to make it easier to outdent it.
webStatus.Content = strings.ReplaceAll(webStatus.Content, "<pre>", "\n<pre>") webStatus.Content = strings.ReplaceAll(webStatus.Content, "<pre>", "\n<pre>")
webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\n") webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\n")

View file

@ -1401,6 +1401,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"emojis": [], "emojis": [],
"fields": [] "fields": []
}, },
"reblog_account": null,
"media_attachments": [ "media_attachments": [
{ {
"id": "01HE7Y3C432WRSNS10EZM86SA5", "id": "01HE7Y3C432WRSNS10EZM86SA5",

View file

@ -39,6 +39,12 @@
func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*feeds.Item, error) { func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*feeds.Item, error) {
// see https://cyber.harvard.edu/rss/rss.html // see https://cyber.harvard.edu/rss/rss.html
// If status is a boost,
// display the boost instead.
if s.BoostOf != nil {
s = s.BoostOf
}
// Title -- The title of the item. // Title -- The title of the item.
// example: Venice Film Festival Tries to Quit Sinking // example: Venice Film Festival Tries to Quit Sinking
var title string var title string

View file

@ -657,7 +657,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
Sensitive: util.Ptr(false), Sensitive: util.Ptr(false),
Language: "en", Language: "en",
EnableRSS: util.Ptr(false), EnableRSS: util.Ptr(false),
HideBoosts: util.Ptr(false), HideBoosts: util.Ptr(false),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic, WebVisibility: gtsmodel.VisibilityPublic,
}, },
@ -669,7 +669,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
Sensitive: util.Ptr(false), Sensitive: util.Ptr(false),
Language: "en", Language: "en",
EnableRSS: util.Ptr(true), EnableRSS: util.Ptr(true),
HideBoosts: util.Ptr(false), HideBoosts: util.Ptr(false),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic, WebVisibility: gtsmodel.VisibilityPublic,
}, },
@ -681,7 +681,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
Sensitive: util.Ptr(false), Sensitive: util.Ptr(false),
Language: "en", Language: "en",
EnableRSS: util.Ptr(true), EnableRSS: util.Ptr(true),
HideBoosts: util.Ptr(false), HideBoosts: util.Ptr(false),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityUnlocked, WebVisibility: gtsmodel.VisibilityUnlocked,
}, },
@ -693,7 +693,7 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
Sensitive: util.Ptr(true), Sensitive: util.Ptr(true),
Language: "fr", Language: "fr",
EnableRSS: util.Ptr(false), EnableRSS: util.Ptr(false),
HideBoosts: util.Ptr(false), HideBoosts: util.Ptr(false),
HideCollections: util.Ptr(true), HideCollections: util.Ptr(true),
WebVisibility: gtsmodel.VisibilityPublic, WebVisibility: gtsmodel.VisibilityPublic,
}, },

View file

@ -41,6 +41,12 @@ main {
text-decoration: none; text-decoration: none;
} }
.boosted {
padding: 0 0.75rem 0.75rem;
color: var(--fg-reduced);
font-weight: bold;
}
.status-header > address { .status-header > address {
/* /*
Avoid stretching so wide that user Avoid stretching so wide that user
@ -59,17 +65,27 @@ main {
"avatar author-strap author-strap"; "avatar author-strap author-strap";
gap: 0 0.5rem; gap: 0 0.5rem;
font-style: normal; font-style: normal;
.avatar { .avatar {
grid-area: avatar; grid-area: avatar;
height: 3.5rem; height: 3.5rem;
width: 3.5rem; width: 3.5rem;
object-fit: cover; object-fit: cover;
position: relative;
border: 0.15rem solid $avatar-border; border: 0.15rem solid $avatar-border;
border-radius: $br; border-radius: $br;
overflow: hidden; /* hides corners from img overflowing */ overflow: hidden; /* hides corners from img overflowing */
.boosted-avatar {
height: 50%;
width: 50%;
z-index: 10;
position: absolute;
bottom: 0;
inset-inline-end: 0;
}
img { img {
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -77,7 +93,7 @@ main {
background: $bg; background: $bg;
} }
} }
.author-strap { .author-strap {
grid-area: author-strap; grid-area: author-strap;
display: grid; display: grid;
@ -87,7 +103,7 @@ main {
"display display" "display display"
"user user"; "user user";
gap: 0 0.5rem; gap: 0 0.5rem;
.displayname, .username { .displayname, .username {
justify-self: start; justify-self: start;
align-self: start; align-self: start;
@ -95,12 +111,12 @@ main {
font-size: 1rem; font-size: 1rem;
line-height: 1.3rem; line-height: 1.3rem;
} }
.displayname { .displayname {
grid-area: display; grid-area: display;
font-weight: bold; font-weight: bold;
} }
.username { .username {
grid-area: user; grid-area: user;
color: $link-fg; color: $link-fg;
@ -200,34 +216,34 @@ main {
.poll { .poll {
background-color: $gray2; background-color: $gray2;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: $br; border-radius: $br;
padding: 0.5rem; padding: 0.5rem;
margin: 0; margin: 0;
gap: 1rem; gap: 1rem;
.poll-options { .poll-options {
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
.poll-option { .poll-option {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.1rem; gap: 0.1rem;
label { label {
cursor: default; cursor: default;
} }
meter { meter {
width: 100%; width: 100%;
} }
.poll-vote-summary { .poll-vote-summary {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -236,7 +252,7 @@ main {
} }
} }
} }
.poll-info { .poll-info {
background-color: $gray4; background-color: $gray4;
display: flex; display: flex;
@ -245,7 +261,7 @@ main {
border-radius: $br-inner; border-radius: $br-inner;
padding: 0.25rem; padding: 0.25rem;
gap: 0.25rem; gap: 0.25rem;
span { span {
justify-self: center; justify-self: center;
white-space: nowrap; white-space: nowrap;
@ -301,12 +317,12 @@ main {
width: 100%; width: 100%;
z-index: 3; z-index: 3;
overflow: hidden; overflow: hidden;
display: grid; display: grid;
padding: 1rem; padding: 1rem;
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
grid-template-areas: grid-template-areas:
"eye sensitive ." "eye sensitive ."
". sensitive ."; ". sensitive .";
@ -369,7 +385,7 @@ main {
height: 100%; height: 100%;
padding: 0.8rem; padding: 0.8rem;
border: 0.2rem dashed $white2; border: 0.2rem dashed $white2;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -518,4 +534,4 @@ main {
.plyr { .plyr {
max-height: 100%; max-height: 100%;
} }
} }

View file

@ -247,6 +247,16 @@
class="status expanded" class="status expanded"
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }} {{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
> >
{{- if .ReblogAccount }}
<div class="boosted text-cutoff">
<i class="fa fa-retweet" aria-hidden="true"></i>&nbsp
{{- if $.account.DisplayName }}
{{- emojify $.account.Emojis (escape $.account.DisplayName) }} boosted
{{- else }}
{{- $.account.Username }} boosted
{{- end }}
</div>
{{- end }}
{{- include "status.tmpl" . | indent 6 }} {{- include "status.tmpl" . | indent 6 }}
</article> </article>
{{- end }} {{- end }}

View file

@ -48,6 +48,16 @@
alt="Avatar for {{ .Username -}}" alt="Avatar for {{ .Username -}}"
title="Avatar for {{ .Username -}}" title="Avatar for {{ .Username -}}"
> >
{{ if $.ReblogAccount }}
<img
class="boosted-avatar"
src="{{ $.ReblogAccount.Avatar }}"
alt="Avatar for {{ $.ReblogAccount.Username -}}"
title="Avatar for {{ $.ReblogAccount.Username -}}"
>
{{ end }}
</picture> </picture>
<div class="author-strap"> <div class="author-strap">
<span class="displayname text-cutoff"> <span class="displayname text-cutoff">
@ -63,4 +73,4 @@
<span class="sr-only">(open profile)</span> <span class="sr-only">(open profile)</span>
</a> </a>
</address> </address>
{{- end }} {{- end }}