Add lists

This commit is contained in:
r 2022-02-11 11:18:02 +00:00
parent c2f237e901
commit c390a0c327
13 changed files with 322 additions and 33 deletions

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 { func (c *Client) AddToList(ctx context.Context, list string, accounts ...string) error {
params := url.Values{} params := url.Values{}
for _, acct := range accounts { 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) 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 { func (c *Client) RemoveFromList(ctx context.Context, list string, accounts ...string) error {
params := url.Values{} params := url.Values{}
for _, acct := range accounts { 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) return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)

View File

@ -301,7 +301,7 @@ func (c *Client) DeleteStatus(ctx context.Context, id string) error {
} }
// Search search content with query. // 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 var results Results
params := url.Values{} params := url.Values{}
params.Set("q", q) params.Set("q", q)
@ -309,6 +309,7 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
params.Set("limit", fmt.Sprint(limit)) params.Set("limit", fmt.Sprint(limit))
params.Set("resolve", fmt.Sprint(resolve)) params.Set("resolve", fmt.Sprint(resolve))
params.Set("offset", fmt.Sprint(offset)) params.Set("offset", fmt.Sprint(offset))
params.Set("following", fmt.Sprint(following))
if len(accountID) > 0 { if len(accountID) > 0 {
params.Set("account_id", accountID) params.Set("account_id", accountID)
} }

View File

@ -62,6 +62,19 @@ type TimelineData struct {
PrevLink string 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 { type ThreadData struct {
*CommonData *CommonData
Statuses []*mastodon.Status Statuses []*mastodon.Status

View File

@ -19,6 +19,8 @@ const (
NavPage = "nav.tmpl" NavPage = "nav.tmpl"
RootPage = "root.tmpl" RootPage = "root.tmpl"
TimelinePage = "timeline.tmpl" TimelinePage = "timeline.tmpl"
ListsPage = "lists.tmpl"
ListPage = "list.tmpl"
ThreadPage = "thread.tmpl" ThreadPage = "thread.tmpl"
QuickReplyPage = "quickreply.tmpl" QuickReplyPage = "quickreply.tmpl"
NotificationPage = "notification.tmpl" NotificationPage = "notification.tmpl"

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) return s.renderer.Render(c.rctx, c.w, renderer.NavPage, data)
} }
func (s *service) TimelinePage(c *client, tType string, instance string, func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
maxID string, minID string) (err error) { minID string) (err error) {
var nextLink, prevLink, title string var nextLink, prevLink, title string
var statuses []*mastodon.Status var statuses []*mastodon.Status
@ -179,25 +179,47 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
return errInvalidArgument return errInvalidArgument
case "home": case "home":
statuses, err = c.GetTimelineHome(c.ctx, &pg) statuses, err = c.GetTimelineHome(c.ctx, &pg)
if err != nil {
return err
}
title = "Timeline" title = "Timeline"
case "direct": case "direct":
statuses, err = c.GetTimelineDirect(c.ctx, &pg) statuses, err = c.GetTimelineDirect(c.ctx, &pg)
if err != nil {
return err
}
title = "Direct Timeline" title = "Direct Timeline"
case "local": case "local":
statuses, err = c.GetTimelinePublic(c.ctx, true, "", &pg) statuses, err = c.GetTimelinePublic(c.ctx, true, "", &pg)
if err != nil {
return err
}
title = "Local Timeline" title = "Local Timeline"
case "remote": case "remote":
if len(instance) > 0 { if len(instance) > 0 {
statuses, err = c.GetTimelinePublic(c.ctx, false, instance, &pg) statuses, err = c.GetTimelinePublic(c.ctx, false, instance, &pg)
if err != nil {
return err
}
} }
title = "Remote Timeline" title = "Remote Timeline"
case "twkn": case "twkn":
statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg) statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
title = "The Whole Known Network"
}
if err != nil { if err != nil {
return err return err
} }
title = "The Whole Known Network"
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 { for i := range statuses {
if statuses[i].Reblog != nil { if statuses[i].Reblog != nil {
@ -211,6 +233,9 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
if len(instance) > 0 { if len(instance) > 0 {
v.Set("instance", instance) v.Set("instance", instance)
} }
if len(listId) > 0 {
v.Set("list", listId)
}
prevLink = "/timeline/" + tType + "?" + v.Encode() prevLink = "/timeline/" + tType + "?" + v.Encode()
} }
@ -220,6 +245,9 @@ func (s *service) TimelinePage(c *client, tType string, instance string,
if len(instance) > 0 { if len(instance) > 0 {
v.Set("instance", instance) v.Set("instance", instance)
} }
if len(listId) > 0 {
v.Set("list", listId)
}
nextLink = "/timeline/" + tType + "?" + v.Encode() 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}) 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) { func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
var pctx model.PostContext var pctx model.PostContext
@ -608,7 +700,7 @@ func (s *service) UserSearchPage(c *client,
var results *mastodon.Results var results *mastodon.Results
if len(q) > 0 { 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 { if err != nil {
return err return err
} }
@ -666,7 +758,7 @@ func (s *service) SearchPage(c *client,
var results *mastodon.Results var results *mastodon.Results
if len(q) > 0 { 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 { if err != nil {
return err return err
} }

View File

@ -160,9 +160,10 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
tType, _ := mux.Vars(c.r)["type"] tType, _ := mux.Vars(c.r)["type"]
q := c.r.URL.Query() q := c.r.URL.Query()
instance := q.Get("instance") instance := q.Get("instance")
list := q.Get("list")
maxID := q.Get("max_id") maxID := q.Get("max_id")
minID := q.Get("min_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) }, SESSION, HTML)
defaultTimelinePage := handle(func(c *client) error { defaultTimelinePage := handle(func(c *client) error {
@ -597,6 +598,72 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
return nil return nil
}, CSRF, HTML) }, 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 { signout := handle(func(c *client) error {
s.Signout(c) s.Signout(c)
setSessionCookie(c.w, "", 0) setSessionCookie(c.w, "", 0)
@ -685,6 +752,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
r.HandleFunc("/unbookmark/{id}", unBookmark).Methods(http.MethodPost) r.HandleFunc("/unbookmark/{id}", unBookmark).Methods(http.MethodPost)
r.HandleFunc("/filter", filter).Methods(http.MethodPost) r.HandleFunc("/filter", filter).Methods(http.MethodPost)
r.HandleFunc("/unfilter/{id}", unFilter).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("/signout", signout).Methods(http.MethodPost)
r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost) r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost) r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)

View File

@ -290,6 +290,10 @@ textarea {
display: inline; display: inline;
} }
.p-0 {
padding: 0;
}
.btn-link { .btn-link {
border: none; border: none;
outline: none; outline: none;
@ -422,9 +426,6 @@ img.emoji {
margin-right: 2px; margin-right: 2px;
} }
.user-list-container {
}
.user-list-item { .user-list-item {
overflow: auto; overflow: auto;
margin: 0 0 12px 0; margin: 0 0 12px 0;
@ -441,6 +442,10 @@ img.emoji {
overflow: auto; overflow: auto;
} }
.user-list-action {
margin: 0 12px;
}
#settings-form { #settings-form {
margin: 8px 0; margin: 8px 0;
} }

View File

@ -46,11 +46,11 @@
<td> <kbd>6</kbd> </td> <td> <kbd>6</kbd> </td>
</tr> </tr>
<tr> <tr>
<td> Settings </td> <td> Lists </td>
<td> <kbd>7</kbd> </td> <td> <kbd>7</kbd> </td>
</tr> </tr>
<tr> <tr>
<td> Signout </td> <td> Settings </td>
<td> <kbd>8</kbd> </td> <td> <kbd>8</kbd> </td>
</tr> </tr>
<tr> <tr>

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 | html}}">
</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

@ -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/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/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/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/twkn" accesskey="4" 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/remote" accesskey="5" title="Remote timeline (5)">remote</a>
<a class="nav-link" href="/search" accesskey="6" title="Search (6)">search</a> <a class="nav-link" href="/search" accesskey="6" title="Search (6)">search</a>
</div> </div>
<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"> <form class="signout" action="/signout" method="post" target="_top">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> <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> </form>
<a class="nav-link" href="/about" accesskey="9" title="About (9)">about</a> <a class="nav-link" href="/about" accesskey="9" title="About (9)">about</a>
</div> </div>

View File

@ -1,19 +1,7 @@
{{with .Data}} {{with .Data}}
<div> <div>
{{range .}} {{range .}}
<div class="user-list-item"> {{template "userlistitem.tmpl" (WithContext . $.Ctx)}}
<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>
{{else}} {{else}}
<div class="no-data-found">No data found</div> <div class="no-data-found">No data found</div>
{{end}} {{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}} </div>
<a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div>
</a>
</div>
</div>
{{end}}