Browse Source

Support user search and display more info

pull/8/head
Rasmus Lindroth 6 years ago
parent
commit
bcddfa234f
  1. 2
      README.md
  2. 30
      api.go
  3. 4
      config.go
  4. 248
      feed.go
  5. 16
      main.go
  6. 23
      statusview.go
  7. 10
      ui.go

2
README.md

@ -13,6 +13,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:timeline` home, local, federated, direct, notifications
* `:tl` h, l, f, d, n (a shorter form of the former)
* `:tag` followed by the hashtag e.g. `:tag linux`
* `:user` followed by a username e.g. `:user rasmus` to narrow a search include
the instance like this `:user rasmus@mastodon.acc.sunet.se`.
Explanation of the non obvious keys when viewing a toot
* `V` = view. In this mode you can scroll throught the text of the toot if it doesn't fit the screen

30
api.go

@ -38,7 +38,13 @@ func (api *API) getStatuses(tl TimelineType, pg *mastodon.Pagination) ([]*mastod
case TimelineHome:
statuses, err = api.Client.GetTimelineHome(context.Background(), pg)
case TimelineDirect:
statuses, err = api.Client.GetTimelineDirect(context.Background(), pg)
var conv []*mastodon.Conversation
conv, err = api.Client.GetConversations(context.Background(), pg)
var cStatuses []*mastodon.Status
for _, c := range conv {
cStatuses = append(cStatuses, c.LastStatus)
}
statuses = cStatuses
case TimelineLocal:
statuses, err = api.Client.GetTimelinePublic(context.Background(), true, pg)
case TimelineFederated:
@ -141,6 +147,28 @@ func (api *API) GetNotificationsNewer(n *mastodon.Notification) ([]*mastodon.Not
return api.Client.GetNotifications(context.Background(), pg)
}
type UserSearchData struct {
User *mastodon.Account
Relationship *mastodon.Relationship
}
func (api *API) GetUsers(s string) ([]*UserSearchData, error) {
var ud []*UserSearchData
users, err := api.Client.AccountsSearch(context.Background(), s, 10)
if err != nil {
return nil, err
}
for _, u := range users {
r, err := api.UserRelation(*u)
if err != nil {
return ud, err
}
ud = append(ud, &UserSearchData{User: u, Relationship: r})
}
return ud, nil
}
func (api *API) GetUserByID(id mastodon.ID) (*mastodon.Account, error) {
a, err := api.Client.GetAccount(context.Background(), id)
return a, err

4
config.go

@ -164,7 +164,9 @@ func parseMedia(cfg *ini.File) MediaConfig {
}
func ParseConfig(filepath string) (Config, error) {
cfg, err := ini.Load(filepath)
cfg, err := ini.LoadSources(ini.LoadOptions{
SpaceBeforeInlineComment: true,
}, filepath)
conf := Config{}
if err != nil {
return conf, err

248
feed.go

@ -16,6 +16,7 @@ const (
TimelineFeedType FeedType = iota
ThreadFeedType
UserFeedType
UserSearchFeedType
NotificationFeedType
TagFeedType
)
@ -28,6 +29,7 @@ type Feed interface {
DrawToot()
FeedType() FeedType
GetSavedIndex() int
GetDesc() string
Input(event *tcell.EventKey)
}
@ -49,27 +51,29 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str
strippedContent, urls = cleanTootHTML(status.Content)
subtleColor := fmt.Sprintf("[#%x]", app.Config.Style.Subtle.Hex())
special1 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial1.Hex())
special2 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial2.Hex())
if status.Sensitive {
strippedSpoiler, u = cleanTootHTML(status.SpoilerText)
strippedSpoiler = tview.Escape(strippedSpoiler)
urls = append(urls, u...)
}
if status.Sensitive && !showSensitive {
strippedSpoiler += "\n" + line
strippedSpoiler += "Press [s] to show hidden text"
strippedSpoiler += "\n" + subtleColor + line
strippedSpoiler += subtleColor + tview.Escape("Press [s] to show hidden text")
stripped = strippedSpoiler
}
if status.Sensitive && showSensitive {
stripped = strippedSpoiler + "\n\n" + strippedContent
stripped = strippedSpoiler + "\n\n" + tview.Escape(strippedContent)
}
if !status.Sensitive {
stripped = strippedContent
stripped = tview.Escape(strippedContent)
}
app.UI.LinkOverlay.SetLinks(urls, status)
subtleColor := fmt.Sprintf("[#%x]", app.Config.Style.Subtle.Hex())
special1 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial1.Hex())
special2 := fmt.Sprintf("[#%x]", app.Config.Style.TextSpecial2.Hex())
var head string
if status.Reblog != nil {
if status.Account.DisplayName != "" {
@ -87,7 +91,7 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str
}
head += fmt.Sprintf(special1+"%s\n\n", status.Account.Acct)
output := head
content := tview.Escape(stripped)
content := stripped
if content != "" {
output += content + "\n\n"
}
@ -135,6 +139,9 @@ func showTootOptions(app *App, status *mastodon.Status, showSensitive bool) (str
if shouldDisplay {
output += poll + media + card
}
output += "\n" + subtleColor + line
output += fmt.Sprintf("%sReplies %s%d %sBoosts %s%d %sFavorites %s%d\n\n",
subtleColor, special1, status.RepliesCount, subtleColor, special1, status.ReblogsCount, subtleColor, special1, status.FavouritesCount)
app.UI.StatusView.ScrollToBeginning()
var info []string
@ -213,6 +220,20 @@ func (t *TimelineFeed) FeedType() FeedType {
return TimelineFeedType
}
func (t *TimelineFeed) GetDesc() string {
switch t.timelineType {
case TimelineHome:
return "Timeline home"
case TimelineDirect:
return "Timeline direct"
case TimelineLocal:
return "Timeline local"
case TimelineFederated:
return "Timeline federated"
}
return "Timeline"
}
func (t *TimelineFeed) GetCurrentStatus() *mastodon.Status {
index := t.app.UI.StatusView.GetCurrentItem()
if index >= len(t.statuses) {
@ -383,6 +404,10 @@ func (t *ThreadFeed) FeedType() FeedType {
return ThreadFeedType
}
func (t *ThreadFeed) GetDesc() string {
return "Thread"
}
func (t *ThreadFeed) GetCurrentStatus() *mastodon.Status {
index := t.app.UI.StatusView.GetCurrentItem()
if index >= len(t.statuses) {
@ -534,6 +559,10 @@ func (u *UserFeed) FeedType() FeedType {
return UserFeedType
}
func (u *UserFeed) GetDesc() string {
return "User " + u.user.Acct
}
func (u *UserFeed) GetCurrentStatus() *mastodon.Status {
index := u.app.UI.app.UI.StatusView.GetCurrentItem()
if index > 0 && index-1 >= len(u.statuses) {
@ -821,6 +850,10 @@ func (n *NotificationsFeed) FeedType() FeedType {
return NotificationFeedType
}
func (n *NotificationsFeed) GetDesc() string {
return "Notifications"
}
func (n *NotificationsFeed) GetCurrentNotification() *mastodon.Notification {
index := n.app.UI.StatusView.GetCurrentItem()
if index >= len(n.notifications) {
@ -1044,6 +1077,10 @@ func (t *TagFeed) FeedType() FeedType {
return TagFeedType
}
func (t *TagFeed) GetDesc() string {
return "Tag #" + t.tag
}
func (t *TagFeed) GetCurrentStatus() *mastodon.Status {
index := t.app.UI.StatusView.GetCurrentItem()
if index >= len(t.statuses) {
@ -1187,3 +1224,198 @@ func (t *TagFeed) Input(event *tcell.EventKey) {
}
}
}
func NewUserSearchFeed(app *App, s string) *UserSearchFeed {
u := &UserSearchFeed{
app: app,
}
users, err := app.API.GetUsers(s)
if err != nil {
u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't load users. Error: %v\n", err))
return u
}
u.users = users
return u
}
type UserSearchFeed struct {
app *App
users []*UserSearchData
index int
search string
}
func (u *UserSearchFeed) FeedType() FeedType {
return UserSearchFeedType
}
func (u *UserSearchFeed) GetDesc() string {
return "User search: " + u.search
}
func (u *UserSearchFeed) GetCurrentUser() *UserSearchData {
index := u.app.UI.app.UI.StatusView.GetCurrentItem()
if index > 0 && index-1 >= len(u.users) {
return nil
}
return u.users[index-1]
}
func (u *UserSearchFeed) GetFeedList() <-chan string {
ch := make(chan string)
users := u.users
go func() {
for _, user := range users {
var username string
if user.User.DisplayName == "" {
username = user.User.Acct
} else {
username = fmt.Sprintf("%s (%s)", user.User.DisplayName, user.User.Acct)
}
ch <- username
}
close(ch)
}()
return ch
}
func (u *UserSearchFeed) LoadNewer() int {
return 0
}
func (u *UserSearchFeed) LoadOlder() int {
return 0
}
func (u *UserSearchFeed) DrawList() {
u.app.UI.StatusView.SetList(u.GetFeedList())
}
func (u *UserSearchFeed) DrawToot() {
u.index = u.app.UI.StatusView.GetCurrentItem()
index := u.index
if index > len(u.users)-1 || len(u.users) == 0 {
return
}
user := u.users[index]
var text string
var controls string
n := fmt.Sprintf("[#%x]", u.app.Config.Style.Text.Hex())
s1 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial1.Hex())
s2 := fmt.Sprintf("[#%x]", u.app.Config.Style.TextSpecial2.Hex())
if user.User.DisplayName != "" {
text = fmt.Sprintf(s2+"%s\n", user.User.DisplayName)
}
text += fmt.Sprintf(s1+"%s\n\n", user.User.Acct)
text += fmt.Sprintf("Toots %s%d %sFollowers %s%d %sFollowing %s%d\n\n",
s2, user.User.StatusesCount, n, s2, user.User.FollowersCount, n, s2, user.User.FollowingCount)
note, urls := cleanTootHTML(user.User.Note)
text += note + "\n\n"
for _, f := range user.User.Fields {
value, fu := cleanTootHTML(f.Value)
text += fmt.Sprintf("%s%s: %s%s\n", s2, f.Name, n, value)
urls = append(urls, fu...)
}
u.app.UI.LinkOverlay.SetLinks(urls, nil)
var controlItems []string
if u.app.Me.ID != user.User.ID {
if user.Relationship.Following {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "F", "ollow"))
} else {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "F", "ollow"))
}
if user.Relationship.Blocking {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "B", "lock"))
} else {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "B", "lock"))
}
if user.Relationship.Muting {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "Un", "M", "ute"))
} else {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "M", "ute"))
}
if len(urls) > 0 {
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "O", "pen"))
}
}
controlItems = append(controlItems, ColorKey(u.app.Config.Style, "", "U", "ser"))
controls = strings.Join(controlItems, " ")
u.app.UI.StatusView.SetText(text)
u.app.UI.StatusView.SetControls(controls)
}
func (u *UserSearchFeed) GetSavedIndex() int {
return u.index
}
func (u *UserSearchFeed) Input(event *tcell.EventKey) {
index := u.GetSavedIndex()
if index > len(u.users)-1 || len(u.users) == 0 {
return
}
user := u.users[index]
if event.Key() == tcell.KeyRune {
switch event.Rune() {
case 'f', 'F':
var relation *mastodon.Relationship
var err error
if user.Relationship.Following {
relation, err = u.app.API.UnfollowUser(*user.User)
} else {
relation, err = u.app.API.FollowUser(*user.User)
}
if err != nil {
u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't follow/unfollow user. Error: %v\n", err))
return
}
user.Relationship = relation
u.DrawToot()
case 'b', 'B':
var relation *mastodon.Relationship
var err error
if user.Relationship.Blocking {
relation, err = u.app.API.UnblockUser(*user.User)
} else {
relation, err = u.app.API.BlockUser(*user.User)
}
if err != nil {
u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't block/unblock user. Error: %v\n", err))
return
}
user.Relationship = relation
u.DrawToot()
case 'm', 'M':
var relation *mastodon.Relationship
var err error
if user.Relationship.Muting {
relation, err = u.app.API.UnmuteUser(*user.User)
} else {
relation, err = u.app.API.MuteUser(*user.User)
}
if err != nil {
u.app.UI.CmdBar.ShowError(fmt.Sprintf("Couldn't mute/unmute user. Error: %v\n", err))
return
}
user.Relationship = relation
u.DrawToot()
case 'r', 'R':
//toots and replies?
case 'o', 'O':
u.app.UI.ShowLinks()
case 'u', 'U':
u.app.UI.StatusView.AddFeed(
NewUserFeed(u.app, *user.User),
)
}
}
}

16
main.go

@ -206,7 +206,7 @@ func main() {
)
app.UI.CmdBar.Input.SetAutocompleteFunc(func(currentText string) (entries []string) {
words := strings.Split(":tag,:timeline,:tl,:quit,:q", ",")
words := strings.Split(":compose,:tag,:timeline,:tl,:user,:quit,:q", ",")
if currentText == "" {
return
}
@ -240,6 +240,9 @@ func main() {
fallthrough
case ":quit":
app.UI.Root.Stop()
case ":compose":
app.UI.NewToot()
app.UI.CmdBar.ClearInput()
case ":timeline", ":tl":
if len(parts) < 2 {
break
@ -277,6 +280,17 @@ func main() {
app.UI.StatusView.AddFeed(NewTagFeed(app, tag))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
case ":user":
if len(parts) < 2 {
break
}
user := strings.TrimSpace(parts[1])
if len(user) == 0 {
break
}
app.UI.StatusView.AddFeed(NewUserSearchFeed(app, user))
app.UI.SetFocus(LeftPaneFocus)
app.UI.CmdBar.ClearInput()
}
})

23
statusview.go

@ -1,6 +1,7 @@
package main
import (
"fmt"
"time"
"github.com/gdamore/tcell"
@ -27,12 +28,6 @@ func NewStatusView(app *App, tl TimelineType) *StatusView {
t.list.ShowSecondaryText(false)
t.list.SetHighlightFullLine(true)
t.list.SetChangedFunc(func(i int, _ string, _ string, _ rune) {
if app.HaveAccount {
t.showToot(i)
}
})
t.text.SetWordWrap(true).SetDynamicColors(true)
t.text.SetBackgroundColor(app.Config.Style.Background)
t.text.SetTextColor(app.Config.Style.Text)
@ -71,6 +66,7 @@ func (t *StatusView) AddFeed(f Feed) {
f.DrawList()
t.list.SetCurrentItem(f.GetSavedIndex())
f.DrawToot()
t.drawDesc()
}
func (t *StatusView) RemoveLatestFeed() {
@ -79,6 +75,7 @@ func (t *StatusView) RemoveLatestFeed() {
feed.DrawList()
t.list.SetCurrentItem(feed.GetSavedIndex())
feed.DrawToot()
t.drawDesc()
}
func (t *StatusView) GetLeftView() tview.Primitive {
@ -206,10 +203,16 @@ func (t *StatusView) SetControls(text string) {
t.controls.SetText(text)
}
func (t *StatusView) showToot(index int) {
}
func (t *StatusView) showTootOptions(index int, showSensitive bool) {
func (t *StatusView) drawDesc() {
if len(t.feeds) == 0 {
t.app.UI.SetTopText("")
return
}
l := len(t.feeds)
f := t.feeds[l-1]
t.app.UI.SetTopText(
fmt.Sprintf("%s (%d/%d)", f.GetDesc(), l, l),
)
}
func (t *StatusView) prev() {

10
ui.go

@ -51,6 +51,7 @@ func (ui *UI) Init() {
}
return 0, 0, 0, 0
})
ui.SetTopText("")
ui.Pages.AddPage("main",
tview.NewFlex().
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
@ -222,6 +223,14 @@ func (ui *UI) OpenMedia(status *mastodon.Status) {
}
}
func (ui *UI) SetTopText(s string) {
if s == "" {
ui.Top.Text.SetText("tut")
} else {
ui.Top.Text.SetText(fmt.Sprintf("tut - %s", s))
}
}
func (ui *UI) LoggedIn() {
ui.StatusView = NewStatusView(ui.app, ui.Timeline)
@ -249,7 +258,6 @@ func (ui *UI) LoggedIn() {
ui.Pages.SendToBack("main")
ui.SetFocus(LeftPaneFocus)
fmt.Fprint(ui.Top.Text, "tut\n")
me, err := ui.app.API.Client.GetAccountCurrentUser(context.Background())
if err != nil {

Loading…
Cancel
Save