Show issue reactions (#421)

```
$ tea issue 230

   #230 issue/pull details: show reactions (open)

  @6543 created 2020-10-22 16:39

  since reactions are utf8 now and most terminals too, we can display them nicely :)

  https://gitea.com/api/v1/repos/gitea/tea/issues/230/reactions

  --------

  1x 🎉  |  1x 👀  |  1x :gitea:  |  1x 👍  |  1x 👎  |  1x 😆  |  1x 😕  |  1x ❤️
```

caveats:
- reactions are not returned as UTF8 (as was claimed in #230), so they need to be parsed. the library I use doesn't (and can't → :gitea:) support all reactions available in gitea
- currently only for issues, as reactions for comments mean an additional API request for each comment..

fixes #230

Co-authored-by: Norwin <git@nroo.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/421
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Co-authored-by: Norwin <noerw@noreply.gitea.io>
Co-committed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
Norwin 2021-10-01 16:13:32 +08:00 committed by 6543
parent 7a05be436c
commit 58aaa17e7e
18 changed files with 5146 additions and 7 deletions

View File

@ -54,11 +54,16 @@ func runIssueDetail(cmd *cli.Context, index string) error {
if err != nil { if err != nil {
return err return err
} }
issue, _, err := ctx.Login.Client().GetIssue(ctx.Owner, ctx.Repo, idx) client := ctx.Login.Client()
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
if err != nil { if err != nil {
return err return err
} }
print.IssueDetails(issue) reactions, _, err := client.GetIssueReactions(ctx.Owner, ctx.Repo, idx)
if err != nil {
return err
}
print.IssueDetails(issue, reactions)
if issue.Comments > 0 { if issue.Comments > 0 {
err = interact.ShowCommentsMaybeInteractive(ctx, idx, issue.Comments) err = interact.ShowCommentsMaybeInteractive(ctx, idx, issue.Comments)

View File

@ -47,6 +47,6 @@ func editIssueState(cmd *cli.Context, opts gitea.EditIssueOption) error {
return err return err
} }
print.IssueDetails(issue) print.IssueDetails(issue, nil)
return nil return nil
} }

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/charmbracelet/glamour v0.3.0 github.com/charmbracelet/glamour v0.3.0
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/enescakir/emoji v1.0.0
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/hashicorp/go-version v1.3.0 // indirect github.com/hashicorp/go-version v1.3.0 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect

2
go.sum
View File

@ -53,6 +53,8 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=

View File

@ -9,11 +9,12 @@ import (
"strings" "strings"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/enescakir/emoji"
) )
// IssueDetails print an issue rendered to stdout // IssueDetails print an issue rendered to stdout
func IssueDetails(issue *gitea.Issue) { func IssueDetails(issue *gitea.Issue, reactions []*gitea.Reaction) {
outputMarkdown(fmt.Sprintf( out := fmt.Sprintf(
"# #%d %s (%s)\n@%s created %s\n\n%s\n", "# #%d %s (%s)\n@%s created %s\n\n%s\n",
issue.Index, issue.Index,
issue.Title, issue.Title,
@ -21,7 +22,27 @@ func IssueDetails(issue *gitea.Issue) {
issue.Poster.UserName, issue.Poster.UserName,
FormatTime(issue.Created), FormatTime(issue.Created),
issue.Body, issue.Body,
), issue.HTMLURL) )
if len(reactions) > 0 {
out += fmt.Sprintf("\n---\n\n%s\n", formatReactions(reactions))
}
outputMarkdown(out, issue.HTMLURL)
}
func formatReactions(reactions []*gitea.Reaction) string {
reactionCounts := make(map[string]uint16)
for _, r := range reactions {
reactionCounts[r.Reaction] += 1
}
reactionStrings := make([]string, 0, len(reactionCounts))
for reaction, count := range reactionCounts {
reactionStrings = append(reactionStrings, fmt.Sprintf("%dx :%s:", count, reaction))
}
return emoji.Parse(strings.Join(reactionStrings, " | "))
} }
// IssuesPullsList prints a listing of issues & pulls // IssuesPullsList prints a listing of issues & pulls

View File

@ -25,7 +25,7 @@ func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.Cre
return fmt.Errorf("could not create issue: %s", err) return fmt.Errorf("could not create issue: %s", err)
} }
print.IssueDetails(issue) print.IssueDetails(issue, nil)
fmt.Println(issue.HTMLURL) fmt.Println(issue.HTMLURL)

15
vendor/github.com/enescakir/emoji/.gitignore generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

21
vendor/github.com/enescakir/emoji/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Enes Çakır
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

99
vendor/github.com/enescakir/emoji/README.md generated vendored Normal file
View File

@ -0,0 +1,99 @@
# emoji :rocket: :school_satchel: :tada:
[![Build Status](https://github.com/enescakir/emoji/workflows/build/badge.svg?branch=master)](https://github.com/enescakir/emoji/actions)
[![godoc](https://godoc.org/github.com/enescakir/emoji?status.svg)](https://godoc.org/github.com/enescakir/emoji)
[![Go Report Card](https://goreportcard.com/badge/github.com/enescakir/emoji)](https://goreportcard.com/report/github.com/enescakir/emoji)
[![Codecov](https://img.shields.io/codecov/c/github/enescakir/emoji)](https://codecov.io/gh/enescakir/emoji)
[![MIT License](https://img.shields.io/github/license/enescakir/emoji)](https://github.com/enescakir/emoji/blob/master/LICENSE)
`emoji` is a minimalistic emoji library for Go. It lets you use emoji characters in strings.
Inspired by [spatie/emoji](https://github.com/spatie/emoji)
## Install :floppy_disk:
``` bash
go get github.com/enescakir/emoji
```
## Usage :surfer:
```go
package main
import (
"fmt"
"github.com/enescakir/emoji"
)
func main() {
fmt.Printf("Hello %v\n", emoji.WavingHand)
fmt.Printf("I am %v from %v\n",
emoji.ManTechnologist,
emoji.FlagForTurkey,
)
fmt.Printf("Different skin tones.\n default: %v light: %v dark: %v\n",
emoji.ThumbsUp,
emoji.OkHand.Tone(emoji.Light),
emoji.CallMeHand.Tone(emoji.Dark),
)
fmt.Printf("Emojis with multiple skin tones.\n both medium: %v light and dark: %v\n",
emoji.PeopleHoldingHands.Tone(emoji.Medium),
emoji.PeopleHoldingHands.Tone(emoji.Light, emoji.Dark),
)
fmt.Println(emoji.Parse("Emoji aliases are :sunglasses:"))
emoji.Println("Use fmt wrappers :+1: with emoji support :tada:")
}
/* OUTPUT
Hello 👋
I am 👨‍💻 from 🇹🇷
Different skin tones.
default: 👍 light: 👌🏻 dark: 🤙🏿
Emojis with multiple skin tones.
both medium: 🧑🏽‍🤝‍🧑🏽 light and dark: 🧑🏻‍🤝‍🧑🏿
Emoji aliases are 😎
Use fmt wrappers 👍 with emoji support 🎉
*/
```
This package contains emojis constants based on [Full Emoji List v13.0](https://unicode.org/Public/emoji/13.0/emoji-test.txt).
```go
emoji.CallMeHand // 🤙
emoji.CallMeHand.Tone(emoji.Dark) // 🤙🏿
```
Also, it has additional emoji aliases from [github/gemoji](https://github.com/github/gemoji).
```go
emoji.Parse(":+1:") // 👍
emoji.Parse(":100:") // 💯
```
You can generate country flag emoji with [ISO 3166 Alpha2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes:
```go
emoji.CountryFlag("tr") // 🇹🇷
emoji.CountryFlag("US") // 🇺🇸
emoji.Parse("country flag alias :flag-gb:") // country flag alias 🇬🇧
```
All constants are generated by `internal/generator`.
## Testing :hammer:
``` bash
go test
```
## Todo :pushpin:
* Add examples to `godoc`
## Contributing :man_technologist:
I am accepting PRs that add aliases to the package.
You have to add it to `customEmojis` list at `internal/generator/main`.
If you think an emoji constant is not correct, open an issue.
Please use [this list](http://unicode.org/emoji/charts/full-emoji-list.html)
to look up the correct unicode value and the name of the character.
## Credits :star:
- [Enes Çakır](https://github.com/enescakir)
## License :scroll:
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

1944
vendor/github.com/enescakir/emoji/constants.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

4
vendor/github.com/enescakir/emoji/doc.go generated vendored Normal file
View File

@ -0,0 +1,4 @@
/*
Package emoji makes working with emojis easier.
*/
package emoji

124
vendor/github.com/enescakir/emoji/emoji.go generated vendored Normal file
View File

@ -0,0 +1,124 @@
package emoji
import (
"fmt"
"strings"
)
// Base attributes
const (
TonePlaceholder = "@"
flagBaseIndex = '\U0001F1E6' - 'a'
)
// Skin tone colors
const (
Default Tone = ""
Light Tone = "\U0001F3FB"
MediumLight Tone = "\U0001F3FC"
Medium Tone = "\U0001F3FD"
MediumDark Tone = "\U0001F3FE"
Dark Tone = "\U0001F3FF"
)
// Emoji defines an emoji object with no skin variations.
type Emoji string
// String returns string representation of the simple emoji.
func (e Emoji) String() string {
return string(e)
}
// EmojiWithTone defines an emoji object that has skin tone options.
type EmojiWithTone struct {
oneTonedCode string
twoTonedCode string
defaultTone Tone
}
// newEmojiWithTone constructs a new emoji object that has skin tone options.
func newEmojiWithTone(codes ...string) EmojiWithTone {
if len(codes) == 0 {
return EmojiWithTone{}
}
one := codes[0]
two := codes[0]
if len(codes) > 1 {
two = codes[1]
}
return EmojiWithTone{
oneTonedCode: one,
twoTonedCode: two,
}
}
// withDefaultTone sets default tone for an emoji and returns it.
func (e EmojiWithTone) withDefaultTone(tone string) EmojiWithTone {
e.defaultTone = Tone(tone)
return e
}
// String returns string representation of the emoji with default skin tone.
func (e EmojiWithTone) String() string {
return strings.ReplaceAll(e.oneTonedCode, TonePlaceholder, e.defaultTone.String())
}
// Tone returns string representation of the emoji with given skin tone.
func (e EmojiWithTone) Tone(tones ...Tone) string {
// if no tone given, return with default skin tone
if len(tones) == 0 {
return e.String()
}
str := e.twoTonedCode
replaceCount := 1
// if one tone given or emoji doesn't have twoTonedCode, use oneTonedCode
// Also, replace all with one tone
if len(tones) == 1 {
str = e.oneTonedCode
replaceCount = -1
}
// replace tone one by one
for _, t := range tones {
// use emoji's default tone
if t == Default {
t = e.defaultTone
}
str = strings.Replace(str, TonePlaceholder, t.String(), replaceCount)
}
return str
}
// Tone defines skin tone options for emojis.
type Tone string
// String returns string representation of the skin tone.
func (t Tone) String() string {
return string(t)
}
// CountryFlag returns a country flag emoji from given country code.
// Full list of country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
func CountryFlag(code string) (Emoji, error) {
if len(code) != 2 {
return "", fmt.Errorf("not valid country code: %q", code)
}
code = strings.ToLower(code)
flag := countryCodeLetter(code[0]) + countryCodeLetter(code[1])
return Emoji(flag), nil
}
// countryCodeLetter shifts given letter byte as flagBaseIndex.
func countryCodeLetter(l byte) string {
return string(rune(l) + flagBaseIndex)
}

56
vendor/github.com/enescakir/emoji/fmt.go generated vendored Normal file
View File

@ -0,0 +1,56 @@
package emoji
import (
"fmt"
"io"
)
// Sprint wraps fmt.Sprint with emoji support
func Sprint(a ...interface{}) string {
return Parse(fmt.Sprint(a...))
}
// Sprintf wraps fmt.Sprintf with emoji support
func Sprintf(format string, a ...interface{}) string {
return Parse(fmt.Sprintf(format, a...))
}
// Sprintln wraps fmt.Sprintln with emoji support
func Sprintln(a ...interface{}) string {
return Parse(fmt.Sprintln(a...))
}
// Print wraps fmt.Print with emoji support
func Print(a ...interface{}) (n int, err error) {
return fmt.Print(Sprint(a...))
}
// Println wraps fmt.Println with emoji support
func Println(a ...interface{}) (n int, err error) {
return fmt.Println(Sprint(a...))
}
// Printf wraps fmt.Printf with emoji support
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Print(Sprintf(format, a...))
}
// Fprint wraps fmt.Fprint with emoji support
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, Sprint(a...))
}
// Fprintf wraps fmt.Fprintf with emoji support
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, Sprintf(format, a...))
}
// Fprintln wraps fmt.Fprintln with emoji support
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, Sprint(a...))
}
// Errorf wraps fmt.Errorf with emoji support
func Errorf(format string, a ...interface{}) error {
return fmt.Errorf(Sprintf(format, a...))
}

3
vendor/github.com/enescakir/emoji/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/enescakir/emoji
go 1.13

0
vendor/github.com/enescakir/emoji/go.sum generated vendored Normal file
View File

2715
vendor/github.com/enescakir/emoji/map.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

127
vendor/github.com/enescakir/emoji/parser.go generated vendored Normal file
View File

@ -0,0 +1,127 @@
package emoji
import (
"fmt"
"regexp"
"strings"
"unicode"
)
var (
flagRegex = regexp.MustCompile(`^:flag-([a-zA-Z]{2}):$`)
)
// Parse replaces emoji aliases (:pizza:) with unicode representation.
func Parse(input string) string {
var matched strings.Builder
var output strings.Builder
for _, r := range input {
// when it's not `:`, it might be inner or outer of the emoji alias
if r != ':' {
// if matched is empty, it's the outer of the emoji alias
if matched.Len() == 0 {
output.WriteRune(r)
continue
}
matched.WriteRune(r)
// if it's space, the alias's not valid.
// reset matched for breaking the emoji alias
if unicode.IsSpace(r) {
output.WriteString(matched.String())
matched.Reset()
}
continue
}
// r is `:` now
// if matched is empty, it's the beginning of the emoji alias
if matched.Len() == 0 {
matched.WriteRune(r)
continue
}
// it's the end of the emoji alias
match := matched.String()
alias := match + ":"
// check for emoji alias
if code, ok := Find(alias); ok {
output.WriteString(code)
matched.Reset()
continue
}
// not found any emoji
output.WriteString(match)
// it might be the beginning of the another emoji alias
matched.Reset()
matched.WriteRune(r)
}
// if matched not empty, add it to output
if matched.Len() != 0 {
output.WriteString(matched.String())
matched.Reset()
}
return output.String()
}
// Map returns the emojis map.
// Key is the alias of the emoji.
// Value is the code of the emoji.
func Map() map[string]string {
return emojiMap
}
// AppendAlias adds new emoji pair to the emojis map.
func AppendAlias(alias, code string) error {
if c, ok := emojiMap[alias]; ok {
return fmt.Errorf("emoji already exist: %q => %+q", alias, c)
}
for _, r := range alias {
if unicode.IsSpace(r) {
return fmt.Errorf("emoji alias is not valid: %q", alias)
}
}
emojiMap[alias] = code
return nil
}
// Exist checks existence of the emoji by alias.
func Exist(alias string) bool {
_, ok := Find(alias)
return ok
}
// Find returns the emoji code by alias.
func Find(alias string) (string, bool) {
if code, ok := emojiMap[alias]; ok {
return code, true
}
if flag := checkFlag(alias); len(flag) > 0 {
return flag, true
}
return "", false
}
// checkFlag finds flag emoji for `flag-[CODE]` pattern
func checkFlag(alias string) string {
if matches := flagRegex.FindStringSubmatch(alias); len(matches) == 2 {
flag, _ := CountryFlag(matches[1])
return flag.String()
}
return ""
}

2
vendor/modules.txt vendored
View File

@ -93,6 +93,8 @@ github.com/emirpasic/gods/lists/arraylist
github.com/emirpasic/gods/trees github.com/emirpasic/gods/trees
github.com/emirpasic/gods/trees/binaryheap github.com/emirpasic/gods/trees/binaryheap
github.com/emirpasic/gods/utils github.com/emirpasic/gods/utils
# github.com/enescakir/emoji v1.0.0
github.com/enescakir/emoji
# github.com/go-git/gcfg v1.5.0 # github.com/go-git/gcfg v1.5.0
github.com/go-git/gcfg github.com/go-git/gcfg
github.com/go-git/gcfg/scanner github.com/go-git/gcfg/scanner