diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-11-20 00:34:38 +0300 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2015-11-20 03:50:58 +0300 |
commit | 48e86ec6105b879dffe30408f265b478ec21b706 (patch) | |
tree | 670c23492cc24f56887ca8ae4682fb5b7fef61f4 /app | |
parent | 978f0bac8a4ea5cd695e43d35d88f767936f1399 (diff) |
Merge branch 'emoji_votes' into 'master'
Award Emoji
This it first iteration of award emoji feature.
We have plan to extend emoji picker by the next release.
For now, you can add award by clicking to the emoji picker or posting a regular comment with emoji like ":+1:" and any other. You can post not only emoji that listed in the emoji picker.
See merge request !1825
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/awards_handler.coffee | 91 | ||||
-rw-r--r-- | app/assets/javascripts/notes.js.coffee | 9 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/issuable.scss | 68 | ||||
-rw-r--r-- | app/controllers/projects/issues_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/projects/merge_requests_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/projects/notes_controller.rb | 26 | ||||
-rw-r--r-- | app/finders/notes_finder.rb | 4 | ||||
-rw-r--r-- | app/helpers/issues_helper.rb | 25 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 49 | ||||
-rw-r--r-- | app/models/note.rb | 48 | ||||
-rw-r--r-- | app/services/notes/create_service.rb | 17 | ||||
-rw-r--r-- | app/services/notification_service.rb | 1 | ||||
-rw-r--r-- | app/views/projects/issues/_discussion.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/issues/_issue.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/merge_requests/_merge_request.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/notes/_note.html.haml | 20 | ||||
-rw-r--r-- | app/views/votes/_votes_block.html.haml | 42 | ||||
-rw-r--r-- | app/views/votes/_votes_inline.html.haml | 9 |
18 files changed, 276 insertions, 143 deletions
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee new file mode 100644 index 00000000000..ae42e390c43 --- /dev/null +++ b/app/assets/javascripts/awards_handler.coffee @@ -0,0 +1,91 @@ +class @AwardsHandler + constructor: (@post_emoji_url, @noteable_type, @noteable_id) -> + + addAward: (emoji) -> + @postEmoji emoji, => + @addAwardToEmojiBar(emoji) + + addAwardToEmojiBar: (emoji, custom_path = '') -> + if @exist(emoji) + if @isActive(emoji) + @decrementCounter(emoji) + else + counter = @findEmojiIcon(emoji).siblings(".counter") + counter.text(parseInt(counter.text()) + 1) + counter.parent().addClass("active") + @addMeToAuthorList(emoji) + else + @createEmoji(emoji, custom_path) + + exist: (emoji) -> + @findEmojiIcon(emoji).length > 0 + + isActive: (emoji) -> + @findEmojiIcon(emoji).parent().hasClass("active") + + decrementCounter: (emoji) -> + counter = @findEmojiIcon(emoji).siblings(".counter") + + if parseInt(counter.text()) > 1 + counter.text(parseInt(counter.text()) - 1) + counter.parent().removeClass("active") + @removeMeFromAuthorList(emoji) + else + award = counter.parent() + award.tooltip("destroy") + award.remove() + + removeMeFromAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors = _.without(authors, "me").join(", ") + award_block.attr("title", authors) + @resetTooltip(award_block) + + addMeToAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors.push("me") + award_block.attr("title", authors.join(", ")) + @resetTooltip(award_block) + + resetTooltip: (award) -> + award.tooltip("destroy") + + # "destroy" call is asynchronous, this is why we need to set timeout. + setTimeout (-> + award.tooltip() + ), 200 + + + createEmoji: (emoji, custom_path) -> + nodes = [] + nodes.push("<div class='award active' title='me'>") + nodes.push("<div class='icon' data-emoji='" + emoji + "'>") + nodes.push(@getImage(emoji, custom_path)) + nodes.push("</div>") + nodes.push("<div class='counter'>1") + nodes.push("</div></div>") + + $(".awards-controls").before(nodes.join("\n")) + + $(".award").tooltip() + + getImage: (emoji, custom_path) -> + if custom_path + $(".awards-menu li").first().html().replace(/emoji\/.*\.png/, custom_path) + else + $("li[data-emoji='" + emoji + "']").html() + + + postEmoji: (emoji, callback) -> + $.post @post_emoji_url, { note: { + note: emoji + noteable_type: @noteable_type + noteable_id: @noteable_id + }},(data) -> + if data.ok + callback.call() + + findEmojiIcon: (emoji) -> + $(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ea75c656bcc..7de7632201d 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -113,13 +113,16 @@ class @Notes renderNote: (note) -> # render note if it not present in loaded list # or skip if rendered - if @isNewNote(note) + if @isNewNote(note) && !note.award @note_ids.push(note.id) $('ul.main-notes-list'). append(note.html). syntaxHighlight() @initTaskList() + if note.award + awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) + ### Check if note does not exists on page ### @@ -255,7 +258,6 @@ class @Notes ### addNote: (xhr, note, status) => @renderNote(note) - @updateVotes() ### Called in response to the new note form being submitted @@ -473,9 +475,6 @@ class @Notes form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) - updateVotes: -> - true - ### Called after an attachment file has been selected. diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index abc27a19e32..3a08ee70bc7 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -101,3 +101,71 @@ background-color: $background-color; } } + +.awards { + @include clearfix; + line-height: 34px; + margin: 2px 0; + + .award { + @include border-radius(5px); + + border: 1px solid; + padding: 0px 10px; + float: left; + margin: 0 5px; + border-color: $border-color; + cursor: pointer; + + &.active { + border-color: $border-gray-light; + background-color: $gray-light; + + .counter { + font-weight: bold; + } + } + + .icon { + float: left; + margin-right: 10px; + } + + .counter { + float: left; + } + } + + .awards-controls { + margin-left: 10px; + float: left; + + .add-award { + font-size: 24px; + color: $gl-gray; + position: relative; + top: 2px; + + &:hover, + &:link { + text-decoration: none; + } + } + + .awards-menu { + padding: $gl-padding; + min-width: 214px; + + > li { + margin: 5px; + } + } + } + + .awards-menu{ + li { + float: left; + margin: 3px; + } + } +} diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e74c2905e48..5250a0f5e67 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,7 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.with_associations.fresh + @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue respond_with(@issue) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 188f0cc4cea..6378a1f56b0 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -254,7 +254,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @notes = @merge_request.mr_and_commit_notes.inc_author.fresh + @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @discussions = Note.discussions_from_notes(@notes) @noteable = @merge_request diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 41cd08c93c6..263b8b8d94e 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment] + before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] def index current_fetched_at = Time.now.to_i @@ -58,6 +58,27 @@ class Projects::NotesController < Projects::ApplicationController end end + def award_toggle + noteable = note_params[:noteable_type] == "issue" ? Issue : MergeRequest + noteable = noteable.find_by!(id: note_params[:noteable_id], project: project) + + data = { + author: current_user, + is_award: true, + note: note_params[:note] + } + + note = noteable.notes.find_by(data) + + if note + note.destroy + else + Notes::CreateService.new(project, current_user, note_params).execute + end + + render json: { ok: true } + end + private def note @@ -111,6 +132,9 @@ class Projects::NotesController < Projects::ApplicationController id: note.id, discussion_id: note.discussion_id, html: note_to_html(note), + award: note.is_award, + emoji_path: note.is_award ? ::AwardEmoji.path_to_emoji_image(note.note) : "", + note: note.note, discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) } diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index ab252821b52..fa4c635f55c 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,9 +12,9 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).not_inline when "issue" - project.issues.find(target_id).notes.inc_author + project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index beb083d82dc..2c791aa5682 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -87,6 +87,31 @@ module IssuesHelper merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') end + def url_to_emoji(name) + emoji_path = ::AwardEmoji.path_to_emoji_image(name) + url_to_image(emoji_path) + end + + def emoji_author_list(notes, current_user) + list = notes.map do |note| + note.author == current_user ? "me" : note.author.username + end + + list.join(", ") + end + + def emoji_list + ::AwardEmoji::EMOJI_LIST + end + + def note_active_class(notes, current_user) + if current_user && notes.pluck(:author_id).include?(current_user.id) + "active" + else + "" + end + end + # Required for Gitlab::Markdown::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 492a026add9..91da6797df7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -89,41 +89,6 @@ module Issuable opened? || reopened? end - # - # Votes - # - - # Return the number of -1 comments (downvotes) - def downvotes - filter_superceded_votes(notes.select(&:downvote?), notes).size - end - - def downvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 - upvotes_in_percent - end - end - - # Return the number of +1 comments (upvotes) - def upvotes - filter_superceded_votes(notes.select(&:upvote?), notes).size - end - - def upvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 / votes_count * upvotes - end - end - - # Return the total number of votes - def votes_count - upvotes + downvotes - end - def subscribed?(user) subscription = subscriptions.find_by_user_id(user.id) @@ -183,18 +148,4 @@ module Issuable def notes_with_associations notes.includes(:author, :project) end - - private - - def filter_superceded_votes(votes, notes) - filteredvotes = [] + votes - - votes.each do |vote| - if vote.superceded?(notes) - filteredvotes.delete(vote) - end - end - - filteredvotes - end end diff --git a/app/models/note.rb b/app/models/note.rb index 0b3aa30abd7..e30be444eb5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -40,16 +40,20 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true validates :note, :project, presence: true + validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } + validates :author, presence: true mount_uploader :attachment, AttachmentUploader # Scopes + scope :awards, ->{ where(is_award: true) } + scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } @@ -97,6 +101,12 @@ class Note < ActiveRecord::Base def search(query) where("LOWER(note) like :query", query: "%#{query.downcase}%") end + + def grouped_awards + awards.select(:note).distinct.map do |note| + [ note.note, where(note: note.note) ] + end + end end def cross_reference? @@ -288,44 +298,6 @@ class Note < ActiveRecord::Base nil end - DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) - - # Check if the note is a downvote - def downvote? - votable? && note.start_with?(*DOWNVOTES) - end - - UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) - - # Check if the note is an upvote - def upvote? - votable? && note.start_with?(*UPVOTES) - end - - def superceded?(notes) - return false unless vote? - - notes.each do |note| - next if note == self - - if note.vote? && - self[:author_id] == note[:author_id] && - self[:created_at] <= note[:created_at] - return true - end - end - - false - end - - def vote? - upvote? || downvote? - end - - def votable? - for_issue? || (for_merge_request? && !for_diff_line?) - end - # Mentionable override. def gfm_reference(from_project = nil) noteable.gfm_reference(from_project) diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2001dc89c33..25a985df4d8 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,11 +5,16 @@ module Notes note.author = current_user note.system = false + if contains_emoji_only?(params[:note]) + note.is_award = true + note.note = emoji_name(params[:note]) + end + if note.save notification_service.new_note(note) - # Skip system notes, like status changes and cross-references. - unless note.system + # Skip system notes, like status changes and cross-references and awards + unless note.system || note.is_award event_service.leave_note(note, note.author) note.create_cross_references! execute_hooks(note) @@ -28,5 +33,13 @@ module Notes note.project.execute_hooks(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks) end + + def contains_emoji_only?(note) + note =~ /\A:?[-_+[:alnum:]]*:?\s?\z/ + end + + def emoji_name(note) + note.match(/\A:?([-_+[:alnum:]]*):?\s?/)[1] + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 6ed6ebca587..dc78f85105c 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -102,6 +102,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true + return true if note.is_award target = note.noteable diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index c5fd863ae99..020952dd001 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -7,7 +7,7 @@ = render 'shared/show_aside' -.gray-content-block.second-block +.gray-content-block.second-block.oneline-block .row .col-md-9 .votes-holder.pull-right diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 55ce912829d..d7657ee7e40 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -29,8 +29,6 @@ .issue-info = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - - if issue.votes_count > 0 - = render 'votes/votes_inline', votable: issue - if issue.milestone %span diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c5234c0618c..83e8ad11989 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -34,8 +34,6 @@ .merge-request-info = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe - - if merge_request.votes_count > 0 - = render 'votes/votes_inline', votable: merge_request - if merge_request.milestone_id? %span diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index efa7dd01cc2..dd0abc8c746 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -35,26 +35,6 @@ - if note.updated_by && note.updated_by != note.author by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - - if note.superceded?(@notes) - - if note.upvote? - %span.vote.upvote.label.label-gray.strikethrough - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-gray.strikethrough - = icon('thumbs-down') - \-1 - - else - - if note.upvote? - %span.vote.upvote.label.label-success - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-danger - = icon('thumbs-down') - \-1 - - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 36ea6742064..7eb27c12d33 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,10 +1,32 @@ -.votes.votes-block - .btn-group - - unless votable.upvotes.zero? - .btn.btn-sm.disabled.cgreen - %i.fa.fa-thumbs-up - = votable.upvotes - - unless votable.downvotes.zero? - .btn.btn-sm.disabled.cred - %i.fa.fa-thumbs-down - = votable.downvotes +.awards.votes-block + - votable.notes.awards.grouped_awards.each do |emoji, notes| + .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} + .icon{"data-emoji" => "#{emoji}"} + = image_tag url_to_emoji(emoji), height: "20px", width: "20px" + .counter + = notes.count + + - if current_user + .dropdown.awards-controls + %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"} + = icon('smile-o') + %ul.dropdown-menu.awards-menu + - emoji_list.each do |emoji| + %li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px" + +- if current_user + :coffeescript + post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" + noteable_type = "#{votable.class.name.underscore}" + noteable_id = "#{votable.id}" + window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id) + + $(".awards-menu li").click (e)-> + emoji = $(this).data("emoji") + awards_handler.addAward(emoji) + + $(".awards").on "click", ".award", (e)-> + emoji = $(this).find(".icon").data("emoji") + awards_handler.addAward(emoji) + + $(".award").tooltip() diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml deleted file mode 100644 index 2cb3ae04e1a..00000000000 --- a/app/views/votes/_votes_inline.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.votes.votes-inline - - unless votable.upvotes.zero? - %span.upvotes.cgreen - + #{votable.upvotes} - - unless votable.downvotes.zero? - \/ - - unless votable.downvotes.zero? - %span.downvotes.cred - \- #{votable.downvotes} |