mirror of
https://git.freesoftwareextremist.com/bloat
synced 2024-12-22 17:10:42 +00:00
Merge branch 'master' into absolute_fluoride
This commit is contained in:
commit
b8c0133bcd
29 changed files with 214 additions and 263 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
|||
bloat
|
||||
database
|
||||
bloat.def.conf
|
||||
|
|
12
INSTALL
12
INSTALL
|
@ -15,12 +15,12 @@ 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".
|
||||
# $EDITOR /etc/bloat.conf
|
||||
|
||||
4. Create database directory
|
||||
Create a directory to store session information. Optionally, create a user
|
||||
|
|
13
Makefile
13
Makefile
|
@ -14,17 +14,11 @@ 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
|
||||
|
||||
install: bloat
|
||||
mkdir -p $(DESTDIR)$(BINPATH) \
|
||||
$(DESTDIR)$(SHAREPATH)/templates \
|
||||
|
@ -35,6 +29,10 @@ install: bloat
|
|||
chmod 0644 $(DESTDIR)$(SHAREPATH)/templates/*
|
||||
cp -r static/* $(DESTDIR)$(SHAREPATH)/static
|
||||
chmod 0644 $(DESTDIR)$(SHAREPATH)/static/*
|
||||
sed -e "s%=database%=/var/bloat%g" \
|
||||
-e "s%=templates%=$(SHAREPATH)/templates%g" \
|
||||
-e "s%=static%=$(SHAREPATH)/static%g" \
|
||||
< bloat.conf > /etc/bloat.conf
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(BINPATH)/bloat
|
||||
|
@ -42,4 +40,3 @@ uninstall:
|
|||
|
||||
clean:
|
||||
rm -f bloat
|
||||
rm -f bloat.def.conf
|
||||
|
|
4
README
4
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.
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("invalid config file")
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil, errors.New("invalid config file")
|
||||
}
|
||||
|
||||
return Parse(f)
|
||||
return nil, lastErr
|
||||
}
|
||||
|
|
19
main.go
19
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)
|
||||
}
|
||||
|
|
|
@ -243,9 +243,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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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: "",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package renderer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
@ -39,29 +39,28 @@ 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)(>.*)(<br|$)")
|
||||
|
||||
var replacements []string
|
||||
var r string
|
||||
func statusContentFilter(spoiler, content string, emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
|
||||
if len(spoiler) > 0 {
|
||||
content = spoiler + "<br />" + content
|
||||
content = spoiler + "<br/>" + content
|
||||
}
|
||||
content = quoteRE.ReplaceAllString(content, `$1<span class="quote">$2</span>$3`)
|
||||
var replacements []string
|
||||
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+`"`)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -417,18 +418,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
|
||||
}
|
||||
|
@ -914,8 +921,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
|
||||
}
|
||||
|
||||
|
|
|
@ -415,7 +415,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 +490,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)
|
||||
|
|
|
@ -298,20 +298,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,7 +328,9 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -517,18 +517,18 @@ img.emoji {
|
|||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.notification-title-container {
|
||||
.page-title-container {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.page-refresh {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.notification-refresh {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.notification-read {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -575,6 +575,10 @@ kbd {
|
|||
position: fixed;
|
||||
}
|
||||
|
||||
.quote {
|
||||
color: #789922;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: #222222;
|
||||
background-image: none;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}} </bdi>
|
||||
<a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">
|
||||
<span class="status-uname"> @{{.User.Acct}} </span>
|
||||
</a>
|
||||
|
|
|
@ -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}} </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}} </bdi>
|
||||
<span class="notification-text"> wants to follow you -
|
||||
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
|
||||
</span>
|
||||
|
|
|
@ -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}} </div>
|
||||
<a class="img-link" href="/user/{{.ID}}">
|
||||
<div class="status-uname"> @{{.Acct}} </div>
|
||||
</a>
|
||||
|
|
|
@ -61,6 +61,11 @@
|
|||
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">
|
||||
<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>
|
||||
|
|
|
@ -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}} </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}} </bdi>
|
||||
<a href="/user/{{.Account.ID}}">
|
||||
<span class="status-uname"> @{{.Account.Acct}} </span>
|
||||
</a>
|
||||
|
@ -227,8 +227,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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}} </bdi>
|
||||
<span class="status-uname"> @{{.User.Acct}} </span>
|
||||
<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
|
||||
source
|
||||
|
@ -83,6 +83,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}}
|
||||
-
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="user-list-name">
|
||||
<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div>
|
||||
<div class="status-dname"> {{EmojiFilter (html .DisplayName) .Emojis}} </div>
|
||||
<a class="img-link" href="/user/{{.ID}}">
|
||||
<div class="status-uname"> @{{.Acct}} </div>
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{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}}'s statuses </div>
|
||||
|
||||
<form class="search-form" action="/usersearch/{{.User.ID}}" method="GET">
|
||||
<span class="post-form-field>
|
||||
|
|
122
util/getopt.go
122
util/getopt.go
|
@ -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
|
||||
}
|
20
util/rand.go
20
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) {
|
||||
|
|
Loading…
Reference in a new issue