Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2f7564e
link: bypass this restriction
jsoref May 3, 2026
115ebee
spelling: ; otherwise
jsoref May 3, 2026
a8733e7
spelling: a quoted
jsoref May 3, 2026
4be3c21
spelling: a
jsoref May 3, 2026
b4ec70e
spelling: absence
jsoref May 3, 2026
4dd0033
spelling: acquired
jsoref May 3, 2026
12ac6a0
spelling: an
jsoref May 3, 2026
4903083
spelling: automatically
jsoref May 3, 2026
6b367ab
spelling: cannot
jsoref May 3, 2026
3877320
spelling: doesn't
jsoref May 3, 2026
347dc74
spelling: english
jsoref May 3, 2026
07325c1
spelling: environment
jsoref May 3, 2026
c47c352
spelling: fall back
jsoref May 3, 2026
f798011
spelling: falls back
jsoref May 3, 2026
aa3cb02
spelling: instance
jsoref May 3, 2026
15ae966
spelling: message
jsoref May 3, 2026
fea71f7
spelling: missing a
jsoref May 3, 2026
276d9f8
spelling: multiple
jsoref May 3, 2026
69cc6b4
spelling: needs
jsoref May 3, 2026
c5302ab
spelling: notifications
jsoref May 3, 2026
7f6f30f
spelling: occurred
jsoref May 3, 2026
ab02690
spelling: onerous
jsoref May 3, 2026
524d21e
spelling: or falls back
jsoref May 3, 2026
0a4a4de
spelling: original
jsoref May 3, 2026
782c9ee
spelling: panic error
jsoref May 3, 2026
36d5855
spelling: prefix
jsoref May 3, 2026
6e98ca3
spelling: quote
jsoref May 3, 2026
8229e35
spelling: quoting
jsoref May 3, 2026
0fe08f5
spelling: represents
jsoref May 3, 2026
ff2626f
spelling: search
jsoref May 3, 2026
1a912d1
spelling: set up
jsoref May 3, 2026
ed67ac0
spelling: simplest
jsoref May 3, 2026
27d86e8
spelling: slug
jsoref May 3, 2026
6ee1682
spelling: stringify
jsoref May 4, 2026
466eed7
spelling: subscribers
jsoref May 3, 2026
c8682cc
spelling: successfully
jsoref May 3, 2026
8fd09e9
spelling: supported
jsoref May 3, 2026
d8e83ec
spelling: suppress
jsoref May 4, 2026
0733484
spelling: tabular
jsoref May 3, 2026
185086e
spelling: temporary
jsoref May 3, 2026
175eb0e
spelling: the
jsoref May 3, 2026
dc364ca
spelling: tooltip
jsoref May 3, 2026
1eadbb8
spelling: under
jsoref May 3, 2026
eeffff2
spelling: unknown
jsoref May 3, 2026
07b79a7
spelling: unnamed
jsoref May 3, 2026
bb8e0c1
spelling: uppercase
jsoref May 3, 2026
c104c02
spelling: whether or not
jsoref May 3, 2026
d86c267
More spelling fixes
mattwoberts May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

There are many ways you can contribute to Fider.

- **Send us a Pull Request** on GitHub. Make sure you read our [Getting Started](#getting-started-with-fider-codebase) guide to learn how to setup the development environment;
- **Send us a Pull Request** on GitHub. Make sure you read our [Getting Started](#getting-started-with-fider-codebase) guide to learn how to set up the development environment;
- **Translate Fider** to your language. See our [translation guide](/locale/README.md) to learn how to add a new language;
- **Report issues** and bug reports on https://github.com/getfider/fider/issues;
- **Give feedback** and vote on features you'd like to see at https://feedback.fider.io;
Expand Down Expand Up @@ -41,7 +41,7 @@ If you know these technologies or would like to learn them, lucky you! This is t
a fake SMTP server running at port **1025** and a UI (to check sent emails) at http://localhost:8025. The `.example.env` is already
configured to use it. If you want to, you can edit `.env` file and configure the `EMAIL_*` environment variables with your own SMTP server
details. If you don't have an SMTP server, you can either sign up for a [Mailgun account](https://www.mailgun.com/) (it's Free) or sign
up for a [Mailtrap account](https://mailtrap.io), which is a free SMTP mocking server. If you prefer not to setup an email service, keep
up for a [Mailtrap account](https://mailtrap.io), which is a free SMTP mocking server. If you prefer not to set up an email service, keep
an eye on the server logs. Sometimes it's necessary to navigate to some URLs that are only sent by email, but are also written to the logs.

#### 3. To start the application
Expand Down
2 changes: 1 addition & 1 deletion GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ Fider uses a combination of BEM and Utility Classes.

- `is-<state>`, `has-<state>` are global style modifiers that have a broader impact.

- Utility classes do not have a preffix.
- Utility classes do not have a prefix.
12 changes: 6 additions & 6 deletions MODERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ This document explains everything that needs to change in Fider to facilitate th

## Settings

This is a optional feature. Admins will be able to toggle this. This is done in the public/pages/Administration/pages/PrivacySettings.page.tsx page. Similar to how the other settings are controlled in this page. There needs to be a new column in the "tenants" database table called "is_moderation_enabled" to control this, so you're going to need a new migration file in migrations/
This is an optional feature. Admins will be able to toggle this. This is done in the public/pages/Administration/pages/PrivacySettings.page.tsx page. Similar to how the other settings are controlled in this page. There needs to be a new column in the "tenants" database table called "is_moderation_enabled" to control this, so you're going to need a new migration file in migrations/

## New posts and comments

Posts and comments will need a new column "is_approved" to determine if the post or comment has been approved to be shown. Again, this will need adding to the migration.

When a new post or comment is added, if moderation is enabled then is_approved will be false, otherwise it will be true. New posts are added via public/pages/Home/components/ShareFeedback.tsx and comments via public/pages/ShowPost/components/CommentInput.tsx.
When a new post or comment is added, if moderation is enabled then is_approved will be false; otherwise, it will be true. New posts are added via public/pages/Home/components/ShareFeedback.tsx and comments via public/pages/ShowPost/components/CommentInput.tsx.

Once added, the post is only visible to the person who added it (and admins, see below). When you view the post (or the comment) via public/pages/ShowPost/ShowPost.page.tsx, there needs to be a message to tell you that it's awaiting moderation.

Expand All @@ -21,9 +21,9 @@ Once added, the post is only visible to the person who added it (and admins, see
The "admin" section of fider looks like this (public/pages/Administration/components/AdminBasePage.tsx):
![alt text](<CleanShot 2025-07-01 at 20.39.05@2x.png>)

There neeeds to be a new menu item on the left for "Moderation"
There needs to be a new menu item on the left for "Moderation"

Clicking on that presents you with a tablular view of all non-moderated posts and comments.
Clicking on that presents you with a tabular view of all non-moderated posts and comments.
For each row, display the following columns:
_ A checkbox to allow you to select multiple rows
_ User's name and date of post (e.g. "Matt, 10 minutes ago")
Expand All @@ -32,7 +32,7 @@ _ If comment: "New comment: <comment>" (truncated to 200 chars)
_ If post: "New post: <post title>"
_ Thumbs up button to approve \* Thumbs down button to decline

If you click the description for a post or comment, it will take you to the post, and if you clicked a comment, will highlight the comment (this is already supporeted, see how the public/pages/ShowPost/ShowPost.page.tsx page highlights comments). When you are an admin, and it's a post that's awaiting moderation, the in place of the voting button, we need 2 buttons - one to approve, one to decline. The same is true for comments, there should be an approve / decline set of buttons udner the comment.
If you click the description for a post or comment, it will take you to the post, and if you clicked a comment, will highlight the comment (this is already supported, see how the public/pages/ShowPost/ShowPost.page.tsx page highlights comments). When you are an admin, and it's a post that's awaiting moderation, the in place of the voting button, we need 2 buttons - one to approve, one to decline. The same is true for comments, there should be an approve / decline set of buttons under the comment.

Declining a post or comment will delete it entirely. We should ask the user to confirm the action.

Expand All @@ -50,7 +50,7 @@ We've decided to make some changes to the moderation admin:

1. Rather than have it part of the admin menu, remove the entry from the side menu in admin. Instead, we want an icon in the top Header (public/components/Header.tsx) that when clicked, takes you to the moderation admin page, without the side menu. Ideally the icon will have a little counter in the top-right of how many items are awaiting moderation.

2. We've decided to make the moderation less onourous to the admins by making some more changes:
2. We've decided to make the moderation less onerous to the admins by making some more changes:

2.1) As well as decline, you have another option - "decline and block". This option will decline the post or comment, and block the user who made it from posting again. We already have the ability to block users in Fider (see BlockUser in app/handlers/user.go), so we can hook into that. So if you had 1 user who had 5 posts and some comments, and you declined and blocked them, we would also decline all other posts and comments they made, and block them from posting again.

Expand Down
10 changes: 5 additions & 5 deletions app/actions/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func (action *AddNewComment) Validate(ctx context.Context, user *entity.User) *v
return result
}

// SetResponse represents the action to update an post response
// SetResponse represents the action to update a post response
type SetResponse struct {
Number int `route:"number"`
Status enum.PostStatus `json:"status"`
Expand Down Expand Up @@ -264,8 +264,8 @@ func (action *SetResponse) Validate(ctx context.Context, user *entity.User) *val
result.AddFieldFailure("originalNumber", i18n.T(ctx, "validation.custom.selfduplicate"))
}

getOriginaPost := &query.GetPostByNumber{Number: action.OriginalNumber}
err := bus.Dispatch(ctx, getOriginaPost)
getOriginalPost := &query.GetPostByNumber{Number: action.OriginalNumber}
err := bus.Dispatch(ctx, getOriginalPost)
if err != nil {
if errors.Cause(err) == app.ErrNotFound {
result.AddFieldFailure("originalNumber", i18n.T(ctx, "validation.custom.originalpostnotfound"))
Expand All @@ -274,8 +274,8 @@ func (action *SetResponse) Validate(ctx context.Context, user *entity.User) *val
}
}

if getOriginaPost.Result != nil {
action.Original = getOriginaPost.Result
if getOriginalPost.Result != nil {
action.Original = getOriginalPost.Result
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/actions/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (action *DeleteTag) Validate(ctx context.Context, user *entity.User) *valid
return validate.Success()
}

// AssignUnassignTag is used to assign or remove a tag to/from an post
// AssignUnassignTag is used to assign or remove a tag to/from a post
type AssignUnassignTag struct {
Slug string `route:"slug"`
Number int `route:"number"`
Expand Down
8 changes: 4 additions & 4 deletions app/actions/tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import (

// CreateTenant is the input model used to create a tenant
type CreateTenant struct {
Token string `json:"token"`
Name string `json:"name"`
Email string `json:"email" format:"lower"`
Token string `json:"token"`
Name string `json:"name"`
Email string `json:"email" format:"lower"`
VerificationKey string `json:"-"`
TenantName string `json:"tenantName"`
LegalAgreement bool `json:"legalAgreement"`
Expand Down Expand Up @@ -290,7 +290,7 @@ func (action *UpdateTenantPrivacySettings) IsAuthorized(ctx context.Context, use
// Validate if current model is valid
func (action *UpdateTenantPrivacySettings) Validate(ctx context.Context, user *entity.User) *validate.Result {
if action.IsPrivate && action.IsFeedEnabled {
return validate.Failed("Feed can not be enabled when set to private.")
return validate.Failed("Feed cannot be enabled when set to private.")
}
return validate.Success()
}
Expand Down
2 changes: 1 addition & 1 deletion app/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func RunServer() int {
func startJobs(ctx context.Context) {
c := cron.New()
_ = c.AddJob(jobs.NewJob(ctx, "PurgeExpiredNotificationsJob", jobs.PurgeExpiredNotificationsJobHandler{}))
_ = c.AddJob(jobs.NewJob(ctx, "EmailSupressionJob", jobs.EmailSupressionJobHandler{}))
_ = c.AddJob(jobs.NewJob(ctx, "EmailSuppressionJob", jobs.EmailSuppressionJobHandler{}))

c.Start()
}
Expand Down
2 changes: 1 addition & 1 deletion app/handlers/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func LetterAvatar() web.HandlerFunc {
}
}

// Gravatar returns a gravatar picture of fallsback to letter avatar based on name
// Gravatar returns a gravatar picture or falls back to letter avatar based on name
func Gravatar() web.HandlerFunc {
return func(c *web.Context) error {
id, err := c.ParamAsInt("id")
Expand Down
3 changes: 1 addition & 2 deletions app/handlers/oauth_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestHasAllowedRole_WhitespaceOnlyConfig_AllowsAll(t *testing.T) {

func TestHasAllowedRole_NoRolesPath_SkipsCheck(t *testing.T) {
// Provider without a roles path must always be allowed through,
// regardless of whether the user carries matching roles or not.
// regardless of whether or not the user carries matching roles.
if !hasAllowedRole([]string{}, "", "ROLE_ADMIN") {
t.Error("expected true when provider has no JSONUserRolesPath (check should be skipped)")
}
Expand Down Expand Up @@ -116,4 +116,3 @@ func TestHasAllowedRole_ConfigWithOnlyCommas_AllowsAll(t *testing.T) {
t.Error("expected true when config contains only separators (no valid roles)")
}
}

2 changes: 1 addition & 1 deletion app/handlers/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestDetailsHandler(t *testing.T) {
Expect(code).Equals(http.StatusOK)
}

func TestDetailsHandler_RedirectOnDifferentSlu(t *testing.T) {
func TestDetailsHandler_RedirectOnDifferentSlug(t *testing.T) {
RegisterT(t)

post := &entity.Post{Number: 1, Title: "My Post Title", Slug: "my-post-title"}
Expand Down
18 changes: 9 additions & 9 deletions app/jobs/email_supression_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,37 @@ import (
"github.com/getfider/fider/app/pkg/log"
)

type EmailSupressionJobHandler struct {
type EmailSuppressionJobHandler struct {
}

func (e EmailSupressionJobHandler) Schedule() string {
func (e EmailSuppressionJobHandler) Schedule() string {
return "0 5 * * * *" // every hour at minute 5
}

func (e EmailSupressionJobHandler) Run(ctx Context) error {
func (e EmailSuppressionJobHandler) Run(ctx Context) error {
startTime := ctx.LastSuccessfulRun
if startTime == nil {
twoDaysAgo := time.Now().AddDate(0, 0, -2)
startTime = &twoDaysAgo
}

q := &query.FetchRecentSupressions{
q := &query.FetchRecentSuppressions{
StartTime: *startTime,
}

if err := bus.Dispatch(ctx, q); err != nil {
return errors.Wrap(err, "failed to fetch recent supressions")
return errors.Wrap(err, "failed to fetch recent suppressions")
}

c := &cmd.SupressEmail{
c := &cmd.SuppressEmail{
EmailAddresses: q.EmailAddresses,
}
if err := bus.Dispatch(ctx, c); err != nil {
return errors.Wrap(err, "failed to supress emails")
return errors.Wrap(err, "failed to suppress emails")
}

log.Debugf(ctx, "@{Count} account(s) marked with supressed email", dto.Props{
"Count": c.NumOfSupressedEmailAddresses,
log.Debugf(ctx, "@{Count} account(s) marked with suppressed email", dto.Props{
"Count": c.NumOfSuppressedEmailAddresses,
})

return nil
Expand Down
12 changes: 6 additions & 6 deletions app/jobs/email_supression_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,29 @@ import (
"github.com/getfider/fider/app/pkg/bus"
)

func TestEmailSupressionJob_Schedule_IsCorrect(t *testing.T) {
func TestEmailSuppressionJob_Schedule_IsCorrect(t *testing.T) {
RegisterT(t)

job := &jobs.EmailSupressionJobHandler{}
job := &jobs.EmailSuppressionJobHandler{}
Expect(job.Schedule()).Equals("0 5 * * * *")
}

func TestEmailSupressionJob_ShouldSupressRecentFailures(t *testing.T) {
func TestEmailSuppressionJob_ShouldSuppressRecentFailures(t *testing.T) {
RegisterT(t)

bus.AddHandler(func(ctx context.Context, q *query.FetchRecentSupressions) error {
bus.AddHandler(func(ctx context.Context, q *query.FetchRecentSuppressions) error {
q.EmailAddresses = []string{
"test1@gmail.com", "test2@gmail.com",
}
return nil
})

bus.AddHandler(func(ctx context.Context, c *cmd.SupressEmail) error {
bus.AddHandler(func(ctx context.Context, c *cmd.SuppressEmail) error {
Expect(c.EmailAddresses).Equals([]string{"test1@gmail.com", "test2@gmail.com"})
return nil
})

job := &jobs.EmailSupressionJobHandler{}
job := &jobs.EmailSuppressionJobHandler{}
err := job.Run(jobs.Context{
Context: context.Background(),
})
Expand Down
4 changes: 2 additions & 2 deletions app/models/cmd/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ type RemoveSubscriber struct {
User *entity.User
}

type SupressEmail struct {
type SuppressEmail struct {
EmailAddresses []string

//Output
NumOfSupressedEmailAddresses int
NumOfSuppressedEmailAddresses int
}
2 changes: 1 addition & 1 deletion app/models/entity/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ReactionCounts struct {
IncludesMe bool `json:"includesMe"`
}

// Comment represents an user comment on an post
// Comment represents a user comment on a post
type Comment struct {
ID int `json:"id"`
Content string `json:"content"`
Expand Down
2 changes: 1 addition & 1 deletion app/models/entity/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/getfider/fider/app/models/enum"
)

//Post represents an post on a tenant board
//Post represents a post on a tenant board
type Post struct {
ID int `json:"id"`
Number int `json:"number"`
Expand Down
2 changes: 1 addition & 1 deletion app/models/enum/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (t AvatarType) MarshalText() ([]byte, error) {
return []byte(avatarTypesIDs[t]), nil
}

// UnmarshalText parse string into a avatar type
// UnmarshalText parse string into an avatar type
func (t *AvatarType) UnmarshalText(text []byte) error {
*t = avatarTypesName[string(text)]
return nil
Expand Down
2 changes: 1 addition & 1 deletion app/models/enum/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
type NotificationChannel int

var (
//NotificationChannelWeb is a in-app notification
//NotificationChannelWeb is an in-app notification
NotificationChannelWeb NotificationChannel = 1
//NotificationChannelEmail is an email notification
NotificationChannelEmail NotificationChannel = 2
Expand Down
4 changes: 2 additions & 2 deletions app/models/enum/post_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ var (
PostStarted PostStatus = 1
//PostCompleted is used when the post has been accepted and already implemented
PostCompleted PostStatus = 2
//PostDeclined is used when organizers decide to decline an post
//PostDeclined is used when organizers decide to decline a post
PostDeclined PostStatus = 3
//PostPlanned is used when organizers have accepted an post and it's on the roadmap
//PostPlanned is used when organizers have accepted a post and it's on the roadmap
PostPlanned PostStatus = 4
//PostDuplicate is used when the post has already been posted before
PostDuplicate PostStatus = 5
Expand Down
2 changes: 1 addition & 1 deletion app/models/enum/webhook_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const (
WebhookEnabled WebhookStatus = 1
// WebhookDisabled means the webhook cannot be triggered
WebhookDisabled WebhookStatus = 2
// WebhookFailed means an error occured when the webhook was previously triggered and has been disabled
// WebhookFailed means an error occurred when the webhook was previously triggered and has been disabled
WebhookFailed WebhookStatus = 3
)

Expand Down
2 changes: 1 addition & 1 deletion app/models/query/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package query

import "time"

type FetchRecentSupressions struct {
type FetchRecentSuppressions struct {
StartTime time.Time

//Output
Expand Down
2 changes: 1 addition & 1 deletion app/pkg/dbx/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func hash(s string) uint32 {
}

// Try to obtain an advisory lock
// returns true and an unlock function if lock was aquired
// returns true and an unlock function if lock was acquired
func TryLock(ctx context.Context, trx *Trx, key string) (bool, func()) {
var locked bool
if err := trx.Scalar(&locked, "SELECT pg_try_advisory_xact_lock($1)", hash(key)); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions app/pkg/dbx/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"encoding/json"
)

// NullInt representa a nullable integer
// NullInt represents a nullable integer
type NullInt struct {
sql.NullInt64
}
Expand All @@ -18,7 +18,7 @@ func (r NullInt) MarshalJSON() ([]byte, error) {
return json.Marshal(nil)
}

// NullString representa a nullable string
// NullString represents a nullable string
type NullString struct {
sql.NullString
}
Expand All @@ -31,7 +31,7 @@ func (r NullString) MarshalJSON() ([]byte, error) {
return json.Marshal(nil)
}

// NullTime representa a nullable time.Time
// NullTime represents a nullable time.Time
type NullTime struct {
sql.NullTime
}
Expand Down
4 changes: 2 additions & 2 deletions app/pkg/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func init() {
Reload()
}

// Reload configuration from current Enviornment Variables
// Reload configuration from current Environment Variables
func Reload() {
Config = config{}
err := envdecode.Decode(&Config)
Expand All @@ -177,7 +177,7 @@ func Reload() {
}
}

// Email Type can be inferred if absense
// Email Type can be inferred if absent
if Config.Email.Type == "" {
if Config.Email.Mailgun.APIKey != "" {
Config.Email.Type = "mailgun"
Expand Down
2 changes: 1 addition & 1 deletion app/pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func Wrap(err error, format string, a ...any) error {
return wrap(err, 0, format, a...)
}

// Panicked wraps panick errow with extra details of error
// Panicked wraps panic error with extra details of error
func Panicked(r any) error {
err, ok := r.(error)
if !ok {
Expand Down
Loading