Dan Anglin
eb016b96e9
Summary: This commit updates and enhances poll interaction. From now on users will interact with a poll via the status that contains it. Direct interaction with the poll (via the poll's ID) is no longer supported. This helps resolve an issue where it wasn't possible to find the owner of the poll when interacting with it directly. Changes: - Users can no longer view a poll directly using the Poll ID. Instead polls can be viewed when viewing statuses or timelines. - More details about a poll is shown in statuses and timelines. - Votes are now added to polls via statuses. - Poll results are hidden unless the following conditions are met. - The user is the owner of the poll. - The poll has expired. - The user has already voted in the poll. - Enbas can now detect and stop a poll owner from voting in their own poll. - When a status is created Enbas will now only print the ID of the created status instead of the whole thing. PR: apollo/enbas#43 Resolves apollo/enbas#39
263 lines
8 KiB
Go
263 lines
8 KiB
Go
package printer
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
|
)
|
|
|
|
func (p Printer) PrintStatus(status model.Status, userAccountID string) {
|
|
var builder strings.Builder
|
|
|
|
// The account information
|
|
builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct))
|
|
|
|
// The ID of the status
|
|
builder.WriteString("\n\n" + p.headerFormat("STATUS ID:"))
|
|
builder.WriteString("\n" + status.ID)
|
|
|
|
// The content of the status.
|
|
builder.WriteString("\n\n" + p.headerFormat("CONTENT:"))
|
|
builder.WriteString(p.convertHTMLToText(status.Content, true))
|
|
|
|
// Details of media attachments (if any).
|
|
if len(status.MediaAttachments) > 0 {
|
|
builder.WriteString("\n\n" + p.headerFormat("MEDIA ATTACHMENTS:"))
|
|
|
|
for ind, media := range status.MediaAttachments {
|
|
builder.WriteString("\n\n[" + strconv.Itoa(ind) + "] " + p.fieldFormat("ID:") + " " + media.ID)
|
|
builder.WriteString("\n " + p.fieldFormat("Type:") + " " + media.Type)
|
|
|
|
description := media.Description
|
|
if description == "" {
|
|
description = noMediaDescription
|
|
}
|
|
|
|
builder.WriteString("\n " + p.fieldFormat("Description:") + " " + description)
|
|
builder.WriteString("\n " + p.fieldFormat("Media URL:") + " " + media.URL)
|
|
}
|
|
}
|
|
|
|
// If a poll exists in a status, write the contents to the builder.
|
|
if status.Poll != nil {
|
|
pollOwner := false
|
|
if status.Account.ID == userAccountID {
|
|
pollOwner = true
|
|
}
|
|
|
|
builder.WriteString("\n\n" + p.headerFormat("POLL DETAILS:"))
|
|
builder.WriteString(p.pollDetails(*status.Poll, pollOwner))
|
|
}
|
|
|
|
// Status creation time
|
|
builder.WriteString("\n\n" + p.headerFormat("CREATED AT:"))
|
|
builder.WriteString("\n" + p.formatDateTime(status.CreatedAt))
|
|
|
|
// Status stats
|
|
builder.WriteString("\n\n" + p.headerFormat("STATS:"))
|
|
builder.WriteString("\n" + p.fieldFormat("Boosts: ") + strconv.Itoa(status.ReblogsCount))
|
|
builder.WriteString("\n" + p.fieldFormat("Likes: ") + strconv.Itoa(status.FavouritesCount))
|
|
builder.WriteString("\n" + p.fieldFormat("Replies: ") + strconv.Itoa(status.RepliesCount))
|
|
|
|
// The user's actions on the status
|
|
builder.WriteString("\n\n" + p.headerFormat("YOUR ACTIONS:"))
|
|
builder.WriteString("\n" + p.fieldFormat("Boosted: ") + strconv.FormatBool(status.Reblogged))
|
|
builder.WriteString("\n" + p.fieldFormat("Liked: ") + strconv.FormatBool(status.Favourited))
|
|
builder.WriteString("\n" + p.fieldFormat("Bookmarked: ") + strconv.FormatBool(status.Bookmarked))
|
|
|
|
// Status visibility
|
|
builder.WriteString("\n\n" + p.headerFormat("VISIBILITY:"))
|
|
builder.WriteString("\n" + status.Visibility.String())
|
|
|
|
// Status URL
|
|
builder.WriteString("\n\n" + p.headerFormat("URL:"))
|
|
builder.WriteString("\n" + status.URL)
|
|
builder.WriteString("\n\n")
|
|
|
|
p.print(builder.String())
|
|
}
|
|
|
|
func (p Printer) PrintStatusList(list model.StatusList, userAccountID string) {
|
|
p.print(p.statusList(list, userAccountID))
|
|
}
|
|
|
|
func (p Printer) statusList(list model.StatusList, userAccountID string) string {
|
|
var builder strings.Builder
|
|
|
|
builder.WriteString(p.headerFormat(list.Name) + "\n")
|
|
|
|
for _, status := range list.Statuses {
|
|
statusID := status.ID
|
|
statusOwnerID := status.Account.ID
|
|
createdAt := p.formatDateTime(status.CreatedAt)
|
|
boostedAt := ""
|
|
content := status.Content
|
|
poll := status.Poll
|
|
mediaAttachments := status.MediaAttachments
|
|
|
|
switch {
|
|
case status.Reblog != nil:
|
|
builder.WriteString("\n" + p.wrapLines(
|
|
p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)+
|
|
" boosted this status from "+
|
|
p.fullDisplayNameFormat(status.Reblog.Account.DisplayName, status.Reblog.Account.Acct)+
|
|
":",
|
|
0,
|
|
))
|
|
|
|
statusID = status.Reblog.ID
|
|
statusOwnerID = status.Reblog.Account.ID
|
|
createdAt = p.formatDateTime(status.Reblog.CreatedAt)
|
|
boostedAt = p.formatDateTime(status.CreatedAt)
|
|
content = status.Reblog.Content
|
|
poll = status.Reblog.Poll
|
|
mediaAttachments = status.Reblog.MediaAttachments
|
|
case status.InReplyToID != "":
|
|
builder.WriteString("\n" + p.wrapLines(
|
|
p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)+
|
|
" posted in reply to "+
|
|
status.InReplyToID+
|
|
":",
|
|
0,
|
|
))
|
|
default:
|
|
builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct) + " posted:")
|
|
}
|
|
|
|
builder.WriteString("\n" + p.convertHTMLToText(content, true))
|
|
|
|
if poll != nil {
|
|
pollOwner := false
|
|
if statusOwnerID == userAccountID {
|
|
pollOwner = true
|
|
}
|
|
|
|
builder.WriteString(p.pollDetails(*poll, pollOwner))
|
|
}
|
|
|
|
for _, media := range mediaAttachments {
|
|
builder.WriteString("\n\n" + symbolImage + " " + p.fieldFormat("Media attachment: ") + media.ID)
|
|
builder.WriteString("\n " + p.fieldFormat("Media type: ") + media.Type + "\n")
|
|
|
|
description := " " + p.fieldFormat("Description: ")
|
|
|
|
if media.Description == "" {
|
|
description += noMediaDescription
|
|
} else {
|
|
description += media.Description
|
|
}
|
|
|
|
builder.WriteString(p.wrapLines(description, 2))
|
|
}
|
|
|
|
boosted := symbolBoosted
|
|
if status.Reblogged {
|
|
boosted = p.theme.boldyellow + symbolBoosted + p.theme.reset
|
|
}
|
|
|
|
liked := symbolNotLiked
|
|
if status.Favourited {
|
|
liked = p.theme.boldyellow + symbolLiked + p.theme.reset
|
|
}
|
|
|
|
bookmarked := symbolNotBookmarked
|
|
if status.Bookmarked {
|
|
bookmarked = p.theme.boldyellow + symbolBookmarked + p.theme.reset
|
|
}
|
|
|
|
builder.WriteString("\n\n" + boosted + " " + p.fieldFormat("boosted: ") + strconv.FormatBool(status.Reblogged))
|
|
builder.WriteString("\n" + liked + " " + p.fieldFormat("liked: ") + strconv.FormatBool(status.Favourited))
|
|
builder.WriteString("\n" + bookmarked + " " + p.fieldFormat("bookmarked: ") + strconv.FormatBool(status.Bookmarked))
|
|
|
|
builder.WriteString(
|
|
"\n\n" +
|
|
p.fieldFormat("Status ID: ") + statusID +
|
|
"\n" + p.fieldFormat("Created at: ") + createdAt,
|
|
)
|
|
|
|
if boostedAt != "" {
|
|
builder.WriteString("\n" + p.fieldFormat("Boosted at: ") + boostedAt)
|
|
}
|
|
|
|
builder.WriteString("\n" + p.statusSeparator + "\n")
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func (p Printer) pollDetails(poll model.Poll, owner bool) string {
|
|
var builder strings.Builder
|
|
|
|
for ind, option := range poll.Options {
|
|
var (
|
|
votage float64
|
|
percentage int
|
|
)
|
|
|
|
// Show the poll results under any of the following conditions:
|
|
// - the user is the owner of the poll
|
|
// - the poll has expired
|
|
// - the user has voted in the poll
|
|
if owner || poll.Expired || poll.Voted {
|
|
if poll.VotesCount == 0 {
|
|
percentage = 0
|
|
} else {
|
|
votage = float64(option.VotesCount) / float64(poll.VotesCount)
|
|
percentage = int(math.Floor(100 * votage))
|
|
}
|
|
|
|
optionTitle := "\n\n" + "[" + strconv.Itoa(ind) + "] " + option.Title
|
|
|
|
for _, vote := range poll.OwnVotes {
|
|
if ind == vote {
|
|
optionTitle += " " + symbolCheckMark
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
builder.WriteString(optionTitle)
|
|
builder.WriteString(p.pollMeter(votage))
|
|
builder.WriteString("\n" + strconv.Itoa(option.VotesCount) + " votes " + "(" + strconv.Itoa(percentage) + "%)")
|
|
} else {
|
|
builder.WriteString("\n" + "[" + strconv.Itoa(ind) + "] " + option.Title)
|
|
}
|
|
}
|
|
|
|
pollStatusField := "Poll is open until: "
|
|
if poll.Expired {
|
|
pollStatusField = "Poll was closed on: "
|
|
}
|
|
|
|
builder.WriteString("\n\n" + p.fieldFormat(pollStatusField) + p.formatDateTime(poll.ExpiredAt))
|
|
builder.WriteString("\n" + p.fieldFormat("Total votes: ") + strconv.Itoa(poll.VotesCount))
|
|
builder.WriteString("\n" + p.fieldFormat("Multiple choices allowed: ") + strconv.FormatBool(poll.Multiple))
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
func (p Printer) pollMeter(votage float64) string {
|
|
numVoteBlocks := int(math.Floor(float64(p.lineWrapCharacterLimit) * votage))
|
|
numBackgroundBlocks := p.lineWrapCharacterLimit - numVoteBlocks
|
|
|
|
voteBlockColour := p.theme.boldgreen
|
|
backgroundBlockColor := p.theme.grey
|
|
|
|
if p.noColor {
|
|
voteBlockColour = p.theme.reset
|
|
|
|
if numVoteBlocks == 0 {
|
|
numVoteBlocks = 1
|
|
}
|
|
}
|
|
|
|
meter := "\n" + voteBlockColour + strings.Repeat(symbolPollMeter, numVoteBlocks) + p.theme.reset
|
|
|
|
if !p.noColor {
|
|
meter += backgroundBlockColor + strings.Repeat(symbolPollMeter, numBackgroundBlocks) + p.theme.reset
|
|
}
|
|
|
|
return meter
|
|
}
|