aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--INSTALL13
-rw-r--r--Makefile8
-rw-r--r--README4
-rw-r--r--config/config.go39
-rw-r--r--main.go19
-rw-r--r--mastodon/accounts.go9
-rw-r--r--mastodon/helper.go23
-rw-r--r--mastodon/lists.go4
-rw-r--r--mastodon/notification.go5
-rw-r--r--mastodon/status.go45
-rw-r--r--model/settings.go46
-rw-r--r--renderer/model.go13
-rw-r--r--renderer/renderer.go87
-rw-r--r--service/service.go129
-rw-r--r--service/transport.go108
-rw-r--r--static/fluoride.js34
-rw-r--r--static/style.css51
-rw-r--r--templates/about.tmpl6
-rw-r--r--templates/emoji.tmpl2
-rw-r--r--templates/header.tmpl4
-rw-r--r--templates/list.tmpl63
-rw-r--r--templates/lists.tmpl35
-rw-r--r--templates/nav.tmpl11
-rw-r--r--templates/notification.tmpl8
-rw-r--r--templates/postform.tmpl2
-rw-r--r--templates/requestlist.tmpl2
-rw-r--r--templates/search.tmpl2
-rw-r--r--templates/settings.tmpl5
-rw-r--r--templates/status.tmpl25
-rw-r--r--templates/thread.tmpl4
-rw-r--r--templates/timeline.tmpl5
-rw-r--r--templates/user.tmpl21
-rw-r--r--templates/userlist.tmpl14
-rw-r--r--templates/userlistitem.tmpl15
-rw-r--r--templates/usersearch.tmpl6
-rw-r--r--util/getopt.go122
-rw-r--r--util/rand.go20
38 files changed, 632 insertions, 379 deletions
diff --git a/.gitignore b/.gitignore
index 037bea6..79904fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
bloat
database
-bloat.def.conf
+bloat.gen.conf
diff --git a/INSTALL b/INSTALL
index 032f612..8f8c6d4 100644
--- a/INSTALL
+++ b/INSTALL
@@ -15,12 +15,13 @@ This will perform a system wide installation of bloat. By default, it will
install the binary in /usr/local/bin and data files in /usr/local/share/bloat.
You can change these paths by editing the Makefile.
-3. Edit and copy the config file
-Edit the generated config file to you liking and then copy it to the default
-config location. Comments in the config file describe what each config value
-does. For most cases, you only need to change the value of "client_website".
-$ $EDITOR bloat.def.conf
-# cp bloat.def.conf /etc/bloat.conf
+3. Edit the config file
+bloat looks for a file named bloat.conf in the working directory and
+/etc/bloat in that order. You can also specify another file by using the -f
+flag. Comments in the config file describe what each config value does. For
+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
diff --git a/Makefile b/Makefile
index 4231015..c38de6b 100644
--- a/Makefile
+++ b/Makefile
@@ -14,16 +14,14 @@ SRC=main.go \
service/*.go \
util/*.go \
-all: bloat bloat.def.conf
+all: bloat
bloat: $(SRC) $(TMPL)
$(GO) build $(GOFLAGS) -o bloat main.go
-
-bloat.def.conf:
sed -e "s%=database%=/var/bloat%g" \
-e "s%=templates%=$(SHAREPATH)/templates%g" \
-e "s%=static%=$(SHAREPATH)/static%g" \
- < bloat.conf > bloat.def.conf
+ < bloat.conf > bloat.gen.conf
install: bloat
mkdir -p $(DESTDIR)$(BINPATH) \
@@ -42,4 +40,4 @@ uninstall:
clean:
rm -f bloat
- rm -f bloat.def.conf
+ rm -f bloat.gen.conf
diff --git a/README b/README
index 9c76da1..b00592f 100644
--- a/README
+++ b/README
@@ -15,11 +15,11 @@ Building and Installation:
Typing make will build the binary
$ make
-Edit the provided config file. See the bloat.conf file for more details.
+Edit the default config file. See the bloat.conf file for more details.
$ ed bloat.conf
Run the binary
-$ ./bloat -f bloat.conf
+$ ./bloat
You can now access the frontend at http://127.0.0.1:8080, which is the default
listen address. See the INSTALL file for more details.
diff --git a/config/config.go b/config/config.go
index 8678f52..bbb327c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -108,21 +108,30 @@ func Parse(r io.Reader) (c *config, err error) {
return
}
-func ParseFile(file string) (c *config, err error) {
- f, err := os.Open(file)
- if err != nil {
- return
- }
- defer f.Close()
-
- info, err := f.Stat()
- if err != nil {
- return
+func ParseFiles(files []string) (c *config, err error) {
+ var lastErr error
+ for _, file := range files {
+ f, err := os.Open(file)
+ if err != nil {
+ lastErr = err
+ if os.IsNotExist(err) {
+ continue
+ }
+ return nil, err
+ }
+ defer f.Close()
+ info, err := f.Stat()
+ if err != nil {
+ lastErr = err
+ return nil, err
+ }
+ if info.IsDir() {
+ continue
+ }
+ return Parse(f)
}
-
- if info.IsDir() {
- return nil, errors.New("invalid config file")
+ if lastErr == nil {
+ lastErr = errors.New("invalid config file")
}
-
- return Parse(f)
+ return nil, lastErr
}
diff --git a/main.go b/main.go
index cac5eee..3b5ccba 100644
--- a/main.go
+++ b/main.go
@@ -2,6 +2,7 @@ package main
import (
"errors"
+ "flag"
"fmt"
"log"
"net/http"
@@ -17,7 +18,7 @@ import (
)
var (
- configFile = "/etc/bloat.conf"
+ configFiles = []string{"bloat.conf", "/etc/bloat.conf"}
)
func errExit(err error) {
@@ -26,19 +27,13 @@ func errExit(err error) {
}
func main() {
- opts, _, err := util.Getopts(os.Args, "f:")
- if err != nil {
- errExit(err)
- }
+ configFile := flag.String("f", "", "config file")
+ flag.Parse()
- for _, opt := range opts {
- switch opt.Option {
- case 'f':
- configFile = opt.Value
- }
+ if len(*configFile) > 0 {
+ configFiles = []string{*configFile}
}
-
- config, err := config.ParseFile(configFile)
+ config, err := config.ParseFiles(configFiles)
if err != nil {
errExit(err)
}
diff --git a/mastodon/accounts.go b/mastodon/accounts.go
index 694e672..dbd0a48 100644
--- a/mastodon/accounts.go
+++ b/mastodon/accounts.go
@@ -189,6 +189,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,9 +244,13 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship,
}
// AccountMute mute the account.
-func (c *Client) AccountMute(ctx context.Context, id string) (*Relationship, error) {
+func (c *Client) AccountMute(ctx context.Context, id string, notifications *bool) (*Relationship, error) {
+ params := url.Values{}
+ if notifications != nil {
+ params.Set("notifications", strconv.FormatBool(*notifications))
+ }
var relationship Relationship
- err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil)
+ err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), params, &relationship, nil)
if err != nil {
return nil, err
}
diff --git a/mastodon/helper.go b/mastodon/helper.go
index 05af20f..cb0013d 100644
--- a/mastodon/helper.go
+++ b/mastodon/helper.go
@@ -3,12 +3,28 @@ package mastodon
import (
"encoding/base64"
"encoding/json"
- "errors"
"fmt"
"net/http"
"os"
)
+type Error struct {
+ code int
+ err string
+}
+
+func (e Error) Error() string {
+ return e.err
+}
+
+func (e Error) IsAuthError() bool {
+ switch e.code {
+ case http.StatusForbidden, http.StatusUnauthorized:
+ return true
+ }
+ return false
+}
+
// Base64EncodeFileName returns the base64 data URI format string of the file with the file name.
func Base64EncodeFileName(filename string) (string, error) {
file, err := os.Open(filename)
@@ -51,5 +67,8 @@ func parseAPIError(prefix string, resp *http.Response) error {
errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error)
}
- return errors.New(errMsg)
+ return Error{
+ code: resp.StatusCode,
+ err: errMsg,
+ }
}
diff --git a/mastodon/lists.go b/mastodon/lists.go
index d323b79..1b76bdc 100644
--- a/mastodon/lists.go
+++ b/mastodon/lists.go
@@ -90,7 +90,7 @@ func (c *Client) DeleteList(ctx context.Context, id string) error {
func (c *Client) AddToList(ctx context.Context, list string, accounts ...string) error {
params := url.Values{}
for _, acct := range accounts {
- params.Add("account_ids", string(acct))
+ params.Add("account_ids[]", string(acct))
}
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
@@ -100,7 +100,7 @@ func (c *Client) AddToList(ctx context.Context, list string, accounts ...string)
func (c *Client) RemoveFromList(ctx context.Context, list string, accounts ...string) error {
params := url.Values{}
for _, acct := range accounts {
- params.Add("account_ids", string(acct))
+ params.Add("account_ids[]", string(acct))
}
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
diff --git a/mastodon/notification.go b/mastodon/notification.go
index 656e6a1..e94f901 100644
--- a/mastodon/notification.go
+++ b/mastodon/notification.go
@@ -23,9 +23,12 @@ type Notification struct {
}
// GetNotifications return notifications.
-func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, excludes []string) ([]*Notification, error) {
+func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, includes, excludes []string) ([]*Notification, error) {
var notifications []*Notification
params := url.Values{}
+ for _, include := range includes {
+ params.Add("include_types[]", include)
+ }
for _, exclude := range excludes {
params.Add("exclude_types[]", exclude)
}
diff --git a/mastodon/status.go b/mastodon/status.go
index 80e7e0e..f860c31 100644
--- a/mastodon/status.go
+++ b/mastodon/status.go
@@ -19,6 +19,19 @@ type ReplyInfo struct {
Number int `json:"number"`
}
+type CreatedAt struct {
+ time.Time
+}
+
+func (t *CreatedAt) UnmarshalJSON(d []byte) error {
+ // Special case to handle retweets from GNU Social
+ // which returns empty string ("") in created_at
+ if len(d) == 2 && string(d) == `""` {
+ return nil
+ }
+ return t.Time.UnmarshalJSON(d)
+}
+
// Status is struct to hold status.
type Status struct {
ID string `json:"id"`
@@ -29,7 +42,7 @@ type Status struct {
InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
Reblog *Status `json:"reblog"`
Content string `json:"content"`
- CreatedAt time.Time `json:"created_at"`
+ CreatedAt CreatedAt `json:"created_at"`
Emojis []Emoji `json:"emojis"`
RepliesCount int64 `json:"replies_count"`
ReblogsCount int64 `json:"reblogs_count"`
@@ -43,7 +56,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"`
@@ -64,22 +76,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
@@ -110,16 +106,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
@@ -288,7 +274,7 @@ func (c *Client) DeleteStatus(ctx context.Context, id string) error {
}
// Search search content with query.
-func (c *Client) Search(ctx context.Context, q string, qType string, limit int, resolve bool, offset int, accountID string) (*Results, error) {
+func (c *Client) Search(ctx context.Context, q string, qType string, limit int, resolve bool, offset int, accountID string, following bool) (*Results, error) {
var results Results
params := url.Values{}
params.Set("q", q)
@@ -296,6 +282,7 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
params.Set("limit", fmt.Sprint(limit))
params.Set("resolve", fmt.Sprint(resolve))
params.Set("offset", fmt.Sprint(offset))
+ params.Set("following", fmt.Sprint(following))
if len(accountID) > 0 {
params.Set("account_id", accountID)
}
diff --git a/model/settings.go b/model/settings.go
index c4e8aec..1f83c75 100644
--- a/model/settings.go
+++ b/model/settings.go
@@ -1,31 +1,33 @@
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"`
- CSS string `json:"css"`
+ 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,
- CSS: "",
+ 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 385ac7c..726cee9 100644
--- a/renderer/model.go
+++ b/renderer/model.go
@@ -62,6 +62,19 @@ type TimelineData struct {
PrevLink string
}
+type ListsData struct {
+ *CommonData
+ Lists []*mastodon.List
+}
+
+type ListData struct {
+ *CommonData
+ List *mastodon.List
+ Accounts []*mastodon.Account
+ Q string
+ SearchAccounts []*mastodon.Account
+}
+
type ThreadData struct {
*CommonData
Statuses []*mastodon.Status
diff --git a/renderer/renderer.go b/renderer/renderer.go
index 6c9877a..50fced7 100644
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -1,11 +1,11 @@
package renderer
import (
- "fmt"
+ "html/template"
"io"
+ "regexp"
"strconv"
"strings"
- "text/template"
"time"
"bloat/mastodon"
@@ -19,6 +19,8 @@ const (
NavPage = "nav.tmpl"
RootPage = "root.tmpl"
TimelinePage = "timeline.tmpl"
+ ListsPage = "lists.tmpl"
+ ListPage = "list.tmpl"
ThreadPage = "thread.tmpl"
QuickReplyPage = "quickreply.tmpl"
StatusPopup = "status.tmpl"
@@ -39,29 +41,25 @@ type TemplateData struct {
Ctx *Context
}
+func emojiHTML(e mastodon.Emoji, height string) string {
+ return `<img class="emoji" src="` + e.URL + `" alt=":` + e.ShortCode + `:" title=":` + e.ShortCode + `:" height="` + height + `"/>`
+}
+
func emojiFilter(content string, emojis []mastodon.Emoji) string {
var replacements []string
- var r string
for _, e := range emojis {
- r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"24\" />",
- e.URL, e.ShortCode, e.ShortCode)
- replacements = append(replacements, ":"+e.ShortCode+":", r)
+ replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "24"))
}
return strings.NewReplacer(replacements...).Replace(content)
}
-func statusContentFilter(spoiler string, content string,
- emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
+var quoteRE = regexp.MustCompile("(?mU)(^|> *|\n)(&gt;.*)(<br|$)")
+func statusContentFilter(content string, emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
+ content = quoteRE.ReplaceAllString(content, `$1<span class="quote">$2</span>$3`)
var replacements []string
- var r string
- if len(spoiler) > 0 {
- content = spoiler + "<br />" + content
- }
for _, e := range emojis {
- r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"32\" />",
- e.URL, e.ShortCode, e.ShortCode)
- replacements = append(replacements, ":"+e.ShortCode+":", r)
+ replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "32"))
}
for _, m := range mentions {
replacements = append(replacements, `"`+m.URL+`"`, `"/user/`+m.ID+`" title="@`+m.Acct+`"`)
@@ -76,45 +74,41 @@ func displayInteractionCount(c int64) string {
return ""
}
-func DurToStr(dur time.Duration) string {
- s := dur.Seconds()
+func durUnit(s int64) (dur int64, unit string) {
if s < 60 {
- return strconv.Itoa(int(s)) + "s"
+ if s < 0 {
+ s = 0
+ }
+ return s, "s"
}
- m := dur.Minutes()
- if m < 60*2 {
- return strconv.Itoa(int(m)) + "m"
- }
- h := dur.Hours()
- if h < 24*2 {
- return strconv.Itoa(int(h)) + "h"
+ m := s / 60
+ h := m / 60
+ if h < 2 {
+ return m, "m"
}
d := h / 24
- if d < 30*2 {
- return strconv.Itoa(int(d)) + "d"
+ if d < 2 {
+ return h, "h"
}
mo := d / 30
- if mo < 12*2 {
- return strconv.Itoa(int(mo)) + "mo"
+ if mo < 2 {
+ return d, "d"
+ }
+ y := d / 365
+ if y < 2 {
+ return mo, "mo"
}
- y := mo / 12
- return strconv.Itoa(int(y)) + "y"
+ return y, "y"
}
func timeSince(t time.Time) string {
- d := time.Since(t)
- if d < 0 {
- d = 0
- }
- return DurToStr(d)
+ d, u := durUnit(time.Now().Unix() - t.Unix())
+ return strconv.FormatInt(d, 10) + u
}
func timeUntil(t time.Time) string {
- d := time.Until(t)
- if d < 0 {
- d = 0
- }
- return DurToStr(d)
+ d, u := durUnit(t.Unix() - time.Now().Unix())
+ return strconv.FormatInt(d, 10) + u
}
func formatTimeRFC3339(t time.Time) string {
@@ -129,6 +123,14 @@ func withContext(data interface{}, ctx *Context) TemplateData {
return TemplateData{data, ctx}
}
+func raw(s string) template.HTML {
+ return template.HTML(s)
+}
+
+func rawCSS(s string) template.CSS {
+ return template.CSS(s)
+}
+
type Renderer interface {
Render(ctx *Context, writer io.Writer, page string, data interface{}) (err error)
}
@@ -148,6 +150,9 @@ func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
"FormatTimeRFC3339": formatTimeRFC3339,
"FormatTimeRFC822": formatTimeRFC822,
"WithContext": withContext,
+ "HTML": template.HTMLEscapeString,
+ "Raw": raw,
+ "RawCSS": rawCSS,
}).ParseGlob(templateGlobPattern)
if err != nil {
return
diff --git a/service/service.go b/service/service.go
index a846322..8bccd45 100644
--- a/service/service.go
+++ b/service/service.go
@@ -114,7 +114,8 @@ func (s *service) ErrorPage(c *client, err error, retry bool) error {
var sessionErr bool
if err != nil {
errStr = err.Error()
- if err == errInvalidSession || err == errInvalidCSRFToken {
+ if me, ok := err.(mastodon.Error); ok && me.IsAuthError() ||
+ err == errInvalidSession || err == errInvalidCSRFToken {
sessionErr = true
}
}
@@ -162,8 +163,8 @@ func (s *service) NavPage(c *client) (err error) {
return s.renderer.Render(c.rctx, c.w, renderer.NavPage, data)
}
-func (s *service) TimelinePage(c *client, tType string, instance string,
- maxID string, minID string) (err error) {
+func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
+ minID string) (err error) {
var nextLink, prevLink, title string
var statuses []*mastodon.Status
@@ -178,24 +179,46 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
return errInvalidArgument
case "home":
statuses, err = c.GetTimelineHome(c.ctx, &pg)
+ if err != nil {
+ return err
+ }
title = "Timeline"
case "direct":
statuses, err = c.GetTimelineDirect(c.ctx, &pg)
+ if err != nil {
+ return err
+ }
title = "Direct Timeline"
case "local":
statuses, err = c.GetTimelinePublic(c.ctx, true, "", &pg)
+ if err != nil {
+ return err
+ }
title = "Local Timeline"
case "remote":
if len(instance) > 0 {
statuses, err = c.GetTimelinePublic(c.ctx, false, instance, &pg)
+ if err != nil {
+ return err
+ }
}
title = "Remote Timeline"
case "twkn":
statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
+ if err != nil {
+ return err
+ }
title = "The Whole Known Network"
- }
- if err != nil {
- return err
+ case "list":
+ statuses, err = c.GetTimelineList(c.ctx, listId, &pg)
+ if err != nil {
+ return err
+ }
+ list, err := c.GetList(c.ctx, listId)
+ if err != nil {
+ return err
+ }
+ title = "List Timeline - " + list.Title
}
for i := range statuses {
@@ -210,6 +233,9 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
if len(instance) > 0 {
v.Set("instance", instance)
}
+ if len(listId) > 0 {
+ v.Set("list", listId)
+ }
prevLink = "/timeline/" + tType + "?" + v.Encode()
}
@@ -219,6 +245,9 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
if len(instance) > 0 {
v.Set("instance", instance)
}
+ if len(listId) > 0 {
+ v.Set("list", listId)
+ }
nextLink = "/timeline/" + tType + "?" + v.Encode()
}
@@ -251,6 +280,70 @@ func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
}
+func (s *service) ListsPage(c *client) (err error) {
+ lists, err := c.GetLists(c.ctx)
+ if err != nil {
+ return
+ }
+
+ cdata := s.cdata(c, "Lists", 0, 0, "")
+ data := renderer.ListsData{
+ Lists: lists,
+ CommonData: cdata,
+ }
+ return s.renderer.Render(c.rctx, c.w, renderer.ListsPage, data)
+}
+
+func (s *service) AddList(c *client, title string) (err error) {
+ _, err = c.CreateList(c.ctx, title)
+ return err
+}
+
+func (s *service) RemoveList(c *client, id string) (err error) {
+ return c.DeleteList(c.ctx, id)
+}
+
+func (s *service) RenameList(c *client, id, title string) (err error) {
+ _, err = c.RenameList(c.ctx, id, title)
+ return err
+}
+
+func (s *service) ListPage(c *client, id string, q string) (err error) {
+ list, err := c.GetList(c.ctx, id)
+ if err != nil {
+ return
+ }
+ accounts, err := c.GetListAccounts(c.ctx, id)
+ if err != nil {
+ return
+ }
+ var searchAccounts []*mastodon.Account
+ if len(q) > 0 {
+ result, err := c.Search(c.ctx, q, "accounts", 20, true, 0, id, true)
+ if err != nil {
+ return err
+ }
+ searchAccounts = result.Accounts
+ }
+ cdata := s.cdata(c, "List "+list.Title, 0, 0, "")
+ data := renderer.ListData{
+ List: list,
+ Accounts: accounts,
+ Q: q,
+ SearchAccounts: searchAccounts,
+ CommonData: cdata,
+ }
+ return s.renderer.Render(c.rctx, c.w, renderer.ListPage, data)
+}
+
+func (s *service) ListAddUser(c *client, id string, uid string) (err error) {
+ return c.AddToList(c.ctx, id, uid)
+}
+
+func (s *service) ListRemoveUser(c *client, id string, uid string) (err error) {
+ return c.RemoveFromList(c.ctx, id, uid)
+}
+
func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
var pctx model.PostContext
@@ -417,18 +510,24 @@ func (s *service) NotificationPage(c *client, maxID string,
var nextLink string
var unreadCount int
var readID string
- var excludes []string
+ var includes, excludes []string
var pg = mastodon.Pagination{
MaxID: maxID,
MinID: minID,
Limit: 20,
}
+ if c.s.Settings.HideUnsupportedNotifs {
+ // Explicitly include the supported types.
+ // For now, only Pleroma supports this option, Mastadon
+ // will simply ignore the unknown params.
+ includes = []string{"follow", "follow_request", "mention", "reblog", "favourite"}
+ }
if c.s.Settings.AntiDopamineMode {
- excludes = []string{"follow", "favourite", "reblog"}
+ excludes = append(excludes, "follow", "favourite", "reblog")
}
- notifications, err := c.GetNotifications(c.ctx, &pg, excludes)
+ notifications, err := c.GetNotifications(c.ctx, &pg, includes, excludes)
if err != nil {
return
}
@@ -609,7 +708,7 @@ func (s *service) UserSearchPage(c *client,
var results *mastodon.Results
if len(q) > 0 {
- results, err = c.Search(c.ctx, q, "statuses", 20, true, offset, id)
+ results, err = c.Search(c.ctx, q, "statuses", 20, true, offset, id, false)
if err != nil {
return err
}
@@ -620,7 +719,7 @@ func (s *service) UserSearchPage(c *client,
if len(results.Statuses) == 20 {
offset += 20
nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id,
- url.QueryEscape(q), offset)
+ q, offset)
}
if len(q) > 0 {
@@ -667,7 +766,7 @@ func (s *service) SearchPage(c *client,
var results *mastodon.Results
if len(q) > 0 {
- results, err = c.Search(c.ctx, q, qType, 20, true, offset, "")
+ results, err = c.Search(c.ctx, q, qType, 20, true, offset, "", false)
if err != nil {
return err
}
@@ -679,7 +778,7 @@ func (s *service) SearchPage(c *client,
(qType == "statuses" && len(results.Statuses) == 20) {
offset += 20
nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d",
- url.QueryEscape(q), qType, offset)
+ q, qType, offset)
}
if len(q) > 0 {
@@ -914,8 +1013,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) (err error) {
- _, err = c.AccountMute(c.ctx, id)
+func (s *service) Mute(c *client, id string, notifications *bool) (err error) {
+ _, err = c.AccountMute(c.ctx, id, notifications)
return
}
diff --git a/service/transport.go b/service/transport.go
index a022b02..1fbce99 100644
--- a/service/transport.go
+++ b/service/transport.go
@@ -169,9 +169,10 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
tType, _ := mux.Vars(c.r)["type"]
q := c.r.URL.Query()
instance := q.Get("instance")
+ list := q.Get("list")
maxID := q.Get("max_id")
minID := q.Get("min_id")
- return s.TimelinePage(c, tType, instance, maxID, minID)
+ return s.TimelinePage(c, tType, instance, list, maxID, minID)
}, SESSION, HTML)
defaultTimelinePage := handle(func(c *client) error {
@@ -415,7 +416,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
mute := handle(func(c *client) error {
id, _ := mux.Vars(c.r)["id"]
- err := s.Mute(c, 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)
if err != nil {
return err
}
@@ -484,20 +491,22 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
fluorideMode := c.r.FormValue("fluoride_mode") == "true"
darkMode := c.r.FormValue("dark_mode") == "true"
antiDopamineMode := c.r.FormValue("anti_dopamine_mode") == "true"
+ hideUnsupportedNotifs := c.r.FormValue("hide_unsupported_notifs") == "true"
css := c.r.FormValue("css")
settings := &model.Settings{
- DefaultVisibility: visibility,
- DefaultFormat: format,
- CopyScope: copyScope,
- ThreadInNewTab: threadInNewTab,
- HideAttachments: hideAttachments,
- MaskNSFW: maskNSFW,
- NotificationInterval: ni,
- FluorideMode: fluorideMode,
- DarkMode: darkMode,
- AntiDopamineMode: antiDopamineMode,
- CSS: css,
+ DefaultVisibility: visibility,
+ DefaultFormat: format,
+ CopyScope: copyScope,
+ ThreadInNewTab: threadInNewTab,
+ HideAttachments: hideAttachments,
+ MaskNSFW: maskNSFW,
+ NotificationInterval: ni,
+ FluorideMode: fluorideMode,
+ DarkMode: darkMode,
+ AntiDopamineMode: antiDopamineMode,
+ HideUnsupportedNotifs: hideUnsupportedNotifs,
+ CSS: css,
}
err := s.SaveSettings(c, settings)
@@ -598,6 +607,72 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
return nil
}, CSRF, HTML)
+ listsPage := handle(func(c *client) error {
+ return s.ListsPage(c)
+ }, SESSION, HTML)
+
+ addList := handle(func(c *client) error {
+ title := c.r.FormValue("title")
+ err := s.AddList(c, title)
+ if err != nil {
+ return err
+ }
+ redirect(c, c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
+ removeList := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ err := s.RemoveList(c, id)
+ if err != nil {
+ return err
+ }
+ redirect(c, c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
+ renameList := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ title := c.r.FormValue("title")
+ err := s.RenameList(c, id, title)
+ if err != nil {
+ return err
+ }
+ redirect(c, c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
+ listPage := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ q := c.r.URL.Query()
+ sq := q.Get("q")
+ return s.ListPage(c, id, sq)
+ }, SESSION, HTML)
+
+ listAddUser := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ q := c.r.URL.Query()
+ uid := q.Get("uid")
+ err := s.ListAddUser(c, id, uid)
+ if err != nil {
+ return err
+ }
+ redirect(c, c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
+ listRemoveUser := handle(func(c *client) error {
+ id, _ := mux.Vars(c.r)["id"]
+ q := c.r.URL.Query()
+ uid := q.Get("uid")
+ err := s.ListRemoveUser(c, id, uid)
+ if err != nil {
+ return err
+ }
+ redirect(c, c.r.FormValue("referrer"))
+ return nil
+ }, CSRF, HTML)
+
signout := handle(func(c *client) error {
s.Signout(c)
setSessionCookie(c.w, "", 0)
@@ -691,6 +766,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
r.HandleFunc("/unbookmark/{id}", unBookmark).Methods(http.MethodPost)
r.HandleFunc("/filter", filter).Methods(http.MethodPost)
r.HandleFunc("/unfilter/{id}", unFilter).Methods(http.MethodPost)
+ r.HandleFunc("/lists", listsPage).Methods(http.MethodGet)
+ r.HandleFunc("/list", addList).Methods(http.MethodPost)
+ r.HandleFunc("/list/{id}", listPage).Methods(http.MethodGet)
+ r.HandleFunc("/list/{id}/remove", removeList).Methods(http.MethodPost)
+ r.HandleFunc("/list/{id}/rename", renameList).Methods(http.MethodPost)
+ r.HandleFunc("/list/{id}/adduser", listAddUser).Methods(http.MethodPost)
+ r.HandleFunc("/list/{id}/removeuser", listRemoveUser).Methods(http.MethodPost)
r.HandleFunc("/signout", signout).Methods(http.MethodPost)
r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)
diff --git a/static/fluoride.js b/static/fluoride.js
index c7f3109..e6624d1 100644
--- a/static/fluoride.js
+++ b/static/fluoride.js
@@ -152,6 +152,7 @@ function replyToLinkLocal(a) {
var copy = status.cloneNode(true);
copy.id = "reply-to-popup";
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";
@@ -245,6 +246,7 @@ function handleReplyLink(a) {
var copy = status.cloneNode(true);
copy.id = "reply-popup";
var ract = event.target.getBoundingClientRect();
+ copy.style["max-width"] = (window.innerWidth - 98) + "px";
if (ract.left > window.innerWidth / 2) {
copy.style.right = (window.innerWidth -
ract.right - 12) + "px";
@@ -298,20 +300,24 @@ function setPos(el, cx, cy, mw, mh) {
}
var imgPrev = null;
+var imgX = 0;
+var imgY = 0;
function handleImgPreview(a) {
a.onmouseenter = function(e) {
var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24;
+ imgX = e.clientX;
+ imgY = e.clientY;
var img = document.createElement("img");
img.id = "img-preview";
img.src = e.target.getAttribute("href");
img.style["max-width"] = mw + "px";
img.style["max-height"] = mh + "px";
+ imgPrev = img;
img.onload = function(e2) {
- setPos(e2.target, e.clientX, e.clientY, mw, mh);
+ setPos(imgPrev, imgX, imgY, mw, mh);
}
document.body.appendChild(img);
- imgPrev = img;
}
a.onmouseleave = function(e) {
var img = document.getElementById("img-preview");
@@ -324,8 +330,24 @@ function handleImgPreview(a) {
return;
var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24;
- setPos(imgPrev, e.clientX, e.clientY, mw, mh);
+ imgX = e.clientX;
+ imgY = e.clientY;
+ setPos(imgPrev, imgX, imgY, mw, mh);
+ }
+}
+
+function onPaste(e) {
+ if (!e.clipboardData.files)
+ return;
+ var fp = document.querySelector("#post-file-picker")
+ var dt = new DataTransfer();
+ for (var i = 0; i < fp.files.length; i++) {
+ dt.items.add(fp.files[i]);
}
+ for (var i = 0; i < e.clipboardData.files.length; i++) {
+ dt.items.add(e.clipboardData.files[i]);
+ }
+ fp.files = dt.files;
}
document.addEventListener("DOMContentLoaded", function() {
@@ -357,7 +379,7 @@ document.addEventListener("DOMContentLoaded", function() {
}
}
- var links = document.querySelectorAll(".user-profile-decription a");
+ var links = document.querySelectorAll(".user-profile-decription a, .user-fields a");
for (var j = 0; j < links.length; j++) {
links[j].target = "_blank";
}
@@ -366,6 +388,10 @@ document.addEventListener("DOMContentLoaded", function() {
for (var j = 0; j < links.length; j++) {
handleImgPreview(links[j]);
}
+
+ var pf = document.querySelector(".post-form")
+ if (pf)
+ pf.addEventListener("paste", onPaste);
});
// @license-end
diff --git a/static/style.css b/static/style.css
index cd7e98c..19cceab 100644
--- a/static/style.css
+++ b/static/style.css
@@ -194,11 +194,14 @@ textarea {
border-color: #777777;
}
-.notification-container.favourite .status-container,
-.notification-container.reblog .status-container {
+.notification-container .status-container {
opacity: 0.6;
}
+.notification-container.mention .status-container {
+ opacity: unset;
+}
+
.notification-info-text span {
vertical-align: middle;
}
@@ -277,7 +280,8 @@ textarea {
margin-top: 2px;
}
-.user-profile-decription {
+.user-profile-decription,
+.user-fields {
overflow-wrap: break-word;
margin: 8px 0;
}
@@ -286,10 +290,22 @@ textarea {
margin: 0;
}
+.user-profile-decription img {
+ height: auto;
+ width: auto;
+ max-height: 240px;
+ max-width: 280px;
+ object-fit: contain;
+}
+
.d-inline {
display: inline;
}
+.p-0 {
+ padding: 0;
+}
+
.btn-link {
border: none;
outline: none;
@@ -354,10 +370,6 @@ a:hover,
display: none;
}
-.post-form-field>* {
- vertical-align: middle;
-}
-
.emoji-item-container {
width: 220px;
display: inline-block;
@@ -422,9 +434,6 @@ img.emoji {
margin-right: 2px;
}
-.user-list-container {
-}
-
.user-list-item {
overflow: auto;
margin: 0 0 12px 0;
@@ -441,6 +450,10 @@ img.emoji {
overflow: auto;
}
+.user-list-action {
+ margin: 0 12px;
+}
+
#settings-form {
margin: 8px 0;
}
@@ -449,10 +462,6 @@ img.emoji {
margin: 4px 0;
}
-.settings-form-field>* {
- vertical-align: middle;
-}
-
#settings-form button[type=submit] {
margin-top: 8px;
}
@@ -517,16 +526,16 @@ img.emoji {
margin-top: 6px;
}
-.notification-title-container {
+.page-title-container {
margin: 8px 0;
}
-.notification-text {
- vertical-align: middle;
+.page-refresh {
+ margin-right: 8px;
}
-.notification-refresh {
- margin-right: 8px;
+.notification-text {
+ vertical-align: middle;
}
.notification-read {
@@ -575,6 +584,10 @@ kbd {
position: fixed;
}
+.quote {
+ color: #789922;
+}
+
.dark {
background-color: #222222;
background-image: none;
diff --git a/templates/about.tmpl b/templates/about.tmpl
index c0b8418..0e4d001 100644
--- a/templates/about.tmpl
+++ b/templates/about.tmpl
@@ -46,11 +46,11 @@
<td> <kbd>6</kbd> </td>
</tr>
<tr>
- <td> Settings </td>
+ <td> Lists </td>
<td> <kbd>7</kbd> </td>
</tr>
<tr>
- <td> Signout </td>
+ <td> Settings </td>
<td> <kbd>8</kbd> </td>
</tr>
<tr>
@@ -94,7 +94,7 @@
<td> <kbd>C</kbd> </td>
</tr>
<tr>
- <td> Refresh thread page </td>
+ <td> Refresh timeline/thread page </td>
<td> <kbd>T</kbd> </td>
</tr>
</table>
diff --git a/templates/emoji.tmpl b/templates/emoji.tmpl
index ee84522..4b07e81 100644
--- a/templates/emoji.tmpl
+++ b/templates/emoji.tmpl
@@ -6,7 +6,7 @@
{{range .Emojis}}
<div class="emoji-item-container">
<div class="emoji-item">
- <img class="emoji" src="{{.URL}}" alt="{{.ShortCode}}" height="32" />
+ <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/header.tmpl b/templates/header.tmpl
index 8eb53f6..8a1b0ca 100644
--- a/templates/header.tmpl
+++ b/templates/header.tmpl
@@ -17,7 +17,7 @@
{{if .RefreshInterval}}
<meta http-equiv="refresh" content="{{.RefreshInterval}}">
{{end}}
- <title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title | html}} </title>
+ <title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>
<link rel="stylesheet" href="/static/style.css">
{{if .CustomCSS}}
<link rel="stylesheet" href="{{.CustomCSS}}">
@@ -26,7 +26,7 @@
<script src="/static/fluoride.js"></script>
{{end}}
{{if $.Ctx.UserCSS}}
- <style>{{$.Ctx.UserCSS}}</style>
+ <style>{{RawCSS $.Ctx.UserCSS}}</style>
{{end}}
</head>
<body {{if $.Ctx.DarkMode}}class="dark"{{end}}>
diff --git a/templates/list.tmpl b/templates/list.tmpl
new file mode 100644
index 0000000..dcc6ee8
--- /dev/null
+++ b/templates/list.tmpl
@@ -0,0 +1,63 @@
+{{with .Data}}
+{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
+<div class="page-title"> List {{.List.Title}} </div>
+
+<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>
+</form>
+
+<div class="page-title"> Users </div>
+{{if .Accounts}}
+<table>
+{{range .Accounts}}
+ <tr>
+ <td class="p-0"> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
+ <td class="p-0">
+ <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}}">
+ <button type="submit"> Remove </button>
+ </form>
+ </td>
+ </tr>
+{{end}}
+</table>
+{{else}}
+<div class="no-data-found">No data found</div>
+{{end}}
+
+<div class="page-title"> Add user </div>
+<form class="search-form" action="/list/{{.List.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>
+
+{{if .Q}}
+{{if .SearchAccounts}}
+<table>
+{{range .SearchAccounts}}
+ <tr>
+ <td> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}} </td>
+ <td>
+ <form class="user-list-action" action="/list/{{$.Data.List.ID}}/adduser?uid={{.ID}}" method="POST">
+ <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+ <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
+ <button type="submit"> Add </button>
+ </form>
+ </td>
+ </tr>
+{{end}}
+</table>
+{{else}}
+<div class="no-data-found">No data found</div>
+{{end}}
+{{end}}
+
+{{template "footer.tmpl"}}
+{{end}}
diff --git a/templates/lists.tmpl b/templates/lists.tmpl
new file mode 100644
index 0000000..27979cb
--- /dev/null
+++ b/templates/lists.tmpl
@@ -0,0 +1,35 @@
+{{with .Data}}
+{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
+<div class="page-title"> Lists </div>
+
+{{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>
+{{else}}
+<div class="no-data-found">No data found</div>
+{{end}}
+
+<div class="page-title"> Add list </div>
+<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>
+ <button type="submit"> Add </button>
+</form>
+
+{{template "footer.tmpl"}}
+{{end}}
diff --git a/templates/nav.tmpl b/templates/nav.tmpl
index cbf65c9..db88aa0 100644
--- a/templates/nav.tmpl
+++ b/templates/nav.tmpl
@@ -8,7 +8,7 @@
</div>
<div class="user-info-details-container">
<div class="user-info-details-name">
- <bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>
+ <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>
@@ -17,16 +17,17 @@
<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="5" title="The Whole Known Netwwork (4)">twkn</a>
- <a class="nav-link" href="/timeline/remote" accesskey="4" title="Remote timeline (5)">remote</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="/settings" target="_top" accesskey="7" title="Settings (7)">settings</a>
+ <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" accesskey="8" title="Signout (8)">
+ <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>
diff --git a/templates/notification.tmpl b/templates/notification.tmpl
index 2778ef0..f62726b 100644
--- a/templates/notification.tmpl
+++ b/templates/notification.tmpl
@@ -1,13 +1,13 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="notification-title-container">
+<div class="page-title-container">
<span class="page-title">
Notifications
{{if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}}
({{.UnreadCount }})
{{end}}
</span>
- <a class="notification-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
+ <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}}">
@@ -28,7 +28,7 @@
</div>
<div class="notification-follow">
<div class="notification-info-text">
- <bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
+ <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>
@@ -48,7 +48,7 @@
</div>
<div class="notification-follow">
<div class="notification-info-text">
- <bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
+ <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>
diff --git a/templates/postform.tmpl b/templates/postform.tmpl
index 35171a4..0af50fb 100644
--- a/templates/postform.tmpl
+++ b/templates/postform.tmpl
@@ -5,7 +5,7 @@
{{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>
+ <label for="post-content" class="post-form-title"> Reply to @{{.ReplyContext.InReplyToName}} </label>
{{else}}
<label for="post-content" class="post-form-title"> New post </label>
{{end}}
diff --git a/templates/requestlist.tmpl b/templates/requestlist.tmpl
index 8142620..1a51e31 100644
--- a/templates/requestlist.tmpl
+++ b/templates/requestlist.tmpl
@@ -9,7 +9,7 @@
</div>
<div class="user-list-name">
<div>
- <div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </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>
diff --git a/templates/search.tmpl b/templates/search.tmpl
index 7338cad..0473d4a 100644
--- a/templates/search.tmpl
+++ b/templates/search.tmpl
@@ -5,7 +5,7 @@
<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 | html}}">
+ <input id="query" name="q" value="{{.Q}}">
</span>
<span class="post-form-field">
<label for="type"> Type </label>
diff --git a/templates/settings.tmpl b/templates/settings.tmpl
index 6bae9c7..ebb0458 100644
--- a/templates/settings.tmpl
+++ b/templates/settings.tmpl
@@ -62,6 +62,11 @@
<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">
+ <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">
<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>
diff --git a/templates/status.tmpl b/templates/status.tmpl
index a7cc10d..5ada84e 100644
--- a/templates/status.tmpl
+++ b/templates/status.tmpl
@@ -5,7 +5,7 @@
<a class="img-link" href="/user/{{.Account.ID}}">
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />
</a>
- <bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </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>
</a>
@@ -23,7 +23,7 @@
</div>
<div class="status">
<div class="status-name">
- <bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </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>
</a>
@@ -91,7 +91,10 @@
{{end}}
</div>
{{if (or .Content .SpoilerText)}}
- <div class="status-content"> {{StatusContentFilter (html .SpoilerText) .Content .Emojis .Mentions}} </div>
+ <div class="status-content">
+ {{if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br/>{{end}}
+ {{StatusContentFilter .Content .Emojis .Mentions | Raw}}
+ </div>
{{end}}
{{if .MediaAttachments}}
<div class="status-media-container">
@@ -100,7 +103,7 @@
{{if eq .Type "image"}}
{{if $.Ctx.HideAttachments}}
<a href="{{.URL}}" target="_blank">
- {{if .Description}}[{{.Description}}]{{else}}[image]{{end}}
+ [image{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
</a>
{{else}}
<a class="img-link" href="{{.URL}}" target="_blank" title="{{.Description}}">
@@ -114,7 +117,7 @@
{{else if eq .Type "audio"}}
{{if $.Ctx.HideAttachments}}
<a href="{{.URL}}" target="_blank">
- {{if .Description}}[{{.Description}}]{{else}}[audio]{{end}}
+ [audio{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
</a>
{{else}}
<audio class="status-audio" controls title="{{.Description}}">
@@ -126,7 +129,7 @@
{{else if eq .Type "video"}}
{{if $.Ctx.HideAttachments}}
<a href="{{.URL}}" target="_blank">
- {{if .Description}}[{{.Description}}]{{else}}[video]{{end}}
+ [video{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
</a>
{{else}}
<div class="status-video-container" title="{{.Description}}">
@@ -142,7 +145,7 @@
{{else}}
<a href="{{.URL}}" target="_blank">
- {{if .Description}}[{{.Description}}]{{else}}[attachment]{{end}}
+ [attachment{{if $s.Sensitive}}/nsfw{{end}}{{if .Description}}: {{.Description}}{{end}}]
</a>
{{end}}
{{end}}
@@ -156,12 +159,12 @@
{{range $i, $o := .Poll.Options}}
<div class="poll-option">
{{if (or $s.Poll.Expired $s.Poll.Voted)}}
- <div> {{EmojiFilter (html $o.Title) $s.Emojis}} - {{$o.VotesCount}} votes </div>
+ <div> {{EmojiFilter (HTML $o.Title) $s.Emojis | Raw}} - {{$o.VotesCount}} votes </div>
{{else}}
<input type="{{if $s.Poll.Multiple}}checkbox{{else}}radio{{end}}" name="choices"
id="poll-{{$s.ID}}-{{$i}}" value="{{$i}}">
<label for="poll-{{$s.ID}}-{{$i}}">
- {{EmojiFilter (html $o.Title) $s.Emojis}}
+ {{EmojiFilter (HTML $o.Title) $s.Emojis | Raw}}
</label>
{{end}}
</div>
@@ -227,8 +230,8 @@
<div class="status-action status-action-last">
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}"
{{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
- <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">
- {{TimeSince .CreatedAt}}
+ <time datetime="{{FormatTimeRFC3339 .CreatedAt.Time}}" title="{{FormatTimeRFC822 .CreatedAt.Time}}">
+ {{TimeSince .CreatedAt.Time}}
</time>
</a>
</div>
diff --git a/templates/thread.tmpl b/templates/thread.tmpl
index bb9f14f..d6a1c7d 100644
--- a/templates/thread.tmpl
+++ b/templates/thread.tmpl
@@ -1,8 +1,8 @@
{{with $s := .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="notification-title-container">
+<div class="page-title-container">
<span class="page-title"> Thread </span>
- <a class="notification-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
+ <a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
</div>
{{range .Statuses}}
diff --git a/templates/timeline.tmpl b/templates/timeline.tmpl
index bde050a..38659dc 100644
--- a/templates/timeline.tmpl
+++ b/templates/timeline.tmpl
@@ -1,6 +1,9 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> {{.Title}} </div>
+<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>
{{if eq .Type "remote"}}
<form class="search-form" action="/timeline/remote" method="GET">
diff --git a/templates/user.tmpl b/templates/user.tmpl
index c7b3164..5342efe 100644
--- a/templates/user.tmpl
+++ b/templates/user.tmpl
@@ -11,7 +11,7 @@
</div>
<div class="user-profile-details-container">
<div>
- <bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>
+ <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
@@ -20,6 +20,7 @@
{{if not .IsCurrent}}
<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}}">
@@ -83,6 +84,12 @@
<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>
{{end}}
{{if .User.Pleroma.Relationship.Following}}
-
@@ -123,11 +130,15 @@
</div>
</div>
<div class="user-profile-decription">
- {{EmojiFilter .User.Note .User.Emojis}}
+ {{EmojiFilter .User.Note .User.Emojis | Raw}}
</div>
- {{if .User.Fields}}{{range .User.Fields}}
- <div>{{.Name}} - {{.Value}}</div>
- {{end}}{{end}}
+ {{if .User.Fields}}
+ <div class="user-fields">
+ {{range .User.Fields}}
+ <div>{{.Name}} - {{.Value | Raw}}</div>
+ {{end}}
+ </div>
+ {{end}}
</div>
</div>
diff --git a/templates/userlist.tmpl b/templates/userlist.tmpl
index 3f75085..f206397 100644
--- a/templates/userlist.tmpl
+++ b/templates/userlist.tmpl
@@ -1,19 +1,7 @@
{{with .Data}}
<div>
{{range .}}
- <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" />
- </a>
- </div>
- <div class="user-list-name">
- <div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div>
- <a class="img-link" href="/user/{{.ID}}">
- <div class="status-uname"> @{{.Acct}} </div>
- </a>
- </div>
- </div>
+ {{template "userlistitem.tmpl" (WithContext . $.Ctx)}}
{{else}}
<div class="no-data-found">No data found</div>
{{end}}
diff --git a/templates/userlistitem.tmpl b/templates/userlistitem.tmpl
new file mode 100644
index 0000000..50b9d0c
--- /dev/null
+++ b/templates/userlistitem.tmpl
@@ -0,0 +1,15 @@
+{{with .Data}}
+<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" />
+ </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>
+ </div>
+</div>
+{{end}}
diff --git a/templates/usersearch.tmpl b/templates/usersearch.tmpl
index ee84143..78fa7b8 100644
--- a/templates/usersearch.tmpl
+++ b/templates/usersearch.tmpl
@@ -1,11 +1,11 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
-<div class="page-title"> Search {{EmojiFilter .User.DisplayName .User.Emojis}}'s statuses </div>
+<div class="page-title"> Search {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}}'s statuses </div>
<form class="search-form" action="/usersearch/{{.User.ID}}" method="GET">
- <span class="post-form-field>
+ <span class="post-form-field">
<label for="query"> Query </label>
- <input id="query" name="q" value="{{.Q | html}}">
+ <input id="query" name="q" value="{{.Q}}">
</span>
<button type="submit"> Search </button>
</form>
diff --git a/util/getopt.go b/util/getopt.go
deleted file mode 100644
index 10926a8..0000000
--- a/util/getopt.go
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
-Copyright 2019 Drew DeVault <sir@cmpwn.com>
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice, this
-list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright notice,
-this list of conditions and the following disclaimer in the documentation
-and/or other materials provided with the distribution.
-
-3. Neither the name of the copyright holder nor the names of its contributors
-may be used to endorse or promote products derived from this software without
-specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
-
-package util
-
-import (
- "fmt"
- "os"
-)
-
-// In the case of "-o example", Option is 'o' and "example" is Value. For
-// options which do not take an argument, Value is "".
-type Option struct {
- Option rune
- Value string
-}
-
-// This is returned when an unknown option is found in argv, but not in the
-// option spec.
-type UnknownOptionError rune
-
-func (e UnknownOptionError) Error() string {
- return fmt.Sprintf("%s: unknown option -%c", os.Args[0], rune(e))
-}
-
-// This is returned when an option with a mandatory argument is missing that
-// argument.
-type MissingOptionError rune
-
-func (e MissingOptionError) Error() string {
- return fmt.Sprintf("%s: expected argument for -%c", os.Args[0], rune(e))
-}
-
-// Getopts implements a POSIX-compatible options interface.
-//
-// Returns a slice of options and the index of the first non-option argument.
-//
-// If an error is returned, you must print it to stderr to be POSIX complaint.
-func Getopts(argv []string, spec string) ([]Option, int, error) {
- optmap := make(map[rune]bool)
- runes := []rune(spec)
- for i, rn := range spec {
- if rn == ':' {
- if i == 0 {
- continue
- }
- optmap[runes[i-1]] = true
- } else {
- optmap[rn] = false
- }
- }
-
- var (
- i int
- opts []Option
- )
- for i = 1; i < len(argv); i++ {
- arg := argv[i]
- runes = []rune(arg)
- if len(arg) == 0 || arg == "-" {
- break
- }
- if arg[0] != '-' {
- break
- }
- if arg == "--" {
- i++
- break
- }
- for j, opt := range runes[1:] {
- if optopt, ok := optmap[opt]; !ok {
- opts = append(opts, Option{'?', ""})
- return opts, i, UnknownOptionError(opt)
- } else if optopt {
- if j+1 < len(runes)-1 {
- opts = append(opts, Option{opt, string(runes[j+2:])})
- break
- } else {
- if i+1 >= len(argv) {
- if len(spec) >= 1 && spec[0] == ':' {
- opts = append(opts, Option{':', string(opt)})
- } else {
- return opts, i, MissingOptionError(opt)
- }
- } else {
- opts = append(opts, Option{opt, argv[i+1]})
- i++
- }
- }
- } else {
- opts = append(opts, Option{opt, ""})
- }
- }
- }
- return opts, i, nil
-}
diff --git a/util/rand.go b/util/rand.go
index 1e4ec95..90e66a5 100644
--- a/util/rand.go
+++ b/util/rand.go
@@ -2,24 +2,18 @@ package util
import (
"crypto/rand"
- "math/big"
+ "encoding/base64"
)
-var (
- runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
- runes_length = len(runes)
-)
+var enc = base64.URLEncoding
func NewRandID(n int) (string, error) {
- data := make([]rune, n)
- for i := range data {
- num, err := rand.Int(rand.Reader, big.NewInt(int64(runes_length)))
- if err != nil {
- return "", err
- }
- data[i] = runes[num.Int64()]
+ data := make([]byte, enc.DecodedLen(n))
+ _, err := rand.Read(data)
+ if err != nil {
+ return "", err
}
- return string(data), nil
+ return enc.EncodeToString(data), nil
}
func NewSessionID() (string, error) {