Add custom emoji support (#16004)
parent
aac663e0da
commit
65548359cc
|
@ -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
|
||||||
;;
|
;;
|
||||||
|
|
|
@ -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 New Issue