[Feature] Custom Reactions (#8886)
* add [ui] Reactions * move contend check from form to go functions * use else if * check if reaction is allowed only on react (so previous custom reaction can be still removed) * use $.AllowedReactions in templates * use ctx.Flash.Error * use it there too * add redirection * back to server error because a wrong reaction is a template issue ... * add emoji list link * add docs entry * small wording nit suggestions from @jolheiser - thx * same reactions as github * fix PR reactions * handle error so template JS could check * Add Integrations Test * add REACTIONS setting to cheat-sheet doc page
This commit is contained in:
		
							parent
							
								
									674bc772fb
								
							
						
					
					
						commit
						668eaf95d5
					
				
					 13 changed files with 76 additions and 18 deletions
				
			
		|  | @ -149,6 +149,9 @@ SHOW_USER_EMAIL = true | ||||||
| DEFAULT_THEME = gitea | DEFAULT_THEME = gitea | ||||||
| ; 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. Allow users react with different emoji's | ||||||
|  | : For the whole list look at https://gitea.com/gitea/gitea.com/issues/8 | ||||||
|  | REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes | ||||||
| ; 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 | ||||||
| ; Whether to search within description at repository search on explore page. | ; Whether to search within description at repository search on explore page. | ||||||
|  |  | ||||||
|  | @ -118,6 +118,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. | ||||||
| - `DEFAULT_THEME`: **gitea**: \[gitea, arc-green\]: Set the default theme for the Gitea install. | - `DEFAULT_THEME`: **gitea**: \[gitea, arc-green\]: Set the default theme for the Gitea install. | ||||||
| - `THEMES`:  **gitea,arc-green**: All available themes. Allow users select personalized themes | - `THEMES`:  **gitea,arc-green**: All available themes. Allow users select personalized themes | ||||||
|   regardless of the value of `DEFAULT_THEME`. |   regardless of the value of `DEFAULT_THEME`. | ||||||
|  | - `REACTIONS`: All available reactions. Allow users react with different emoji's. | ||||||
| - `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. | ||||||
|  |  | ||||||
|  | @ -161,6 +161,15 @@ Locales may change between versions, so keeping track of your customized locales | ||||||
| 
 | 
 | ||||||
| To add a custom Readme, add a markdown formatted file (without an `.md` extension) to `custom/options/readme` | To add a custom Readme, add a markdown formatted file (without an `.md` extension) to `custom/options/readme` | ||||||
| 
 | 
 | ||||||
|  | ### Reactions | ||||||
|  | 
 | ||||||
|  | To change reaction emoji's you can set allowed reactions at app.ini | ||||||
|  | ``` | ||||||
|  | [ui] | ||||||
|  | REACTIONS = +1, -1, laugh, confused, heart, hooray, eyes | ||||||
|  | ``` | ||||||
|  | A full list of supported emoji's is at [emoji list](https://gitea.com/gitea/gitea.com/issues/8) | ||||||
|  | 
 | ||||||
| ## Customizing the look of Gitea | ## Customizing the look of Gitea | ||||||
| 
 | 
 | ||||||
| As of version 1.6.0 Gitea has built-in themes. The two built-in themes are, the default theme `gitea`, and a dark theme `arc-green`. To change the look of your Gitea install change the value of `DEFAULT_THEME` in the [ui](https://docs.gitea.io/en-us/config-cheat-sheet/#ui-ui) section of `app.ini` to another one of the available options.   | As of version 1.6.0 Gitea has built-in themes. The two built-in themes are, the default theme `gitea`, and a dark theme `arc-green`. To change the look of your Gitea install change the value of `DEFAULT_THEME` in the [ui](https://docs.gitea.io/en-us/config-cheat-sheet/#ui-ui) section of `app.ini` to another one of the available options.   | ||||||
|  |  | ||||||
|  | @ -194,6 +194,32 @@ func TestIssueCommentClose(t *testing.T) { | ||||||
| 	assert.Equal(t, "Description", val) | 	assert.Equal(t, "Description", val) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestIssueReaction(t *testing.T) { | ||||||
|  | 	defer prepareTestEnv(t)() | ||||||
|  | 	session := loginUser(t, "user2") | ||||||
|  | 	issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") | ||||||
|  | 
 | ||||||
|  | 	req := NewRequest(t, "GET", issueURL) | ||||||
|  | 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	htmlDoc := NewHTMLParser(t, resp.Body) | ||||||
|  | 
 | ||||||
|  | 	req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{ | ||||||
|  | 		"_csrf":   htmlDoc.GetCSRF(), | ||||||
|  | 		"content": "8ball", | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusInternalServerError) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{ | ||||||
|  | 		"_csrf":   htmlDoc.GetCSRF(), | ||||||
|  | 		"content": "eyes", | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusOK) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/unreact"), map[string]string{ | ||||||
|  | 		"_csrf":   htmlDoc.GetCSRF(), | ||||||
|  | 		"content": "eyes", | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusOK) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestIssueCrossReference(t *testing.T) { | func TestIssueCrossReference(t *testing.T) { | ||||||
| 	defer prepareTestEnv(t)() | 	defer prepareTestEnv(t)() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -347,7 +347,7 @@ func (f *CreateCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) | ||||||
| 
 | 
 | ||||||
| // ReactionForm form for adding and removing reaction
 | // ReactionForm form for adding and removing reaction
 | ||||||
| type ReactionForm struct { | type ReactionForm struct { | ||||||
| 	Content string `binding:"Required;In(+1,-1,laugh,confused,heart,hooray)"` | 	Content string `binding:"Required"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Validate validates the fields
 | // Validate validates the fields
 | ||||||
|  |  | ||||||
|  | @ -169,6 +169,7 @@ var ( | ||||||
| 		DefaultShowFullName   bool | 		DefaultShowFullName   bool | ||||||
| 		DefaultTheme          string | 		DefaultTheme          string | ||||||
| 		Themes                []string | 		Themes                []string | ||||||
|  | 		Reactions             []string | ||||||
| 		SearchRepoDescription bool | 		SearchRepoDescription bool | ||||||
| 		UseServiceWorker      bool | 		UseServiceWorker      bool | ||||||
| 
 | 
 | ||||||
|  | @ -198,6 +199,7 @@ var ( | ||||||
| 		MaxDisplayFileSize:  8388608, | 		MaxDisplayFileSize:  8388608, | ||||||
| 		DefaultTheme:        `gitea`, | 		DefaultTheme:        `gitea`, | ||||||
| 		Themes:              []string{`gitea`, `arc-green`}, | 		Themes:              []string{`gitea`, `arc-green`}, | ||||||
|  | 		Reactions:           []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | ||||||
| 		Admin: struct { | 		Admin: struct { | ||||||
| 			UserPagingNum   int | 			UserPagingNum   int | ||||||
| 			RepoPagingNum   int | 			RepoPagingNum   int | ||||||
|  |  | ||||||
|  | @ -673,6 +673,7 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["IssueWatch"] = iw | 	ctx.Data["IssueWatch"] = iw | ||||||
|  | 	ctx.Data["AllowedReactions"] = setting.UI.Reactions | ||||||
| 
 | 
 | ||||||
| 	issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink, | 	issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink, | ||||||
| 		ctx.Repo.Repository.ComposeMetas())) | 		ctx.Repo.Repository.ComposeMetas())) | ||||||
|  | @ -1447,6 +1448,12 @@ func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) { | ||||||
| 
 | 
 | ||||||
| 	switch ctx.Params(":action") { | 	switch ctx.Params(":action") { | ||||||
| 	case "react": | 	case "react": | ||||||
|  | 		if !util.IsStringInSlice(form.Content, setting.UI.Reactions) { | ||||||
|  | 			err := fmt.Errorf("ChangeIssueReaction: '%s' is not an allowed reaction", form.Content) | ||||||
|  | 			ctx.ServerError(err.Error(), err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content) | 		reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Info("CreateIssueReaction: %s", err) | 			log.Info("CreateIssueReaction: %s", err) | ||||||
|  | @ -1542,6 +1549,12 @@ func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) { | ||||||
| 
 | 
 | ||||||
| 	switch ctx.Params(":action") { | 	switch ctx.Params(":action") { | ||||||
| 	case "react": | 	case "react": | ||||||
|  | 		if !util.IsStringInSlice(form.Content, setting.UI.Reactions) { | ||||||
|  | 			err := fmt.Errorf("ChangeIssueReaction: '%s' is not an allowed reaction", form.Content) | ||||||
|  | 			ctx.ServerError(err.Error(), err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content) | 		reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Info("CreateCommentReaction: %s", err) | 			log.Info("CreateCommentReaction: %s", err) | ||||||
|  |  | ||||||
|  | @ -422,6 +422,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["NumCommits"] = compareInfo.Commits.Len() | 	ctx.Data["NumCommits"] = compareInfo.Commits.Len() | ||||||
| 	ctx.Data["NumFiles"] = compareInfo.NumFiles | 	ctx.Data["NumFiles"] = compareInfo.NumFiles | ||||||
|  | 	ctx.Data["AllowedReactions"] = setting.UI.Reactions | ||||||
| 	return compareInfo | 	return compareInfo | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ | ||||||
| 		{{$reactions := .Reactions.GroupByType}} | 		{{$reactions := .Reactions.GroupByType}} | ||||||
| 		{{if $reactions}} | 		{{if $reactions}} | ||||||
| 			<div class="ui attached segment reactions"> | 			<div class="ui attached segment reactions"> | ||||||
| 			{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions }} | 			{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions "AllowedReactions" $.AllowedReactions }} | ||||||
| 			</div> | 			</div> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ | ||||||
| 					{{end}} | 					{{end}} | ||||||
| 						{{if not $.Repository.IsArchived}} | 						{{if not $.Repository.IsArchived}} | ||||||
| 							<div class="ui right actions"> | 							<div class="ui right actions"> | ||||||
| 								{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) }} | 								{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "AllowedReactions" $.AllowedReactions}} | ||||||
| 								{{template "repo/issue/view_content/context_menu" Dict "ctx" $ "item" .Issue "delete" false "diff" false }} | 								{{template "repo/issue/view_content/context_menu" Dict "ctx" $ "item" .Issue "delete" false "diff" false }} | ||||||
| 							</div> | 							</div> | ||||||
| 						{{end}} | 						{{end}} | ||||||
|  | @ -47,7 +47,7 @@ | ||||||
| 					{{$reactions := .Issue.Reactions.GroupByType}} | 					{{$reactions := .Issue.Reactions.GroupByType}} | ||||||
| 					{{if $reactions}} | 					{{if $reactions}} | ||||||
| 						<div class="ui attached segment reactions"> | 						<div class="ui attached segment reactions"> | ||||||
| 							{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions }} | 							{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions "AllowedReactions" $.AllowedReactions}} | ||||||
| 						</div> | 						</div> | ||||||
| 					{{end}} | 					{{end}} | ||||||
| 					{{if .Issue.Attachments}} | 					{{if .Issue.Attachments}} | ||||||
|  |  | ||||||
|  | @ -7,12 +7,15 @@ | ||||||
| 	<div class="menu has-emoji"> | 	<div class="menu has-emoji"> | ||||||
| 		<div class="header">{{ .ctx.i18n.Tr "repo.pick_reaction"}}</div> | 		<div class="header">{{ .ctx.i18n.Tr "repo.pick_reaction"}}</div> | ||||||
| 		<div class="divider"></div> | 		<div class="divider"></div> | ||||||
| 		<div class="item" data-content="+1">:+1:</div> | 		{{range $value := .AllowedReactions}} | ||||||
| 		<div class="item" data-content="-1">:-1:</div> | 			{{if eq $value "hooray"}} | ||||||
| 		<div class="item" data-content="laugh">:laughing:</div> | 				<div class="item" data-content="hooray">:tada:</div> | ||||||
| 		<div class="item" data-content="confused">:confused:</div> | 			{{else if eq $value "laugh"}} | ||||||
| 		<div class="item" data-content="heart">:heart:</div> | 				<div class="item" data-content="laugh">:laughing:</div> | ||||||
| 		<div class="item" data-content="hooray">:tada:</div> | 			{{else}} | ||||||
|  | 				<div class="item" data-content="{{$value}}">:{{$value}}:</div> | ||||||
|  | 			{{end}} | ||||||
|  | 		{{end}} | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| {{end}} | {{end}} | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
| 									{{end}} | 									{{end}} | ||||||
| 								</div> | 								</div> | ||||||
| 							{{end}} | 							{{end}} | ||||||
| 							{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) }} | 							{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "AllowedReactions" $.AllowedReactions}} | ||||||
| 							{{template "repo/issue/view_content/context_menu" Dict "ctx" $ "item" . "delete" true "diff" false }} | 							{{template "repo/issue/view_content/context_menu" Dict "ctx" $ "item" . "delete" true "diff" false }} | ||||||
| 						</div> | 						</div> | ||||||
| 					{{end}} | 					{{end}} | ||||||
|  | @ -55,7 +55,7 @@ | ||||||
| 				{{$reactions := .Reactions.GroupByType}} | 				{{$reactions := .Reactions.GroupByType}} | ||||||
| 				{{if $reactions}} | 				{{if $reactions}} | ||||||
| 					<div class="ui attached segment reactions"> | 					<div class="ui attached segment reactions"> | ||||||
| 						{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions }} | 						{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions "AllowedReactions" $.AllowedReactions}} | ||||||
| 					</div> | 					</div> | ||||||
| 				{{end}} | 				{{end}} | ||||||
| 				{{if .Attachments}} | 				{{if .Attachments}} | ||||||
|  |  | ||||||
|  | @ -2,14 +2,14 @@ | ||||||
| 	<a class="ui label basic{{if $value.HasUser $.ctx.SignedUserID}} blue{{end}}{{if not $.ctx.IsSigned}} disabled{{end}} has-emoji" data-title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ $.ctx.i18n.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}" data-content="{{ $key }}" data-action-url="{{ $.ActionURL }}"> | 	<a class="ui label basic{{if $value.HasUser $.ctx.SignedUserID}} blue{{end}}{{if not $.ctx.IsSigned}} disabled{{end}} has-emoji" data-title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ $.ctx.i18n.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}" data-content="{{ $key }}" data-action-url="{{ $.ActionURL }}"> | ||||||
| 		{{if eq $key "hooray"}} | 		{{if eq $key "hooray"}} | ||||||
| 			:tada: | 			:tada: | ||||||
|  | 		{{else if eq $key "laugh"}} | ||||||
|  | 			:laughing: | ||||||
| 		{{else}} | 		{{else}} | ||||||
| 			{{if eq $key "laugh"}} | 			:{{$key}}: | ||||||
| 				:laughing: |  | ||||||
| 			{{else}} |  | ||||||
| 				:{{$key}}: |  | ||||||
| 			{{end}} |  | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 		{{len $value}} | 		{{len $value}} | ||||||
| 	</a> | 	</a> | ||||||
| {{end}} | {{end}} | ||||||
| {{template "repo/issue/view_content/add_reaction" Dict "ctx" $.ctx "ActionURL" .ActionURL }} | {{if $.AllowedReactions}} | ||||||
|  | 	{{template "repo/issue/view_content/add_reaction" Dict "ctx" $.ctx "ActionURL" .ActionURL "AllowedReactions" $.AllowedReactions}} | ||||||
|  | {{end}} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue