Compare commits

...

7 commits

Author SHA1 Message Date
Victor Dyotte df74bcf7af
Merge 40c33ccc49 into fab7d17031 2024-10-19 10:47:27 +00:00
tobi fab7d17031
[bugfix] Fix filter title unique constraint (#3458) 2024-10-19 11:04:07 +02:00
vdyotte 40c33ccc49
Fix: update swagger doc 2024-09-24 16:13:49 -04:00
Victor Dyotte 90b773ae2a
Merge branch 'main' into profile-boosts 2024-09-24 15:51:41 -04:00
vdyotte 4b7d7f9b8b
Feat: document new hide boots setting 2024-09-24 15:49:56 -04:00
vdyotte af5a766f62
Feat: display boosts on public profile 2024-09-24 15:22:10 -04:00
S0yKaf d9e59820ed Feat: add "HideBoots" option to account settings 2024-09-23 12:53:21 -04:00
23 changed files with 453 additions and 47 deletions

View file

@ -284,6 +284,12 @@ definitions:
example: https://example.org/media/some_user/header/static/header.png example: https://example.org/media/some_user/header/static/header.png
type: string type: string
x-go-name: HeaderStatic x-go-name: HeaderStatic
hide_boosts:
description: |-
Account has opted to hide boosts from their profile.
Key/value omitted if false.
type: boolean
x-go-name: HideBoosts
hide_collections: hide_collections:
description: |- description: |-
Account has opted to hide their followers/following collections. Account has opted to hide their followers/following collections.
@ -2289,6 +2295,12 @@ definitions:
example: https://example.org/media/some_user/header/static/header.png example: https://example.org/media/some_user/header/static/header.png
type: string type: string
x-go-name: HeaderStatic x-go-name: HeaderStatic
hide_boosts:
description: |-
Account has opted to hide boosts from their profile.
Key/value omitted if false.
type: boolean
x-go-name: HideBoosts
hide_collections: hide_collections:
description: |- description: |-
Account has opted to hide their followers/following collections. Account has opted to hide their followers/following collections.

View file

@ -88,9 +88,9 @@ This setting does not affect visibility of your posts over the ActivityPub proto
!!! warning !!! warning
Be aware that changes to this setting also apply retroactively. Be aware that changes to this setting also apply retroactively.
That is, if you previously made a post on Unlisted visibility, while set to show only Public posts on your profile, and you change this setting to show Public and Unlisted, then the Unlisted post you previously made will be visible on your profile alongside your Public posts. That is, if you previously made a post on Unlisted visibility, while set to show only Public posts on your profile, and you change this setting to show Public and Unlisted, then the Unlisted post you previously made will be visible on your profile alongside your Public posts.
Likewise, if you change this setting to show no posts, then all your posts will be hidden from your profile, regardless of when you created them, and what this option was set to at the time. This will apply until you change this setting again. Likewise, if you change this setting to show no posts, then all your posts will be hidden from your profile, regardless of when you created them, and what this option was set to at the time. This will apply until you change this setting again.
!!! tip !!! tip
@ -134,6 +134,11 @@ This feed only includes posts set as 'Public' (see [Privacy Settings](./posts.md
!!! warning !!! warning
Exposing your RSS feed allows *anyone* to subscribe to updates on your Public posts anonymously, bypassing follows and follow requests. Exposing your RSS feed allows *anyone* to subscribe to updates on your Public posts anonymously, bypassing follows and follow requests.
#### Hide boosts from your public page
By default, GoToSocial will display posts boosted by you on your public web profile. If you do not wish to display them, You can hide them by checking this box.
#### Hide Who You Follow / Are Followed By #### Hide Who You Follow / Are Followed By
By default, GoToSocial shows your following/followers counts on your public web profile, and allows others to see who you follow and are followed by. This can be useful for account discovery purposes. However, for privacy + safety reasons you may wish to hide these counts, and to hide your following/followers lists from other accounts. You can do this by checking this box. By default, GoToSocial shows your following/followers counts on your public web profile, and allows others to see who you follow and are followed by. This can be useful for account discovery purposes. However, for privacy + safety reasons you may wish to hide these counts, and to hide your following/followers lists from other accounts. You can do this by checking this box.
@ -196,7 +201,7 @@ If you want to reset all your policies to the initial defaults, you can click on
!!! danger !!! danger
While GoToSocial respects interaction policies, it is not guaranteed that other server softwares will, and it is possible that accounts on other servers will still send out replies and boosts of your post to their followers, even if your instance forbids these interactions. While GoToSocial respects interaction policies, it is not guaranteed that other server softwares will, and it is possible that accounts on other servers will still send out replies and boosts of your post to their followers, even if your instance forbids these interactions.
As more ActivityPub servers roll out support for interaction policies, this issue will hopefully diminish, but in the meantime GoToSocial can offer only a "best effort" attempt to restrict interactions with your posts according to the policies you have set. As more ActivityPub servers roll out support for interaction policies, this issue will hopefully diminish, but in the meantime GoToSocial can offer only a "best effort" attempt to restrict interactions with your posts according to the policies you have set.
## Email & Password ## Email & Password

View file

@ -348,6 +348,7 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
form.Theme == nil && form.Theme == nil &&
form.CustomCSS == nil && form.CustomCSS == nil &&
form.EnableRSS == nil && form.EnableRSS == nil &&
form.HideBoosts == nil &&
form.HideCollections == nil && form.HideCollections == nil &&
form.WebVisibility == nil) { form.WebVisibility == nil) {
return nil, errors.New("empty form submitted") return nil, errors.New("empty form submitted")

View file

@ -104,6 +104,9 @@ type Account struct {
// Account has enabled RSS feed. // Account has enabled RSS feed.
// Key/value omitted if false. // Key/value omitted if false.
EnableRSS bool `json:"enable_rss,omitempty"` EnableRSS bool `json:"enable_rss,omitempty"`
// Account has opted to hide boosts from their profile.
// Key/value omitted if false.
HideBoosts bool `json:"hide_boosts,omitempty"`
// Account has opted to hide their followers/following collections. // Account has opted to hide their followers/following collections.
// Key/value omitted if false. // Key/value omitted if false.
HideCollections bool `json:"hide_collections,omitempty"` HideCollections bool `json:"hide_collections,omitempty"`
@ -225,6 +228,8 @@ type UpdateCredentialsRequest struct {
CustomCSS *string `form:"custom_css" json:"custom_css"` CustomCSS *string `form:"custom_css" json:"custom_css"`
// Enable RSS feed of public toots for this account at /@[username]/feed.rss // Enable RSS feed of public toots for this account at /@[username]/feed.rss
EnableRSS *bool `form:"enable_rss" json:"enable_rss"` EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
// Hide boosts from this account's profile page.
HideBoosts *bool `form:"hide_boosts" json:"hide_boosts"`
// Hide this account's following/followers collections. // Hide this account's following/followers collections.
HideCollections *bool `form:"hide_collections" json:"hide_collections"` HideCollections *bool `form:"hide_collections" json:"hide_collections"`
// Visibility of statuses to show via the web view. // Visibility of statuses to show via the web view.

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

@ -247,6 +247,54 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
} }
} }
func (suite *FilterTestSuite) TestFilterTitleOverlap() {
var (
ctx = context.Background()
account1 = "01HNEJXCPRTJVJY9MV0VVHGD47"
account2 = "01JAG5BRJPJYA0FSA5HR2MMFJH"
)
// Create an empty filter for account 1.
account1filter1 := &gtsmodel.Filter{
ID: "01HNEJNVZZVXJTRB3FX3K2B1YF",
AccountID: account1,
Title: "my filter",
Action: gtsmodel.FilterActionWarn,
ContextHome: util.Ptr(true),
}
if err := suite.db.PutFilter(ctx, account1filter1); err != nil {
suite.FailNow("", "error putting account1filter1: %s", err)
}
// Create a filter for account 2 with
// the same title, should be no issue.
account2filter1 := &gtsmodel.Filter{
ID: "01JAG5GPXG7H5Y4ZP78GV1F2ET",
AccountID: account2,
Title: "my filter",
Action: gtsmodel.FilterActionWarn,
ContextHome: util.Ptr(true),
}
if err := suite.db.PutFilter(ctx, account2filter1); err != nil {
suite.FailNow("", "error putting account2filter1: %s", err)
}
// Try to create another filter for
// account 1 with the same name as
// an existing filter of theirs.
account1filter2 := &gtsmodel.Filter{
ID: "01JAG5J8NYKQE2KYCD28Y4P05V",
AccountID: account1,
Title: "my filter",
Action: gtsmodel.FilterActionWarn,
ContextHome: util.Ptr(true),
}
err := suite.db.PutFilter(ctx, account1filter2)
if !errors.Is(err, db.ErrAlreadyExists) {
suite.FailNow("", "wanted ErrAlreadyExists, got %s", err)
}
}
func TestFilterTestSuite(t *testing.T) { func TestFilterTestSuite(t *testing.T) {
suite.Run(t, new(FilterTestSuite)) suite.Run(t, new(FilterTestSuite))
} }

View file

@ -20,7 +20,7 @@
import ( import (
"context" "context"
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240126064004_add_filters"
"github.com/uptrace/bun" "github.com/uptrace/bun"
) )

View file

@ -0,0 +1,65 @@
// 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 gtsmodel
import (
"regexp"
"time"
)
// Filter stores a filter created by a local account.
type Filter struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
ExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Time filter should expire. If null, should not expire.
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter.
Title string `bun:",nullzero,notnull,unique"` // The name of the filter.
Action string `bun:",nullzero,notnull"` // The action to take.
Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter.
Statuses []*FilterStatus `bun:"-"` // Statuses for this filter.
ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications.
ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread.
ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile.
}
// FilterKeyword stores a single keyword to filter statuses against.
type FilterKeyword struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_keywords_filter_id_keyword_uniq"` // ID of the filter that this keyword belongs to.
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
Keyword string `bun:",nullzero,notnull,unique:filter_keywords_filter_id_keyword_uniq"` // The keyword or phrase to filter against.
WholeWord *bool `bun:",nullzero,notnull,default:false"` // Should the filter consider word boundaries?
Regexp *regexp.Regexp `bun:"-"` // pre-prepared regular expression
}
// FilterStatus stores a single status to filter.
type FilterStatus struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter keyword.
FilterID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the filter that this keyword belongs to.
Filter *Filter `bun:"-"` // Filter corresponding to FilterID
StatusID string `bun:"type:CHAR(26),notnull,nullzero,unique:filter_statuses_filter_id_status_id_uniq"` // ID of the status to filter.
}

View file

@ -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/>.
package migrations
import (
"context"
"strings"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? BOOLEAN DEFAULT FALSE", bun.Ident("account_settings"), bun.Ident("hide_boosts"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
return nil
}
down := func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("account_settings"), bun.Ident("hide_boosts"))
return err
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -0,0 +1,131 @@
// 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/gtsmodel"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Create the new filters table
// with the unique constraint
// set on AccountID + Title.
if _, err := tx.
NewCreateTable().
ModelTableExpr("new_filters").
Model((*gtsmodel.Filter)(nil)).
Exec(ctx); err != nil {
return err
}
// Explicitly specify columns to bring
// from old table to new, to avoid any
// potential Postgres shenanigans.
columns := []string{
"id",
"created_at",
"updated_at",
"expires_at",
"account_id",
"title",
"action",
"context_home",
"context_notifications",
"context_public",
"context_thread",
"context_account",
}
// Copy all data for existing
// filters to the new table.
if _, err := tx.
NewInsert().
Table("new_filters").
Table("filters").
Column(columns...).
Exec(ctx); err != nil {
return err
}
// Drop the old table.
if _, err := tx.
NewDropTable().
Table("filters").
Exec(ctx); err != nil {
return err
}
// Rename new table to old table.
if _, err := tx.
ExecContext(
ctx,
"ALTER TABLE ? RENAME TO ?",
bun.Ident("new_filters"),
bun.Ident("filters"),
); err != nil {
return err
}
// Index the new version
// of the filters table.
if _, err := tx.
NewCreateIndex().
Table("filters").
Index("filters_account_id_idx").
Column("account_id").
IfNotExists().
Exec(ctx); err != nil {
return err
}
if db.Dialect().Name() == dialect.PG {
// Rename "new_filters_pkey" from the
// new table to just "filters_pkey".
// This is only necessary on Postgres.
if _, err := tx.ExecContext(
ctx,
"ALTER TABLE ? RENAME CONSTRAINT ? TO ?",
bun.Ident("public.filters"),
bun.Safe("new_filters_pkey"),
bun.Safe("filters_pkey"),
); err != nil {
return err
}
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -33,6 +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.
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

@ -26,20 +26,20 @@
// Filter stores a filter created by a local account. // Filter stores a filter created by a local account.
type Filter struct { type Filter struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
ExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Time filter should expire. If null, should not expire. ExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Time filter should expire. If null, should not expire.
AccountID string `bun:"type:CHAR(26),notnull,nullzero"` // ID of the local account that created the filter. AccountID string `bun:"type:CHAR(26),notnull,nullzero,unique:filters_account_id_title_uniq"` // ID of the local account that created the filter.
Title string `bun:",nullzero,notnull,unique"` // The name of the filter. Title string `bun:",nullzero,notnull,unique:filters_account_id_title_uniq"` // The name of the filter.
Action FilterAction `bun:",nullzero,notnull"` // The action to take. Action FilterAction `bun:",nullzero,notnull"` // The action to take.
Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter. Keywords []*FilterKeyword `bun:"-"` // Keywords for this filter.
Statuses []*FilterStatus `bun:"-"` // Statuses for this filter. Statuses []*FilterStatus `bun:"-"` // Statuses for this filter.
ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists. ContextHome *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications. ContextNotifications *bool `bun:",nullzero,notnull,default:false"` // Apply filter to notifications.
ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists. ContextPublic *bool `bun:",nullzero,notnull,default:false"` // Apply filter to home timeline and lists.
ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread. ContextThread *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing a status's associated thread.
ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile. ContextAccount *bool `bun:",nullzero,notnull,default:false"` // Apply filter when viewing an account profile.
} }
// Expired returns whether the filter has expired at a given time. // Expired returns whether the filter has expired at a given time.

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

@ -274,6 +274,11 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
settingsColumns = append(settingsColumns, "enable_rss") settingsColumns = append(settingsColumns, "enable_rss")
} }
if form.HideBoosts != nil {
account.Settings.HideBoosts = form.HideBoosts
settingsColumns = append(settingsColumns, "hide_boosts")
}
if form.HideCollections != nil { if form.HideCollections != nil {
account.Settings.HideCollections = form.HideCollections account.Settings.HideCollections = form.HideCollections
settingsColumns = append(settingsColumns, "hide_collections") settingsColumns = append(settingsColumns, "hide_collections")

View file

@ -302,7 +302,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
// Bits that vary between remote + local accounts: // Bits that vary between remote + local accounts:
// - Account (acct) string. // - Account (acct) string.
// - Role. // - Role.
// - Settings things (enableRSS, theme, customCSS, hideCollections). // - Settings things (enableRSS, theme, customCSS, hideBoosts ,hideCollections).
var ( var (
acct string acct string
@ -310,6 +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
hideCollections bool hideCollections bool
) )
@ -338,6 +339,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
enableRSS = *a.Settings.EnableRSS enableRSS = *a.Settings.EnableRSS
theme = a.Settings.Theme theme = a.Settings.Theme
customCSS = a.Settings.CustomCSS customCSS = a.Settings.CustomCSS
hideBoosts = *a.Settings.HideBoosts
hideCollections = *a.Settings.HideCollections hideCollections = *a.Settings.HideCollections
} }
@ -380,6 +382,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
Theme: theme, Theme: theme,
CustomCSS: customCSS, CustomCSS: customCSS,
EnableRSS: enableRSS, EnableRSS: enableRSS,
HideBoosts: hideBoosts,
HideCollections: hideCollections, HideCollections: hideCollections,
Roles: roles, Roles: roles,
} }
@ -1092,7 +1095,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.
@ -1103,7 +1114,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
} }
@ -1113,6 +1124,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

@ -1402,6 +1402,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,6 +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),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic, WebVisibility: gtsmodel.VisibilityPublic,
}, },
@ -668,6 +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),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityPublic, WebVisibility: gtsmodel.VisibilityPublic,
}, },
@ -679,6 +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),
HideCollections: util.Ptr(false), HideCollections: util.Ptr(false),
WebVisibility: gtsmodel.VisibilityUnlocked, WebVisibility: gtsmodel.VisibilityUnlocked,
}, },
@ -690,6 +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),
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

@ -77,7 +77,7 @@ function UserProfileForm({ data: profile }) {
maxPinnedFields: instance?.configuration?.accounts?.max_profile_fields ?? 6 maxPinnedFields: instance?.configuration?.accounts?.max_profile_fields ?? 6
}; };
}, [instance]); }, [instance]);
// Parse out available theme options into nice format. // Parse out available theme options into nice format.
const { data: themes } = useAccountThemesQuery(); const { data: themes } = useAccountThemesQuery();
const themeOptions = useMemo(() => { const themeOptions = useMemo(() => {
@ -114,6 +114,7 @@ function UserProfileForm({ data: profile }) {
locked: useBoolInput("locked", { source: profile }), locked: useBoolInput("locked", { source: profile }),
discoverable: useBoolInput("discoverable", { source: profile}), discoverable: useBoolInput("discoverable", { source: profile}),
enableRSS: useBoolInput("enable_rss", { source: profile }), enableRSS: useBoolInput("enable_rss", { source: profile }),
hideBoosts: useBoolInput("hide_boosts", { source: profile }),
hideCollections: useBoolInput("hide_collections", { 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) => p.source?.web_visibility }),
fields: useFieldArrayInput("fields_attributes", { fields: useFieldArrayInput("fields_attributes", {
@ -158,7 +159,7 @@ function UserProfileForm({ data: profile }) {
autoCapitalize="sentences" autoCapitalize="sentences"
/> />
</div> </div>
<div className="file-input-with-image-description"> <div className="file-input-with-image-description">
<FileInput <FileInput
label="Avatar (1:1 images look best)" label="Avatar (1:1 images look best)"
@ -257,6 +258,10 @@ function UserProfileForm({ data: profile }) {
field={form.enableRSS} field={form.enableRSS}
label="Enable RSS feed of posts." label="Enable RSS feed of posts."
/> />
<Checkbox
field={form.hideBoosts}
label="Hide boosts from your public page"
/>
<Checkbox <Checkbox
field={form.hideCollections} field={form.hideCollections}
label="Hide who you follow / are followed by." label="Hide who you follow / are followed by."

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 }}
@ -264,4 +274,4 @@
</div> </div>
</div> </div>
</main> </main>
{{- 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 }}