Merge branch 'master' into absolute_fluoride

This commit is contained in:
r 2022-09-23 17:22:54 +00:00
commit f55595ddb6
27 changed files with 437 additions and 135 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
bloat
database
bloat.gen.conf

View file

@ -20,6 +20,7 @@ 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

View file

@ -18,6 +18,10 @@ all: bloat
bloat: $(SRC) $(TMPL)
$(GO) build $(GOFLAGS) -o bloat main.go
sed -e "s%=database%=/var/bloat%g" \
-e "s%=templates%=$(SHAREPATH)/templates%g" \
-e "s%=static%=$(SHAREPATH)/static%g" \
< bloat.conf > bloat.gen.conf
install: bloat
mkdir -p $(DESTDIR)$(BINPATH) \
@ -29,10 +33,6 @@ 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
@ -40,3 +40,4 @@ uninstall:
clean:
rm -f bloat
rm -f bloat.gen.conf

View file

@ -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"`

View file

@ -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)

View file

@ -56,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"`
@ -77,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
@ -123,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
@ -301,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)
@ -309,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)
}

View file

@ -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

View file

@ -1,11 +1,11 @@
package renderer
import (
"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"
@ -53,10 +55,7 @@ func emojiFilter(content string, emojis []mastodon.Emoji) string {
var quoteRE = regexp.MustCompile("(?mU)(^|> *|\n)(&gt;.*)(<br|$)")
func statusContentFilter(spoiler, content string, emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
if len(spoiler) > 0 {
content = spoiler + "<br/>" + content
}
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
for _, e := range emojis {
@ -75,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 := mo / 12
return strconv.Itoa(int(y)) + "y"
y := d / 365
if y < 2 {
return mo, "mo"
}
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 {
@ -128,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)
}
@ -147,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

View file

@ -163,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
@ -179,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 {
@ -211,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()
}
@ -220,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()
}
@ -252,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
@ -616,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
}
@ -627,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 {
@ -674,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
}
@ -686,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 {

View file

@ -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 {
@ -606,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)
@ -699,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)

View file

@ -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";
@ -334,6 +336,20 @@ function handleImgPreview(a) {
}
}
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() {
checkCSRFToken();
checkAntiDopamineMode();
@ -363,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";
}
@ -372,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

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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}}>

63
templates/list.tmpl Normal file
View file

@ -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}}

35
templates/lists.tmpl Normal file
View file

@ -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}}

View file

@ -8,7 +8,7 @@
</div>
<div class="user-info-details-container">
<div class="user-info-details-name">
<bdi class="status-dname"> {{EmojiFilter (html .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>

View file

@ -28,7 +28,7 @@
</div>
<div class="notification-follow">
<div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter (html .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 (html .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>

View file

@ -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}}

View file

@ -9,7 +9,7 @@
</div>
<div class="user-list-name">
<div>
<div class="status-dname"> {{EmojiFilter (html .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>

View file

@ -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>

View file

@ -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 (html .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 (html .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>

View file

@ -11,7 +11,7 @@
</div>
<div class="user-profile-details-container">
<div>
<bdi class="status-dname"> {{EmojiFilter (html .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}}">
@ -129,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>

View file

@ -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 (html .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}}

View file

@ -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}}

View file

@ -1,11 +1,11 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> Search {{EmojiFilter (html .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>