mirror of https://github.com/go-gitea/gitea.git
Add user blocking (#29028)
Fixes #17453 This PR adds the abbility to block a user from a personal account or organization to restrict how the blocked user can interact with the blocker. The docs explain what's the consequence of blocking a user. Screenshots: ![grafik](https://github.com/go-gitea/gitea/assets/1666336/4ed884f3-e06a-4862-afd3-3b8aa2488dc6) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ae6d4981-f252-4f50-a429-04f0f9f1cdf1) ![grafik](https://github.com/go-gitea/gitea/assets/1666336/ca153599-5b0f-4b4a-90fe-18bdfd6f0b6b) --------- Co-authored-by: Lauris BH <lauris@nix.lv>pull/28894/head^2
parent
8e12ba34ba
commit
c337ff0ec7
@ -0,0 +1,56 @@
|
||||
---
|
||||
date: "2024-01-31T00:00:00+00:00"
|
||||
title: "Blocking a user"
|
||||
slug: "blocking-user"
|
||||
sidebar_position: 25
|
||||
toc: false
|
||||
draft: false
|
||||
aliases:
|
||||
- /en-us/webhooks
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
name: "Blocking a user"
|
||||
sidebar_position: 30
|
||||
identifier: "blocking-user"
|
||||
---
|
||||
|
||||
# Blocking a user
|
||||
|
||||
Gitea supports blocking of users to restrict how they can interact with you and your content.
|
||||
|
||||
You can block a user in your account settings, from the user's profile or from comments created by the user.
|
||||
The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you.
|
||||
Organization owners can block anyone who is not a member of the organization too.
|
||||
If a blocked user has admin permissions, they can still perform all actions even if blocked.
|
||||
|
||||
### When you block a user
|
||||
|
||||
- the user stops following you
|
||||
- you stop following the user
|
||||
- the user's stars are removed from your repositories
|
||||
- your stars are removed from their repositories
|
||||
- the user stops watching your repositories
|
||||
- you stop watching their repositories
|
||||
- the user's issue assignments are removed from your repositories
|
||||
- your issue assignments are removed from their repositories
|
||||
- the user is removed as a collaborator on your repositories
|
||||
- you are removed as a collaborator on their repositories
|
||||
- any pending repository transfers to or from the blocked user are canceled
|
||||
|
||||
### When you block a user, the user cannot
|
||||
|
||||
- follow you
|
||||
- watch your repositories
|
||||
- star your repositories
|
||||
- fork your repositories
|
||||
- transfer repositories to you
|
||||
- open issues or pull requests on your repositories
|
||||
- comment on issues or pull requests you've created
|
||||
- comment on issues or pull requests on your repositories
|
||||
- react to your comments on issues or pull requests
|
||||
- react to comments on issues or pull requests on your repositories
|
||||
- assign you to issues or pull requests
|
||||
- add you as a collaborator on their repositories
|
||||
- send you notifications by @mentioning your username
|
||||
- be added as team member (if blocked by an organization)
|
@ -0,0 +1,19 @@
|
||||
-
|
||||
id: 1
|
||||
blocker_id: 2
|
||||
blockee_id: 29
|
||||
|
||||
-
|
||||
id: 2
|
||||
blocker_id: 17
|
||||
blockee_id: 28
|
||||
|
||||
-
|
||||
id: 3
|
||||
blocker_id: 2
|
||||
blockee_id: 34
|
||||
|
||||
-
|
||||
id: 4
|
||||
blocker_id: 50
|
||||
blockee_id: 34
|
@ -0,0 +1,26 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BlockerID int64 `xorm:"UNIQUE(block)"`
|
||||
BlockeeID int64 `xorm:"UNIQUE(block)"`
|
||||
Note string
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func (*Blocking) TableName() string {
|
||||
return "user_blocking"
|
||||
}
|
||||
|
||||
func AddUserBlockingTable(x *xorm.Engine) error {
|
||||
return x.Sync(&Blocking{})
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization")
|
||||
ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user")
|
||||
ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user")
|
||||
ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked")
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BlockerID int64 `xorm:"UNIQUE(block)"`
|
||||
Blocker *User `xorm:"-"`
|
||||
BlockeeID int64 `xorm:"UNIQUE(block)"`
|
||||
Blockee *User `xorm:"-"`
|
||||
Note string
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
func (*Blocking) TableName() string {
|
||||
return "user_blocking"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Blocking))
|
||||
}
|
||||
|
||||
func UpdateBlockingNote(ctx context.Context, id int64, note string) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note})
|
||||
return err
|
||||
}
|
||||
|
||||
func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool {
|
||||
if len(blockerIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if blockee.IsAdmin {
|
||||
return false
|
||||
}
|
||||
|
||||
cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}.
|
||||
And(builder.In("user_blocking.blocker_id", blockerIDs))
|
||||
|
||||
has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{})
|
||||
return has
|
||||
}
|
||||
|
||||
type FindBlockingOptions struct {
|
||||
db.ListOptions
|
||||
BlockerID int64
|
||||
BlockeeID int64
|
||||
}
|
||||
|
||||
func (opts *FindBlockingOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.BlockerID != 0 {
|
||||
cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID})
|
||||
}
|
||||
if opts.BlockeeID != 0 {
|
||||
cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) {
|
||||
return db.FindAndCount[Blocking](ctx, opts)
|
||||
}
|
||||
|
||||
func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) {
|
||||
blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{
|
||||
BlockerID: blockerID,
|
||||
BlockeeID: blockeeID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return blocks[0], nil
|
||||
}
|
||||
|
||||
type BlockingList []*Blocking
|
||||
|
||||
func (blocks BlockingList) LoadAttributes(ctx context.Context) error {
|
||||
ids := make(container.Set[int64], len(blocks)*2)
|
||||
for _, b := range blocks {
|
||||
ids.Add(b.BlockerID)
|
||||
ids.Add(b.BlockeeID)
|
||||
}
|
||||
|
||||
userList, err := GetUsersByIDs(ctx, ids.Values())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userMap := make(map[int64]*User, len(userList))
|
||||
for _, u := range userList {
|
||||
userMap[u.ID] = u
|
||||
}
|
||||
|
||||
for _, b := range blocks {
|
||||
b.Blocker = userMap[b.BlockerID]
|
||||
b.Blockee = userMap[b.BlockeeID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
|
||||
// ---
|
||||
// summary: List users blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
|
||||
// ---
|
||||
// summary: Check if a user is blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
|
||||
// ---
|
||||
// summary: Block a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
// in: query
|
||||
// description: optional note for the block
|
||||
// type: string
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
BlockerID: blocker.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]*api.User, 0, len(blocks))
|
||||
for _, b := range blocks {
|
||||
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, &users)
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
status := http.StatusNotFound
|
||||
blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
|
||||
return
|
||||
}
|
||||
if blocking != nil {
|
||||
status = http.StatusNoContent
|
||||
}
|
||||
|
||||
ctx.Status(status)
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Error(http.StatusBadRequest, "BlockUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "BlockUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
|
||||
blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
|
||||
if err != nil {
|
||||
ctx.NotFound("GetUserByName", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Error(http.StatusBadRequest, "UnblockUser", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/blocks user userListBlocks
|
||||
// ---
|
||||
// summary: List users blocked by the authenticated user
|
||||
// parameters:
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
shared.ListBlocks(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/blocks/{username} user userCheckUserBlock
|
||||
// ---
|
||||
// summary: Check if a user is blocked by the authenticated user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.CheckUserBlock(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /user/blocks/{username} user userBlockUser
|
||||
// ---
|
||||
// summary: Block a user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
// in: query
|
||||
// description: optional note for the block
|
||||
// type: string
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.BlockUser(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /user/blocks/{username} user userUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("user.block.list")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsBlockedUsers"] = true
|
||||
|
||||
shared_user.BlockedUsers(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context) {
|
||||
shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
|
||||
blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
|
||||
BlockerID: blocker.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBlockings", err)
|
||||
return
|
||||
}
|
||||
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["UserBlocks"] = blocks
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
|
||||
form := web.GetForm(ctx).(*forms.BlockUserForm)
|
||||
if ctx.HasError() {
|
||||
ctx.ServerError("FormValidation", nil)
|
||||
return
|
||||
}
|
||||
|
||||
blockee, err := user_model.GetUserByName(ctx, form.Blockee)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByName", nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch form.Action {
|
||||
case "block":
|
||||
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
|
||||
} else {
|
||||
ctx.ServerError("BlockUser", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "unblock":
|
||||
if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
|
||||
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
|
||||
ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
|
||||
} else {
|
||||
ctx.ServerError("UnblockUser", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "note":
|
||||
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlocking", err)
|
||||
return
|
||||
}
|
||||
if block != nil {
|
||||
if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
|
||||
ctx.ServerError("UpdateBlockingNote", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("user.block.list")
|
||||
ctx.Data["PageIsSettingsBlockedUsers"] = true
|
||||
|
||||
shared_user.BlockedUsers(ctx, ctx.Doer)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context) {
|
||||
shared_user.BlockedUsersPost(ctx, ctx.Doer)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// CreateIssueReaction creates a reaction on an issue.
|
||||
func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on a comment.
|
||||
func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comment.Issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
|
||||
return nil, user_model.ErrBlockedUser
|
||||
}
|
||||
|
||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doer.ID,
|
||||
IssueID: comment.Issue.ID,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
}
|
@ -0,0 +1,308 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
|
||||
if blocker.ID == blockee.ID {
|
||||
return false
|
||||
}
|
||||
if doer.ID == blockee.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
if blockee.IsOrganization() {
|
||||
return false
|
||||
}
|
||||
|
||||
if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
|
||||
return false
|
||||
}
|
||||
|
||||
if blocker.IsOrganization() {
|
||||
org := org_model.OrgFromUser(blocker)
|
||||
if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
|
||||
return false
|
||||
}
|
||||
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
|
||||
return false
|
||||
}
|
||||
} else if !doer.IsAdmin && doer.ID != blocker.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
|
||||
if doer.ID == blockee.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
|
||||
return false
|
||||
}
|
||||
|
||||
if blocker.IsOrganization() {
|
||||
org := org_model.OrgFromUser(blocker)
|
||||
if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
|
||||
return false
|
||||
}
|
||||
} else if !doer.IsAdmin && doer.ID != blocker.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
|
||||
if blockee.IsOrganization() {
|
||||
return user_model.ErrBlockOrganization
|
||||
}
|
||||
|
||||
if !CanBlockUser(ctx, doer, blocker, blockee) {
|
||||
return user_model.ErrCanNotBlock
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// unfollow each other
|
||||
if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unstar each other
|
||||
if err := unstarRepos(ctx, blocker, blockee); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unstarRepos(ctx, blockee, blocker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unwatch each others repositories
|
||||
if err := unwatchRepos(ctx, blocker, blockee); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unwatchRepos(ctx, blockee, blocker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unassign each other from issues
|
||||
if err := unassignIssues(ctx, blocker, blockee); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unassignIssues(ctx, blockee, blocker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove each other from repository collaborations
|
||||
if err := removeCollaborations(ctx, blocker, blockee); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := removeCollaborations(ctx, blockee, blocker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cancel each other repository transfers
|
||||
if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Insert(ctx, &user_model.Blocking{
|
||||
BlockerID: blocker.ID,
|
||||
BlockeeID: blockee.ID,
|
||||
Note: note,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
|
||||
opts := &repo_model.StarredReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 25,
|
||||
},
|
||||
StarrerID: starrer.ID,
|
||||
RepoOwnerID: repoOwner.ID,
|
||||
}
|
||||
|
||||
for {
|
||||
repos, err := repo_model.GetStarredRepos(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
}
|
||||
|
||||
func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
|
||||
opts := &repo_model.WatchedReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 25,
|
||||
},
|
||||
WatcherID: watcher.ID,
|
||||
RepoOwnerID: repoOwner.ID,
|
||||
}
|
||||
|
||||
for {
|
||||
repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
}
|
||||
|
||||
func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
|
||||
transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
|
||||
SenderID: sender.ID,
|
||||
RecipientID: recipient.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, transfer := range transfers {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
|
||||
opts := &issues_model.AssignedIssuesOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 25,
|
||||
},
|
||||
AssigneeID: assignee.ID,
|
||||
RepoOwnerID: repoOwner.ID,
|
||||
}
|
||||
|
||||
for {
|
||||
issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if err := issue.LoadAssignees(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
}
|
||||
|
||||
func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
|
||||
opts := &repo_model.FindCollaborationOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 25,
|
||||
},
|
||||
CollaboratorID: collaborator.ID,
|
||||
RepoOwnerID: repoOwner.ID,
|
||||
}
|
||||
|
||||
for {
|
||||
collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(collaborations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, collaboration := range collaborations {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.Page++
|
||||
}
|
||||
}
|
||||
|
||||
func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
|
||||
if blockee.IsOrganization() {
|
||||
return user_model.ErrBlockOrganization
|
||||
}
|
||||
|
||||
if !CanUnblockUser(ctx, doer, blocker, blockee) {
|
||||
return user_model.ErrCanNotUnblock
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if block != nil {
|
||||
_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCanBlockUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
|
||||
// Doer can't self block
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
|
||||
// Blocker can't be blockee
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
|
||||
// Can't block already blocked user
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
|
||||
// Blockee can't be an organization
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
|
||||
// Doer must be blocker or admin
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
|
||||
// Organization can't block a member
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
|
||||
// Doer must be organization owner or admin if blocker is an organization
|
||||
assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
|
||||
|
||||
assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
|
||||
assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
|
||||
assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
|
||||
}
|
||||
|
||||
func TestCanUnblockUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
|
||||
|
||||
// Doer can't self unblock
|
||||
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
|
||||
// Can't unblock not blocked user
|
||||
assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
|
||||
// Doer must be blocker or admin
|
||||
assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
|
||||
// Doer must be organization owner or admin if blocker is an organization
|
||||
assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
|
||||
|
||||
assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
|
||||
assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
|
||||
assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}}
|
||||
<div class="org-setting-content">
|
||||
{{template "shared/user/blocked_users" .}}
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
@ -0,0 +1,23 @@
|
||||
<div class="ui small modal" id="block-user-modal">
|
||||
<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
|
||||
<div class="content">
|
||||
<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div>
|
||||
<form class="ui form modal-form" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="action" value="block" />
|
||||
<input type="hidden" name="blockee" class="modal-blockee" />
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
|
||||
<input id="block-note" name="note">
|
||||
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
|
||||
</div>
|
||||
<div class="text right actions">
|
||||
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
|
||||
<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,83 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "user.block.title"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
|
||||
<ul>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
|
||||
<li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui segment">
|
||||
<form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="action" value="block" />
|
||||
<div id="search-user-box" class="field ui fluid search input">
|
||||
<input class="prompt gt-mr-3" name="blockee" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
|
||||
<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
|
||||
<input name="note">
|
||||
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "user.block.list"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="flex-list">
|
||||
{{range .UserBlocks}}
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-leading">
|
||||
{{ctx.AvatarUtils.Avatar .Blockee}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
|
||||
</div>
|
||||
{{if .Note}}
|
||||
<div class="flex-item-body">
|
||||
<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
|
||||
<form action="{{$.Link}}" method="post">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="action" value="unblock" />
|
||||
<input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
|
||||
<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small modal" id="block-user-note-modal">
|
||||
<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
|
||||
<div class="content">
|
||||
<form class="ui form" action="{{$.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="action" value="note" />
|
||||
<input type="hidden" name="blockee" class="modal-blockee" />
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
|
||||
<input name="note" class="modal-note" />
|
||||
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
|
||||
</div>
|
||||
<div class="text right actions">
|
||||
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,5 @@
|
||||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}}
|
||||
<div class="user-setting-content">
|
||||
{{template "shared/user/blocked_users" .}}
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue