diff options
53 files changed, 1533 insertions, 1420 deletions
@@ -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. @@ -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 @@ -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) } @@ -5,4 +5,4 @@ require ( github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 ) -go 1.13 +go 1.11 @@ -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) } |