Add custom emoji support (#16004)

release/v1.15
6543 2021-06-29 16:28:38 +02:00 committed by GitHub
parent aac663e0da
commit 65548359cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 57 additions and 23 deletions

View File

@ -1034,6 +1034,11 @@ PATH =
;; For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png ;; For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png
;REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes ;REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes
;; ;;
;; Additional Emojis not defined in the utf8 standard
;; By default we support gitea (:gitea:), to add more copy them to public/emoji/img/emoji_name.png and add it to this config.
;; Dont mistake it for Reactions.
;CUSTOM_EMOJIS = gitea
;;
;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. ;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
;DEFAULT_SHOW_FULL_NAME = false ;DEFAULT_SHOW_FULL_NAME = false
;; ;;

View File

@ -181,6 +181,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `REACTIONS`: All available reactions users can choose on issues/prs and comments - `REACTIONS`: All available reactions users can choose on issues/prs and comments
Values can be emoji alias (:smile:) or a unicode emoji. Values can be emoji alias (:smile:) or a unicode emoji.
For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png
- `CUSTOM_EMOJIS`: **gitea**: Additional Emojis not defined in the utf8 standard.
By default we support gitea (:gitea:), to add more copy them to public/emoji/img/emoji_name.png and
add it to this config.
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. - `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page. - `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets. - `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets.

View File

@ -6,7 +6,6 @@ package markup
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
@ -66,7 +65,7 @@ var (
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
// EmojiShortCodeRegex find emoji by alias like :smile: // EmojiShortCodeRegex find emoji by alias like :smile:
EmojiShortCodeRegex = regexp.MustCompile(`\:[\w\+\-]+\:{1}`) EmojiShortCodeRegex = regexp.MustCompile(`:[\w\+\-]+:`)
) )
// CSS class for action keywords (e.g. "closes: #1") // CSS class for action keywords (e.g. "closes: #1")
@ -460,17 +459,14 @@ func createEmoji(content, class, name string) *html.Node {
return span return span
} }
func createCustomEmoji(alias, class string) *html.Node { func createCustomEmoji(alias string) *html.Node {
span := &html.Node{ span := &html.Node{
Type: html.ElementNode, Type: html.ElementNode,
Data: atom.Span.String(), Data: atom.Span.String(),
Attr: []html.Attribute{}, Attr: []html.Attribute{},
} }
if class != "" { span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias}) span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
}
img := &html.Node{ img := &html.Node{
Type: html.ElementNode, Type: html.ElementNode,
@ -478,10 +474,8 @@ func createCustomEmoji(alias, class string) *html.Node {
Data: "img", Data: "img",
Attr: []html.Attribute{}, Attr: []html.Attribute{},
} }
if class != "" { img.Attr = append(img.Attr, html.Attribute{Key: "alt", Val: ":" + alias + ":"})
img.Attr = append(img.Attr, html.Attribute{Key: "alt", Val: fmt.Sprintf(`:%s:`, alias)}) img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: setting.StaticURLPrefix + "/assets/img/emoji/" + alias + ".png"})
img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: fmt.Sprintf(`%s/assets/img/emoji/%s.png`, setting.StaticURLPrefix, alias)})
}
span.AppendChild(img) span.AppendChild(img)
return span return span
@ -948,9 +942,8 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
converted := emoji.FromAlias(alias) converted := emoji.FromAlias(alias)
if converted == nil { if converted == nil {
// check if this is a custom reaction // check if this is a custom reaction
s := strings.Join(setting.UI.Reactions, " ") + "gitea" if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
if strings.Contains(s, alias) { replaceContent(node, m[0], m[1], createCustomEmoji(alias))
replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji"))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
start = 0 start = 0
continue continue

View File

@ -284,7 +284,18 @@ func TestRender_emoji(t *testing.T) {
test( test(
":gitea:", ":gitea:",
`<p><span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`) `<p><span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
test(
":custom-emoji:",
`<p>:custom-emoji:</p>`)
setting.UI.CustomEmojisMap["custom-emoji"] = ":custom-emoji:"
test(
":custom-emoji:",
`<p><span class="emoji" aria-label="custom-emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span></p>`)
test(
"这是字符:1::+1: some🐊 \U0001f44d:custom-emoji: :gitea:",
`<p>这是字符:1:<span class="emoji" aria-label="thumbs up">👍</span> some<span class="emoji" aria-label="crocodile">🐊</span> `+
`<span class="emoji" aria-label="thumbs up">👍</span><span class="emoji" aria-label="custom-emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span> `+
`<span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
test( test(
"Some text with 😄 in the middle", "Some text with 😄 in the middle",
`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span> in the middle</p>`) `<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span> in the middle</p>`)

View File

@ -208,7 +208,9 @@ var (
DefaultTheme string DefaultTheme string
Themes []string Themes []string
Reactions []string Reactions []string
ReactionsMap map[string]bool ReactionsMap map[string]bool `ini:"-"`
CustomEmojis []string
CustomEmojisMap map[string]string `ini:"-"`
SearchRepoDescription bool SearchRepoDescription bool
UseServiceWorker bool UseServiceWorker bool
@ -256,6 +258,8 @@ var (
DefaultTheme: `gitea`, DefaultTheme: `gitea`,
Themes: []string{`gitea`, `arc-green`}, Themes: []string{`gitea`, `arc-green`},
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
CustomEmojis: []string{`gitea`},
CustomEmojisMap: map[string]string{"gitea": ":gitea:"},
Notification: struct { Notification: struct {
MinTimeout time.Duration MinTimeout time.Duration
TimeoutStep time.Duration TimeoutStep time.Duration
@ -983,6 +987,10 @@ func NewContext() {
for _, reaction := range UI.Reactions { for _, reaction := range UI.Reactions {
UI.ReactionsMap[reaction] = true UI.ReactionsMap[reaction] = true
} }
UI.CustomEmojisMap = make(map[string]string)
for _, emoji := range UI.CustomEmojis {
UI.CustomEmojisMap[emoji] = ":" + emoji + ":"
}
} }
func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) { func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {

View File

@ -18,6 +18,7 @@ type GeneralRepoSettings struct {
type GeneralUISettings struct { type GeneralUISettings struct {
DefaultTheme string `json:"default_theme"` DefaultTheme string `json:"default_theme"`
AllowedReactions []string `json:"allowed_reactions"` AllowedReactions []string `json:"allowed_reactions"`
CustomEmojis []string `json:"custom_emojis"`
} }
// GeneralAPISettings contains global api settings exposed by it // GeneralAPISettings contains global api settings exposed by it

View File

@ -90,6 +90,9 @@ func NewFuncMap() []template.FuncMap {
"AllowedReactions": func() []string { "AllowedReactions": func() []string {
return setting.UI.Reactions return setting.UI.Reactions
}, },
"CustomEmojis": func() map[string]string {
return setting.UI.CustomEmojisMap
},
"Safe": Safe, "Safe": Safe,
"SafeJS": SafeJS, "SafeJS": SafeJS,
"JSEscape": JSEscape, "JSEscape": JSEscape,

View File

@ -25,6 +25,7 @@ func GetGeneralUISettings(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, api.GeneralUISettings{ ctx.JSON(http.StatusOK, api.GeneralUISettings{
DefaultTheme: setting.UI.DefaultTheme, DefaultTheme: setting.UI.DefaultTheme,
AllowedReactions: setting.UI.Reactions, AllowedReactions: setting.UI.Reactions,
CustomEmojis: setting.UI.CustomEmojis,
}) })
} }

View File

@ -30,6 +30,7 @@
AppVer: '{{AppVer}}', AppVer: '{{AppVer}}',
AppSubUrl: '{{AppSubUrl}}', AppSubUrl: '{{AppSubUrl}}',
AssetUrlPrefix: '{{AssetUrlPrefix}}', AssetUrlPrefix: '{{AssetUrlPrefix}}',
CustomEmojis: {{CustomEmojis}},
UseServiceWorker: {{UseServiceWorker}}, UseServiceWorker: {{UseServiceWorker}},
csrf: '{{.CsrfToken}}', csrf: '{{.CsrfToken}}',
HighlightJS: {{if .RequireHighlightJS}}true{{else}}false{{end}}, HighlightJS: {{if .RequireHighlightJS}}true{{else}}false{{end}},

View File

@ -14481,6 +14481,13 @@
}, },
"x-go-name": "AllowedReactions" "x-go-name": "AllowedReactions"
}, },
"custom_emojis": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "CustomEmojis"
},
"default_theme": { "default_theme": {
"type": "string", "type": "string",
"x-go-name": "DefaultTheme" "x-go-name": "DefaultTheme"

View File

@ -1,8 +1,9 @@
import emojis from '../../../assets/emoji.json'; import emojis from '../../../assets/emoji.json';
const {AssetUrlPrefix} = window.config; const {AssetUrlPrefix} = window.config;
const {CustomEmojis} = window.config;
const tempMap = {gitea: ':gitea:'}; const tempMap = {...CustomEmojis};
for (const {emoji, aliases} of emojis) { for (const {emoji, aliases} of emojis) {
for (const alias of aliases || []) { for (const alias of aliases || []) {
tempMap[alias] = emoji; tempMap[alias] = emoji;
@ -23,8 +24,8 @@ for (const key of emojiKeys) {
// retrieve HTML for given emoji name // retrieve HTML for given emoji name
export function emojiHTML(name) { export function emojiHTML(name) {
let inner; let inner;
if (name === 'gitea') { if (Object.prototype.hasOwnProperty.call(CustomEmojis, name)) {
inner = `<img alt=":${name}:" src="${AssetUrlPrefix}/img/emoji/gitea.png">`; inner = `<img alt=":${name}:" src="${AssetUrlPrefix}/img/emoji/${name}.png">`;
} else { } else {
inner = emojiString(name); inner = emojiString(name);
} }