Add custom emoji support (#16004)
This commit is contained in:
		
							parent
							
								
									aac663e0da
								
							
						
					
					
						commit
						65548359cc
					
				
					 11 changed files with 57 additions and 23 deletions
				
			
		|  | @ -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); | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue