mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Add custom emoji support (#16004)
This commit is contained in:
		@@ -1029,11 +1029,16 @@ PATH =
 | 
				
			|||||||
;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`.
 | 
					;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`.
 | 
				
			||||||
;THEMES = gitea,arc-green
 | 
					;THEMES = gitea,arc-green
 | 
				
			||||||
;;
 | 
					;;
 | 
				
			||||||
;;All available reactions users can choose on issues/prs and comments.
 | 
					;; 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
 | 
				
			||||||
;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
 | 
				
			||||||
;;
 | 
					;;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>`)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user