aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL10
-rw-r--r--Makefile4
-rw-r--r--bloat.conf11
-rw-r--r--config/config.go11
-rw-r--r--go.mod2
-rw-r--r--main.go51
-rw-r--r--mastodon/accounts.go176
-rw-r--r--mastodon/apps.go3
-rw-r--r--mastodon/http.go45
-rw-r--r--mastodon/mastodon.go99
-rw-r--r--mastodon/status.go66
-rw-r--r--model/app.go21
-rw-r--r--model/session.go61
-rw-r--r--model/settings.go33
-rw-r--r--renderer/model.go23
-rw-r--r--renderer/renderer.go4
-rw-r--r--repo/appRepo.go42
-rw-r--r--repo/sessionRepo.go47
-rw-r--r--service/client.go118
-rw-r--r--service/service.go266
-rw-r--r--service/transport.go253
-rw-r--r--static/fluoride.js21
-rw-r--r--static/style.css609
-rw-r--r--templates/about.tmpl8
-rw-r--r--templates/emoji.tmpl4
-rw-r--r--templates/error.tmpl4
-rw-r--r--templates/filters.tmpl22
-rw-r--r--templates/header.tmpl5
-rw-r--r--templates/likedby.tmpl2
-rw-r--r--templates/list.tmpl30
-rw-r--r--templates/lists.tmpl46
-rw-r--r--templates/mute.tmpl29
-rw-r--r--templates/nav.tmpl55
-rw-r--r--templates/notification.tmpl122
-rw-r--r--templates/postform.tmpl50
-rw-r--r--templates/profile.tmpl64
-rw-r--r--templates/quickreply.tmpl2
-rw-r--r--templates/requestlist.tmpl38
-rw-r--r--templates/retweetedby.tmpl2
-rw-r--r--templates/root.tmpl9
-rw-r--r--templates/search.tmpl35
-rw-r--r--templates/settings.tmpl32
-rw-r--r--templates/signin.tmpl21
-rw-r--r--templates/status.tmpl88
-rw-r--r--templates/thread.tmpl5
-rw-r--r--templates/timeline.tmpl15
-rw-r--r--templates/user.tmpl129
-rw-r--r--templates/userlist.tmpl4
-rw-r--r--templates/userlistfollow.tmpl30
-rw-r--r--templates/userlistitem.tmpl10
-rw-r--r--templates/usersearch.tmpl21
-rw-r--r--util/kv.go91
-rw-r--r--util/rand.go4
53 files changed, 1533 insertions, 1420 deletions
diff --git a/INSTALL b/INSTALL
index 8f8c6d4..2d8047f 100644
--- a/INSTALL
+++ b/INSTALL
@@ -23,16 +23,8 @@ most cases, you only need to change the value of "client_website".
# cp bloat.gen.conf /etc/bloat.conf
# $EDITOR /etc/bloat.conf
-4. Create database directory
-Create a directory to store session information. Optionally, create a user
-to run bloat and change the ownership of the database directory accordingly.
-# mkdir /var/bloat
-# useradd _bloat
-# chown -R _bloat:_bloat /var/bloat
-Replace /var/bloat with the value you specified in the config file.
-
5. Run the binary
-# su _bloat -c bloat
+$ bloat
Now you should create an init script to automatically start bloat at system
startup.
diff --git a/Makefile b/Makefile
index c38de6b..104bfb1 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,6 @@ SRC=main.go \
mastodon/*.go \
model/*.go \
renderer/*.go \
- repo/*.go \
service/*.go \
util/*.go \
@@ -18,8 +17,7 @@ all: bloat
bloat: $(SRC) $(TMPL)
$(GO) build $(GOFLAGS) -o bloat main.go
- sed -e "s%=database%=/var/bloat%g" \
- -e "s%=templates%=$(SHAREPATH)/templates%g" \
+ sed -e "s%=templates%=$(SHAREPATH)/templates%g" \
-e "s%=static%=$(SHAREPATH)/static%g" \
< bloat.conf > bloat.gen.conf
diff --git a/bloat.conf b/bloat.conf
index dd77875..9bd2781 100644
--- a/bloat.conf
+++ b/bloat.conf
@@ -3,10 +3,6 @@
# - Key and Value are separated by a single '='
# - Leading and trailing white spaces in Key and Value are ignored
# - Quoting and multi-line values are not supported
-#
-# Changing values of client_name, client_scope or client_website will cause
-# previously generated access tokens and client tokens to be invalid. Issue the
-# `rm -r database_path/*` command to clean the database afterwards.
# Address to listen to. Value can be of "HOSTNAME:PORT" or "IP:PORT" form. In
# case of empty HOSTNAME or IP, "0.0.0.0:PORT" is used.
@@ -25,9 +21,6 @@ client_name=bloat
# See https://docs.joinmastodon.org/api/oauth-scopes/
client_scope=read write follow
-# Path of database directory. It's used to store session information.
-database_path=database
-
# Path of directory containing template files.
templates_path=templates
@@ -38,9 +31,6 @@ static_directory=static
# Empty value will disable the format selection in frontend.
post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:text/bbcode
-# Log file. Will log to stdout if value is empty.
-# log_file=log
-
# In single instance mode, bloat will not ask for instance domain name and
# user will be directly redirected to login form. User login from other
# instances is not allowed in this mode.
@@ -48,5 +38,4 @@ post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:t
# single_instance=pl.mydomain.com
# Path to custom CSS. Value can be a file path relative to the static directory.
-# or a URL starting with either "http://" or "https://".
# custom_css=custom.css
diff --git a/config/config.go b/config/config.go
index bbb327c..141cb39 100644
--- a/config/config.go
+++ b/config/config.go
@@ -18,10 +18,8 @@ type config struct {
SingleInstance string
StaticDirectory string
TemplatesPath string
- DatabasePath string
CustomCSS string
PostFormats []model.PostFormat
- LogFile string
}
func (c *config) IsValid() bool {
@@ -30,8 +28,7 @@ func (c *config) IsValid() bool {
len(c.ClientScope) < 1 ||
len(c.ClientWebsite) < 1 ||
len(c.StaticDirectory) < 1 ||
- len(c.TemplatesPath) < 1 ||
- len(c.DatabasePath) < 1 {
+ len(c.TemplatesPath) < 1 {
return false
}
return true
@@ -75,10 +72,10 @@ func Parse(r io.Reader) (c *config, err error) {
c.StaticDirectory = val
case "templates_path":
c.TemplatesPath = val
- case "database_path":
- c.DatabasePath = val
case "custom_css":
c.CustomCSS = val
+ case "database_path":
+ // ignore
case "post_formats":
vals := strings.Split(val, ",")
var formats []model.PostFormat
@@ -99,7 +96,7 @@ func Parse(r io.Reader) (c *config, err error) {
}
c.PostFormats = formats
case "log_file":
- c.LogFile = val
+ // ignore
default:
return nil, errors.New("invalid config key " + key)
}
diff --git a/go.mod b/go.mod
index 508d0be..caba203 100644
--- a/go.mod
+++ b/go.mod
@@ -5,4 +5,4 @@ require (
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
)
-go 1.13
+go 1.11
diff --git a/main.go b/main.go
index 3b5ccba..9e9ba4e 100644
--- a/main.go
+++ b/main.go
@@ -8,13 +8,10 @@ import (
"net/http"
"os"
"path/filepath"
- "strings"
"bloat/config"
"bloat/renderer"
- "bloat/repo"
"bloat/service"
- "bloat/util"
)
var (
@@ -28,6 +25,7 @@ func errExit(err error) {
func main() {
configFile := flag.String("f", "", "config file")
+ verbose := flag.Bool("v", false, "verbose mode")
flag.Parse()
if len(*configFile) > 0 {
@@ -48,51 +46,12 @@ func main() {
errExit(err)
}
- err = os.Mkdir(config.DatabasePath, 0755)
- if err != nil && !os.IsExist(err) {
- errExit(err)
- }
-
- sessionDBPath := filepath.Join(config.DatabasePath, "session")
- sessionDB, err := util.NewDatabse(sessionDBPath)
- if err != nil {
- errExit(err)
- }
-
- appDBPath := filepath.Join(config.DatabasePath, "app")
- appDB, err := util.NewDatabse(appDBPath)
- if err != nil {
- errExit(err)
- }
-
- sessionRepo := repo.NewSessionRepo(sessionDB)
- appRepo := repo.NewAppRepo(appDB)
-
- customCSS := config.CustomCSS
- if len(customCSS) > 0 && !strings.HasPrefix(customCSS, "http://") &&
- !strings.HasPrefix(customCSS, "https://") {
- customCSS = "/static/" + customCSS
- }
-
- var logger *log.Logger
- if len(config.LogFile) < 1 {
- logger = log.New(os.Stdout, "", log.LstdFlags)
- } else {
- lf, err := os.OpenFile(config.LogFile,
- os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
- if err != nil {
- errExit(err)
- }
- defer lf.Close()
- logger = log.New(lf, "", log.LstdFlags)
- }
-
s := service.NewService(config.ClientName, config.ClientScope,
- config.ClientWebsite, customCSS, config.SingleInstance,
- config.PostFormats, renderer, sessionRepo, appRepo)
- handler := service.NewHandler(s, logger, config.StaticDirectory)
+ config.ClientWebsite, config.CustomCSS, config.SingleInstance,
+ config.PostFormats, renderer)
+ handler := service.NewHandler(s, *verbose, config.StaticDirectory)
- logger.Println("listening on", config.ListenAddress)
+ log.Println("listening on", config.ListenAddress)
err = http.ListenAndServe(config.ListenAddress, handler)
if err != nil {
errExit(err)
diff --git a/mastodon/accounts.go b/mastodon/accounts.go
index df0a3b7..31021fd 100644
--- a/mastodon/accounts.go
+++ b/mastodon/accounts.go
@@ -1,16 +1,20 @@
package mastodon
import (
+ "bytes"
"context"
"fmt"
+ "io"
+ "mime/multipart"
"net/http"
"net/url"
+ "path/filepath"
"strconv"
"time"
)
type AccountPleroma struct {
- Relationship Relationship `json:"relationship"`
+ Relationship *Relationship `json:"relationship"`
}
// Account hold information for mastodon account.
@@ -34,7 +38,11 @@ type Account struct {
Moved *Account `json:"moved"`
Fields []Field `json:"fields"`
Bot bool `json:"bot"`
+ Source *AccountSource `json:"source"`
Pleroma *AccountPleroma `json:"pleroma"`
+
+ // Duplicate field for compatibilty with Pleroma
+ FollowRequestsCount int64 `json:"follow_requests_count"`
}
// Field is a Mastodon account profile field.
@@ -46,17 +54,20 @@ type Field struct {
// AccountSource is a Mastodon account profile field.
type AccountSource struct {
- Privacy *string `json:"privacy"`
- Sensitive *bool `json:"sensitive"`
- Language *string `json:"language"`
- Note *string `json:"note"`
- Fields *[]Field `json:"fields"`
+ Privacy *string `json:"privacy"`
+ Sensitive *bool `json:"sensitive"`
+ Language *string `json:"language"`
+ Note *string `json:"note"`
+ Fields *[]Field `json:"fields"`
+ FollowRequestsCount int64 `json:"follow_requests_count"`
}
// GetAccount return Account.
func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) {
var account Account
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s", url.PathEscape(string(id))), nil, &account, nil)
+ params := url.Values{}
+ params.Set("with_relationships", strconv.FormatBool(true))
+ err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s", url.PathEscape(string(id))), params, &account, nil)
if err != nil {
return nil, err
}
@@ -66,7 +77,7 @@ func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) {
return nil, err
}
if len(rs) > 0 {
- account.Pleroma = &AccountPleroma{*rs[0]}
+ account.Pleroma = &AccountPleroma{rs[0]}
}
}
return &account, nil
@@ -93,54 +104,115 @@ type Profile struct {
Source *AccountSource
// Set the base64 encoded character string of the image.
- Avatar string
- Header string
+ Avatar *multipart.FileHeader
+ Header *multipart.FileHeader
}
// AccountUpdate updates the information of the current user.
func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) {
- params := url.Values{}
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
if profile.DisplayName != nil {
- params.Set("display_name", *profile.DisplayName)
+ err := mw.WriteField("display_name", *profile.DisplayName)
+ if err != nil {
+ return nil, err
+ }
}
if profile.Note != nil {
- params.Set("note", *profile.Note)
+ err := mw.WriteField("note", *profile.Note)
+ if err != nil {
+ return nil, err
+ }
}
if profile.Locked != nil {
- params.Set("locked", strconv.FormatBool(*profile.Locked))
+ err := mw.WriteField("locked", strconv.FormatBool(*profile.Locked))
+ if err != nil {
+ return nil, err
+ }
}
if profile.Fields != nil {
for idx, field := range *profile.Fields {
- params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
- params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
+ err := mw.WriteField(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
+ if err != nil {
+ return nil, err
+ }
+ err = mw.WriteField(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
+ if err != nil {
+ return nil, err
+ }
}
}
- if profile.Source != nil {
- if profile.Source.Privacy != nil {
- params.Set("source[privacy]", *profile.Source.Privacy)
+ if profile.Avatar != nil {
+ f, err := profile.Avatar.Open()
+ if err != nil {
+ return nil, err
}
- if profile.Source.Sensitive != nil {
- params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive))
+ fname := filepath.Base(profile.Avatar.Filename)
+ part, err := mw.CreateFormFile("avatar", fname)
+ if err != nil {
+ return nil, err
}
- if profile.Source.Language != nil {
- params.Set("source[language]", *profile.Source.Language)
+ _, err = io.Copy(part, f)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if profile.Header != nil {
+ f, err := profile.Header.Open()
+ if err != nil {
+ return nil, err
+ }
+ fname := filepath.Base(profile.Header.Filename)
+ part, err := mw.CreateFormFile("header", fname)
+ if err != nil {
+ return nil, err
+ }
+ _, err = io.Copy(part, f)
+ if err != nil {
+ return nil, err
}
}
- if profile.Avatar != "" {
- params.Set("avatar", profile.Avatar)
+ err := mw.Close()
+ if err != nil {
+ return nil, err
}
- if profile.Header != "" {
- params.Set("header", profile.Header)
+ params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
+ var account Account
+ err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil)
+ if err != nil {
+ return nil, err
}
+ return &account, nil
+}
+func (c *Client) accountDeleteField(ctx context.Context, field string) (*Account, error) {
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
+ _, err := mw.CreateFormField(field)
+ if err != nil {
+ return nil, err
+ }
+ err = mw.Close()
+ if err != nil {
+ return nil, err
+ }
+ params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
var account Account
- err := c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil)
+ err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil)
if err != nil {
return nil, err
}
return &account, nil
}
+func (c *Client) AccountDeleteAvatar(ctx context.Context) (*Account, error) {
+ return c.accountDeleteField(ctx, "avatar")
+}
+
+func (c *Client) AccountDeleteHeader(ctx context.Context) (*Account, error) {
+ return c.accountDeleteField(ctx, "header")
+}
+
// GetAccountStatuses return statuses by specified accuont.
func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bool, pg *Pagination) ([]*Status, error) {
var statuses []*Status
@@ -153,10 +225,40 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bo
return statuses, nil
}
+func (c *Client) getMissingRelationships(ctx context.Context, accounts []*Account) ([]*Account, error) {
+ var ids []string
+ for _, a := range accounts {
+ if a.Pleroma == nil || len(a.Pleroma.Relationship.ID) < 1 {
+ ids = append(ids, a.ID)
+ }
+ }
+ if len(ids) < 1 {
+ return accounts, nil
+ }
+ rs, err := c.GetAccountRelationships(ctx, ids)
+ if err != nil {
+ return nil, err
+ }
+ rsm := make(map[string]*Relationship, len(rs))
+ for _, r := range rs {
+ rsm[r.ID] = r
+ }
+ for _, a := range accounts {
+ a.Pleroma = &AccountPleroma{rsm[a.ID]}
+ }
+ return accounts, nil
+}
+
// GetAccountFollowers return followers list.
func (c *Client) GetAccountFollowers(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
var accounts []*Account
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), nil, &accounts, pg)
+ params := url.Values{}
+ params.Set("with_relationships", strconv.FormatBool(true))
+ err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), params, &accounts, pg)
+ if err != nil {
+ return nil, err
+ }
+ accounts, err = c.getMissingRelationships(ctx, accounts)
if err != nil {
return nil, err
}
@@ -166,7 +268,13 @@ func (c *Client) GetAccountFollowers(ctx context.Context, id string, pg *Paginat
// GetAccountFollowing return following list.
func (c *Client) GetAccountFollowing(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
var accounts []*Account
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), nil, &accounts, pg)
+ params := url.Values{}
+ params.Set("with_relationships", strconv.FormatBool(true))
+ err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), params, &accounts, pg)
+ if err != nil {
+ return nil, err
+ }
+ accounts, err = c.getMissingRelationships(ctx, accounts)
if err != nil {
return nil, err
}
@@ -189,6 +297,7 @@ type Relationship struct {
Following bool `json:"following"`
FollowedBy bool `json:"followed_by"`
Blocking bool `json:"blocking"`
+ BlockedBy bool `json:"blocked_by"`
Muting bool `json:"muting"`
MutingNotifications bool `json:"muting_notifications"`
Subscribing bool `json:"subscribing"`
@@ -243,11 +352,10 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship,
}
// AccountMute mute the account.
-func (c *Client) AccountMute(ctx context.Context, id string, notifications *bool) (*Relationship, error) {
+func (c *Client) AccountMute(ctx context.Context, id string, notifications bool, duration int) (*Relationship, error) {
params := url.Values{}
- if notifications != nil {
- params.Set("notifications", strconv.FormatBool(*notifications))
- }
+ params.Set("notifications", strconv.FormatBool(notifications))
+ params.Set("duration", strconv.Itoa(duration))
var relationship Relationship
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), params, &relationship, nil)
if err != nil {
diff --git a/mastodon/apps.go b/mastodon/apps.go
index 5d925c3..12d2e86 100644
--- a/mastodon/apps.go
+++ b/mastodon/apps.go
@@ -11,7 +11,6 @@ import (
// AppConfig is a setting for registering applications.
type AppConfig struct {
- http.Client
Server string
ClientName string
@@ -62,7 +61,7 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- resp, err := appConfig.Do(req)
+ resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
diff --git a/mastodon/http.go b/mastodon/http.go
new file mode 100644
index 0000000..7d1c1c4
--- /dev/null
+++ b/mastodon/http.go
@@ -0,0 +1,45 @@
+package mastodon
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+)
+
+type lr struct {
+ io.ReadCloser
+ n int64
+ r *http.Request
+}
+
+func (r *lr) Read(p []byte) (n int, err error) {
+ if r.n <= 0 {
+ return 0, fmt.Errorf("%s \"%s\": response body too large", r.r.Method, r.r.URL)
+ }
+ if int64(len(p)) > r.n {
+ p = p[0:r.n]
+ }
+ n, err = r.ReadCloser.Read(p)
+ r.n -= int64(n)
+ return
+}
+
+type transport struct {
+ t http.RoundTripper
+}
+
+func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) {
+ resp, err := t.t.RoundTrip(r)
+ if resp != nil && resp.Body != nil {
+ resp.Body = &lr{resp.Body, 8 << 20, r}
+ }
+ return resp, err
+}
+
+var httpClient = &http.Client{
+ Transport: &transport{
+ t: http.DefaultTransport,
+ },
+ Timeout: 30 * time.Second,
+}
diff --git a/mastodon/mastodon.go b/mastodon/mastodon.go
index 8678314..194ca30 100644
--- a/mastodon/mastodon.go
+++ b/mastodon/mastodon.go
@@ -2,18 +2,14 @@
package mastodon
import (
- "bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
- "mime/multipart"
"net/http"
"net/url"
- "os"
"path"
- "path/filepath"
"strings"
"github.com/tomnomnom/linkheader"
@@ -33,6 +29,11 @@ type Client struct {
config *Config
}
+type multipartRequest struct {
+ Data io.Reader
+ ContentType string
+}
+
func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error {
u, err := url.Parse(c.config.Server)
if err != nil {
@@ -56,83 +57,12 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
if err != nil {
return err
}
- } else if file, ok := params.(string); ok {
- f, err := os.Open(file)
- if err != nil {
- return err
- }
- defer f.Close()
-
- var buf bytes.Buffer
- mw := multipart.NewWriter(&buf)
- part, err := mw.CreateFormFile("file", filepath.Base(file))
- if err != nil {
- return err
- }
- _, err = io.Copy(part, f)
- if err != nil {
- return err
- }
- err = mw.Close()
- if err != nil {
- return err
- }
- req, err = http.NewRequest(method, u.String(), &buf)
- if err != nil {
- return err
- }
- ct = mw.FormDataContentType()
- } else if file, ok := params.(*multipart.FileHeader); ok {
- f, err := file.Open()
- if err != nil {
- return err
- }
- defer f.Close()
-
- var buf bytes.Buffer
- mw := multipart.NewWriter(&buf)
- fname := filepath.Base(file.Filename)
- err = mw.WriteField("description", fname)
- if err != nil {
- return err
- }
- part, err := mw.CreateFormFile("file", fname)
- if err != nil {
- return err
- }
- _, err = io.Copy(part, f)
- if err != nil {
- return err
- }
- err = mw.Close()
- if err != nil {
- return err
- }
- req, err = http.NewRequest(method, u.String(), &buf)
+ } else if mr, ok := params.(*multipartRequest); ok {
+ req, err = http.NewRequest(method, u.String(), mr.Data)
if err != nil {
return err
}
- ct = mw.FormDataContentType()
- } else if reader, ok := params.(io.Reader); ok {
- var buf bytes.Buffer
- mw := multipart.NewWriter(&buf)
- part, err := mw.CreateFormFile("file", "upload")
- if err != nil {
- return err
- }
- _, err = io.Copy(part, reader)
- if err != nil {
- return err
- }
- err = mw.Close()
- if err != nil {
- return err
- }
- req, err = http.NewRequest(method, u.String(), &buf)
- if err != nil {
- return err
- }
- ct = mw.FormDataContentType()
+ ct = mr.ContentType
} else {
if method == http.MethodGet && pg != nil {
u.RawQuery = pg.toValues().Encode()
@@ -168,12 +98,13 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
}
}
return json.NewDecoder(resp.Body).Decode(&res)
+
}
// NewClient return new mastodon API client.
func NewClient(config *Config) *Client {
return &Client{
- Client: http.DefaultClient,
+ Client: httpClient,
config: config,
}
}
@@ -207,6 +138,16 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st
return c.authenticate(ctx, params)
}
+func (c *Client) RevokeToken(ctx context.Context) error {
+ params := url.Values{
+ "client_id": {c.config.ClientID},
+ "client_secret": {c.config.ClientSecret},
+ "token": {c.GetAccessToken(ctx)},
+ }
+
+ return c.doAPI(ctx, http.MethodPost, "/oauth/revoke", params, nil, nil)
+}
+
func (c *Client) authenticate(ctx context.Context, params url.Values) error {
u, err := url.Parse(c.config.Server)
if err != nil {
diff --git a/mastodon/status.go b/mastodon/status.go
index 2fae6ee..20f74a5 100644
--- a/mastodon/status.go
+++ b/mastodon/status.go
@@ -1,12 +1,14 @@
package mastodon
import (
+ "bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
+ "path/filepath"
"time"
)
@@ -56,7 +58,6 @@ type Status struct {
MediaAttachments []Attachment `json:"media_attachments"`
Mentions []Mention `json:"mentions"`
Tags []Tag `json:"tags"`
- Card *Card `json:"card"`
Application Application `json:"application"`
Language string `json:"language"`
Pinned interface{} `json:"pinned"`
@@ -77,22 +78,6 @@ type Context struct {
Descendants []*Status `json:"descendants"`
}
-// Card hold information for mastodon card.
-type Card struct {
- URL string `json:"url"`
- Title string `json:"title"`
- Description string `json:"description"`
- Image string `json:"image"`
- Type string `json:"type"`
- AuthorName string `json:"author_name"`
- AuthorURL string `json:"author_url"`
- ProviderName string `json:"provider_name"`
- ProviderURL string `json:"provider_url"`
- HTML string `json:"html"`
- Width int64 `json:"width"`
- Height int64 `json:"height"`
-}
-
// GetFavourites return the favorite list of the current user.
func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) {
var statuses []*Status
@@ -123,16 +108,6 @@ func (c *Client) GetStatusContext(ctx context.Context, id string) (*Context, err
return &context, nil
}
-// GetStatusCard return status specified by id.
-func (c *Client) GetStatusCard(ctx context.Context, id string) (*Card, error) {
- var card Card
- err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil)
- if err != nil {
- return nil, err
- }
- return &card, nil
-}
-
// GetRebloggedBy returns the account list of the user who reblogged the toot of id.
func (c *Client) GetRebloggedBy(ctx context.Context, id string, pg *Pagination) ([]*Account, error) {
var accounts []*Account
@@ -320,30 +295,35 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
return &results, nil
}
-// UploadMedia upload a media attachment from a file.
-func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) {
- var attachment Attachment
- err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil)
+func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
+ f, err := fh.Open()
if err != nil {
return nil, err
}
- return &attachment, nil
-}
+ defer f.Close()
-// UploadMediaFromReader uploads a media attachment from a io.Reader.
-func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) {
- var attachment Attachment
- err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil)
+ var buf bytes.Buffer
+ mw := multipart.NewWriter(&buf)
+ fname := filepath.Base(fh.Filename)
+ err = mw.WriteField("description", fname)
if err != nil {
return nil, err
}
- return &attachment, nil
-}
-
-// UploadMediaFromReader uploads a media attachment from a io.Reader.
-func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
+ part, err := mw.CreateFormFile("file", fname)
+ if err != nil {
+ return nil, err
+ }
+ _, err = io.Copy(part, f)
+ if err != nil {
+ return nil, err
+ }
+ err = mw.Close()
+ if err != nil {
+ return nil, err
+ }
+ params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
var attachment Attachment
- err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil)
+ err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil)
if err != nil {
return nil, err
}
diff --git a/model/app.go b/model/app.go
deleted file mode 100644
index 8f172c8..0000000
--- a/model/app.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package model
-
-import (
- "errors"
-)
-
-var (
- ErrAppNotFound = errors.New("app not found")
-)
-
-type App struct {
- InstanceDomain string `json:"instance_domain"`
- InstanceURL string `json:"instance_url"`
- ClientID string `json:"client_id"`
- ClientSecret string `json:"client_secret"`
-}
-
-type AppRepo interface {
- Add(app App) (err error)
- Get(instanceDomain string) (app App, err error)
-}
diff --git a/model/session.go b/model/session.go
index 5ff079b..61a409c 100644
--- a/model/session.go
+++ b/model/session.go
@@ -1,28 +1,49 @@
package model
-import (
- "errors"
-)
-
-var (
- ErrSessionNotFound = errors.New("session not found")
-)
-
type Session struct {
- ID string `json:"id"`
- UserID string `json:"user_id"`
- InstanceDomain string `json:"instance_domain"`
- AccessToken string `json:"access_token"`
- CSRFToken string `json:"csrf_token"`
- Settings Settings `json:"settings"`
-}
-
-type SessionRepo interface {
- Add(session Session) (err error)
- Get(sessionID string) (session Session, err error)
- Remove(sessionID string)
+ UserID string `json:"uid,omitempty"`
+ Instance string `json:"ins,omitempty"`
+ ClientID string `json:"cid,omitempty"`
+ ClientSecret string `json:"cs,omitempty"`
+ AccessToken string `json:"at,omitempty"`
+ CSRFToken string `json:"csrf,omitempty"`
+ Settings Settings `json:"sett,omitempty"`
}
func (s Session) IsLoggedIn() bool {
return len(s.AccessToken) > 0
}
+
+type Settings struct {
+ DefaultVisibility string `json:"dv,omitempty"`
+ DefaultFormat string `json:"df,omitempty"`
+ CopyScope bool `json:"cs,omitempty"`
+ ThreadInNewTab bool `json:"tnt,omitempty"`
+ HideAttachments bool `json:"ha,omitempty"`
+ MaskNSFW bool `json:"mn,omitempty"`
+ NotificationInterval int `json:"ni,omitempty"`
+ FluorideMode bool `json:"fm,omitempty"`
+ DarkMode bool `json:"dm,omitempty"`
+ AntiDopamineMode bool `json:"adm,omitempty"`
+ HideUnsupportedNotifs bool `json:"hun,omitempty"`
+ CSS string `json:"css,omitempty"`
+ CSSHash string `json:"cssh,omitempty"`
+}
+
+func NewSettings() *Settings {
+ return &Settings{
+ DefaultVisibility: "public",
+ DefaultFormat: "",
+ CopyScope: true,
+ ThreadInNewTab: false,
+ HideAttachments: false,
+ MaskNSFW: true,
+ NotificationInterval: 0,
+ FluorideMode: false,
+ DarkMode: false,
+ AntiDopamineMode: false,
+ HideUnsupportedNotifs: false,
+ CSS: "",
+ CSSHash: "",
+ }
+}
diff --git a/model/settings.go b/model/settings.go
deleted file mode 100644
index 1f83c75..0000000
--- a/model/settings.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package model
-
-type Settings struct {
- DefaultVisibility string `json:"default_visibility"`
- DefaultFormat string `json:"default_format"`
- CopyScope bool `json:"copy_scope"`
- ThreadInNewTab bool `json:"thread_in_new_tab"`
- HideAttachments bool `json:"hide_attachments"`
- MaskNSFW bool `json:"mask_nfsw"`
- NotificationInterval int `json:"notifications_interval"`
- FluorideMode bool `json:"fluoride_mode"`
- DarkMode bool `json:"dark_mode"`
- AntiDopamineMode bool `json:"anti_dopamine_mode"`
- HideUnsupportedNotifs bool `json:"hide_unsupported_notifs"`
- CSS string `json:"css"`
-}
-
-func NewSettings() *Settings {
- return &Settings{
- DefaultVisibility: "public",
- DefaultFormat: "",
- CopyScope: true,
- ThreadInNewTab: false,
- HideAttachments: false,
- MaskNSFW: true,
- NotificationInterval: 0,
- FluorideMode: false,
- DarkMode: false,
- AntiDopamineMode: false,
- HideUnsupportedNotifs: false,
- CSS: "",
- }
-}
diff --git a/renderer/model.go b/renderer/model.go
index e7cfbfb..3862976 100644
--- a/renderer/model.go
+++ b/renderer/model.go
@@ -49,7 +49,7 @@ type SigninData struct {
}
type RootData struct {
- Title string
+ *CommonData
}
type TimelineData struct {
@@ -99,12 +99,11 @@ type NotificationData struct {
type UserData struct {
*CommonData
- User *mastodon.Account
- IsCurrent bool
- Type string
- Users []*mastodon.Account
- Statuses []*mastodon.Status
- NextLink string
+ User *mastodon.Account
+ Type string
+ Users []*mastodon.Account
+ Statuses []*mastodon.Status
+ NextLink string
}
type UserSearchData struct {
@@ -155,3 +154,13 @@ type FiltersData struct {
*CommonData
Filters []*mastodon.Filter
}
+
+type ProfileData struct {
+ *CommonData
+ User *mastodon.Account
+}
+
+type MuteData struct {
+ *CommonData
+ User *mastodon.Account
+}
diff --git a/renderer/renderer.go b/renderer/renderer.go
index 7afeb14..a88bb9e 100644
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -33,6 +33,8 @@ const (
SearchPage = "search.tmpl"
SettingsPage = "settings.tmpl"
FiltersPage = "filters.tmpl"
+ ProfilePage = "profile.tmpl"
+ MutePage = "mute.tmpl"
)
type TemplateData struct {
@@ -41,7 +43,7 @@ type TemplateData struct {
}
func emojiHTML(e mastodon.Emoji, height string) string {
- return `<img class="emoji" src="` + e.URL + `" alt=":` + e.ShortCode + `:" title=":` + e.ShortCode + `:" height="` + height + `"/>`
+ return `<img class="emoji" src="` + e.URL + `" alt=":` + e.ShortCode + `:" title=":` + e.ShortCode + `:" height="` + height + `">`
}
func emojiFilter(content string, emojis []mastodon.Emoji) string {
diff --git a/repo/appRepo.go b/repo/appRepo.go
deleted file mode 100644
index d97ac1f..0000000
--- a/repo/appRepo.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package repo
-
-import (
- "encoding/json"
-
- "bloat/util"
- "bloat/model"
-)
-
-type appRepo struct {
- db *util.Database
-}
-
-func NewAppRepo(db *util.Database) *appRepo {
- return &appRepo{
- db: db,
- }
-}
-
-func (repo *appRepo) Add(a model.App) (err error) {
- data, err := json.Marshal(a)
- if err != nil {
- return
- }
- err = repo.db.Set(a.InstanceDomain, data)
- return
-}
-
-func (repo *appRepo) Get(instanceDomain string) (a model.App, err error) {
- data, err := repo.db.Get(instanceDomain)
- if err != nil {
- err = model.ErrAppNotFound
- return
- }
-
- err = json.Unmarshal(data, &a)
- if err != nil {
- return
- }
-
- return
-}
diff --git a/repo/sessionRepo.go b/repo/sessionRepo.go
deleted file mode 100644
index 2097c3e..0000000
--- a/repo/sessionRepo.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package repo
-
-import (
- "encoding/json"
-
- "bloat/util"
- "bloat/model"
-)
-
-type sessionRepo struct {
- db *util.Database
-}
-
-func NewSessionRepo(db *util.Database) *sessionRepo {
- return &sessionRepo{
- db: db,
- }
-}
-
-func (repo *sessionRepo) Add(s model.Session) (err error) {
- data, err := json.Marshal(s)
- if err != nil {
- return
- }
- err = repo.db.Set(s.ID, data)
- return
-}
-
-func (repo *sessionRepo) Get(id string) (s model.Session, err error) {
- data, err := repo.db.Get(id)
- if err != nil {
- err = model.ErrSessionNotFound
- return
- }
-
- err = json.Unmarshal(data, &s)
- if err != nil {
- return
- }
-
- return
-}
-
-func (repo *sessionRepo) Remove(id string) {
- repo.db.Remove(id)
- return
-}
diff --git a/service/client.go b/service/client.go
new file mode 100644
index 0000000..18ebb52
--- /dev/null
+++ b/service/client.go
@@ -0,0 +1,118 @@
+package service
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+ "time"
+
+ "bloat/mastodon"
+ "bloat/model"
+ "bloat/renderer"
+)
+
+type client struct {
+ *mastodon.Client
+ w http.ResponseWriter
+ r *http.Request
+ s *model.Session
+ csrf string
+ ctx context.Context
+ rctx *renderer.Context
+}
+
+func (c *client) setSession(sess *model.Session) error {
+ var sb strings.Builder
+ bw := base64.NewEncoder(base64.URLEncoding, &sb)
+ err := json.NewEncoder(bw).Encode(sess)
+ bw.Close()
+ if err != nil {
+ return err
+ }
+ http.SetCookie(c.w, &http.Cookie{
+ Name: "session",
+ Path: "/",
+ HttpOnly: true,
+ Value: sb.String(),
+ Expires: time.Now().Add(365 * 24 * time.Hour),
+ })
+ return nil
+}
+
+func (c *client) getSession() (sess *model.Session, err error) {
+ cookie, _ := c.r.Cookie("session")
+ if cookie == nil {
+ return nil, errInvalidSession
+ }
+ br := base64.NewDecoder(base64.URLEncoding, strings.NewReader(cookie.Value))
+ err = json.NewDecoder(br).Decode(&sess)
+ return
+}
+
+func (c *client) unsetSession() {
+ http.SetCookie(c.w, &http.Cookie{
+ Name: "session",
+ Path: "/",
+ Value: "",
+ Expires: time.Now(),
+ })
+}
+
+func (c *client) writeJson(data interface{}) error {
+ return json.NewEncoder(c.w).Encode(map[string]interface{}{
+ "data": data,
+ })
+}
+
+func (c *client) redirect(url string) {
+ c.w.Header().Add("Location", url)
+ c.w.WriteHeader(http.StatusFound)
+}
+
+func (c *client) authenticate(t int, instance string) (err error) {
+ csrf := c.r.FormValue("csrf_token")
+ ref := c.r.URL.RequestURI()
+ defer func() {
+ if c.s == nil {
+ c.s = &model.Session{
+ Settings: *model.NewSettings(),
+ }
+ }
+ c.rctx = &renderer.Context{
+ HideAttachments: c.s.Settings.HideAttachments,
+ MaskNSFW: c.s.Settings.MaskNSFW,
+ ThreadInNewTab: c.s.Settings.ThreadInNewTab,
+ FluorideMode: c.s.Settings.FluorideMode,
+ DarkMode: c.s.Settings.DarkMode,
+ CSRFToken: c.s.CSRFToken,
+ UserID: c.s.UserID,
+ AntiDopamineMode: c.s.Settings.AntiDopamineMode,
+ UserCSS: c.s.Settings.CSS,
+ Referrer: ref,
+ }
+ }()
+ if t < SESSION {
+ return
+ }
+ sess, err := c.getSession()
+ if err != nil {
+ return err
+ }
+ c.s = sess
+ if len(instance) > 0 && c.s.Instance != instance {
+ return errors.New("invalid instance")
+ }
+ c.Client = mastodon.NewClient(&mastodon.Config{
+ Server: "https://" + c.s.Instance,
+ ClientID: c.s.ClientID,
+ ClientSecret: c.s.ClientSecret,
+ AccessToken: c.s.AccessToken,
+ })
+ if t >= CSRF && (len(csrf) < 1 || csrf != c.s.CSRFToken) {
+ return errInvalidCSRFToken
+ }
+ return
+}
diff --git a/service/service.go b/service/service.go
index cda42f8..24e3f85 100644
--- a/service/service.go
+++ b/service/service.go
@@ -1,6 +1,8 @@
package service
import (
+ "crypto/sha256"
+ "encoding/base64"
"errors"
"fmt"
"mime/multipart"
@@ -27,14 +29,11 @@ type service struct {
instance string
postFormats []model.PostFormat
renderer renderer.Renderer
- sessionRepo model.SessionRepo
- appRepo model.AppRepo
}
func NewService(cname string, cscope string, cwebsite string,
css string, instance string, postFormats []model.PostFormat,
- renderer renderer.Renderer, sessionRepo model.SessionRepo,
- appRepo model.AppRepo) *service {
+ renderer renderer.Renderer) *service {
return &service{
cname: cname,
cscope: cscope,
@@ -43,61 +42,18 @@ func NewService(cname string, cscope string, cwebsite string,
instance: instance,
postFormats: postFormats,
renderer: renderer,
- sessionRepo: sessionRepo,
- appRepo: appRepo,
}
}
-func (s *service) authenticate(c *client, sid string, csrf string, ref string, t int) (err error) {
- var sett *model.Settings
- defer func() {
- if sett == nil {
- sett = model.NewSettings()
- }
- c.rctx = &renderer.Context{
- HideAttachments: sett.HideAttachments,
- MaskNSFW: sett.MaskNSFW,
- ThreadInNewTab: sett.ThreadInNewTab,
- FluorideMode: sett.FluorideMode,
- DarkMode: sett.DarkMode,
- CSRFToken: c.s.CSRFToken,
- UserID: c.s.UserID,
- AntiDopamineMode: sett.AntiDopamineMode,
- UserCSS: sett.CSS,
- Referrer: ref,
- }
- }()
- if t < SESSION {
- return
- }
- if len(sid) < 1 {
- return errInvalidSession
- }
- c.s, err = s.sessionRepo.Get(sid)
- if err != nil {
- return errInvalidSession
- }
- sett = &c.s.Settings
- app, err := s.appRepo.Get(c.s.InstanceDomain)
- if err != nil {
- return err
- }
- c.Client = mastodon.NewClient(&mastodon.Config{
- Server: app.InstanceURL,
- ClientID: app.ClientID,
- ClientSecret: app.ClientSecret,
- AccessToken: c.s.AccessToken,
- })
- if t >= CSRF && (len(csrf) < 1 || csrf != c.s.CSRFToken) {
- return errInvalidCSRFToken
- }
- return
-}
-
func (s *service) cdata(c *client, title string, count int, rinterval int,
target string) (data *renderer.CommonData) {
+ if title == "" {
+ title = s.cname
+ } else {
+ title += " - " + s.cname
+ }
data = &renderer.CommonData{
- Title: title + " - " + s.cname,
+ Title: title,
CustomCSS: s.css,
Count: count,
RefreshInterval: rinterval,
@@ -105,6 +61,7 @@ func (s *service) cdata(c *client, title string, count int, rinterval int,
}
if c != nil && c.s.IsLoggedIn() {
data.CSRFToken = c.s.CSRFToken
+ data.Title += " - " + c.s.Instance
}
return
}
@@ -130,7 +87,7 @@ func (s *service) ErrorPage(c *client, err error, retry bool) error {
}
func (s *service) SigninPage(c *client) (err error) {
- cdata := s.cdata(nil, "signin", 0, 0, "")
+ cdata := s.cdata(nil, "Signin", 0, 0, "")
data := &renderer.SigninData{
CommonData: cdata,
}
@@ -138,8 +95,9 @@ func (s *service) SigninPage(c *client) (err error) {
}
func (s *service) RootPage(c *client) (err error) {
+ cdata := s.cdata(c, "", 0, 0, "")
data := &renderer.RootData{
- Title: s.cname,
+ CommonData: cdata,
}
return s.renderer.Render(c.rctx, c.w, renderer.RootPage, data)
}
@@ -154,7 +112,7 @@ func (s *service) NavPage(c *client) (err error) {
DefaultFormat: c.s.Settings.DefaultFormat,
Formats: s.postFormats,
}
- cdata := s.cdata(c, "nav", 0, 0, "main")
+ cdata := s.cdata(c, "Nav", 0, 0, "main")
data := &renderer.NavData{
User: u,
CommonData: cdata,
@@ -251,7 +209,7 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
nextLink = "/timeline/" + tType + "?" + v.Encode()
}
- cdata := s.cdata(c, tType+" timeline ", 0, 0, "")
+ cdata := s.cdata(c, title, 0, 0, "")
data := &renderer.TimelineData{
Title: title,
Type: tType,
@@ -404,7 +362,7 @@ func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
}
- cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
+ cdata := s.cdata(c, "Post by "+status.Account.DisplayName, 0, 0, "")
data := &renderer.ThreadData{
Statuses: statuses,
PostContext: pctx,
@@ -460,7 +418,7 @@ func (s *service) QuickReplyPage(c *client, id string) (err error) {
},
}
- cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
+ cdata := s.cdata(c, "Post by "+status.Account.DisplayName, 0, 0, "")
data := &renderer.QuickReplyData{
Ancestor: ancestor,
Status: status,
@@ -475,7 +433,7 @@ func (s *service) LikedByPage(c *client, id string) (err error) {
if err != nil {
return
}
- cdata := s.cdata(c, "likes", 0, 0, "")
+ cdata := s.cdata(c, "Likes", 0, 0, "")
data := &renderer.LikedByData{
CommonData: cdata,
Users: likers,
@@ -488,7 +446,7 @@ func (s *service) RetweetedByPage(c *client, id string) (err error) {
if err != nil {
return
}
- cdata := s.cdata(c, "retweets", 0, 0, "")
+ cdata := s.cdata(c, "Retweets", 0, 0, "")
data := &renderer.RetweetedByData{
CommonData: cdata,
Users: retweeters,
@@ -537,7 +495,7 @@ func (s *service) NotificationPage(c *client, maxID string,
nextLink = "/notifications?max_id=" + pg.MaxID
}
- cdata := s.cdata(c, "notifications", unreadCount,
+ cdata := s.cdata(c, "Notifications", unreadCount,
c.s.Settings.NotificationInterval, "main")
data := &renderer.NotificationData{
Notifications: notifications,
@@ -560,12 +518,19 @@ func (s *service) UserPage(c *client, id string, pageType string,
MinID: minID,
Limit: 20,
}
+ isCurrent := c.s.UserID == id
- user, err := c.GetAccount(c.ctx, id)
+ // Some fields like AccountSource are only available in the
+ // CurrentUser API
+ var user *mastodon.Account
+ if isCurrent {
+ user, err = c.GetAccountCurrentUser(c.ctx)
+ } else {
+ user, err = c.GetAccount(c.ctx, id)
+ }
if err != nil {
return
}
- isCurrent := c.s.UserID == user.ID
switch pageType {
case "":
@@ -677,7 +642,6 @@ func (s *service) UserPage(c *client, id string, pageType string,
cdata := s.cdata(c, user.DisplayName+" @"+user.Acct, 0, 0, "")
data := &renderer.UserData{
User: user,
- IsCurrent: isCurrent,
Type: pageType,
Users: users,
Statuses: statuses,
@@ -691,7 +655,7 @@ func (s *service) UserSearchPage(c *client,
id string, q string, offset int) (err error) {
var nextLink string
- var title = "search"
+ var title = "Search"
user, err := c.GetAccount(c.ctx, id)
if err != nil {
@@ -729,8 +693,21 @@ func (s *service) UserSearchPage(c *client,
return s.renderer.Render(c.rctx, c.w, renderer.UserSearchPage, data)
}
+func (s *service) MutePage(c *client, id string) (err error) {
+ user, err := c.GetAccount(c.ctx, id)
+ if err != nil {
+ return
+ }
+ cdata := s.cdata(c, "Mute "+user.DisplayName+" @"+user.Acct, 0, 0, "")
+ data := &renderer.UserData{
+ User: user,
+ CommonData: cdata,
+ }
+ return s.renderer.Render(c.rctx, c.w, renderer.MutePage, data)
+}
+
func (s *service) AboutPage(c *client) (err error) {
- cdata := s.cdata(c, "about", 0, 0, "")
+ cdata := s.cdata(c, "About", 0, 0, "")
data := &renderer.AboutData{
CommonData: cdata,
}
@@ -742,7 +719,7 @@ func (s *service) EmojiPage(c *client) (err error) {
if err != nil {
return
}
- cdata := s.cdata(c, "emojis", 0, 0, "")
+ cdata := s.cdata(c, "Emojis", 0, 0, "")
data := &renderer.EmojiData{
Emojis: emojis,
CommonData: cdata,
@@ -754,7 +731,7 @@ func (s *service) SearchPage(c *client,
q string, qType string, offset int) (err error) {
var nextLink string
- var title = "search"
+ var title = "Search"
var results *mastodon.Results
if len(q) > 0 {
@@ -790,7 +767,7 @@ func (s *service) SearchPage(c *client,
}
func (s *service) SettingsPage(c *client) (err error) {
- cdata := s.cdata(c, "settings", 0, 0, "")
+ cdata := s.cdata(c, "Settings", 0, 0, "")
data := &renderer.SettingsData{
CommonData: cdata,
Settings: &c.s.Settings,
@@ -804,7 +781,7 @@ func (svc *service) FiltersPage(c *client) (err error) {
if err != nil {
return
}
- cdata := svc.cdata(c, "filters", 0, 0, "")
+ cdata := svc.cdata(c, "Filters", 0, 0, "")
data := &renderer.FiltersData{
CommonData: cdata,
Filters: filters,
@@ -812,6 +789,55 @@ func (svc *service) FiltersPage(c *client) (err error) {
return svc.renderer.Render(c.rctx, c.w, renderer.FiltersPage, data)
}
+func (svc *service) ProfilePage(c *client) (err error) {
+ u, err := c.GetAccountCurrentUser(c.ctx)
+ if err != nil {
+ return
+ }
+ // Some instances allow more than 4 fields, but make sure that there are
+ // at least 4 fields in the slice because the template depends on it.
+ if u.Source.Fields == nil {
+ u.Source.Fields = new([]mastodon.Field)
+ }
+ for len(*u.Source.Fields) < 4 {
+ *u.Source.Fields = append(*u.Source.Fields, mastodon.Field{})
+ }
+ cdata := svc.cdata(c, "Edit profile", 0, 0, "")
+ data := &renderer.ProfileData{
+ CommonData: cdata,
+ User: u,
+ }
+ return svc.renderer.Render(c.rctx, c.w, renderer.ProfilePage, data)
+}
+
+func (s *service) ProfileUpdate(c *client, name, bio string, avatar, banner *multipart.FileHeader,
+ fields []mastodon.Field, locked bool) (err error) {
+ // Need to pass empty data to clear fields
+ if len(fields) == 0 {
+ fields = append(fields, mastodon.Field{})
+ }
+ p := &mastodon.Profile{
+ DisplayName: &name,
+ Note: &bio,
+ Avatar: avatar,
+ Header: banner,
+ Fields: &fields,
+ Locked: &locked,
+ }
+ _, err = c.AccountUpdate(c.ctx, p)
+ return err
+}
+
+func (s *service) ProfileDelAvatar(c *client) (err error) {
+ _, err = c.AccountDeleteAvatar(c.ctx)
+ return
+}
+
+func (s *service) ProfileDelBanner(c *client) (err error) {
+ _, err = c.AccountDeleteHeader(c.ctx)
+ return err
+}
+
func (s *service) SingleInstance() (instance string, ok bool) {
if len(s.instance) > 0 {
instance = s.instance
@@ -820,7 +846,7 @@ func (s *service) SingleInstance() (instance string, ok bool) {
return
}
-func (s *service) NewSession(c *client, instance string) (rurl string, sid string, err error) {
+func (s *service) NewSession(c *client, instance string) (rurl string, sess *model.Session, err error) {
var instanceURL string
if strings.HasPrefix(instance, "https://") {
instanceURL = instance
@@ -829,66 +855,29 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sid strin
instanceURL = "https://" + instance
}
- sid, err = util.NewSessionID()
- if err != nil {
- return
- }
csrf, err := util.NewCSRFToken()
if err != nil {
return
}
- sess := model.Session{
- ID: sid,
- InstanceDomain: instance,
- CSRFToken: csrf,
- Settings: *model.NewSettings(),
- }
- err = s.sessionRepo.Add(sess)
+ app, err := mastodon.RegisterApp(c.ctx, &mastodon.AppConfig{
+ Server: instanceURL,
+ ClientName: s.cname,
+ Scopes: s.cscope,
+ Website: s.cwebsite,
+ RedirectURIs: s.cwebsite + "/oauth_callback",
+ })
if err != nil {
return
}
-
- app, err := s.appRepo.Get(instance)
- if err != nil {
- if err != model.ErrAppNotFound {
- return
- }
- mastoApp, err := mastodon.RegisterApp(c.ctx, &mastodon.AppConfig{
- Server: instanceURL,
- ClientName: s.cname,
- Scopes: s.cscope,
- Website: s.cwebsite,
- RedirectURIs: s.cwebsite + "/oauth_callback",
- })
- if err != nil {
- return "", "", err
- }
- app = model.App{
- InstanceDomain: instance,
- InstanceURL: instanceURL,
- ClientID: mastoApp.ClientID,
- ClientSecret: mastoApp.ClientSecret,
- }
- err = s.appRepo.Add(app)
- if err != nil {
- return "", "", err
- }
- }
-
- u, err := url.Parse("/oauth/authorize")
- if err != nil {
- return
+ rurl = app.AuthURI
+ sess = &model.Session{
+ Instance: instance,
+ ClientID: app.ClientID,
+ ClientSecret: app.ClientSecret,
+ CSRFToken: csrf,
+ Settings: *model.NewSettings(),
}
-
- q := make(url.Values)
- q.Set("scope", "read write follow")
- q.Set("client_id", app.ClientID)
- q.Set("response_type", "code")
- q.Set("redirect_uri", s.cwebsite+"/oauth_callback")
- u.RawQuery = q.Encode()
-
- rurl = instanceURL + u.String()
return
}
@@ -907,12 +896,11 @@ func (s *service) Signin(c *client, code string) (err error) {
}
c.s.AccessToken = c.GetAccessToken(c.ctx)
c.s.UserID = u.ID
- return s.sessionRepo.Add(c.s)
+ return c.setSession(c.s)
}
func (s *service) Signout(c *client) (err error) {
- s.sessionRepo.Remove(c.s.ID)
- return
+ return c.RevokeToken(c.ctx)
}
func (s *service) Post(c *client, content string, replyToID string,
@@ -1005,8 +993,8 @@ func (s *service) Reject(c *client, id string) (err error) {
return c.FollowRequestReject(c.ctx, id)
}
-func (s *service) Mute(c *client, id string, notifications *bool) (err error) {
- _, err = c.AccountMute(c.ctx, id, notifications)
+func (s *service) Mute(c *client, id string, notifications bool, duration int) (err error) {
+ _, err = c.AccountMute(c.ctx, id, notifications, duration)
return
}
@@ -1041,15 +1029,21 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error)
default:
return errInvalidArgument
}
- if len(settings.CSS) > 1<<20 {
- return errInvalidArgument
- }
- sess, err := s.sessionRepo.Get(c.s.ID)
- if err != nil {
- return
+ if len(settings.CSS) > 0 {
+ if len(settings.CSS) > 1<<20 {
+ return errInvalidArgument
+ }
+ // For some reason, browsers convert CRLF to LF before calculating
+ // the hash of the inline resources.
+ settings.CSS = strings.Replace(settings.CSS, "\x0d\x0a", "\x0a", -1)
+
+ h := sha256.Sum256([]byte(settings.CSS))
+ settings.CSSHash = base64.StdEncoding.EncodeToString(h[:])
+ } else {
+ settings.CSSHash = ""
}
- sess.Settings = *settings
- return s.sessionRepo.Add(sess)
+ c.s.Settings = *settings
+ return c.setSession(c.s)
}
func (s *service) MuteConversation(c *client, id string) (err error) {
diff --git a/service/transport.go b/service/transport.go
index 4518b1a..f7e31d6 100644
--- a/service/transport.go
+++ b/service/transport.go
@@ -1,25 +1,21 @@
package service
import (
- "context"
"encoding/json"
+ "fmt"
"log"
+ "mime/multipart"
"net/http"
"strconv"
"time"
"bloat/mastodon"
"bloat/model"
- "bloat/renderer"
"github.com/gorilla/mux"
)
const (
- sessionExp = 365 * 24 * time.Hour
-)
-
-const (
HTML int = iota
JSON
)
@@ -30,36 +26,16 @@ const (
CSRF
)
-type client struct {
- *mastodon.Client
- w http.ResponseWriter
- r *http.Request
- s model.Session
- csrf string
- ctx context.Context
- rctx *renderer.Context
-}
-
-func setSessionCookie(w http.ResponseWriter, sid string, exp time.Duration) {
- http.SetCookie(w, &http.Cookie{
- Name: "session_id",
- Value: sid,
- Expires: time.Now().Add(exp),
- })
-}
-
-func writeJson(c *client, data interface{}) error {
- return json.NewEncoder(c.w).Encode(map[string]interface{}{
- "data": data,
- })
-}
-
-func redirect(c *client, url string) {
- c.w.Header().Add("Location", url)
- c.w.WriteHeader(http.StatusFound)
-}
+const csp = "default-src 'none';" +
+ " img-src *;" +
+ " media-src *;" +
+ " font-src *;" +
+ " child-src *;" +
+ " connect-src 'self';" +
+ " script-src 'self';" +
+ " style-src 'self'"
-func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
+func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
r := mux.NewRouter()
writeError := func(c *client, err error, t int, retry bool) {
@@ -75,16 +51,6 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
}
}
- authenticate := func(c *client, t int) error {
- var sid string
- if cookie, _ := c.r.Cookie("session_id"); cookie != nil {
- sid = cookie.Value
- }
- csrf := c.r.FormValue("csrf_token")
- ref := c.r.URL.RequestURI()
- return s.authenticate(c, sid, csrf, ref, t)
- }
-
handle := func(f func(c *client) error, at int, rt int) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var err error
@@ -94,26 +60,35 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
r: req,
}
- defer func(begin time.Time) {
- logger.Printf("path=%s, err=%v, took=%v\n",
- req.URL.Path, err, time.Since(begin))
- }(time.Now())
+ if verbose {
+ defer func(begin time.Time) {
+ log.Printf("path=%s, err=%v, took=%v\n",
+ req.URL.Path, err, time.Since(begin))
+ }(time.Now())
+ }
- var ct string
+ h := c.w.Header()
switch rt {
case HTML:
- ct = "text/html; charset=utf-8"
+ h.Set("Content-Type", "text/html; charset=utf-8")
+ h.Set("Content-Security-Policy", csp)
case JSON:
- ct = "application/json"
+ h.Set("Content-Type", "application/json")
}
- c.w.Header().Add("Content-Type", ct)
- err = authenticate(c, at)
+ err = c.authenticate(at, s.instance)
if err != nil {
writeError(c, err, rt, req.Method == http.MethodGet)
return
}
+ // Override the CSP header to allow custom CSS
+ if rt == HTML && len(c.s.Settings.CSS) > 0 &&
+ len(c.s.Settings.CSSHash) > 0 {
+ v := fmt.Sprintf("%s 'sha256-%s'", csp, c.s.Settings.CSSHash)
+ h.Set("Content-Security-Policy", v)
+ }
+
err = f(c)
if err != nil {
writeError(c, err, rt, req.Method == http.MethodGet)
@@ -123,16 +98,16 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
}
rootPage := handle(func(c *client) error {
- err := authenticate(c, SESSION)
+ err := c.authenticate(SESSION, "")
if err != nil {
if err == errInvalidSession {
- redirect(c, "/signin")
+ c.redirect("/signin")
return nil
}
return err
}
if !c.s.IsLoggedIn() {
- redirect(c, "/signin")
+ c.redirect("/signin")
return nil
}
return s.RootPage(c)
@@ -147,12 +122,12 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if !ok {
return s.SigninPage(c)
}
- url, sid, err := s.NewSession(c, instance)
+ url, sess, err := s.NewSession(c, instance)
if err != nil {
return err
}
- setSessionCookie(c.w, sid, sessionExp)
- redirect(c, url)
+ c.setSession(sess)
+ c.redirect(url)
return nil
}, NOAUTH, HTML)
@@ -167,7 +142,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
}, SESSION, HTML)
defaultTimelinePage := handle(func(c *client) error {
- redirect(c, "/timeline/home")
+ c.redirect("/timeline/home")
return nil
}, SESSION, HTML)
@@ -217,6 +192,11 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
return s.UserSearchPage(c, id, sq, offset)
}, SESSION, HTML)
+ mutePage := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ return s.MutePage(c, id)
+ }, SESSION, HTML)
+
aboutPage := handle(func(c *client) error {
return s.AboutPage(c)
}, SESSION, HTML)
@@ -241,14 +221,65 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
return s.FiltersPage(c)
}, SESSION, HTML)
+ profilePage := handle(func(c *client) error {
+ return s.ProfilePage(c)
+ }, SESSION, HTML)
+
+ profileUpdate := handle(func(c *client) error {
+ name := c.r.FormValue("name")
+ bio := c.r.FormValue("bio")
+ var avatar, banner *multipart.FileHeader
+ if f := c.r.MultipartForm.File["avatar"]; len(f) > 0 {
+ avatar = f[0]
+ }
+ if f := c.r.MultipartForm.File["banner"]; len(f) > 0 {
+ banner = f[0]
+ }
+ var fields []mastodon.Field
+ for i := 0; i < 16; i++ {
+ n := c.r.FormValue(fmt.Sprintf("field-name-%d", i))
+ v := c.r.FormValue(fmt.Sprintf("field-value-%d", i))
+ if len(n) == 0 {
+ continue
+ }
+ f := mastodon.Field{Name: n, Value: v}
+ fields = append(fields, f)
+ }
+ locked := c.r.FormValue("locked") == "true"
+ err := s.ProfileUpdate(c, name, bio, avatar, banner, fields, locked)
+ if err != nil {
+ return err
+ }
+ c.redirect("/")
+ return nil
+ }, CSRF, HTML)
+
+ profileDelAvatar := handle(func(c *client) error {
+ err := s.ProfileDelAvatar(c)
+ if err != nil {
+ return err
+ }
+ c.redirect(c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
+ profileDelBanner := handle(func(c *client) error {
+ err := s.ProfileDelBanner(c)
+ if err != nil {
+ return err
+ }
+ c.redirect(c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
signin := handle(func(c *client) error {
instance := c.r.FormValue("instance")
- url, sid, err := s.NewSession(c, instance)
+ url, sess, err := s.NewSession(c, instance)
if err != nil {
return err
}
- setSessionCookie(c.w, sid, sessionExp)
- redirect(c, url)
+ c.setSession(sess)
+ c.redirect(url)
return nil
}, NOAUTH, HTML)
@@ -259,7 +290,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, "/")
+ c.redirect("/")
return nil
}, SESSION, HTML)
@@ -287,7 +318,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
} else {
location = c.r.FormValue("referrer")
}
- redirect(c, location)
+ c.redirect(location)
return nil
}, CSRF, HTML)
@@ -301,7 +332,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -315,7 +346,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -329,7 +360,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -343,7 +374,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -355,7 +386,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+statusID)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + statusID)
return nil
}, CSRF, HTML)
@@ -371,7 +402,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -381,7 +412,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -391,7 +422,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -401,23 +432,19 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
mute := handle(func(c *client) error {
id, _ := mux.Vars(c.r)["id"]
- q := c.r.URL.Query()
- var notifications *bool
- if r, ok := q["notifications"]; ok && len(r) > 0 {
- notifications = new(bool)
- *notifications = r[0] == "true"
- }
- err := s.Mute(c, id, notifications)
+ notifications, _ := strconv.ParseBool(c.r.FormValue("notifications"))
+ duration, _ := strconv.Atoi(c.r.FormValue("duration"))
+ err := s.Mute(c, id, notifications, duration)
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect("/user/" + id)
return nil
}, CSRF, HTML)
@@ -427,7 +454,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -437,7 +464,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -447,7 +474,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -457,7 +484,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -467,7 +494,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -504,7 +531,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, "/")
+ c.redirect("/")
return nil
}, CSRF, HTML)
@@ -514,7 +541,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -524,7 +551,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -534,7 +561,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -545,7 +572,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -559,7 +586,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -573,7 +600,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if len(rid) > 0 {
id = rid
}
- redirect(c, c.r.FormValue("referrer")+"#status-"+id)
+ c.redirect(c.r.FormValue("referrer") + "#status-" + id)
return nil
}, CSRF, HTML)
@@ -584,7 +611,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -594,7 +621,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -608,7 +635,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -618,7 +645,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -629,7 +656,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -648,7 +675,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
@@ -660,14 +687,17 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- redirect(c, c.r.FormValue("referrer"))
+ c.redirect(c.r.FormValue("referrer"))
return nil
}, CSRF, HTML)
signout := handle(func(c *client) error {
- s.Signout(c)
- setSessionCookie(c.w, "", 0)
- redirect(c, "/")
+ err := s.Signout(c)
+ if err != nil {
+ return err
+ }
+ c.unsetSession()
+ c.redirect("/")
return nil
}, CSRF, HTML)
@@ -677,7 +707,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- return writeJson(c, count)
+ return c.writeJson(count)
}, CSRF, JSON)
fUnlike := handle(func(c *client) error {
@@ -686,7 +716,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- return writeJson(c, count)
+ return c.writeJson(count)
}, CSRF, JSON)
fRetweet := handle(func(c *client) error {
@@ -695,7 +725,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- return writeJson(c, count)
+ return c.writeJson(count)
}, CSRF, JSON)
fUnretweet := handle(func(c *client) error {
@@ -704,7 +734,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
if err != nil {
return err
}
- return writeJson(c, count)
+ return c.writeJson(count)
}, CSRF, JSON)
r.HandleFunc("/", rootPage).Methods(http.MethodGet)
@@ -720,11 +750,16 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
r.HandleFunc("/user/{id}", userPage).Methods(http.MethodGet)
r.HandleFunc("/user/{id}/{type}", userPage).Methods(http.MethodGet)
r.HandleFunc("/usersearch/{id}", userSearchPage).Methods(http.MethodGet)
+ r.HandleFunc("/mute/{id}", mutePage).Methods(http.MethodGet)
r.HandleFunc("/about", aboutPage).Methods(http.MethodGet)
r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
r.HandleFunc("/search", searchPage).Methods(http.MethodGet)
r.HandleFunc("/settings", settingsPage).Methods(http.MethodGet)
r.HandleFunc("/filters", filtersPage).Methods(http.MethodGet)
+ r.HandleFunc("/profile", profilePage).Methods(http.MethodGet)
+ r.HandleFunc("/profile", profileUpdate).Methods(http.MethodPost)
+ r.HandleFunc("/profile/delavatar", profileDelAvatar).Methods(http.MethodPost)
+ r.HandleFunc("/profile/delbanner", profileDelBanner).Methods(http.MethodPost)
r.HandleFunc("/signin", signin).Methods(http.MethodPost)
r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
r.HandleFunc("/post", post).Methods(http.MethodPost)
diff --git a/static/fluoride.js b/static/fluoride.js
index e6a63ef..73d4939 100644
--- a/static/fluoride.js
+++ b/static/fluoride.js
@@ -74,8 +74,10 @@ function handleLikeForm(id, f) {
for (var i = 0; i < counts.length; i++) {
if (count > 0) {
counts[i].innerHTML = "(" + count + ")";
+ counts[i].classList.remove("hidden");
} else {
counts[i].innerHTML = "";
+ counts[i].classList.add("hidden");
}
}
}, function(err) {
@@ -113,8 +115,10 @@ function handleRetweetForm(id, f) {
for (var i = 0; i < counts.length; i++) {
if (count > 0) {
counts[i].innerHTML = "(" + count + ")";
+ counts[i].classList.remove("hidden");
} else {
counts[i].innerHTML = "";
+ counts[i].classList.add("hidden");
}
}
}, function(err) {
@@ -151,8 +155,7 @@ function handleReplyToLink(a) {
var ract = event.target.getBoundingClientRect();
copy.style["max-width"] = (window.innerWidth - ract.left - 32) + "px";
if (ract.top > window.innerHeight / 2) {
- copy.style.bottom = (window.innerHeight -
- window.scrollY - ract.top) + "px";
+ copy.style.bottom = ract.height + 'px';
}
event.target.parentElement.appendChild(copy);
}
@@ -285,6 +288,12 @@ function onPaste(e) {
fp.files = dt.files;
}
+function onKeydown(e) {
+ if (e.key == 'Enter' && e.ctrlKey) {
+ document.querySelector(".post-form").submit();
+ }
+}
+
document.addEventListener("DOMContentLoaded", function() {
checkCSRFToken();
checkAntiDopamineMode();
@@ -314,19 +323,21 @@ document.addEventListener("DOMContentLoaded", function() {
}
}
- var links = document.querySelectorAll(".user-profile-decription a, .user-fields a");
+ var links = document.querySelectorAll(".user-profile-description a, .user-fields a");
for (var j = 0; j < links.length; j++) {
links[j].target = "_blank";
}
- var links = document.querySelectorAll(".status-media-container .img-link");
+ var links = document.querySelectorAll(".status-media-container .img-link, .user-profile-img-container .img-link");
for (var j = 0; j < links.length; j++) {
handleImgPreview(links[j]);
}
var pf = document.querySelector(".post-form")
- if (pf)
+ if (pf) {
pf.addEventListener("paste", onPaste);
+ pf.addEventListener("keydown", onKeydown);
+ }
});
// @license-end
diff --git a/static/style.css b/static/style.css
index 2cb15b0..ad5a360 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,23 +1,58 @@
-body {
- background-color: #d2d2d2;
+body,
+button,
+input,
+select,
+textarea {
+ font-family: sans-serif;
}
-.status-container-container {
- margin: 0 -4px 12px -4px;
- padding: 4px;
+input::file-selector-button {
+ font-family: sans-serif;
+}
+
+input[type=text],
+textarea {
+ font-size: inherit;
+}
+
+h1 {
+ font-weight: normal;
+ font-size: x-large;
+}
+
+frame,
+body,
+.more-content {
+ background-color: #fcfcfc;
+}
+
+.status-container-container,
+.notification-container {
+ background-color: #f0f0f0;
+ background-color: #eaeaea99;
+ margin: 8px 0;
+ padding: 12px 4px;
border-left: 4px solid transparent;
}
-.status-container-container:target {
- border-color: #777777;
+@media only screen and (max-width: 768px) {
+ .status-container-container,
+ .notification-container {
+ margin: 8px -4px;
+ }
}
.status-container-container.highlight {
- background-color: #eeeeee;
+ background-color: #d3d3d3;
+ background-color: #cfcfcf99;
+}
+
+.status-container-container:target {
+ border-left: 4px solid #777777;
}
.status-container {
- display: flex;
+ position: relative;
}
.status-content {
@@ -47,19 +82,19 @@ body {
}
.status-media-container {
- margin: 5px 0 -5px 0;
+ margin: 4px 0;
overflow: auto;
}
-.status-media-container>a {
- margin-bottom: 5px;
+.status-image-container,
+.status-video-container {
display: inline-block;
+ position: relative;
+ margin: 2px 4px 2px 0;
}
.status-profile-img-container {
- margin-right: 8px;
- display: inline-block;
- vertical-align: top;
+ position: absolute;
}
.status-profile-img {
@@ -71,27 +106,20 @@ body {
max-width: 48px;
vertical-align: top;
object-fit: contain;
- margin-top: 2px;
-}
-
-.status {
- display: inline-block;
- vertical-align: top;
- flex: 1;
- min-width: 0;
}
-.status-dname {
- font-weight: 800;
+.user-list-profile-img .img-link,
+.status-profile-img-container .img-link {
+ width: 48px;
+ overflow: hidden;
}
-.status-uname {
- font-style: italic;
- font-size: 10pt;
+.retweet-info .img-link {
+ margin-right: 4px;
}
-.status-action-container {
- margin-top: 4px;
+.status {
+ margin-left: 56px;
}
.status-action {
@@ -103,46 +131,14 @@ body {
margin-right: 4px;
}
-.status-action form {
- display: inline-block;
-}
-
-.status-action a {
- display: inline-block;
-}
-
-.status-action * {
- vertical-align: middle;
-}
-
-.status-action a.status-time {
- width: auto;
-}
-
-.page-title {
- font-size: 18pt;
- margin: 8px 0;
-}
-
-.post-form {
- margin: 4px 0;
-}
-
-.post-form>div {
- margin-bottom: 4px;
-}
-
-.signin-form {
- margin: 8px 0;
-}
-
-.signin-form input {
- margin: 4px 0;
-}
-
.retweet-info {
margin: 0 0 4px 24px;
overflow-wrap: break-word;
+ font-size: smaller;
+}
+
+.retweet-info>* {
+ vertical-align: middle;
}
.retweet-info .status-profile-img {
@@ -155,111 +151,71 @@ body {
vertical-align: middle;
}
-.retweet-info .status-dname {
- margin-left: 4px;
-}
-
textarea {
padding: 4px;
- font-size: 11pt;
- font-family: initial;
+ box-sizing: border-box;
+ width: 644px;
+ max-width: 100%;
}
.post-content {
- box-sizing: border-box;
width: 100%;
}
-#css {
- box-sizing: border-box;
- max-width: 100%;
+.monospace {
+ font-family: monospace;
}
.pagination {
- margin: 4px 4px 12px 4px;
+ margin: 12px 4px;
}
.pagination a {
margin: 0 8px;
- font-size: 13pt;
-}
-
-.notification-container {
- margin: 0 -4px 12px -4px;
- padding: 4px;
- border-left: 4px solid transparent;
+ font-size: large;
}
.notification-container.unread {
border-color: #777777;
}
-.notification-container.favourite .status-container,
-.notification-container.reblog .status-container {
+.notification-container .status-content,
+.notification-container .status-reply-container,
+.notification-container .status-media-container {
opacity: 0.6;
}
-.notification-info-text span {
- vertical-align: middle;
-}
-
-.notification-follow-container {
- overflow: auto;
- display: flex;
- align-items: center;
-}
-
-.notification-follow {
- overflow: auto;
-}
-
-.notification-time {
- margin-left: 8px;
+.notification-container.mention .status-content,
+.notification-container.mention .status-reply-container,
+.notification-container.mention .status-media-container {
+ opacity: 1;
}
-.status-reply-to-link {
- font-size: 10pt
+.status-reply-to-link,
+.status-reply-link,
+.status-reply-text {
+ font-size: smaller;
}
.status-reply-container {
overflow-wrap: break-word;
-}
-
-.status-reply-container .fa {
- font-size: 10pt;
- vertical-align: sub;
- margin-right: -2px;
-}
-
-.status-reply-text {
- font-size: 10pt;
-}
-
-.status-reply-link {
- font-size: 10pt;
+ position: relative;
}
.status-reply-info-divider {
margin: 0 4px;
}
-.post-content-container {
- padding-right: 8px;
-}
-
-.error-text {
- margin: 8px 0;
-}
-
-.post-attachment-div {
- margin: 2px 0;
-}
-
.user-profile-img-container {
display: inline-block;
margin: 0 4px 4px 0;
}
+.user-profile-img-container .img-link {
+ width: 96px;
+ overflow: hidden;
+}
+
.user-profile-details-container {
display: inline-block;
vertical-align: top;
@@ -274,19 +230,19 @@ textarea {
width: 96px;
vertical-align: top;
object-fit: contain;
- margin-top: 2px;
}
-.user-profile-decription {
+.user-profile-description,
+.user-fields {
overflow-wrap: break-word;
margin: 8px 0;
}
-.user-profile-decription p {
+.user-profile-description p {
margin: 0;
}
-.user-profile-decription img {
+.user-profile-description img {
height: auto;
width: auto;
max-height: 240px;
@@ -298,49 +254,42 @@ textarea {
display: inline;
}
-.p-0 {
- padding: 0;
-}
-
.btn-link {
- border: none;
outline: none;
- background: none;
cursor: pointer;
padding: 0;
- font-family: inherit;
font-size: inherit;
+ background: none !important;
+ border: none !important;
}
-a, .btn-link {
- color: #464acc;
+a,
+.btn-link {
+ color: #1449af;
text-decoration: none;
}
a:hover,
.btn-link:hover {
- color: #8387bf;
+ color: #4489bf;
}
-.status-visibility {
- margin-left: 4px;
- display: inline-block;
- color: #222222;
- font-size: 8pt;
+.btn-link:disabled {
+ color: #666666;
+ cursor: unset;
+}
+
+*:focus-visible {
+ outline: 1px solid #000000;
}
.remote-link {
margin-left: 4px;
- font-size: 8pt;
+ font-size: smaller;
}
.img-link {
display: inline-block;
- position: relative;
-}
-
-.status-profile-img-container .img-link {
- width: 48px;
}
.status-nsfw-overlay {
@@ -352,29 +301,19 @@ a:hover,
right: 0;
}
-.img-link:hover .status-nsfw-overlay {
- display: none;
-}
-
-.status-video-container {
- display: inline-block;
- position: relative;
- margin-bottom: 5px;
-}
-
+.status-image-container:hover .status-nsfw-overlay,
.status-video-container:hover .status-nsfw-overlay {
display: none;
}
-.post-form-field>* {
- vertical-align: middle;
-}
-
.emoji-item-container {
width: 220px;
display: inline-block;
- margin: 4px 0;
+ margin: 4px 4px 0 0;
overflow: hidden;
+ padding: 4px;
+ background-color: #f0f0f0;
+ background-color: #eaeaea99;
}
.emoji-item {
@@ -395,115 +334,113 @@ img.emoji {
}
.status-dname img.emoji {
- height: 24px;
- min-height: 24px;
- min-width: 24px;
+ height: 20px;
+ min-height: 20px;
+ min-width: 20px;
+}
+
+.retweet-info>.status-dname img.emoji {
+ height: 16px;
+ min-height: 16px;
+ min-width: 16px;
}
.emoji-shortcode {
margin-left: 4px;
}
-.post-form-emoji-link {
- margin-left: 4px;
+.nav-container {
+ position: relative;
+ margin-bottom: 4px;
+}
+
+.nav-profile-img-container {
+ position: absolute;
}
-.user-info-img {
+.nav-profile-img {
height: 64px;
width: 64px;
vertical-align: middle;
object-fit: contain;
- margin-top: 2px;
}
-.user-info-img-container {
- float: left;
- margin-right: 8px;
+.nav-link-container {
+ margin-left: 72px;
}
-.user-info-details-container {
- overflow: auto;
+nav ul {
+ list-style: none;
+ margin: 4px 0;
+ padding: 0;
}
-.user-info-details-name,
-.user-info-details-nav {
- margin-bottom: 4px;
+nav li {
+ display: inline;
}
.nav-link {
margin-right: 2px;
}
+.nav-profile-link {
+ margin-right: 2px;
+ font-size: smaller;
+}
+
.user-list-item {
overflow: auto;
- margin: 0 0 12px 0;
+ margin: 0 0 4px 0;
+ padding: 4px;
display: flex;
align-items: center;
}
-.user-list-profile-img {
- float: left;
- margin: 0 8px 0 0;
+td .user-list-item {
+ padding: 0;
}
-.user-list-name {
- overflow: auto;
+.user-list-profile-img {
+ margin: 0 8px 0 0;
}
.user-list-action {
margin: 0 12px;
}
-#settings-form {
- margin: 8px 0;
-}
-
-.settings-form-field {
- margin: 4px 0;
-}
-
-.settings-form-field>* {
- vertical-align: middle;
+.form-field {
+ margin-bottom: 8px;
}
-#settings-form button[type=submit] {
- margin-top: 8px;
+.form-field-s {
+ margin-bottom: 4px;
}
-#reply-popup {
- position: absolute;
- background-color: #d2d2d2;
- border: 1px solid #aaaaaa;
- padding: 4px 8px;
- z-index: 3;
- margin: 0;
+.emoji-link {
+ margin-left: 4px;
}
+#reply-popup,
#reply-to-popup {
+ background-color: #f0f0f0;
+ border: 1px solid #bababa;
position: absolute;
- background-color: #d2d2d2;
- border: 1px solid #aaaaaa;
padding: 4px 8px;
z-index: 3;
margin: 0;
}
-.search-form {
- margin: 12px 0;
-}
-
.more-container {
position: relative;
display: inline-block;
}
.more-content {
- display: none;
position: absolute;
- background-color: #d2d2d2;
+ display: none;
padding: 2px 4px;
- border: 1px solid #aaaaaa;
z-index: 1;
+ border: 1px solid #bababa;
}
.more-container:hover .more-content {
@@ -511,133 +448,229 @@ img.emoji {
}
.more-link {
- font-size: 8pt;
+ font-size: smaller;
display: block;
margin: 2px;
}
-.poll-form {
- margin-top: 5px;
+.status-poll {
+ margin: 4px 0;
overflow: auto;
overflow-wrap: break-word;
}
-.poll-form button[type=submit] {
- margin-top: 6px;
+.page-link {
+ font-size: large;
}
-.poll-info {
- margin-top: 6px;
+kbd {
+ padding: 2px 4px;
+ background-color: #f0f0f0;
+ border: 1px solid #bababa;
}
-.page-title-container {
- margin: 8px 0;
+td {
+ padding: 2px 4px;
}
-.page-refresh {
- margin-right: 8px;
+#img-preview {
+ pointer-events: none;
+ z-index: 2;
+ position: fixed;
}
-.notification-text {
+blockquote {
+ margin: 4px 20px;
+ border-left: 2px solid #3e7300;
+ padding-left: 12px;
+}
+
+blockquote,
+.quote {
+ color: #3e7300;
+}
+
+.profile-img-container {
+ margin: 4px 0;
+}
+
+.profile-avatar {
+ height: 96px;
+ width: 96px;
+ object-fit: contain;
vertical-align: middle;
}
-.notification-read {
- display: inline-block;
+.profile-banner {
+ height: 120px;
+ vertical-align: middle;
}
-.no-data-found {
- margin: 12px 0;
+.block-label {
+ margin: 0 0 4px 0;
}
-.signout {
- display: inline;
+.input-w {
+ width: 320px;
+ max-width: 100%;
+ box-sizing: border-box;
}
-.signin-desc {
- margin: 8px 0 16px 0;
+.follow-request-actions {
+ margin-top: 4px;
}
-.keyboard-shortcuts {
- margin-top: 12px;
+.hidden {
+ display: none;
}
-.keyboard-shortcuts td {
- padding: 2px 4px;
+input[type=text],
+textarea {
+ border: 2px solid #bababa;
}
-kbd {
- border-radius: 3px;
- padding: 1px 4px;
- border: 1px solid #444444;
- background-color: #eeeeee;
- font-size: 10pt;
+input[type=submit],
+button,
+select,
+input::file-selector-button {
+ background-color: #eaeaea;
+ border: 2px solid #bababa;
}
-.filters {
- margin: 10px 0;
+input[type=text]:hover,
+textarea:hover {
+ border: 2px solid #8b8b8b;
}
-.filters td {
- padding: 2px 4px;
+input[type=submit]:hover,
+button:hover,
+select:hover,
+input::file-selector-button:hover {
+ background-color: #dfdfdf;
+ border: 2px solid #8b8b8b;
}
-#img-preview {
- pointer-events: none;
- z-index: 2;
- position: fixed;
+input[type=submit]:active,
+button:active,
+select:active,
+input::file-selector-button:active {
+ background-color: #cacaca;
}
-.quote {
- color: #789922;
+input[type=submit]:disabled,
+button:disabled,
+select:disabled,
+input:disabled::file-selector-button {
+ color: #666666;
+ background-color: #eaeaea;
+ border: 2px solid #bababa;
}
-.dark {
- background-color: #222222;
- background-image: none;
- color: #eaeaea;
+.dark,
+.dark body,
+.dark .more-content {
+ background-color: #0f0f0f;
+ color: #dcdcdc;
}
-.dark a {
- color: #81a2be;
+.dark .status-container-container,
+.dark .notification-container,
+.dark .emoji-item-container {
+ background-color: #181818;
+ background-color: #1f1f1f99;
}
-.dark textarea {
- background-color: #333333;
- border: 1px solid #444444;
- color: #eaeaea;
+.dark .status-container-container.highlight {
+ background-color: #222222;
+ background-color: #2f2f2f99;
}
.dark #reply-popup,
.dark #reply-to-popup {
- background-color: #222222;
- border-color: #444444;
+ background-color: #181818;
}
-.dark .status-container-container.highlight {
- background-color: #333333;
+.dark *:focus-visible {
+ outline: 1px solid #dcdcdc;
}
-.dark .btn-link {
- color: #81a2be;
+.dark #reply-popup,
+.dark #reply-to-popup,
+.dark .more-content {
+ border-color: #333333;
}
-.dark a:hover,
-.dark .btn-link:hover {
- color: #497091;
+.dark kbd {
+ background-color: #181818;
+ border: 2px solid #333333;
}
-.dark .status-visibility {
- color: #eaeaea;
+.dark blockquote,
+.dark .quote {
+ color: #779c3b;
}
-.dark .more-content {
- background-color: #222222;
- border-color: #444444;
+.dark input[type=text],
+.dark textarea {
+ color: #dcdcdc;
+ background-color: #0c0c0c;
+ border: 2px solid #333333;
}
-.dark kbd {
- background-color: #333333;
- border-color: #444444;
- color: #eaeaea;
+.dark .post-content {
+ background-color: #111111;
+}
+
+.dark input[type=submit],
+.dark button,
+.dark select,
+.dark input::file-selector-button {
+ color: #dcdcdc;
+ background-color: #242424;
+ border: 2px solid #333333;
+}
+
+.dark input[type=text]:hover,
+.dark textarea:hover {
+ border: 2px solid #555555;
+}
+
+.dark input[type=submit]:hover,
+.dark button:hover,
+.dark select:hover,
+.dark input::file-selector-button:hover {
+ background-color: #2f2f2f;
+ border: 2px solid #555555;
+}
+
+
+.dark input[type=submit]:active,
+.dark button:active,
+.dark select:active,
+.dark input::file-selector-button:active {
+ background-color: #3f3f3f;
+}
+
+.dark input[type=submit]:disabled,
+.dark button:disabled,
+.dark select:disabled,
+.dark input:disabled::file-selector-button {
+ color: #666666;
+ background-color: #242424;
+ border: 2px solid #333333;
+}
+
+.dark a,
+.dark btn-link,
+.dark input.btn-link {
+ color: #6090a3;
+}
+
+.dark a:hover,
+.dark .btn-link:hover {
+ color: #497091;
+}
+
+.dark .btn-link:disabled {
+ color: #666666;
}
diff --git a/templates/about.tmpl b/templates/about.tmpl
index 0e4d001..580c68d 100644
--- a/templates/about.tmpl
+++ b/templates/about.tmpl
@@ -1,7 +1,7 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> About </div>
+<h1>About</h1>
<div>
<p>
A web client for <a href="https://pleroma.social" target="_blank">Mastadon Network</a>.
@@ -14,7 +14,7 @@
</P>
</div>
-<div class="page-title"> Keyboard shortcuts </div>
+<h1>Keyboard Shortcuts</h1>
<div>
<table class="keyboard-shortcuts">
<tr>
@@ -42,11 +42,11 @@
<td> <kbd>5</kbd> </td>
</tr>
<tr>
- <td> Search </td>
+ <td> Lists </td>
<td> <kbd>6</kbd> </td>
</tr>
<tr>
- <td> Lists </td>
+ <td> Search </td>
<td> <kbd>7</kbd> </td>
</tr>
<tr>
diff --git a/templates/emoji.tmpl b/templates/emoji.tmpl
index 4b07e81..86ab285 100644
--- a/templates/emoji.tmpl
+++ b/templates/emoji.tmpl
@@ -1,12 +1,12 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Emojis </div>
+<h1>Emojis</h1>
<div class="emoji-list-container">
{{range .Emojis}}
<div class="emoji-item-container">
<div class="emoji-item">
- <img class="emoji" src="{{.URL}}" alt="{{.ShortCode}}" height="32" loading="lazy" />
+ <img class="emoji" src="{{.URL}}" alt="{{.ShortCode}}" height="32" loading="lazy">
<span title=":{{.ShortCode}}:" class="emoji-shortcode">:{{.ShortCode}}:</span>
</div>
</div>
diff --git a/templates/error.tmpl b/templates/error.tmpl
index c8da1e6..4763fcf 100644
--- a/templates/error.tmpl
+++ b/templates/error.tmpl
@@ -1,8 +1,8 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Error </div>
+<h1>Error</h1>
-<div class="error-text"> {{.Err}} </div>
+<p>{{.Err}}</p>
<div>
<a href="/timeline/home">home</a>
{{if .Retry}}
diff --git a/templates/filters.tmpl b/templates/filters.tmpl
index ef7c024..383a0ad 100644
--- a/templates/filters.tmpl
+++ b/templates/filters.tmpl
@@ -1,9 +1,9 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Filters </div>
+<h1>Filters</h1>
{{if .Filters}}
-<table class="filters">
+<table>
{{range .Filters}}
<tr>
<td> {{.Phrase}}{{if not .WholeWord}}*{{end}} </td>
@@ -11,29 +11,23 @@
<form action="/unfilter/{{.ID}}" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <button type="submit"> Delete </button>
+ <button type="submit">Delete</button>
</form>
</td>
</tr>
{{end}}
</table>
{{else}}
- <div class="filters"> No filters added </div>
+ <p> No filters added </p>
{{end}}
-<div class="page-title"> Add filter </div>
+<h1>Add Filter</h1>
<form action="/filter" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <span class="settings-form-field">
- <label for="phrase"> Phrase </label>
- <input id="phrase" name="phrase" required>
- </span>
- <span class="settings-form-field">
- <input id="whole-word" name="whole_word" type="checkbox" value="true" checked>
- <label for="whole-word"> Whole word </label>
- </span>
- <button type="submit"> Add </button>
+ <label>Phrase <input type="text" name="phrase" required></label>
+ <label><input name="whole_word" type="checkbox" value="true" checked> Whole word</label>
+ <button type="submit">Add</button>
</form>
{{template "footer.tmpl"}}
diff --git a/templates/header.tmpl b/templates/header.tmpl
index 8a1b0ca..713aa96 100644
--- a/templates/header.tmpl
+++ b/templates/header.tmpl
@@ -17,10 +17,13 @@
{{if .RefreshInterval}}
<meta http-equiv="refresh" content="{{.RefreshInterval}}">
{{end}}
+ {{if $.Ctx.DarkMode}}
+ <meta name="color-scheme" content="dark">
+ {{end}}
<title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>
<link rel="stylesheet" href="/static/style.css">
{{if .CustomCSS}}
- <link rel="stylesheet" href="{{.CustomCSS}}">
+ <link rel="stylesheet" href="/static/{{.CustomCSS}}">
{{end}}
{{if $.Ctx.FluorideMode}}
<script src="/static/fluoride.js"></script>
diff --git a/templates/likedby.tmpl b/templates/likedby.tmpl
index 222254c..6f62647 100644
--- a/templates/likedby.tmpl
+++ b/templates/likedby.tmpl
@@ -1,6 +1,6 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Liked By </div>
+<h1>Liked By</h1>
{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
diff --git a/templates/list.tmpl b/templates/list.tmpl
index dcc6ee8..6fa2830 100644
--- a/templates/list.tmpl
+++ b/templates/list.tmpl
@@ -1,21 +1,23 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> List {{.List.Title}} </div>
+<h1>List {{.List.Title}}</h1>
<form action="/list/{{.List.ID}}/rename" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input id="title" name="title" value="{{.List.Title}}">
- <button type="submit"> Rename </button>
+ <div class="form-field">
+ <input type="text" id="title" name="title" value="{{.List.Title}}">
+ <button type="submit"> Rename </button>
+ <div>
</form>
-<div class="page-title"> Users </div>
+<h1>Users</h1>
{{if .Accounts}}
<table>
{{range .Accounts}}
<tr>
- <td class="p-0"> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
- <td class="p-0">
+ <td>{{template "userlistitem.tmpl" (WithContext . $.Ctx)}}</td>
+ <td>
<form class="user-list-action" action="/list/{{$.Data.List.ID}}/removeuser?uid={{.ID}}" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
@@ -26,16 +28,16 @@
{{end}}
</table>
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
-<div class="page-title"> Add user </div>
-<form class="search-form" action="/list/{{.List.ID}}" method="GET">
- <span class="post-form-field">
+<h1>Add User</h1>
+<form action="/list/{{.List.ID}}" method="GET">
+ <div class="form-field">
<label for="query"> Query </label>
- <input id="query" name="q" value="{{.Q}}">
- </span>
- <button type="submit"> Search </button>
+ <input type="text" id="query" name="q" value="{{.Q}}">
+ <button type="submit"> Search </button>
+ </div>
</form>
{{if .Q}}
@@ -55,7 +57,7 @@
{{end}}
</table>
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
{{end}}
diff --git a/templates/lists.tmpl b/templates/lists.tmpl
index 27979cb..59c53fe 100644
--- a/templates/lists.tmpl
+++ b/templates/lists.tmpl
@@ -1,33 +1,37 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Lists </div>
+<h1>Lists</h1>
-{{range .Lists}}
-<div>
- <a href="/timeline/list?list={{.ID}}"> {{.Title}} timeline </a>
- -
- <form class="d-inline" action="/list/{{.ID}}" method="GET">
- <button type="submit" class="btn-link"> edit </button>
- </form>
- -
- <form class="d-inline" action="/list/{{.ID}}/remove" method="POST">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <button type="submit" class="btn-link"> delete </button>
- </form>
-</div>
+{{if .Lists}}
+<table>
+ {{range .Lists}}
+ <tr>
+ <td><a href="/timeline/list?list={{.ID}}">{{.Title}} timeline</a></td>
+ <td>
+ <form action="/list/{{.ID}}" method="GET">
+ <button type="submit">Edit</button>
+ </form>
+ </td>
+ <td>
+ <form action="/list/{{.ID}}/remove" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit">Delete</button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+</table>
{{else}}
-<div class="no-data-found">No data found</div>
+ <p>No lists added</p>
{{end}}
-<div class="page-title"> Add list </div>
+<h1>Add List</h1>
<form action="/list" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <span class="settings-form-field">
- <label for="title"> Title </label>
- <input id="title" name="title" required>
- </span>
+ <label for="title">Title</label>
+ <input type="text" id="title" name="title" required>
<button type="submit"> Add </button>
</form>
diff --git a/templates/mute.tmpl b/templates/mute.tmpl
new file mode 100644
index 0000000..0defc80
--- /dev/null
+++ b/templates/mute.tmpl
@@ -0,0 +1,29 @@
+{{with .Data}}
+{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
+<h1>Mute {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} @{{.User.Acct}}</h1>
+
+<form action="/mute/{{.User.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <div class="form-field-s">
+ <input id="notifications" name="notifications" type="checkbox" value="true" checked>
+ <label for="notifications"> Mute notifications </label>
+ </div>
+ <div class="form-field-s">
+ <label for="duration"> Auto unmute </label>
+ <select id="duration" name="duration">
+ <option value="0" selected>Disabled</option>
+ <option value="300">After 5m</option>
+ <option value="1800">After 30m</option>
+ <option value="3600">After 1h</option>
+ <option value="21600">After 6h</option>
+ <option value="86400">After 1d</option>
+ <option value="259200">After 3d</option>
+ <option value="604800">After 7d</option>
+ </select>
+ </div>
+ <button type="submit"> Mute </button>
+</form>
+
+{{template "footer.tmpl"}}
+{{end}}
diff --git a/templates/nav.tmpl b/templates/nav.tmpl
index db88aa0..c01f64e 100644
--- a/templates/nav.tmpl
+++ b/templates/nav.tmpl
@@ -1,36 +1,35 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="user-info">
- <div class="user-info-img-container">
+<div class="nav-container">
+ <div class="nav-profile-img-container">
<a class="img-link" href="/timeline/home" title="Home (1)">
- <img class="user-info-img" src="{{.User.Avatar}}" alt="profile-avatar" height="64" />
+ <img class="nav-profile-img" src="{{.User.Avatar}}" alt="avatar" height="64">
</a>
</div>
- <div class="user-info-details-container">
- <div class="user-info-details-name">
- <bdi class="status-dname"> {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} </bdi>
- <a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">
- <span class="status-uname"> @{{.User.Acct}} </span>
- </a>
- </div>
- <div class="user-info-details-nav">
- <a class="nav-link" href="/timeline/home" accesskey="1" title="Home timeline (1)">home</a>
- <a class="nav-link" href="/timeline/direct" accesskey="2" title="Direct timeline (2)">direct</a>
- <a class="nav-link" href="/timeline/local" accesskey="3" title="Local timeline (3)">local</a>
- <a class="nav-link" href="/timeline/twkn" accesskey="4" title="The Whole Known Netwwork (4)">twkn</a>
- <a class="nav-link" href="/timeline/remote" accesskey="5" title="Remote timeline (5)">remote</a>
- <a class="nav-link" href="/search" accesskey="6" title="Search (6)">search</a>
- </div>
- <div>
- <a class="nav-link" href="/lists" accesskey="7" title="Lists (7)">lists</a>
- <a class="nav-link" href="/settings" target="_top" accesskey="8" title="Settings (8)">settings</a>
- <form class="signout" action="/signout" method="post" target="_top">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="signout" class="btn-link nav-link" title="Signout">
- </form>
- <a class="nav-link" href="/about" accesskey="9" title="About (9)">about</a>
- </div>
+ <div class="nav-link-container">
+ <bdi class="status-dname">{{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}</bdi>
+ <a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)"><span class="status-uname">@{{.User.Acct}}</span></a>
+ <a class="nav-profile-link" href="/profile" title="edit profile" target="_top">edit</a>
+ <form class="d-inline" action="/signout" method="post" target="_top">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <input type="submit" value="signout" class="btn-link nav-profile-link" title="Signout">
+ </form>
+ <nav>
+ <ul>
+ <li><a class="nav-link" href="/timeline/home" accesskey="1" title="Home timeline (1)">home</a></li>
+ <li><a class="nav-link" href="/timeline/direct" accesskey="2" title="Direct timeline (2)">direct</a></li>
+ <li><a class="nav-link" href="/timeline/local" accesskey="3" title="Local timeline (3)">local</a></li>
+ <li><a class="nav-link" href="/timeline/twkn" accesskey="4" title="The Whole Known Netwwork (4)">twkn</a></li>
+ <li><a class="nav-link" href="/timeline/remote" accesskey="5" title="Remote timeline (5)">remote</a></li>
+ </ul>
+ <ul>
+ <li><a class="nav-link" href="/lists" accesskey="6" title="Lists (6)">lists</a></li>
+ <li><a class="nav-link" href="/search" accesskey="7" title="Search (7)">search</a></li>
+ <li><a class="nav-link" href="/settings" target="_top" accesskey="8" title="Settings (8)">settings</a></li>
+ <li><a class="nav-link" href="/about" accesskey="9" title="About (9)">about</a></li>
+ </ul>
+ </nav>
</div>
</div>
diff --git a/templates/notification.tmpl b/templates/notification.tmpl
index f62726b..395987e 100644
--- a/templates/notification.tmpl
+++ b/templates/notification.tmpl
@@ -1,73 +1,66 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title-container">
- <span class="page-title">
- Notifications
- {{if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}}
+
+<form action="/notifications/read?max_id={{.ReadID}}" method="post" target="_self">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <h1>Notifications
+ {{- if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}}
({{.UnreadCount }})
{{end}}
- </span>
- <a class="page-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
- {{if .ReadID}}
- <form class="notification-read" action="/notifications/read?max_id={{.ReadID}}" method="post" target="_self">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="read" class="btn-link" accesskey="C" title="Clear unread notifications (C)">
- </form>
- {{end}}
-</div>
+ <a class="page-link" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
+ {{if .ReadID}}
+ <input type="submit" value="read" class="btn-link page-link" accesskey="C" title="Clear unread notifications (C)">
+ {{end}}
+ </h1>
+</form>
{{range .Notifications}}
-<div class="notification-container {{.Type}} {{if .Pleroma}}{{if not .Pleroma.IsSeen}}unread{{end}}{{end}}">
+<article class="notification-container {{.Type}} {{if .Pleroma}}{{if not .Pleroma.IsSeen}}unread{{end}}{{end}}">
{{if eq .Type "follow"}}
- <div class="notification-follow-container">
- <div class="status-profile-img-container">
+ <div class="user-list-item">
+ <div class="user-list-profile-img">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="profile-avatar" height="48" />
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
</div>
- <div class="notification-follow">
- <div class="notification-info-text">
- <bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi>
- <span class="notification-text"> followed you -
- <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
- </span>
- </div>
- <div>
- <a href="/user/{{.Account.ID}}"> <span class="status-uname"> @{{.Account.Acct}} </span> </a>
- </div>
+ <div class="user-list-name">
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ followed you - <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
+ <br>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
</div>
+ <br class="hidden">
</div>
{{else if eq .Type "follow_request"}}
- <div class="notification-follow-container">
- <div class="status-profile-img-container">
+ <div class="user-list-item">
+ <div class="user-list-profile-img">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="profile-avatar" height="48" />
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
</div>
- <div class="notification-follow">
- <div class="notification-info-text">
- <bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi>
- <span class="notification-text"> wants to follow you -
- <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
- </span>
- </div>
- <div>
- <a href="/user/{{.Account.ID}}"> <span class="status-uname"> @{{.Account.Acct}} </span> </a>
+ <div class="user-list-name">
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ wants to follow you -
+ <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
+ <br>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
+ <div class="follow-request-actions">
+ <form class="d-inline" action="/accept/{{.Account.ID}}" method="post" target="_self">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <input type="submit" value="accept" class="btn-link">
+ </form>
+ -
+ <form class="d-inline" action="/reject/{{.Account.ID}}" method="post" target="_self">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <input type="submit" value="reject" class="btn-link">
+ </form>
</div>
- <form class="d-inline" action="/accept/{{.Account.ID}}" method="post" target="_self">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="accept" class="btn-link">
- </form>
- -
- <form class="d-inline" action="/reject/{{.Account.ID}}" method="post" target="_self">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="reject" class="btn-link">
- </form>
</div>
+ <br class="hidden">
</div>
{{else if eq .Type "mention"}}
@@ -76,11 +69,10 @@
{{else if eq .Type "reblog"}}
<div class="retweet-info">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
- </a>
- <a href="/user/{{.Account.ID}}">
- <span class="status-uname"> @{{.Account.Acct}} </span>
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
<span class="notification-text"> retweeted your post -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span>
@@ -90,11 +82,10 @@
{{else if eq .Type "favourite"}}
<div class="retweet-info">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
- </a>
- <a href="/user/{{.Account.ID}}">
- <span class="status-uname"> @{{.Account.Acct}} </span>
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
<span class="notification-text"> liked your post -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span>
@@ -104,25 +95,24 @@
{{else}}
<div class="retweet-info">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
- </a>
- <a href="/user/{{.Account.ID}}">
- <span class="status-uname"> @{{.Account.Acct}} </span>
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
<span class="notification-text"> {{.Type}} -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span>
</div>
{{if .Status}}{{template "status" (WithContext .Status $.Ctx)}}{{end}}
{{end}}
-</div>
+</article>
{{end}}
-<div class="pagination">
+<nav class="pagination">
{{if .NextLink}}
<a href="{{.NextLink}}" target="_self">[next]</a>
{{end}}
-</div>
+</nav>
{{template "footer.tmpl"}}
{{end}}
diff --git a/templates/postform.tmpl b/templates/postform.tmpl
index 0af50fb..5ee1a51 100644
--- a/templates/postform.tmpl
+++ b/templates/postform.tmpl
@@ -3,49 +3,41 @@
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
{{if .ReplyContext}}
- <input type="hidden" name="reply_to_id" value="{{.ReplyContext.InReplyToID}}" />
- <input type="hidden" name="quickreply" value="{{.ReplyContext.QuickReply}}" />
- <label for="post-content" class="post-form-title"> Reply to @{{.ReplyContext.InReplyToName}} </label>
+ <input type="hidden" name="reply_to_id" value="{{.ReplyContext.InReplyToID}}">
+ <input type="hidden" name="quickreply" value="{{.ReplyContext.QuickReply}}">
+ <label for="post-content">Reply to @{{.ReplyContext.InReplyToName}}</label>
{{else}}
- <label for="post-content" class="post-form-title"> New post </label>
+ <label for="post-content">New post</label>
{{end}}
- <a class="post-form-emoji-link" href="/emojis" target="_blank" title="Emoji list (L)" accesskey="L">
- emoji list
- </a>
- <div class="post-form-content-container">
+ <a class="emoji-link" href="/emojis" target="_blank" title="Emoji list (L)" accesskey="L">emoji list</a>
+ <div class="form-field-s">
<textarea id="post-content" name="content" class="post-content" cols="34" rows="5" accesskey="E" title="Edit post (E)">{{if .ReplyContext}}{{.ReplyContext.ReplyContent}}{{end}}</textarea>
</div>
- <div>
+ <div class="form-field-s">
{{if .Formats}}
- <span class="post-form-field">
{{$defFormat := .DefaultFormat}}
<select id="post-format" name="format" accesskey="F" title="Format (F)">
{{range .Formats}}
<option value="{{.Type}}" {{if eq $defFormat .Type}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
- </span>
{{end}}
- <span class="post-form-field">
- <select id="post-visilibity" name="visibility" {{if .ReplyContext}}{{if .ReplyContext.ForceVisibility}}disabled{{end}}{{end}} accesskey="S" title="Scope (S)">
- <option value="public" {{if eq .DefaultVisibility "public"}}selected{{end}}>Public</option>
- <option value="unlisted" {{if eq .DefaultVisibility "unlisted"}}selected{{end}}>Unlisted</option>
- <option value="private" {{if eq .DefaultVisibility "private"}}selected{{end}}>Private</option>
- <option value="direct" {{if eq .DefaultVisibility "direct"}}selected{{end}}>Direct</option>
- </select>
- </span>
- <span class="post-form-field">
- <input type="checkbox" id="nsfw-checkbox" name="is_nsfw" value="true" accesskey="N" title="NSFW (N)">
- <label for="nsfw-checkbox"> NSFW </label>
- </span>
+ <select id="post-visilibity" name="visibility" {{if .ReplyContext}}{{if .ReplyContext.ForceVisibility}}disabled{{end}}{{end}} accesskey="S" title="Scope (S)">
+ <option value="public" {{if eq .DefaultVisibility "public"}}selected{{end}}>Public</option>
+ <option value="unlisted" {{if eq .DefaultVisibility "unlisted"}}selected{{end}}>Unlisted</option>
+ <option value="private" {{if eq .DefaultVisibility "private"}}selected{{end}}>Private</option>
+ <option value="direct" {{if eq .DefaultVisibility "direct"}}selected{{end}}>Direct</option>
+ </select>
+ <input type="checkbox" id="nsfw-checkbox" name="is_nsfw" value="true" accesskey="N" title="NSFW (N)">
+ <label for="nsfw-checkbox"> NSFW </label>
+ </div>
+ <div class="form-field-s">
+ <input id="post-file-picker" type="file" name="attachments" multiple accesskey="A" title="Attachments (A)">
</div>
- <div>
- <span class="post-form-field">
- <input id="post-file-picker" type="file" name="attachments" multiple accesskey="A" title="Attachments (A)">
- </span>
+ <div class="form-field-s">
+ <button type="submit" accesskey="P" title="Post (P)"> Post </button>
+ <button type="reset" title="Reset"> Reset </button>
</div>
- <button type="submit" accesskey="P" title="Post (P)"> Post </button>
- <button type="reset" title="Reset"> Reset </button>
</form>
{{end}}
diff --git a/templates/profile.tmpl b/templates/profile.tmpl
new file mode 100644
index 0000000..0b2573c
--- /dev/null
+++ b/templates/profile.tmpl
@@ -0,0 +1,64 @@
+{{with .Data}}
+{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
+<h1>Edit Profile</h1>
+
+<form action="/profile" method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <div class="form-field">
+ <div class="block-label">
+ <label for="avatar">Avatar</label> -
+ <input class="btn-link" type="submit" formaction="/profile/delavatar" formmethod="POST" value="delete">
+ </div>
+ <div class="profile-img-container">
+ <a class="img-link" href="{{.User.Avatar}}" target="_blank">
+ <img class="profile-avatar" src="{{.User.Avatar}}" alt="profile-avatar" height="96">
+ </a>
+ </div>
+ <div><input id="avatar" name="avatar" type="file"></div>
+ </div>
+ <br class="hidden">
+ <div class="form-field">
+ <div class="block-label">
+ <label for="banner">Banner</label> -
+ <input class="btn-link" type="submit" formaction="/profile/delbanner" formmethod="POST" value="delete">
+ </div>
+ <div class="profile-img-container">
+ <a class="img-link" href="{{.User.Header}}" target="_blank">
+ <img class="profile-banner" src="{{.User.Header}}" alt="profile-banner" height="120">
+ </a>
+ </div>
+ <input id="banner" name="banner" type="file">
+ </div>
+ <br class="hidden">
+ <div class="form-field">
+ <div class="block-label"><label for="name">Name</label></div>
+ <div><input id="name" name="name" type="text" class="input-w" value="{{.User.DisplayName}}"></div>
+ </div>
+ <br class="hidden">
+ <div class="form-field">
+ <div class="block-label"><label for="bio">Bio</label></div>
+ <textarea id="bio" name="bio" cols="80" rows="8">{{.User.Source.Note}}</textarea>
+ </div>
+ <br class="hidden">
+ <div class="form-field">
+ <div class="block-label"><label>Metadata</label></div>
+ {{range $i, $f := .User.Source.Fields}}
+ <div class="form-field">
+ <input id="field-name-{{$i}}" name="field-name-{{$i}}" type="text" class="input-w" value="{{$f.Name}}" placeholder="name">
+ <input id="field-value-{{$i}}" name="field-value-{{$i}}" type="text" class="input-w" value="{{$f.Value}}" placeholder="value">
+ </div>
+ {{end}}
+ </div>
+ <br class="hidden">
+ <div class="form-field">
+ <input id="locked" name="locked" type="checkbox" value="true" {{if .User.Locked}}checked{{end}}>
+ <label for="locked">Require manual approval of follow requests</label>
+ </div>
+ <br class="hidden">
+ <button type="submit"> Save </button>
+ <button type="reset"> Reset </button>
+</form>
+
+{{template "footer.tmpl"}}
+{{end}}
diff --git a/templates/quickreply.tmpl b/templates/quickreply.tmpl
index 97ff20a..245cd21 100644
--- a/templates/quickreply.tmpl
+++ b/templates/quickreply.tmpl
@@ -1,6 +1,6 @@
{{with $s := .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Quick Reply </div>
+<h1>Quick Reply</h1>
{{if .Ancestor}}
{{template "status.tmpl" (WithContext .Ancestor $.Ctx)}}
diff --git a/templates/requestlist.tmpl b/templates/requestlist.tmpl
index 1a51e31..1826404 100644
--- a/templates/requestlist.tmpl
+++ b/templates/requestlist.tmpl
@@ -4,33 +4,33 @@
<div class="user-list-item">
<div class="user-list-profile-img">
<a class="img-link" href="/user/{{.ID}}">
- <img class="status-profile-img" src="{{.Avatar}}" title="@{{.Acct}}" alt="avatar" height="48" />
+ <img class="status-profile-img" src="{{.Avatar}}" title="@{{.Acct}}" alt="@{{.Acct}}" height="48">
</a>
</div>
<div class="user-list-name">
- <div>
- <div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </div>
- <a class="img-link" href="/user/{{.ID}}">
- <div class="status-uname"> @{{.Acct}} </div>
- </a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .DisplayName) .Emojis | Raw}}</bdi>
+ <br>
+ <a class="img-link" href="/user/{{.ID}}"> <div class="status-uname">{{.Acct}}</div> </a>
+ <div class="follow-request-actions">
+ <form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <input type="submit" value="accept" class="btn-link">
+ </form>
+ -
+ <form class="d-inline" action="/reject/{{.ID}}" method="post" target="_self">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <input type="submit" value="reject" class="btn-link">
+ </form>
</div>
- <form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="accept" class="btn-link">
- </form>
- -
- <form class="d-inline" action="/reject/{{.ID}}" method="post" target="_self">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="reject" class="btn-link">
- </form>
</div>
</div>
+ <br class="hidden">
{{else}}
- <div class="no-data-found">No data found</div>
+ <p>No data found</p>
{{end}}
</div>
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
diff --git a/templates/retweetedby.tmpl b/templates/retweetedby.tmpl
index 9492ee6..f8548c6 100644
--- a/templates/retweetedby.tmpl
+++ b/templates/retweetedby.tmpl
@@ -1,6 +1,6 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Retweeted By </div>
+<h1>Retweeted By</h1>
{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
diff --git a/templates/root.tmpl b/templates/root.tmpl
index b1305f5..b04638b 100644
--- a/templates/root.tmpl
+++ b/templates/root.tmpl
@@ -4,14 +4,15 @@
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="icon" type="image/png" href="/static/favicon.png">
- <title>{{.Title}}</title>
+ <link rel="stylesheet" href="/static/style.css">
+ <title>{{.CommonData.Title}}</title>
</head>
<frameset cols="424px,*">
<frameset rows="316px,*">
- <frame name="nav" src="/nav">
- <frame name="notification" src="/notifications">
+ <frame name="nav" src="/nav" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
+ <frame name="notification" src="/notifications" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
</frameset>
- <frame name="main" src="/timeline/home">
+ <frame name="main" src="/timeline/home" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
</frameset>
</html>
{{end}}
diff --git a/templates/search.tmpl b/templates/search.tmpl
index 0473d4a..076858e 100644
--- a/templates/search.tmpl
+++ b/templates/search.tmpl
@@ -1,27 +1,28 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Search </div>
+<h1>Search</h1>
-<form class="search-form" action="/search" method="GET">
- <span class="post-form-field">
- <label for="query"> Query </label>
- <input id="query" name="q" value="{{.Q}}">
- </span>
- <span class="post-form-field">
- <label for="type"> Type </label>
- <select id="type" name="type">
- <option value="statuses" {{if eq .Type "statuses"}}selected{{end}}>Statuses</option>
- <option value="accounts" {{if eq .Type "accounts"}}selected{{end}}>Accounts</option>
- </select>
- </span>
- <button type="submit"> Search </button>
+<form action="/search" method="GET">
+ <p>
+ <label>
+ Query <input type="text" name="q" value="{{.Q}}">
+ </label>
+ <label>
+ Type
+ <select name="type">
+ <option value="statuses" {{if eq .Type "statuses"}}selected{{end}}>Statuses</option>
+ <option value="accounts" {{if eq .Type "accounts"}}selected{{end}}>Accounts</option>
+ </select>
+ </label>
+ <button type="submit"> Search </button>
+ </p>
</form>
{{if eq .Type "statuses"}}
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-{{if .Q}}<div class="no-data-found">No data found</div>{{end}}
+{{if .Q}}<p>No data found</p>{{end}}
{{end}}
{{end}}
@@ -29,11 +30,11 @@
{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
{{end}}
-<div class="pagination">
+<nav class="pagination">
{{if .NextLink}}
<a href="{{.NextLink}}">[next]</a>
{{end}}
-</div>
+</nav>
{{template "footer.tmpl"}}
{{end}}
diff --git a/templates/settings.tmpl b/templates/settings.tmpl
index ebb0458..1f0f8a2 100644
--- a/templates/settings.tmpl
+++ b/templates/settings.tmpl
@@ -1,12 +1,12 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Settings </div>
+<h1>Settings</h1>
-<form id="settings-form" action="/settings" method="POST">
+<form action="/settings" method="POST">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
{{if .PostFormats}}
- <div class="settings-form-field">
+ <div class="form-field">
<label for="post-format"> Default format </label>
{{$defFormat := .Settings.DefaultFormat}}
<select id="post-format" name="format">
@@ -16,7 +16,7 @@
</select>
</div>
{{end}}
- <div class="settings-form-field">
+ <div class="form-field">
<label for="visibility"> Default scope </label>
<select id="visibility" name="visibility">
<option value="public" {{if eq .Settings.DefaultVisibility "public"}}selected{{end}}>Public</option>
@@ -25,7 +25,7 @@
<option value="direct" {{if eq .Settings.DefaultVisibility "direct"}}selected{{end}}>Direct</option>
</select>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<label for="notification-interval"> Refresh Notifications </label>
<select id="notification-interval" name="notification_interval">
<option value="0" {{if eq .Settings.NotificationInterval 0}}selected{{end}}>Disabled</option>
@@ -36,45 +36,45 @@
<option value="600" {{if eq .Settings.NotificationInterval 600}}selected{{end}}>After 10m</option>
</select>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="copy-scope" name="copy_scope" type="checkbox" value="true" {{if .Settings.CopyScope}}checked{{end}}>
<label for="copy-scope"> Copy scope when replying </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="thread-tab" name="thread_in_new_tab" type="checkbox" value="true" {{if .Settings.ThreadInNewTab}}checked{{end}}>
<label for="thread-tab"> Open threads in new tab from timeline </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="hide-attachments" name="hide_attachments" type="checkbox" value="true" {{if .Settings.HideAttachments}}checked{{end}}>
<label for="hide-attachments"> Hide attachments </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="mask-nsfw" name="mask_nsfw" type="checkbox" value="true" {{if .Settings.MaskNSFW}}checked{{end}}>
<label for="mask-nsfw"> Mask NSFW attachments </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="fluoride-mode" name="fluoride_mode" type="checkbox" value="true" {{if .Settings.FluorideMode}}checked{{end}}>
<label for="fluoride-mode"> Enable <abbr title="Enable JavaScript based functionality, e.g., like/retweet without page reload and reply preview on thread page">fluoride mode</abbr> </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="anti-dopamine-mode" name="anti_dopamine_mode" type="checkbox"
value="true" {{if .Settings.AntiDopamineMode}}checked{{end}}>
<label for="anti-dopamine-mode"> Enable <abbr title="Remove like/retweet/unread notification count and disable like/retweet/follow notifications">anti-dopamine mode</abbr> </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="hide-unsupported-notifs" name="hide_unsupported_notifs" type="checkbox"
value="true" {{if .Settings.HideUnsupportedNotifs}}checked{{end}}>
<label for="hide-unsupported-notifs"> Hide unsupported notifications </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<input id="dark-mode" name="dark_mode" type="checkbox" value="true" {{if .Settings.DarkMode}}checked{{end}}>
<label for="dark-mode"> Use dark theme </label>
</div>
- <div class="settings-form-field">
+ <div class="form-field">
<label for="css"> Custom CSS: </label>
</div>
- <div>
- <textarea id="css" name="css" cols="80" rows="8">{{.Settings.CSS}}</textarea>
+ <div class="form-field">
+ <textarea id="css" class="monospace" name="css" cols="80" rows="8">{{.Settings.CSS}}</textarea>
</div>
<button type="submit"> Save </button>
</form>
diff --git a/templates/signin.tmpl b/templates/signin.tmpl
index c7699f7..2c97de2 100644
--- a/templates/signin.tmpl
+++ b/templates/signin.tmpl
@@ -1,18 +1,17 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Bloat </div>
-<div class="signin-desc">
- A web client for <a href="https://pleroma.social" target="_blank">Mastadon Network</a>.
-</div>
-<form class="signin-form" action="/signin" method="post">
- Enter the domain name of your instance to continue
- <br/>
- <input type="text" name="instance" placeholder="example.com" required>
- <br/>
- <button type="submit"> Signin </button>
+<h1>bloat</h1>
+<p>A web client for <a href="https://pleroma.social" target="_blank">Mastadon Network</a>.</p>
+<form action="/signin" method="post">
+ <div class="form-field-s">
+ <label for="instance">Enter the domain name of your instance to continue</label>
+ </div>
+ <div class="form-field-s">
+ <input id="instance" type="text" class="input-w" name="instance" placeholder="example.com" required>
+ </div>
+ <div class="form-field-s"><button type="submit">Signin</button></div>
</form>
-
<p>
See
<a href="https://git.freesoftwareextremist.com/bloat" target="_blank">git.freesoftwareextremist.com/bloat</a>
diff --git a/templates/status.tmpl b/templates/status.tmpl
index 5ada84e..503b6f1 100644
--- a/templates/status.tmpl
+++ b/templates/status.tmpl
@@ -1,15 +1,15 @@
{{with .Data}}
-<div id="status-{{.ID}}" class="status-container-container">
+<article id="status-{{.ID}}" class="status-container-container">
{{if .Reblog}}
<div class="retweet-info">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="24">
</a>
- <bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi>
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
<a href="/user/{{.Account.ID}}">
- <span class="status-uname"> @{{.Account.Acct}} </span>
+ <span class="status-uname">@{{.Account.Acct}}</span>
</a>
- retweeted
+ <span>retweeted</span>
</div>
{{template "status" (WithContext .Reblog $.Ctx)}}
{{else}}
@@ -18,26 +18,20 @@
<div class="status-container status-{{.ID}}" data-id="{{.ID}}">
<div class="status-profile-img-container">
<a class="img-link" href="/user/{{.Account.ID}}">
- <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="48" />
+ <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="@{{.Account.Acct}}" height="48">
</a>
</div>
<div class="status">
<div class="status-name">
- <bdi class="status-dname"> {{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}} </bdi>
- <a href="/user/{{.Account.ID}}">
- <span class="status-uname"> @{{.Account.Acct}} </span>
- </a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .Account.DisplayName) .Account.Emojis | Raw}}</bdi>
+ <a href="/user/{{.Account.ID}}"><span class="status-uname">@{{.Account.Acct}}</span></a>
<div class="more-container">
<div class="remote-link">
{{if .IDNumbers}}#{{index .IDNumbers .ID}}{{end}} {{.Visibility}}
</div>
<div class="more-content">
- <a class="more-link" href="{{.URL}}" target="_blank">
- source
- </a>
- <a class="more-link" href="/quickreply/{{.ID}}#status-{{.ID}}">
- quickreply
- </a>
+ <a class="more-link" href="{{.URL}}" target="_blank">source</a>
+ <a class="more-link" href="/quickreply/{{.ID}}#status-{{.ID}}">quickreply</a>
{{if .Muted}}
<form action="/unmuteconv/{{.ID}}" method="post" target="_self">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
@@ -76,6 +70,7 @@
</div>
</div>
</div>
+ {{if (or .InReplyToID .ShowReplies)}}
<div class="status-reply-container">
{{if .InReplyToID}}
<a class="status-reply-to-link" href="{{if not .ShowReplies}}/thread/{{.InReplyToID}}{{end}}#status-{{.InReplyToID}}">
@@ -90,52 +85,47 @@
{{end}}
{{end}}
</div>
+ {{end}}
{{if (or .Content .SpoilerText)}}
<div class="status-content">
- {{if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br/>{{end}}
- {{StatusContentFilter .Content .Emojis .Mentions | Raw}}
+ {{- if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br>{{end -}}
+ {{- StatusContentFilter .Content .Emojis .Mentions | Raw -}}
</div>
{{end}}
{{if .MediaAttachments}}
<div class="status-media-container">
{{range .MediaAttachments}}
- {{if eq .Type "image"}}
+ {{- if eq .Type "image" -}}
{{if $.Ctx.HideAttachments}}
- <a href="{{.URL}}" target="_blank">
- [image{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
- </a>
- {{else}}
- <a class="img-link" href="{{.URL}}" target="_blank" title="{{.Description}}">
- <img class="status-image" src="{{.PreviewURL}}" alt="status-image" height="240" />
+ <a href="{{.URL}}" target="_blank">[image{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]</a>
+ {{- else -}}
+ <a class="img-link status-image-container" href="{{.URL}}" target="_blank" title="{{.Description}}">
+ <img class="status-image" src="{{.PreviewURL}}" alt="{{.Description}}" height="240">
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
<div class="status-nsfw-overlay"></div>
{{end}}
</a>
- {{end}}
+ {{- end -}}
{{else if eq .Type "audio"}}
{{if $.Ctx.HideAttachments}}
- <a href="{{.URL}}" target="_blank">
- [audio{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
- </a>
+ <a href="{{.URL}}" target="_blank">[audio{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]</a>
{{else}}
<audio class="status-audio" controls title="{{.Description}}">
<source src="{{.URL}}">
- <a href="{{.URL}}" target="_blank"> [audio] </a>
+ <a href="{{.URL}}" target="_blank">[audio]</a>
</audio>
{{end}}
{{else if eq .Type "video"}}
{{if $.Ctx.HideAttachments}}
- <a href="{{.URL}}" target="_blank">
- [video{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
- </a>
+ <a href="{{.URL}}" target="_blank">[video{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]</a>
{{else}}
<div class="status-video-container" title="{{.Description}}">
<video class="status-video" controls height="240">
<source src="{{.URL}}">
- <a href="{{.URL}}" target="_blank"> [video] </a>
+ <a href="{{.URL}}" target="_blank">[video]</a>
</video>
{{if (and $.Ctx.MaskNSFW $s.Sensitive)}}
<div class="status-nsfw-overlay"></div>
@@ -144,20 +134,18 @@
{{end}}
{{else}}
- <a href="{{.URL}}" target="_blank">
- [attachment{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
- </a>
- {{end}}
+ <a href="{{.URL}}" target="_blank">[attachment{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]</a>
{{end}}
+ {{- end -}}
</div>
{{end}}
{{if .Poll}}
- <form class="poll-form" action="/vote/{{.Poll.ID}}" method="POST" target="_self">
+ <form class="status-poll" action="/vote/{{.Poll.ID}}" method="POST" target="_self">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="hidden" name="status_id" value="{{$s.ID}}">
{{range $i, $o := .Poll.Options}}
- <div class="poll-option">
+ <div class="form-field-s">
{{if (or $s.Poll.Expired $s.Poll.Voted)}}
<div> {{EmojiFilter (HTML $o.Title) $s.Emojis | Raw}} - {{$o.VotesCount}} votes </div>
{{else}}
@@ -170,9 +158,11 @@
</div>
{{end}}
{{if not (or .Poll.Expired .Poll.Voted)}}
+ <div class="form-field-s">
<button type="submit"> Vote </button>
+ </div>
{{end}}
- <div class="poll-info">
+ <div>
<span>{{.Poll.VotesCount}} votes</span>
{{if .Poll.Expired}}
<span> - poll expired </span>
@@ -189,10 +179,9 @@
{{end}}
<div class="status-action-container">
<div class="status-action">
- <a href="/thread/{{.ID}}?reply=true#status-{{.ID}}">
- reply
- </a>
- <a class="status-reply-count" href="/thread/{{.ID}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
+ <a href="/thread/{{.ID}}?reply=true#status-{{.ID}}">reply</a>
+ <a class="status-reply-count {{if or $.Ctx.AntiDopamineMode (not .RepliesCount)}}hidden{{end}}"
+ href="/thread/{{.ID}}#status-{{.ID}}" {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}} title="replies">
{{if and (not $.Ctx.AntiDopamineMode) .RepliesCount}}
({{DisplayInteractionCount .RepliesCount}})
{{end}}
@@ -206,7 +195,8 @@
<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">
<input type="submit" value="{{$rt}}" class="btn-link"
{{if or (eq .Visibility "private") (eq .Visibility "direct")}}title="this status cannot be retweeted" disabled{{end}}>
- <a class="status-retweet-count" href="/retweetedby/{{.ID}}" title="click to see the the list">
+ <a class="status-retweet-count {{if or $.Ctx.AntiDopamineMode (not .ReblogsCount)}}hidden{{end}}"
+ href="/retweetedby/{{.ID}}" title="click to see the the list">
{{if and (not $.Ctx.AntiDopamineMode) .ReblogsCount}}
({{DisplayInteractionCount .ReblogsCount}})
{{end}}
@@ -220,7 +210,8 @@
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">
<input type="submit" value="{{$like}}" class="btn-link">
- <a class="status-like-count" href="/likedby/{{.ID}}" title="click to see the the list">
+ <a class="status-like-count {{if or $.Ctx.AntiDopamineMode (not .FavouritesCount)}}hidden{{end}}"
+ href="/likedby/{{.ID}}" title="click to see the the list">
{{if and (not $.Ctx.AntiDopamineMode) .FavouritesCount}}
({{DisplayInteractionCount .FavouritesCount}})
{{end}}
@@ -238,8 +229,9 @@
</div>
</div>
</div>
+ <br class="hidden">
{{end}}
{{end}}
{{end}}
-</div>
+</article>
{{end}}
diff --git a/templates/thread.tmpl b/templates/thread.tmpl
index d6a1c7d..f32b065 100644
--- a/templates/thread.tmpl
+++ b/templates/thread.tmpl
@@ -1,9 +1,6 @@
{{with $s := .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title-container">
- <span class="page-title"> Thread </span>
- <a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
-</div>
+<h1>Thread <a class="page-link" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a></h1>
{{range .Statuses}}
diff --git a/templates/timeline.tmpl b/templates/timeline.tmpl
index 38659dc..8de6705 100644
--- a/templates/timeline.tmpl
+++ b/templates/timeline.tmpl
@@ -1,15 +1,12 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title-container">
- <span class="page-title"> {{.Title}} </span>
- <a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
-</div>
+<h1>{{.Title}} <a class="page-link" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a></h1>
{{if eq .Type "remote"}}
-<form class="search-form" action="/timeline/remote" method="GET">
- <span class="post-form-field">
+<form action="/timeline/remote" method="GET">
+ <span>
<label for="instance"> Instance </label>
- <input id="instance" name="instance" value="{{.Instance}}">
+ <input type="text" id="instance" name="instance" value="{{.Instance}}">
</span>
<button type="submit"> Submit </button>
</form>
@@ -19,14 +16,14 @@
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{end}}
-<div class="pagination">
+<nav class="pagination">
{{if .PrevLink}}
<a href="{{.PrevLink}}">[prev]</a>
{{end}}
{{if .NextLink}}
<a href="{{.NextLink}}">[next]</a>
{{end}}
-</div>
+</nav>
{{template "footer.tmpl"}}
{{end}}
diff --git a/templates/user.tmpl b/templates/user.tmpl
index dccce7c..b279e15 100644
--- a/templates/user.tmpl
+++ b/templates/user.tmpl
@@ -1,25 +1,25 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> User </div>
+<h1>User</h1>
<div class="user-info-container">
-<div>
<div class="user-profile-img-container">
<a class="img-link" href="{{.User.Avatar}}" target="_blank">
- <img class="user-profile-img" src="{{.User.Avatar}}" alt="profile-avatar" height="96" />
+ <img class="user-profile-img" src="{{.User.Avatar}}" alt="@{{.User.Acct}}" height="96">
</a>
</div>
<div class="user-profile-details-container">
<div>
- <bdi class="status-dname"> {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} </bdi>
- <span class="status-uname"> @{{.User.Acct}} </span>
+ <bdi class="status-dname">{{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}</bdi>
+ <span class="status-uname">@{{.User.Acct}}</span>
<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
source
</a>
</div>
- {{if not .IsCurrent}}
+ {{if (ne $.Ctx.UserID .User.ID)}}
<div>
<span> {{if .User.Pleroma.Relationship.FollowedBy}} follows you - {{end}} </span>
+ {{if .User.Pleroma.Relationship.BlockedBy}} blocks you - {{end}}
{{if .User.Pleroma.Relationship.Following}}
<form class="d-inline" action="/unfollow/{{.User.ID}}" method="post">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
@@ -78,17 +78,7 @@
<input type="submit" value="unmute" class="btn-link">
</form>
{{else}}
- <form class="d-inline" action="/mute/{{.User.ID}}" method="post">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="mute" class="btn-link">
- </form>
- -
- <form class="d-inline" action="/mute/{{.User.ID}}?notifications=false" method="post">
- <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
- <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
- <input type="submit" value="mute (keep notifications)" class="btn-link">
- </form>
+ <a href="/mute/{{.User.ID}}">mute</a>
{{end}}
{{if .User.Pleroma.Relationship.Following}}
-
@@ -109,96 +99,131 @@
</div>
{{end}}
<div>
- <a href="/user/{{.User.ID}}"> statuses ({{.User.StatusesCount}}) </a> -
- <a href="/user/{{.User.ID}}/following"> following ({{.User.FollowingCount}}) </a> -
- <a href="/user/{{.User.ID}}/followers"> followers ({{.User.FollowersCount}}) </a> -
- <a href="/user/{{.User.ID}}/media"> media </a>
+ <a href="/user/{{.User.ID}}">statuses ({{.User.StatusesCount}})</a> -
+ <a href="/user/{{.User.ID}}/following">following ({{.User.FollowingCount}})</a> -
+ <a href="/user/{{.User.ID}}/followers">followers ({{.User.FollowersCount}})</a> -
+ <a href="/user/{{.User.ID}}/media">media</a>
</div>
- {{if .IsCurrent}}
+ {{if (eq $.Ctx.UserID .User.ID)}}
<div>
- <a href="/user/{{.User.ID}}/bookmarks"> bookmarks </a>
- - <a href="/user/{{.User.ID}}/likes"> likes </a>
- - <a href="/user/{{.User.ID}}/mutes"> mutes </a>
- - <a href="/user/{{.User.ID}}/blocks"> blocks </a>
- {{if .User.Locked}}- <a href="/user/{{.User.ID}}/requests"> requests </a>{{end}}
+ <a href="/user/{{.User.ID}}/bookmarks">bookmarks</a>
+ - <a href="/user/{{.User.ID}}/likes">likes</a>
+ - <a href="/user/{{.User.ID}}/mutes">mutes</a>
+ - <a href="/user/{{.User.ID}}/blocks">blocks</a>
+ {{if .User.Locked}}- <a href="/user/{{.User.ID}}/requests">requests (
+ {{- if .User.FollowRequestsCount}}{{.User.FollowRequestsCount}}{{else}}{{.User.Source.FollowRequestsCount}}{{end -}}
+ )</a>{{end}}
</div>
{{end}}
<div>
- <a href="/usersearch/{{.User.ID}}"> search statuses </a>
- {{if .IsCurrent}} - <a href="/filters"> filters </a> {{end}}
+ <a href="/usersearch/{{.User.ID}}">search statuses</a>
+ {{if (eq $.Ctx.UserID .User.ID)}} - <a href="/filters">filters</a> {{end}}
</div>
</div>
- <div class="user-profile-decription">
- {{EmojiFilter .User.Note .User.Emojis | Raw}}
+ <div class="user-profile-description">
+ {{- EmojiFilter .User.Note .User.Emojis | Raw -}}
</div>
{{if .User.Fields}}
<div class="user-fields">
{{range .User.Fields}}
- <div>{{.Name}} - {{.Value | Raw}}</div>
+ <div>{{- EmojiFilter (HTML .Name) $.Data.User.Emojis | Raw}} - {{EmojiFilter .Value $.Data.User.Emojis | Raw -}}</div>
{{end}}
</div>
{{end}}
</div>
-</div>
{{if eq .Type ""}}
-<div class="page-title"> Statuses </div>
+<h1>Statuses</h1>
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
{{else if eq .Type "following"}}
-<div class="page-title"> Following </div>
-{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
+<h1>Following</h1>
+{{template "userlistfollow.tmpl" (WithContext .Users $.Ctx)}}
{{else if eq .Type "followers"}}
-<div class="page-title"> Followers </div>
-{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
+<h1>Followers</h1>
+{{template "userlistfollow.tmpl" (WithContext .Users $.Ctx)}}
{{else if eq .Type "media"}}
-<div class="page-title"> Statuses with media </div>
+<h1>Statuses With Media</h1>
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
{{else if eq .Type "bookmarks"}}
-<div class="page-title"> Bookmarks </div>
+<h1>Bookmarks</h1>
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
{{else if eq .Type "likes"}}
-<div class="page-title"> Likes </div>
+<h1>Likes</h1>
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
{{else if eq .Type "mutes"}}
-<div class="page-title"> Mutes </div>
-{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
+<h1>Mutes</h1>
+{{if .Users}}
+<table>
+{{range .Users}}
+ <tr>
+ <td> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
+ <td>
+ <form class="user-list-action" action="/unmute/{{.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit">Unmute</button>
+ </form>
+ </td>
+ </tr>
+{{end}}
+</table>
+{{else}}
+<p>No data found</p>
+{{end}}
{{else if eq .Type "blocks"}}
-<div class="page-title"> Blocks </div>
-{{template "userlist.tmpl" (WithContext .Users $.Ctx)}}
+<h1>Blocks</h1>
+{{if .Users}}
+<table>
+{{range .Users}}
+ <tr>
+ <td> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
+ <td>
+ <form class="user-list-action" action="/unblock/{{.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit">Unblock</button>
+ </form>
+ </td>
+ </tr>
+{{end}}
+</table>
+{{else}}
+<p>No data found</p>
+{{end}}
{{else if eq .Type "requests"}}
-<div class="page-title"> Follow requests </div>
+<h1>Follow Requests</h1>
{{template "requestlist.tmpl" (WithContext .Users $.Ctx)}}
{{end}}
-<div class="pagination">
+<nav class="pagination">
{{if .NextLink}}
<a href="{{.NextLink}}">[next]</a>
{{end}}
-</div>
+</nav>
{{template "footer.tmpl"}}
{{end}}
diff --git a/templates/userlist.tmpl b/templates/userlist.tmpl
index f206397..83ed088 100644
--- a/templates/userlist.tmpl
+++ b/templates/userlist.tmpl
@@ -3,9 +3,9 @@
{{range .}}
{{template "userlistitem.tmpl" (WithContext . $.Ctx)}}
{{else}}
- <div class="no-data-found">No data found</div>
+ <p>No data found</p>
{{end}}
</div>
{{else}}
-<div class="no-data-found">No data found</div>
+<p>No data found</p>
{{end}}
diff --git a/templates/userlistfollow.tmpl b/templates/userlistfollow.tmpl
new file mode 100644
index 0000000..298142f
--- /dev/null
+++ b/templates/userlistfollow.tmpl
@@ -0,0 +1,30 @@
+{{with .Data}}
+{{if .}}
+<table>
+{{range .}}
+ <tr>
+ <td> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
+ <td>
+ {{if (ne $.Ctx.UserID .ID)}}
+ {{if .Pleroma.Relationship.Following}}
+ <form class="user-list-action" action="/unfollow/{{.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit">Unfollow</button>
+ </form>
+ {{else}}
+ <form class="user-list-action" action="/follow/{{.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit">Follow</button>
+ </form>
+ {{end}}
+ {{end}}
+ </td>
+ </tr>
+{{end}}
+</table>
+{{else}}
+<p>No data found</p>
+{{end}}
+{{end}}
diff --git a/templates/userlistitem.tmpl b/templates/userlistitem.tmpl
index 50b9d0c..dd50aa8 100644
--- a/templates/userlistitem.tmpl
+++ b/templates/userlistitem.tmpl
@@ -2,14 +2,14 @@
<div class="user-list-item">
<div class="user-list-profile-img">
<a class="img-link" href="/user/{{.ID}}">
- <img class="status-profile-img" src="{{.Avatar}}" title="@{{.Acct}}" alt="avatar" height="48" />
+ <img class="status-profile-img" src="{{.Avatar}}" title="@{{.Acct}}" alt="@{{.Acct}}" height="48">
</a>
</div>
<div class="user-list-name">
- <div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </div>
- <a class="img-link" href="/user/{{.ID}}">
- <div class="status-uname"> @{{.Acct}} </div>
- </a>
+ <bdi class="status-dname">{{EmojiFilter (HTML .DisplayName) .Emojis | Raw}}</bdi>
+ <br>
+ <a class="img-link" href="/user/{{.ID}}"><span class="status-uname">@{{.Acct}}</span></a>
</div>
+ <br class="hidden">
</div>
{{end}}
diff --git a/templates/usersearch.tmpl b/templates/usersearch.tmpl
index 78fa7b8..5011b99 100644
--- a/templates/usersearch.tmpl
+++ b/templates/usersearch.tmpl
@@ -1,26 +1,27 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Search {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}'s statuses </div>
+<h1>Search {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} @{{.User.Acct}}'s statuses</h1>
-<form class="search-form" action="/usersearch/{{.User.ID}}" method="GET">
- <span class="post-form-field">
- <label for="query"> Query </label>
- <input id="query" name="q" value="{{.Q}}">
- </span>
- <button type="submit"> Search </button>
+<form action="/usersearch/{{.User.ID}}" method="GET">
+ <p>
+ <label>
+ Query <input type="text" name="q" value="{{.Q}}">
+ </label>
+ <button type="submit"> Search </button>
+ </p>
</form>
{{range .Statuses}}
{{template "status.tmpl" (WithContext . $.Ctx)}}
{{else}}
-{{if .Q}}<div class="no-data-found">No data found</div>{{end}}
+{{if .Q}}<p>No data found</p>{{end}}
{{end}}
-<div class="pagination">
+<nav class="pagination">
{{if .NextLink}}
<a href="{{.NextLink}}">[next]</a>
{{end}}
-</div>
+</nav>
{{template "footer.tmpl"}}
{{end}}
diff --git a/util/kv.go b/util/kv.go
deleted file mode 100644
index df61654..0000000
--- a/util/kv.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package util
-
-import (
- "errors"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- "sync"
-)
-
-var (
- errInvalidKey = errors.New("invalid key")
- errNoSuchKey = errors.New("no such key")
-)
-
-type Database struct {
- cache map[string][]byte
- basedir string
- m sync.RWMutex
-}
-
-func NewDatabse(basedir string) (db *Database, err error) {
- err = os.Mkdir(basedir, 0755)
- if err != nil && !os.IsExist(err) {
- return
- }
-
- return &Database{
- cache: make(map[string][]byte),
- basedir: basedir,
- }, nil
-}
-
-func (db *Database) Set(key string, val []byte) (err error) {
- if len(key) < 1 || strings.ContainsRune(key, os.PathSeparator) {
- return errInvalidKey
- }
-
- err = ioutil.WriteFile(filepath.Join(db.basedir, key), val, 0644)
- if err != nil {
- return
- }
-
- db.m.Lock()
- db.cache[key] = val
- db.m.Unlock()
-
- return
-}
-
-func (db *Database) Get(key string) (val []byte, err error) {
- if len(key) < 1 || strings.ContainsRune(key, os.PathSeparator) {
- return nil, errInvalidKey
- }
-
- db.m.RLock()
- data, ok := db.cache[key]
- db.m.RUnlock()
-
- if !ok {
- data, err = ioutil.ReadFile(filepath.Join(db.basedir, key))
- if err != nil {
- err = errNoSuchKey
- return nil, err
- }
-
- db.m.Lock()
- db.cache[key] = data
- db.m.Unlock()
- }
-
- val = make([]byte, len(data))
- copy(val, data)
-
- return
-}
-
-func (db *Database) Remove(key string) {
- if len(key) < 1 || strings.ContainsRune(key, os.PathSeparator) {
- return
- }
-
- os.Remove(filepath.Join(db.basedir, key))
-
- db.m.Lock()
- delete(db.cache, key)
- db.m.Unlock()
-
- return
-}
diff --git a/util/rand.go b/util/rand.go
index 90e66a5..f1692b9 100644
--- a/util/rand.go
+++ b/util/rand.go
@@ -16,10 +16,6 @@ func NewRandID(n int) (string, error) {
return enc.EncodeToString(data), nil
}
-func NewSessionID() (string, error) {
- return NewRandID(24)
-}
-
func NewCSRFToken() (string, error) {
return NewRandID(24)
}