Mijeong Park Mijeong Park 2018-02-13
Merge pull request #2.목록-상세 화면(list-detail view) 개선 from labs/feature/issue-hunting-for-v1.9
Reviewed-by: Mijeong Park
@bc0282c2d6a4628741674205dc46d05997e9fb4a
app/assets/stylesheets/less/_common.less
--- app/assets/stylesheets/less/_common.less
+++ app/assets/stylesheets/less/_common.less
@@ -300,3 +300,9 @@
 .hideFromDisplayOnly {
     display: none;
 }
+
+.fixed-height-my-issues-list {
+    line-height: 36px;
+}
+
+.dimgray { color:dimgray; }
(No newline at end of file)
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -3042,7 +3042,7 @@
         }
 
         .event {
-            padding: 10px 0 10px 55px;
+            padding: 2px 0 2px 55px;
             font-size: 1em;
             line-height: 30px;
 
@@ -3073,6 +3073,17 @@
                 &.sharer-deleted {
                     background-color: #FFCCBC;
                     color: #F4511E;
+                }
+                &.label-added {
+                    background-color: #B2EBF2;
+                    color: #0097A7;
+                }
+                &.label-deleted {
+                    background-color: #FFCCBC;
+                    color: #F4511E;
+                }
+                &.milestone-changed {
+                    background-color: #0088cc;
                 }
              }
 
@@ -3673,6 +3684,7 @@
         line-height: 20px;
         font-size:12px;
         color:#999;
+        overflow: hidden;
 
         .infos-item {
             margin-right:6px;
@@ -3811,7 +3823,6 @@
 .mileston-tag {
     max-width: 135px;
     text-overflow: ellipsis;
-    white-space: nowrap;
     overflow: hidden;
     color: #2196f3;
     font-size:11px;
@@ -7012,7 +7023,7 @@
     -moz-shadow: inset 0 0 5px 5px #222;
     //box-shadow: inset 0 0 5px 5px #222;
     box-shadow: 2px 2px 8px #000;
-    background: #FFF url('/assets/images/loading-gif-2.gif') no-repeat center;
+    background: #FFF url('@{base-image-path}/loading-gif-2.gif') no-repeat center;
 }
 
 .myissues-search-input {
@@ -7034,11 +7045,56 @@
 
 .my-issues {
     .post-item {
-        padding: 6px 10px;
+        padding: 2px 10px;
+        color: #999999;
       
         .title-wrap {
             margin-top: 2px;
+            white-space: normal;
+            overflow: auto;
+            display:table;
+
+            .title-cell {
+                display:table-cell;
+                vertical-align:middle;
+            }
+
+            .item-count-groups {
+                font-size: 10px;
+            }
+            .title {
+                font-size: 14px;
+                font-weight: 400;
+            }
         }
+
+        .author {
+            display:table;
+
+            .author-cell {
+                display:table-cell;
+                vertical-align:middle;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+            }
+        }
+        .meta {
+            display:table;
+
+            .meta-cell {
+                display:table-cell;
+                vertical-align:middle;
+            }
+        }
+
+        .post-id {
+            color:#999;
+            margin-right:5px;
+            font-size: 12px;
+            font-weight: normal;
+        }
+
         .infos {
             margin-top: 4px;
         }
@@ -7101,3 +7157,18 @@
         padding: 1px 8px;
     }
 }
+
+.project-name-in-my-issues {
+    display: flex !important;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    flex-grow: 1;
+    justify-content: space-between;
+    align-items: center;
+
+    .project-name {
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+    }
+}
app/assets/stylesheets/less/_yobiUI.less
--- app/assets/stylesheets/less/_yobiUI.less
+++ app/assets/stylesheets/less/_yobiUI.less
@@ -581,8 +581,8 @@
 .yobiToasts {
     position: fixed;
     overflow: hidden;
-    left: 20px !important;
-    top: 25px !important;
+    right: 20px !important;
+    bottom: 25px !important;
     margin: 10px;
     z-index:9999;
 
app/controllers/AttachmentApp.java
--- app/controllers/AttachmentApp.java
+++ app/controllers/AttachmentApp.java
@@ -1,8 +1,9 @@
 /**
- * Yona, Project Hosting SW
- *
- * Copyright 2016 the original author or authors.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
 package controllers;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -67,7 +68,7 @@
 
         User uploader = findUploader(request().body().asMultipartFormData().asFormUrlEncoded());
         if (uploader.isAnonymous()) {
-            uploader = User.findByUserToken(request().getHeader("Yona-Token"));
+            uploader = User.findByUserToken(User.extractUserTokenFromRequestHeader(request()));
         }
 
         // Anonymous cannot upload a file.
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -1,8 +1,9 @@
 /**
- * Yona, Project Hosting SW
- *
- * Copyright 2016 the original author or authors.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
 package controllers;
 
 import actions.NullProjectCheckAction;
@@ -436,6 +437,7 @@
         return new Runnable() {
             @Override
             public void run() {
+                posting.updatedDate = JodaDateUtil.now();
                 comment.posting = posting;
             }
         };
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -1,7 +1,7 @@
 /**
  * Yona, 21st Century Project Hosting SW
  * <p>
- * Copyright Yona & Yobi Authors & NAVER Corp.
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
  * https://yona.io
  **/
 package controllers;
@@ -385,49 +385,11 @@
                 continue;
             }
 
-            boolean assigneeChanged = false;
-            User oldAssignee = null;
-            if (issueMassUpdate.assignee != null) {
-                if(hasAssignee(issue)) {
-                    oldAssignee = issue.assignee.user;
-                }
-                Assignee newAssignee;
-                if (issueMassUpdate.assignee.isAnonymous()) {
-                    newAssignee = null;
-                } else {
-                    newAssignee = Assignee.add(issueMassUpdate.assignee.id, project.id);
-                }
-                assigneeChanged = !issue.assignedUserEquals(newAssignee);
-                issue.assignee = newAssignee;
-            }
-
-            boolean stateChanged = false;
-            State oldState = null;
-            if ((issueMassUpdate.state != null) && (issue.state != issueMassUpdate.state)) {
-                stateChanged = true;
-                oldState = issue.state;
-                issue.state = issueMassUpdate.state;
-            }
-
-            if (issueMassUpdate.milestone != null) {
-                if(issueMassUpdate.milestone.isNullMilestone()) {
-                    issue.milestone = null;
-                } else {
-                    issue.milestone = issueMassUpdate.milestone;
-                }
-            }
-
-            if (issueMassUpdate.attachingLabelIds != null) {
-                for (Long labelId : issueMassUpdate.attachingLabelIds) {
-                    issue.labels.add(IssueLabel.finder.byId(labelId));
-                }
-            }
-
-            if (issueMassUpdate.detachingLabelIds != null) {
-                for (Long labelId : issueMassUpdate.detachingLabelIds) {
-                    issue.labels.remove(IssueLabel.finder.byId(labelId));
-                }
-            }
+            updateAssigneeIfChanged(issueMassUpdate.assignee, project, issue);
+            updateStateIfChanged(issueMassUpdate.state, issue);
+            updateMilestoneIfChanged(issueMassUpdate.milestone, issue);
+            updateLabelIfChanged(issueMassUpdate.attachingLabelIds,
+                    issueMassUpdate.detachingLabelIds, issue);
 
             if (issueMassUpdate.isDueDateChanged) {
                 issue.dueDate = JodaDateUtil.lastSecondOfDay(issueMassUpdate.dueDate);
@@ -436,15 +398,6 @@
             issue.updatedDate = JodaDateUtil.now();
             issue.update();
             updatedItems++;
-
-            if(assigneeChanged) {
-                NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, issue);
-                IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
-            }
-            if(stateChanged) {
-                NotificationEvent notiEvent = NotificationEvent.afterStateChanged(oldState, issue);
-                IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
-            }
         }
 
         if (updatedItems == 0 && rejectedByPermission > 0) {
@@ -469,6 +422,106 @@
         }
     }
 
+    private static void updateLabelIfChanged(List<Long> attachingLabelIds, List<Long> detachingLabelIds,
+                                             Issue issue) {
+        boolean isLabelChanged = false;
+        StringBuilder addedLabels = new StringBuilder();
+        StringBuilder deletedLabels = new StringBuilder();
+
+        if (attachingLabelIds != null) {
+            for (Long labelId : attachingLabelIds) {
+                IssueLabel label = IssueLabel.finder.byId(labelId);
+                issue.labels.add(label);
+                isLabelChanged = true;
+                addedLabels.append(label.category.name).append(" - ").append(label.name);
+            }
+        }
+
+        if (detachingLabelIds != null) {
+            for (Long labelId : detachingLabelIds) {
+                IssueLabel label = IssueLabel.finder.byId(labelId);
+                issue.labels.remove(label);
+                isLabelChanged = true;
+                deletedLabels.append(label.category.name).append(" - ").append(label.name);
+            }
+        }
+
+        if(isLabelChanged) {
+            NotificationEvent notiEvent = NotificationEvent.afterIssueLabelChanged(
+                    addedLabels.toString(),
+                    deletedLabels.toString(),
+                    issue);
+            IssueEvent.addFromNotificationEventWithoutSkipEvent(notiEvent, issue, UserApp.currentUser().loginId);
+        }
+    }
+
+    private static void updateMilestoneIfChanged(Milestone newMilestone, Issue issue) {
+
+        Long oldMilestoneId = issue.milestoneId();
+
+        if (!isMilestoneChanged(newMilestone, issue.milestone)) {
+            return;
+        }
+
+        if(newMilestone.isNullMilestone()) {
+            issue.milestone = null;
+        } else {
+            issue.milestone = newMilestone;
+        }
+        NotificationEvent notiEvent = NotificationEvent.afterMilestoneChanged(oldMilestoneId, issue);
+        IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
+    }
+
+    private static boolean isMilestoneChanged(Milestone newMilestone, Milestone oldMilestone) {
+        if (newMilestone == null) {
+            return false;
+        }
+
+        if (oldMilestone != null && oldMilestone.id.equals(newMilestone.id)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private static void updateStateIfChanged(State newState, Issue issue) {
+        boolean stateChanged = false;
+        State oldState = null;
+        if ((newState != null) && (issue.state != newState)) {
+            stateChanged = true;
+            oldState = issue.state;
+            issue.state = newState;
+        }
+        if(stateChanged) {
+            NotificationEvent notiEvent = NotificationEvent.afterStateChanged(oldState, issue);
+            IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
+        }
+    }
+
+    private static void updateAssigneeIfChanged(User assignee, Project project, Issue issue) {
+        boolean assigneeChanged = false;
+        User oldAssignee = null;
+
+        if (assignee != null) {
+            if(hasAssignee(issue)) {
+                oldAssignee = issue.assignee.user;
+            }
+            Assignee newAssignee;
+            if (assignee.isAnonymous()) {
+                newAssignee = null;
+            } else {
+                newAssignee = Assignee.add(assignee.id, project.id);
+            }
+            assigneeChanged = !issue.assignedUserEquals(newAssignee);
+            issue.assignee = newAssignee;
+        }
+
+        if(assigneeChanged) {
+            NotificationEvent notiEvent = NotificationEvent.afterAssigneeChanged(oldAssignee, issue);
+            IssueEvent.addFromNotificationEvent(notiEvent, issue, UserApp.currentUser().loginId);
+        }
+    }
+
     @Transactional
     @IsCreatable(ResourceType.ISSUE_POST)
     public static Result newIssue(String ownerName, String projectName) {
app/controllers/OrganizationApp.java
--- app/controllers/OrganizationApp.java
+++ app/controllers/OrganizationApp.java
@@ -1,8 +1,9 @@
 /**
- * Yona, Project Hosting SW
- *
- * Copyright 2017 the original author or authors.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
 package controllers;
 
 import com.avaje.ebean.ExpressionList;
@@ -68,6 +69,10 @@
         return ok(group_pullrequest_list.render("title.pullrequest",  organization, page, condition, category));
     }
 
+    @AnonymousCheck(requiresLogin = false, displaysFlashMessage = true)
+    public static Result organizationClosedPullRequests(String organizationName) {
+        return organizationPullRequests(organizationName, "closed");
+    }
 
     /**
      * show New Group page
app/controllers/ProjectApp.java
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
@@ -1,7 +1,7 @@
 /**
  * Yona, 21st Century Project Hosting SW
  * <p>
- * Copyright Yona & Yobi Authors & NAVER Corp.
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
  * https://yona.io
  **/
 package controllers;
@@ -634,7 +634,6 @@
         return redirect(url);
     }
 
-    @Transactional
     @AnonymousCheck(requiresLogin = true, displaysFlashMessage = true)
     public static synchronized Result acceptTransfer(Long id, String confirmKey) throws IOException, ServletException {
         ProjectTransfer pt = ProjectTransfer.findValidOne(id);
@@ -653,15 +652,25 @@
 
         // Change the project's name and move the repository.
         String newProjectName = Project.newProjectName(pt.destination, project.name);
-        PlayRepository repository = RepositoryService.getRepository(project);
-        repository.move(project.owner, project.name, pt.destination, newProjectName);
 
-        User newOwnerUser = User.findByLoginId(pt.destination);
-        Organization newOwnerOrg = Organization.findByName(pt.destination);
+        // Following three local variables are used for bottom of this method
+        String originalProjectOwner = project.owner;
+        String originalProjectName = project.name;
+        String destinationOwner = pt.destination;
+        Long senderId = pt.sender.id;
+
+        disableProjectTransferLink(pt, project, newProjectName);
+        PlayRepository repository = RepositoryService.getRepository(project);
+
+        // intentionally placed to the last of method
+        repository.move(originalProjectOwner, originalProjectName, destinationOwner, newProjectName);
+
+        User newOwnerUser = User.findByLoginId(destinationOwner);
+        Organization newOwnerOrg = Organization.findByName(destinationOwner);
 
         // Change the project's information.
         project.recordRenameOrTransferHistoryIfLastChangePassed24HoursFrom(project);
-        project.owner = pt.destination;
+        project.owner = destinationOwner;
         project.name = newProjectName;
         if (newOwnerOrg != null) {
             project.organization = newOwnerOrg;
@@ -671,25 +680,27 @@
         project.update();
 
         // Change roles.
-        if (ProjectUser.isManager(pt.sender.id, project.id)) {
-            ProjectUser.assignRole(pt.sender.id, project.id, RoleType.MEMBER);
+        if (ProjectUser.isManager(senderId, project.id)) {
+            ProjectUser.assignRole(senderId, project.id, RoleType.MEMBER);
         }
         if (!newOwnerUser.isAnonymous()) {
             ProjectUser.assignRole(newOwnerUser.id, project.id, RoleType.MANAGER);
         }
 
-        // Change the tranfer's status to be accepted.
-        pt.newProjectName = newProjectName;
-        pt.accepted = true;
-        pt.update();
-
-        // If the opposite request is exists, delete it.
-        ProjectTransfer.deleteExisting(project, pt.sender, pt.destination);
         CacheStore.refreshProjectMap();
 
         return redirect(routes.ProjectApp.project(project.owner, project.name));
     }
 
+    private static void disableProjectTransferLink(ProjectTransfer pt, Project project, String newProjectName) {
+        // Change the tranfer's status to be accepted.
+        pt.newProjectName = newProjectName;
+        pt.accepted = true;
+
+        // If the opposite request is exists, delete it.
+        ProjectTransfer.deleteExisting(project, pt.sender, pt.destination);
+    }
+
     @IsAllowed(Operation.UPDATE)
     public static Result changeVCSForm(String ownerId, String projectName) {
         Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
app/models/AbstractPosting.java
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
@@ -254,12 +254,6 @@
         actualWatchers.addAll(baseWatchers);
 
         actualWatchers.add(getAuthor());
-        for (Comment c : getComments()) {
-            User user = User.find.byId(c.authorId);
-            if (user != null) {
-                actualWatchers.add(user);
-            }
-        }
 
         return Watch.findActualWatchers(actualWatchers, asResource(), allowedWatchersOnly);
     }
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -135,6 +135,13 @@
         return ((assignee != null && assignee.user != null) ? assignee.user.name : null);
     }
 
+    public Long milestoneId() {
+        if (milestone == null) {
+            return Milestone.NULL_MILESTONE_ID;
+        }
+        return milestone.id;
+    }
+
     public boolean hasAssignee() {
         return (assignee != null && assignee.user != null);
     }
@@ -248,6 +255,7 @@
                 Messages.get("issue.label"),
                 Messages.get("issue.createdDate"),
                 Messages.get("issue.dueDate"),
+                Messages.get("milestone"),
                 "URL",
                 Messages.get("common.comment"),
                 Messages.get("common.comment.author"),
@@ -264,6 +272,7 @@
 
             lineNumber++;
             int columnPos = 0;
+            String milestoneName = issue.milestone != null ? issue.milestone.title : "";
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.getNumber().toString(), bodyCellFormat));
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.state.toString(), bodyCellFormat));
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, issue.title, bodyCellFormat));
@@ -272,6 +281,7 @@
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, getIssueLabels(issue), bodyCellFormat));
             sheet.addCell(new jxl.write.DateTime(columnPos++, lineNumber, issue.createdDate, dateCellFormat));
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, JodaDateUtil.geYMDDate(issue.dueDate), bodyCellFormat));
+            sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, milestoneName, bodyCellFormat));
             sheet.addCell(new jxl.write.Label(columnPos++, lineNumber, controllers.routes.IssueApp.issue(issue.project.owner, issue.project.name, issue.number).toString(), bodyCellFormat));
             if (comments.size() > 0) {
                 for (int j = 0; j < comments.size(); j++) {
@@ -691,4 +701,11 @@
     public List<IssueSharer> getSortedSharer() {
         return new ArrayList<>(sharers);
     }
+
+    public static int getCountOfMentionedOpenIssues(Long userId) {
+        return finder.where()
+                .in("id", Mention.getMentioningIssueIds(userId))
+                .eq("state", State.OPEN)
+                .findRowCount();
+    }
 }
app/models/Mention.java
--- app/models/Mention.java
+++ app/models/Mention.java
@@ -1,23 +1,10 @@
 /**
- * Yobi, Project Hosting SW
- *
- * Copyright 2014 NAVER Corp.
- * http://yobi.io
- *
- * @author Yi EungJun
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
+
 package models;
 
 import models.enumeration.ResourceType;
@@ -27,7 +14,13 @@
 import javax.persistence.Entity;
 import javax.persistence.Id;
 import javax.persistence.ManyToOne;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+
+import static models.enumeration.ResourceType.ISSUE_COMMENT;
+import static models.enumeration.ResourceType.ISSUE_POST;
 
 @Entity
 public class Mention extends Model {
@@ -73,4 +66,37 @@
             mention.save();
         }
     }
+
+    public static List<Long> getMentioningIssueIds(Long mentionUserId) {
+        Set<Long> ids = new HashSet<>();
+        Set<Long> commentIds = new HashSet<>();
+
+        for (Mention mention : Mention.find.where()
+                .eq("user.id", mentionUserId)
+                .in("resourceType", ISSUE_POST, ISSUE_COMMENT)
+                .findList()) {
+
+            switch (mention.resourceType) {
+                case ISSUE_POST:
+                    ids.add(Long.valueOf(mention.resourceId));
+                    break;
+                case ISSUE_COMMENT:
+                    commentIds.add(Long.valueOf(mention.resourceId));
+                    break;
+                default:
+                    play.Logger.warn("'" + mention.resourceType + "' is not supported.");
+                    break;
+            }
+        }
+
+        if (!commentIds.isEmpty()) {
+            for (IssueComment comment : IssueComment.find.where()
+                    .idIn(new ArrayList<>(commentIds))
+                    .findList()) {
+                ids.add(comment.issue.id);
+            }
+        }
+
+        return new ArrayList<>(ids);
+    }
 }
app/models/NotificationEvent.java
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
@@ -47,6 +47,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static models.Watch.findWatchers;
 import static models.enumeration.EventType.*;
 
 @Entity
@@ -133,6 +134,8 @@
                 } else {
                     return Messages.get(lang, "notification.issue.assigned", newValue);
                 }
+            case ISSUE_MILESTONE_CHANGED:
+                return Messages.get(lang, "notification.milestone.changed", newValue);
             case NEW_ISSUE:
             case NEW_POSTING:
             case NEW_COMMENT:
@@ -197,9 +200,17 @@
                     return Messages.get(lang, "notification.type.issue.moved", oldValue, newValue);
             case ISSUE_SHARER_CHANGED:
                 if (StringUtils.isNotBlank(newValue)) {
-                    return Messages.get(lang, "notification.issue.sharer.added", User.findByLoginId(newValue).getDisplayName());
+                    User user = User.findByLoginId(newValue);
+                    return Messages.get(lang, "notification.issue.sharer.added", user.getDisplayName(user));
                 } else if (StringUtils.isNotBlank(oldValue)) {
                     return Messages.get(lang, "notification.issue.sharer.deleted");
+                }
+            case ISSUE_LABEL_CHANGED:
+                if (StringUtils.isNotBlank(newValue)) {
+                    User user = User.findByLoginId(newValue);
+                    return Messages.get(lang, "notification.issue.label.added", user.getDisplayName(user));
+                } else if (StringUtils.isNotBlank(oldValue)) {
+                    return Messages.get(lang, "notification.issue.label.deleted");
                 }
             default:
                 play.Logger.error("Unknown event message: " + this);
@@ -714,7 +725,7 @@
         NotificationEvent notiEvent = createFrom(author, comment);
         notiEvent.title = formatReplyTitle(post);
         notiEvent.eventType = eventType;
-        Set<User> receivers = getReceivers(post, author);
+        Set<User> receivers = getCommentReceivers(comment, author);
         receivers.addAll(getMentionedUsers(comment.contents));
         receivers.remove(author);
         notiEvent.receivers = receivers;
@@ -804,12 +815,9 @@
 
         NotificationEvent notiEvent = createFromCurrentUser(issue);
 
-        Set<User> receivers = getReceivers(issue);
+        Set<User> receivers = getReceiversWhenAssigneeChanged(oldAssignee, issue);
         if(oldAssignee != null) {
             notiEvent.oldValue = oldAssignee.loginId;
-            if(!oldAssignee.loginId.equals(UserApp.currentUser().loginId)) {
-                receivers.add(oldAssignee);
-            }
         }
 
         if (issue.assignee != null) {
@@ -822,6 +830,27 @@
         NotificationEvent.add(notiEvent);
 
         return notiEvent;
+    }
+
+    private static Set<User> getReceiversWhenAssigneeChanged(User oldAssignee, Issue issue) {
+        Set<User> receivers = findWatchers(issue.asResource());
+        receivers.add(issue.getAuthor());
+
+        if (issue.assignee != null) {
+            receivers.add(issue.assignee.user);
+        }
+
+        if (oldAssignee != null && !oldAssignee.isAnonymous()) {
+            receivers.add(oldAssignee);
+        }
+
+        for (IssueSharer issueSharer : issue.sharers) {
+            receivers.add(User.findByLoginId(issueSharer.loginId));
+        }
+
+        receivers.remove(UserApp.currentUser());
+
+        return receivers;
     }
 
     public static void afterNewIssue(Issue issue) {
@@ -890,6 +919,49 @@
     private static Set<User> findSharer(String sharerLoginId) {
         Set<User> receivers = new HashSet<>();
         receivers.add(User.findByLoginId(sharerLoginId));
+        return receivers;
+    }
+
+    public static NotificationEvent afterIssueLabelChanged(String addedLabels, String deletedLabels, Issue issue) {
+        NotificationEvent notiEvent = createFromCurrentUser(issue);
+        notiEvent.title = formatReplyTitle(issue);
+        notiEvent.receivers = null; // no receivers
+        notiEvent.eventType = ISSUE_LABEL_CHANGED;
+        notiEvent.oldValue = deletedLabels;
+        notiEvent.newValue = addedLabels;
+
+        NotificationEvent.addWithoutSkipEvent(notiEvent);
+        return notiEvent;
+    }
+
+    public static NotificationEvent afterMilestoneChanged(Long oldMilestoneId, Issue issue) {
+        webhookRequest(ISSUE_MILESTONE_CHANGED, issue, false);
+
+        NotificationEvent notiEvent = createFromCurrentUser(issue);
+
+        Set<User> receivers = getMandatoryReceivers(issue);
+
+        notiEvent.title = formatReplyTitle(issue);
+        notiEvent.receivers = receivers;
+        notiEvent.eventType = ISSUE_MILESTONE_CHANGED;
+        notiEvent.oldValue = oldMilestoneId.toString();
+        notiEvent.newValue = issue.milestoneId().toString();
+
+        NotificationEvent.add(notiEvent);
+
+        return notiEvent;
+    }
+
+    private static Set<User> getMandatoryReceivers(Issue issue) {
+        Set<User> receivers = findWatchers(issue.asResource());
+        receivers.add(issue.getAuthor());
+
+        for (IssueSharer issueSharer : issue.sharers) {
+            receivers.add(User.findByLoginId(issueSharer.loginId));
+        }
+
+        receivers.remove(UserApp.currentUser());
+
         return receivers;
     }
 
@@ -1120,6 +1192,33 @@
         return receivers;
     }
 
+    private static Set<User> getCommentReceivers(Comment comment, User except) {
+        AbstractPosting parent = comment.getParent();
+
+        Set<User> receivers = new HashSet<>(findWatchers(parent.asResource()));
+        receivers.add(comment.getParent().getAuthor());
+        includeAssigneeIfExist(comment, receivers);
+        receivers.remove(except);
+
+        // Filter the watchers who has no permission to read this resource.
+        CollectionUtils.filter(receivers, new Predicate() {
+            @Override
+            public boolean evaluate(Object watcher) {
+                return AccessControl.isAllowed((User) watcher, parent.asResource(), Operation.READ);
+            }
+        });
+        return receivers;
+    }
+
+    private static void includeAssigneeIfExist(Comment comment, Set<User> receivers) {
+        if (comment instanceof IssueComment) {
+            Assignee assignee = ((Issue) comment.getParent()).assignee;
+            if (assignee != null) {
+                receivers.add(assignee.user);
+            }
+        }
+    }
+
     private static String getPrefixedNumber(AbstractPosting posting) {
         if (posting instanceof Issue) {
             return "#" + posting.getNumber();
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -1,7 +1,7 @@
 /**
  * Yona, 21st Century Project Hosting SW
  * <p>
- * Copyright Yona & Yobi Authors & NAVER Corp.
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
  * https://yona.io
  **/
 package models;
@@ -29,6 +29,7 @@
 import play.db.ebean.Model;
 import play.db.ebean.Transactional;
 import play.i18n.Messages;
+import play.mvc.Http;
 import utils.CacheStore;
 import utils.GravatarUtil;
 import utils.JodaDateUtil;
@@ -295,11 +296,21 @@
         if(!user.isAnonymous()){
             return user;
         }
-        String userToken = play.mvc.Http.Context.current().request().getHeader(UserApp.USER_TOKEN_HEADER);
+
+        String userToken = extractUserTokenFromRequestHeader(Http.Context.current().request());
         if( userToken != null) {
             return User.findByUserToken(userToken);
         }
         return User.anonymous;
+    }
+
+    public static String extractUserTokenFromRequestHeader(Http.Request request) {
+        String authHeader = request.getHeader("Authorization");
+        if(authHeader != null &&
+                authHeader.contains("token ")) {
+            return authHeader.split("token ")[1];
+        }
+        return request.getHeader(UserApp.USER_TOKEN_HEADER);
     }
 
     /**
@@ -1047,4 +1058,12 @@
             return name;
         }
     }
+
+    public String getDisplayName(User forCurrentUser){
+        if (StringUtils.isNotBlank(englishName) && lang != null && forCurrentUser.lang.startsWith("en")) {
+            return englishName + " " + extractDepartmentPart();
+        } else {
+            return name;
+        }
+    }
 }
app/models/Webhook.java
--- app/models/Webhook.java
+++ app/models/Webhook.java
@@ -315,10 +315,17 @@
             case ISSUE_MOVED:
                 requestMessage += Messages.get(Lang.defaultLang(), "notification.type.issue.moved");
                 break;
+            case ISSUE_MILESTONE_CHANGED:
+                requestMessage += Messages.get(Lang.defaultLang(), "notification.type.milestone.changed");
+                break;
+            default:
+                play.Logger.warn("Unknown webhook event: " + eventType);
         }
+
         requestMessage += " <" + utils.Config.getScheme() + "://" + utils.Config.getHostport("localhost:9000") + RouteUtil.getUrl(eventIssue) + "|#" + eventIssue.number + ": " + eventIssue.title + ">";
 
-        detailFields.add(buildTitleValueJSON(Messages.get(Lang.defaultLang(), "issue.assignee"), eventIssue.assigneeName(), true));
+        detailFields.add(buildTitleValueJSON(Messages.get(Lang.defaultLang(), "notification.type.milestone.changed"), eventIssue.milestoneId().toString(), true));
+        detailFields.add(buildTitleValueJSON(Messages.get(Lang.defaultLang(), ""), eventIssue.assigneeName(), true));
         detailFields.add(buildTitleValueJSON(Messages.get(Lang.defaultLang(), "issue.state"), eventIssue.state.toString(), true));
         attachments.add(buildAttachmentJSON(eventIssue.body, detailFields));
 
app/models/enumeration/EventType.java
--- app/models/enumeration/EventType.java
+++ app/models/enumeration/EventType.java
@@ -35,7 +35,9 @@
     ORGANIZATION_MEMBER_ENROLL_REQUEST("notification.organization.type.member.enroll",19),
     COMMENT_UPDATED("notification.type.comment.updated", 20),
     ISSUE_MOVED("notification.type.issue.is.moved", 21),
-    ISSUE_SHARER_CHANGED("notification.type.issue.sharer.changed", 22);
+    ISSUE_SHARER_CHANGED("notification.type.issue.sharer.changed", 22),
+    ISSUE_LABEL_CHANGED("notification.type.issue.label.changed", 23),
+    ISSUE_MILESTONE_CHANGED("notification.type.milestone.changed", 24);
 
     private String descr;
 
app/models/support/SearchCondition.java
--- app/models/support/SearchCondition.java
+++ app/models/support/SearchCondition.java
@@ -21,9 +21,6 @@
 import java.text.SimpleDateFormat;
 import java.util.*;
 
-import static models.enumeration.ResourceType.ISSUE_COMMENT;
-import static models.enumeration.ResourceType.ISSUE_POST;
-
 public class SearchCondition extends AbstractPostingApp.SearchCondition implements Cloneable {
     public String state;
     public Boolean commentedCheck;
@@ -304,7 +301,7 @@
         if (mentionId != null) {
             User mentionUser = User.find.byId(mentionId);
             if(!mentionUser.isAnonymous()) {
-                List<Long> ids = getMentioningIssueIds(mentionUser);
+                List<Long> ids = Mention.getMentioningIssueIds(mentionId);
 
                 if (ids.isEmpty()) {
                     // No need to progress because the query matches nothing.
@@ -351,39 +348,6 @@
             }
         }
         return new ArrayList<>(issueIds);
-    }
-
-    private List<Long> getMentioningIssueIds(User mentionUser) {
-        Set<Long> ids = new HashSet<>();
-        Set<Long> commentIds = new HashSet<>();
-
-        for (Mention mention : Mention.find.where()
-                .eq("user", mentionUser)
-                .in("resourceType", ISSUE_POST, ISSUE_COMMENT)
-                .findList()) {
-
-            switch (mention.resourceType) {
-                case ISSUE_POST:
-                    ids.add(Long.valueOf(mention.resourceId));
-                    break;
-                case ISSUE_COMMENT:
-                    commentIds.add(Long.valueOf(mention.resourceId));
-                    break;
-                default:
-                    play.Logger.warn("'" + mention.resourceType + "' is not supported.");
-                    break;
-            }
-        }
-
-        if (!commentIds.isEmpty()) {
-            for (IssueComment comment : IssueComment.find.where()
-                    .idIn(new ArrayList<>(commentIds))
-                    .findList()) {
-                ids.add(comment.issue.id);
-            }
-        }
-
-        return new ArrayList<>(ids);
     }
 
     private List<Long> getSharedIssueIds(User user) {
@@ -458,10 +422,7 @@
 
         setIssueState(el);
 
-        if (CollectionUtils.isNotEmpty(labelIds)) {
-            Set<IssueLabel> labels = IssueLabel.finder.where().idIn(new ArrayList<>(labelIds)).findSet();
-            el.in("id", Issue.finder.where().in("labels", labels).findIds());
-        }
+        setLabelsIfExist(project, el);
 
         setOrderByIfExist(el);
 
@@ -476,6 +437,40 @@
         return el;
     }
 
+    private void setLabelsIfExist(Project project, ExpressionList<Issue> el) {
+        if (CollectionUtils.isNotEmpty(labelIds)) {
+            Set<IssueLabel> labels = IssueLabel.finder.where().idIn(new ArrayList<>(labelIds)).findSet();
+
+            List<Issue> issues = Issue.finder.where()
+                    .eq("project", project)
+                    .in("labels", labels).findList();
+
+            for (IssueLabel issueLabel : labels) {
+                issues = findIssueByLabel(issues, issueLabel);
+            }
+
+            el.in("id", extractIssueIds(issues));
+        }
+    }
+
+    private Set<Long> extractIssueIds(List<Issue> issues) {
+        Set<Long> ids = new HashSet<>();
+        for (Issue issue : issues) {
+            ids.add(issue.id);
+        }
+        return ids;
+    }
+
+    private List<Issue> findIssueByLabel(List<Issue> issues, IssueLabel label) {
+        List<Issue> result = new ArrayList<>();
+        for (Issue issue : issues) {
+            if(issue.labels.contains(label)){
+                result.add(issue);
+            }
+        }
+        return result;
+    }
+
     public String getDueDateString() {
         if (dueDate == null) {
             return null;
app/utils/Config.java
--- app/utils/Config.java
+++ app/utils/Config.java
@@ -1,44 +1,26 @@
 /**
- * Yobi, Project Hosting SW
- *
- * Copyright 2012 NAVER Corp.
- * http://yobi.io
- *
- * @author Yi EungJun
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
+
 package utils;
 
-import com.typesafe.config.ConfigFactory;
 import models.SiteAdmin;
 import org.apache.commons.lang3.ObjectUtils;
 import play.Configuration;
 import play.mvc.Http;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.*;
 import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Enumeration;
 
 public class Config {
     public static final String DEFAULT_SCHEME = "http";
     private static final String YONA_DATA = "yona.data"; //property from java -Dyona.data option string
+    public static boolean isConnectableToGravatarServer = true;
 
     public static void onStart() {
         Diagnostic.register(new SimpleDiagnostic() {
@@ -55,6 +37,8 @@
                 }
             }
         });
+
+        isConnectableToGravatarServer = isConnectableToGravatar();
     }
 
     public static String getSiteName() {
@@ -298,4 +282,14 @@
     public static boolean displayPrivateRepositories() {
         return Configuration.root().getBoolean("application.displayPrivateRepositories", Boolean.FALSE);
     }
+
+    private static boolean isConnectableToGravatar() {
+        try {
+            return InetAddress.getByName("ko.gravatar.com").isReachable(100)
+                    && InetAddress.getByName("www.gravatar.com").isReachable(100);
+        } catch (IOException e) {
+            play.Logger.warn("Gravatar server is unreachable. Gravatar service will not work.");
+            return false;
+        }
+    }
 }
app/utils/GravatarUtil.java
--- app/utils/GravatarUtil.java
+++ app/utils/GravatarUtil.java
@@ -1,25 +1,12 @@
 /**
- * Yobi, Project Hosting SW
- *
- * Copyright 2013 NAVER Corp.
- * http://yobi.io
- *
- * @author Suwon Chae
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ * Yona, 21st Century Project Hosting SW
+ * <p>
+ * Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+ * https://yona.io
+ **/
 package utils;
 
+import controllers.UserApp;
 import org.apache.commons.lang3.StringUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -39,6 +26,10 @@
     }
 
     public static String getAvatar(String email, int size, String defaultImageUrl) {
+        if(!Config.isConnectableToGravatarServer){
+            return UserApp.DEFAULT_AVATAR_URL;
+        }
+
         try {
             String url = "https://www.gravatar.com/avatar/" + MD5Util.md5Hex(email) + "?s=" + size;
             if(StringUtils.isNotEmpty(defaultImageUrl)) {
app/views/board/view.scala.html
--- app/views/board/view.scala.html
+++ app/views/board/view.scala.html
@@ -25,7 +25,7 @@
 }
 @titleForOGTag = @{post.title + " |:| " + post.body.substring(0, Math.min(post.body.length, 200))}
 
-@conatinsCurrentUserInWatchers = @{post.getWatchers(false).contains(UserApp.currentUser())}
+@conatinsCurrentUserInWatchers = @{Watch.isWatching(UserApp.currentUser(), post.asResource())}
 
 @projectLayout(titleForOGTag, project, utils.MenuType.BOARD){
 @projectMenu(project, utils.MenuType.BOARD, "main-menu-only")
@@ -87,7 +87,7 @@
                         <div>
                             @if(isAllowed(UserApp.currentUser(), post.asResource(), Operation.WATCH)) {
                                 <button id="watch-button" type="button" class="ybtn @if(conatinsCurrentUserInWatchers) {ybtn-watching}"
-                                        data-toggle="button" data-watching="@conatinsCurrentUserInWatchers">
+                                        data-toggle="tooltip" data-placement="top" title="@Messages("issue.watch.description")" data-watching="@conatinsCurrentUserInWatchers">
                                 @if(conatinsCurrentUserInWatchers) {
                                     @Messages("post.unwatch")
                                 } else {
app/views/issue/my_list.scala.html
--- app/views/issue/my_list.scala.html
+++ app/views/issue/my_list.scala.html
@@ -36,6 +36,20 @@
                           $yobi.alert("set Default page failed: " + data);
                       });
           });
+
+          /// label text color adjustment
+          $(document).on({
+              "pjax:complete": labelTextColorAdjust
+          });
+          function labelTextColorAdjust() {
+              $(".title-cell > .label").each(function () {
+                  var $this = $(this);
+                  $this.removeClass("dimgray white")
+                          .addClass($yobi.getContrastColor($this.css('background-color')))
+              });
+          }
+
+          labelTextColorAdjust();
       });
     </script>
 }
app/views/issue/my_partial_list.scala.html
--- app/views/issue/my_partial_list.scala.html
+++ app/views/issue/my_partial_list.scala.html
@@ -10,6 +10,35 @@
 @import utils.TemplateHelper._
 @import utils.AccessControl._
 
+@isAuthoredMeTab = @{
+    UserApp.currentUser().id == searchCondition.authorId
+}
+
+@displayAuthorName(isAuthoredMeTab: Boolean, user: User) = {
+    @if(!isAuthoredMeTab) {
+        @if(user.name) {
+            <a href="@routes.UserApp.userInfo(user.loginId)" class="infos-item infos-link-item author-cell" data-toggle="tooltip" data-placement="bottom" title="@user.loginId">
+            @user.getPureNameOnly
+            </a>
+        } else {
+            <span class="infos-item">@Messages("issue.noAuthor")</span>
+        }
+    }
+}
+
+@displayCommentsAndVoterCount(issue:Issue, project:Project) = {
+    @if(issue.comments.size > 0 || issue.voters.size > 0) {
+        <span class="item-count-groups">
+            @if(issue.comments.size > 0) {
+                @views.html.common.commentCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#comments", issue.comments.size)
+            }
+            @if(issue.voters.size > 0) {
+                @views.html.common.voteCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#vote", issue.voters.size)
+            }
+        </span>
+    }
+}
+
 @urlToList(project:Project, state:String) = {@routes.IssueApp.issues(project.owner, project.name, "open", "html", 1)}
 
 @issueLabels(issue:Issue) = {@for(label <- issue.labels.toList.sortBy(r => (r.category.name, r.name))) {@label.category.name,@label.id,@label.name|}}
@@ -17,43 +46,47 @@
 @isAssignedToMeTab = @{
     UserApp.currentUser().id == searchCondition.assigneeId
 }
-@isAuthoredMeTab = @{
-    UserApp.currentUser().id == searchCondition.authorId
-}
+
 <ul class="post-list-wrap my-issues">
 @for(issue <- issueList){
     @defining(issue.project){ project =>
     @defining(User.findByLoginId(issue.authorLoginId)){ user =>
     <li class="post-item title" id="issue-item-@issue.id" href="@routes.IssueApp.issue(project.owner, project.name, issue.getNumber)">
-        <div class="span10 span-hard-wrap">
-            <div class="title-wrap">
-                <a href="@routes.IssueApp.issue(project.owner, project.name, issue.getNumber)" class="title">
-                    @issue.title
-                </a>
-            </div>
-            <div class="infos">
+        <div class="span12 span-hard-wrap">
+            <div class="span2 project-name-in-my-issues fixed-height-my-issues-list">
                 <span class="infos-item project-name">
                     <a href="@routes.ProjectApp.project(project.owner,project.name)" class="title project" data-toggle="tooltip" data-placement="bottom" title="@Messages("project.name")">
                     @project.name
                     </a>
                 </span>
                 <span class="infos-item post-id">#@issue.getNumber</span>
-                @if(!isAuthoredMeTab) {
-                    @if(user.name) {
-                        <a href="@routes.UserApp.userInfo(user.loginId)" class="infos-item infos-link-item" data-toggle="tooltip" data-placement="bottom" title="@user.loginId">
-                        @user.getDisplayName
-                        </a>
-                    } else {
-                        <span class="infos-item">@Messages("issue.noAuthor")</span>
+            </div>
+            <div class="title-wrap span6">
+                <span class="title-cell">
+                    <a href="@routes.IssueApp.issue(project.owner, project.name, issue.getNumber)" class="title">
+                    @issue.title
+                    </a>
+                    @displayCommentsAndVoterCount(issue, project)
+                    @for(label <- issue.labels.toList.sortBy(r => (r.category.name, r.name))) {
+                        <a href="@urlToList(project, searchCondition.state)&labelIds=@label.id" class="label issue-label list-label" data-label-id="@label.id" style="background:@label.color">@label.name</a>
                     }
-                }
-
-                <span class="infos-item" data-toggle="tooltip" data-placement="bottom" title="@JodaDateUtil.getDateString(issue.createdDate)">
-                    @agoOrDateString(issue.createdDate)
+                </span>
+            </div>
+            <div class="span1 hide-in-mobile author project-name-in-my-issues fixed-height-my-issues-list">
+                @displayAuthorName(isAuthoredMeTab, user)
+            </div>
+            <div class="infos @if(!isAssignedToMeTab && issue.assigneeName != null){span2} else {span3} meta">
+                <span class="meta-cell">
+                <span class="hide show-in-mobile">
+                    @displayAuthorName(isAuthoredMeTab, user)
                 </span>
                 @if(!issue.createdDate.equals(issue.updatedDate)) {
-                    <span class="infos-item" data-toggle="tooltip" data-placement="bottom" title="@JodaDateUtil.getDateString(issue.updatedDate)">
-                    update: @agoOrDateString(issue.updatedDate)
+                    <span class="infos-item" data-toggle="tooltip" data-placement="bottom" title="Last Updated @JodaDateUtil.getDateString(issue.updatedDate)">
+                    @agoOrDateString(issue.updatedDate)
+                    </span>
+                } else {
+                    <span class="infos-item" data-toggle="tooltip" data-placement="bottom" title="Created at @JodaDateUtil.getDateString(issue.createdDate)">
+                    @agoOrDateString(issue.createdDate)
                     </span>
                 }
 
@@ -67,46 +100,29 @@
                 </span>
                 }
 
-                @if(issue.comments.size>0 || issue.voters.size>0) {
-                <span class="infos-item item-count-groups">
-                    @if(issue.comments.size>0){
-                        @views.html.common.commentCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#comments", issue.comments.size)
-                    }
-                    @if(issue.voters.size>0){
-                        @views.html.common.voteCount(routes.IssueApp.issue(project.owner, project.name, issue.getNumber).toString + "#vote", issue.voters.size)
-                    }
+                @if(issue.dueDate != null) {
+                    <span class="pull-right @if(issue.isOverDueDate) {overdue}" data-toggle="tooltip" data-placement="top" title="Due date: @issue.getDueDateString">
+                        <i class="yobicon-clock2"></i>
+                        @if(issue.isOpen) {
+                            @if(issue.isOverDueDate) {
+                                @Messages("issue.dueDate.overdue")
+                            } else {
+                                @issue.until
+                            }
+                        } else {
+                            @issue.getDueDateString
+                        }
+                    </span>
+                }
                 </span>
-                }
-
-                @for(label <- issue.labels.toList.sortBy(r => (r.category.name, r.name))) {
-                    <a href="@urlToList(project, searchCondition.state)&labelIds=@label.id" class="label issue-label list-label" data-label-id="@label.id" style="background:@label.color">@label.name</a>
-                }
             </div>
-        </div>
-        <div class="span2 hide-in-mobile">
-            @if(!isAssignedToMeTab){
-            <div class="mt5 pull-right hide-in-mobile">
-                @if(issue.assigneeName != null) {
+            @if(!isAssignedToMeTab && issue.assigneeName != null){
+            <div class="span1 hide-in-mobile">
+                <div class="mt5 pull-right hide-in-mobile">
                     <a href="@routes.UserApp.userInfo(issue.assignee.user.loginId)" class="avatar-wrap assinee" data-toggle="tooltip" data-placement="bottom" title="@Messages("issue.assignee"): @issue.assigneeName">
                         <img src="@issue.assignee.user.avatarUrl" width="32" height="32" alt="@issue.assigneeName">
                     </a>
-                } else {
-                   <div class="empty-avatar-wrap">&nbsp;</div>
-                }
-            </div>
-            }
-            @if(issue.dueDate != null) {
-            <div class="mr20 mt10 pull-right @if(issue.isOverDueDate) {overdue}" data-toggle="tooltip" data-placement="top" title="@issue.getDueDateString">
-                <i class="yobicon-clock2"></i>
-                @if(issue.isOpen) {
-                    @if(issue.isOverDueDate) {
-                        @Messages("issue.dueDate.overdue")
-                    } else {
-                        @issue.until
-                    }
-                } else {
-                    @issue.getDueDateString
-                }
+                </div>
             </div>
             }
         </div>
app/views/issue/my_partial_list_quicksearch.scala.html
--- app/views/issue/my_partial_list_quicksearch.scala.html
+++ app/views/issue/my_partial_list_quicksearch.scala.html
@@ -5,6 +5,7 @@
 * https://yona.io
 **@
 @import models.support.SearchCondition
+@import org.apache.commons.lang3.StringUtils
 @(param:SearchCondition)
 
 @currentUserId = @{
@@ -54,7 +55,9 @@
             data-milestone-id="@param.milestoneId"
             data-mention-id="@currentUserId"
             data-sharer-id="">
-            @Messages("issue.list.mentionedOfMe")
+            @Messages("issue.list.mentionedOfMe") @if(StringUtils.isBlank(param.filter)) {
+                (@Issue.getCountOfMentionedOpenIssues(currentUserId))
+            }
         </a>
     </li>
     <li @if(param.sharerId == currentUserId){ class="active"}>
@@ -65,7 +68,9 @@
         data-milestone-id="@param.milestoneId"
         data-mention-id=""
         data-sharer-id="@currentUserId">
-        @Messages("issue.list.sharedWithMe") (@IssueSharer.getNumberOfIssuesSharedWithUser(currentUserId))
+        @Messages("issue.list.sharedWithMe") @if(StringUtils.isBlank(param.filter)) {
+            (@IssueSharer.getNumberOfIssuesSharedWithUser(currentUserId))
+        }
         </a>
     </li>
     }
 
app/views/issue/partial_comment.scala.html (added)
+++ app/views/issue/partial_comment.scala.html
@@ -0,0 +1,106 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+* https://yona.io
+**@
+@(comment:Comment, project:Project, issue:Issue)
+
+@import org.apache.commons.lang3.StringUtils
+@import utils.TemplateHelper._
+@import utils.AccessControl._
+@import utils.JodaDateUtil
+@import play.libs.Json.toJson
+@import utils.Markdown
+@import controllers.api.IssueApi
+
+@isAuthorComment(commentId: String) = @{
+    if(commentId == UserApp.currentUser().loginId) {"author"}
+}
+
+@VOTER_AVATAR_SHOW_LIMIT = @{ 5 }
+
+<li class="comment @isAuthorComment(comment.authorLoginId)" id="comment-@comment.id">
+    <div class="comment-avatar">
+        <a href="@userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorLoginId">
+            <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl(64)" width="32" height="32" alt="@comment.authorName">
+        </a>
+    </div>
+    <div class="media-body">
+        <div class="meta-info">
+            <span class="comment_author">
+                <span class="resp-comment-avatar">
+                    <a href="@userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
+                        <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl(64)" width="32" height="32" alt="@comment.authorLoginId">
+                    </a>
+                </span>
+                <a href="@userInfo(comment.authorLoginId)" data-toggle="tooltip" data-placement="top" title="@comment.authorLoginId"><strong>@User.findByLoginId(comment.authorLoginId).getDisplayName</strong></a>
+            </span>
+            <span class="ago-date">
+                <a href="#comment-@comment.id" class="ago" title="@JodaDateUtil.getDateString(comment.createdDate)">@utils.TemplateHelper.agoOrDateString(comment.createdDate)</a>
+            </span>
+            <span class="act-row pull-right">
+                @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ) && comment.isInstanceOf[IssueComment]) {
+                    @defining(comment.asInstanceOf[IssueComment]) { issueComment =>
+                        @if(issueComment.voters.size > VOTER_AVATAR_SHOW_LIMIT) {
+                            <span style="margin-right: 2px;" data-toggle="tooltip" data-html="true" title="
+                                @for(voter <- VoteApp.getVotersForName(issueComment.voters, 0, 5)) {
+                                    @voter.name<br>
+                                    }
+                                &hellip;">
+                                <a class="vote-description-people" href="#voters-@issueComment.id" data-toggle="modal">
+                                @if(issueComment.voters.size == 1) {
+                                    @Messages("common.comment.vote.agreement", issueComment.voters.size)
+                                } else {
+                                    @Messages("common.comment.vote.agreements", issueComment.voters.size)
+                                }
+                                </a>
+                            </span>
+
+                            @partial_voter_list("voters-" + issueComment.id, issueComment.voters)
+                        } else {
+                            @for(voter <- issueComment.voters) {
+                                <a href="@userInfo(voter.loginId)" class="avatar-wrap smaller" data-toggle="tooltip" data-placement="top" title="@voter.name">
+                                    <img src="@User.findByLoginId(voter.loginId).avatarUrl">
+                                </a>
+                            }
+                        }
+
+                        @if(issueComment.voters.contains(UserApp.currentUser())) {
+                            <button type="button" class="btn-transparent-with-fontsize-lineheight" title="@Messages("common.comment.unvote")" data-request-type="comment-vote" data-request-uri="@routes.VoteApp.unvoteComment(project.owner, project.name, issue.getNumber, comment.id)">
+                                <i class="yobicon-hearts vote-heart-on"></i>
+                            </button>
+                        } else {
+                            @if(UserApp.currentUser().isAnonymous()) {
+                                <i class="yobicon-hearts vote-heart-off vote-heart-disable-hover"></i>
+                            } else {
+                                <button type="button" class="btn-transparent-with-fontsize-lineheight" title="@Messages("common.comment.vote")" data-request-type="comment-vote" data-request-uri="@routes.VoteApp.voteComment(project.owner, project.name, issue.getNumber, comment.id)">
+                                    <i class="yobicon-hearts vote-heart-off"></i>
+                                </button>
+                            }
+                        }
+                    }
+                }
+
+                @if(StringUtils.isNotBlank(IssueApi.TRANSLATION_API)) {
+                    <button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 comment-translate" data-toggle="tooltip" data-comment-id="@comment.id" title="@Messages("button.translation")"><i class="yobicon-lang"></i></button>
+                }
+
+                @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ)) {
+                    <button type="button" class="btn-transparent-with-fontsize-lineheight ml10" data-toggle="comment-edit" data-comment-id="@comment.id" title="@Messages("common.comment.edit")"><i class="yobicon-edit-2"></i></button>
+                }
+
+                @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)) {
+                    <button type="button" class="btn-transparent-with-fontsize-lineheight ml6" data-toggle="comment-delete" data-request-uri="@routes.IssueApp.deleteComment(project.owner, project.name, issue.getNumber, comment.id)" title="@Messages("common.comment.delete")"><i class="yobicon-trash"></i></button>
+                }
+            </span>
+        </div>
+
+        @common.commentUpdateForm(comment, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString(), comment.contents)
+
+        <div id="comment-body-@comment.id">
+            <div class="comment-body markdown-wrap" data-via-email="@OriginalEmail.exists(comment.asResource)">@Html(Markdown.render(comment.contents, project))</div>
+            <div class="attachments pull-right" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.ISSUE_COMMENT.toString(), comment.id.toString()))"></div>
+        </div>
+    </div>
+</li>
app/views/issue/partial_comments.scala.html
--- app/views/issue/partial_comments.scala.html
+++ app/views/issue/partial_comments.scala.html
@@ -6,197 +6,26 @@
 **@
 @(project:Project, issue:Issue)
 
-@import org.apache.commons.lang3.StringUtils
-@import utils.TemplateHelper._
-@import utils.AccessControl._
-@import utils.JodaDateUtil
-@import play.libs.Json.toJson
-@import utils.Markdown
-@import controllers.api.IssueApi
-
-@avatarByLoginId(loginId: String, loginName: String) = {
-    <a href="@userInfo(loginId)" class="usf-group" data-toggle="tooltip" data-placement="top" title="@loginName">
-        <img src="@getUserAvatarUrl(User.findByLoginId(loginId), 32)" class="avatar-wrap small">
-    </a>
-}
-
-@linkToUser(loginId: String, loginName: String, showAvatar: Boolean = true) = {
-    @loginId match {
-    case (loginId: String) => {
-        @if(showAvatar){ @avatarByLoginId(loginId, loginName) }
-        <a href="@userInfo(loginId)" class="usf-group" data-toggle="tooltip" data-placement="top" title="@loginId">
-            <strong>@loginName</strong>
-        </a>
-    }
-    case _ => { Anonymous }
-    }
-}
-
-@assginedMesssage(newValue: String, user:User) = @{
-    val LoginId = user.loginId
-    newValue match {
-        case LoginId => "issue.event.assignedToMe"
-        case _: String => "issue.event.assigned"
-        case _ => "issue.event.unassigned"
-    }
-}
-@isAuthorComment(commentId: String) = @{
-    if(commentId == UserApp.currentUser().loginId) {"author"}
-}
-
-@linkToPullRequest(pull: PullRequest) ={
-    <strong>@Messages("pullRequest")-@pull.number <a href="@routes.PullRequestApp.pullRequest(pull.toProject.owner, pull.toProject.name, pull.number)" class="link">@pull.title</a></strong>
-}
-
-@linkToProject(owner: String, name: String) ={
-    <strong><a href="@routes.ProjectApp.project(owner, name)" class="link">@owner/@name</a></strong>
-}
-
-@linkToCommit(commitId: String) ={
-    <strong>@Messages("code.commits") <a href="@routes.CodeHistoryApp.show(project.owner, project.name, commitId)" class="link">@{"@"}@commitId</a></strong>
-}
-
-@VOTER_AVATAR_SHOW_LIMIT = @{ 5 }
-
 <div class="comment-header"><i class="yobicon-comments"></i> <strong>@Messages("common.comment")</strong> <strong class="num">@issue.comments.size</strong></div>
 <hr class="nm">
 
 @if(issue.comments.size + issue.events.size > 0) {
-<ul class="comments">
-@for(item <- issue.getTimeline){
-    @item match {
-    case (comment: Comment) => {
-    <li class="comment @isAuthorComment(comment.authorLoginId)" id="comment-@comment.id">
-        <div class="comment-avatar">
-            <a href="@userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorLoginId">
-                <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl(64)" width="32" height="32" alt="@comment.authorName">
-            </a>
-        </div>
-        <div class="media-body">
-            <div class="meta-info">
-                <span class="comment_author">
-                    <span class="resp-comment-avatar">
-                        <a href="@userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
-                            <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl(64)" width="32" height="32" alt="@comment.authorLoginId">
-                        </a>
-                    </span>
-                    <a href="@userInfo(comment.authorLoginId)" data-toggle="tooltip" data-placement="top" title="@comment.authorLoginId"><strong>@User.findByLoginId(comment.authorLoginId).getDisplayName</strong></a>
-                </span>
-                <span class="ago-date">
-                    <a href="#comment-@comment.id" class="ago" title="@JodaDateUtil.getDateString(comment.createdDate)">@utils.TemplateHelper.agoOrDateString(comment.createdDate)</a>
-                </span>
-                <span class="act-row pull-right">
-                    @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ) && comment.isInstanceOf[IssueComment]) {
-                        @defining(comment.asInstanceOf[IssueComment]) { issueComment =>
-                            @if(issueComment.voters.size > VOTER_AVATAR_SHOW_LIMIT) {
-                                <span style="margin-right:2px;" data-toggle="tooltip" data-html="true" title="
-                                    @for(voter <- VoteApp.getVotersForName(issueComment.voters, 0, 5)) {
-                                        @voter.name<br>
-                                    }
-                                    &hellip;">
-                                    <a class="vote-description-people" href="#voters-@issueComment.id" data-toggle="modal">
-                                        @if(issueComment.voters.size == 1) {
-                                            @Messages("common.comment.vote.agreement", issueComment.voters.size)
-                                        } else {
-                                            @Messages("common.comment.vote.agreements", issueComment.voters.size)
-                                        }
-                                    </a>
-                                </span>
-
-                                @partial_voter_list("voters-" + issueComment.id, issueComment.voters)
-                            } else {
-                                @for(voter <- issueComment.voters){
-                                    <a href="@userInfo(voter.loginId)" class="avatar-wrap smaller" data-toggle="tooltip" data-placement="top" title="@voter.name">
-                                        <img src="@User.findByLoginId(voter.loginId).avatarUrl">
-                                    </a>
-                                }
-                            }
-
-                            @if(issueComment.voters.contains(UserApp.currentUser())) {
-                                <button type="button" class="btn-transparent-with-fontsize-lineheight" title="@Messages("common.comment.unvote")" data-request-type="comment-vote" data-request-uri="@routes.VoteApp.unvoteComment(project.owner, project.name, issue.getNumber, comment.id)">
-                                <i class="yobicon-hearts vote-heart-on"></i>
-                                </button>
-                            } else {
-                                @if(UserApp.currentUser().isAnonymous()) {
-                                    <i class="yobicon-hearts vote-heart-off vote-heart-disable-hover"></i>
-                                } else {
-                                    <button type="button" class="btn-transparent-with-fontsize-lineheight" title="@Messages("common.comment.vote")" data-request-type="comment-vote" data-request-uri="@routes.VoteApp.voteComment(project.owner, project.name, issue.getNumber, comment.id)">
-                                    <i class="yobicon-hearts vote-heart-off"></i>
-                                    </button>
-                                }
-                            }
-                        }
-                    }
-
-                    @if(StringUtils.isNotBlank(IssueApi.TRANSLATION_API)){
-                        <button type="button" class="icon btn-transparent-with-fontsize-lineheight ml10 comment-translate" data-toggle="tooltip" data-comment-id="@comment.id" title="@Messages("button.translation")"><i class="yobicon-lang"></i></button>
-                    }
-
-                    @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.READ)) {
-                        <button type="button" class="btn-transparent-with-fontsize-lineheight ml10" data-toggle="comment-edit" data-comment-id="@comment.id" title="@Messages("common.comment.edit")"><i class="yobicon-edit-2"></i></button>
-                    }
-
-                    @if(isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)) {
-                        <button type="button" class="btn-transparent-with-fontsize-lineheight ml6" data-toggle="comment-delete" data-request-uri="@routes.IssueApp.deleteComment(project.owner, project.name, issue.getNumber, comment.id)" title="@Messages("common.comment.delete")"><i class="yobicon-trash"></i></button>
-                    }
-                </span>
-            </div>
-
-            @common.commentUpdateForm(comment, routes.IssueApp.newComment(project.owner, project.name, issue.getNumber).toString(), comment.contents)
-
-            <div id="comment-body-@comment.id">
-                <div class="comment-body markdown-wrap" data-via-email="@OriginalEmail.exists(comment.asResource)">@Html(Markdown.render(comment.contents, project))</div>
-                <div class="attachments pull-right" data-attachments="@toJson(AttachmentApp.getFileList(ResourceType.ISSUE_COMMENT.toString(), comment.id.toString()))"></div>
-            </div>
-        </div>
-    </li>
-
-    }
-    case (event: IssueEvent) => {
-        @if(event.eventType != EventType.ISSUE_BODY_CHANGED) {
-            <li class="event" id="event-@event.id">
-                @defining(User.findByLoginId(event.senderLoginId)) { user =>
-                    @event.eventType match {
-                        case EventType.ISSUE_STATE_CHANGED => {
-                            <span class="state @event.newValue">@Messages("issue.state." + event.newValue)</span> @Html(Messages("issue.event." + event.newValue, linkToUser(user.loginId, user.getDisplayName)))
-                        }
-                        case EventType.ISSUE_ASSIGNEE_CHANGED => {
-                            <span class="state changed">@Messages("issue.state.assigned")</span>
-                            @Html(Messages(assginedMesssage(event.newValue, user), linkToUser(user.loginId, user.getDisplayName), linkToUser(event.newValue,User.findByLoginId(event.newValue).getDisplayName, true)))
-                        }
-                        case EventType.ISSUE_REFERRED_FROM_COMMIT => {
-                            <span class="state changed">@Messages("issue.event.referred.title")</span>
-                            @Html(Messages("issue.event.referred",linkToUser(user.loginId, user.getDisplayName),linkToCommit(event.newValue)))
-                        }
-                        case EventType.ISSUE_MOVED => {
-                            <span class="state changed">@Messages("issue.event.moved.title")</span>
-                            @Html(Messages("issue.event.moved", linkToUser(user.loginId, user.getDisplayName), linkToProject(event.oldValue.split("/")(0), event.oldValue.split("/")(1))))
-                        }
-                        case EventType.ISSUE_REFERRED_FROM_PULL_REQUEST => {
-                            <span class="state changed">@Messages("issue.event.referred.title")</span>
-                            @defining(PullRequest.findById(Long.valueOf(event.newValue))) { pull =>
-                                @Html(Messages("issue.event.referred",linkToUser(user.loginId, user.getDisplayName),linkToPullRequest(pull)))
-                            }
-                        }
-                        case EventType.ISSUE_SHARER_CHANGED => {
-                            @if(StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue)){
-                                <span class="state sharer-added">@Messages("issue.sharer")</span>
-                                @Html(Messages("issue.event.sharer.added", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.newValue, User.findByLoginId(event.newValue).getPureNameOnly)))
-                            } else {
-                                <span class="state sharer-deleted">@Messages("issue.event.sharer.deleted.title")</span>
-                                @Html(Messages("issue.event.sharer.deleted", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.oldValue, User.findByLoginId(event.oldValue).getPureNameOnly)))
-                            }
-                        }
-                        case _ => {
-                            @event.newValue by @linkToUser(user.loginId, user.getDisplayName)
-                        }
+    <ul class="comments">
+    @defining(issue.getTimeline) { timeline =>
+        @for((item, index) <- timeline.view.zipWithIndex) {
+            @item match {
+                case (comment: Comment) => {
+                    @partial_comment(comment, project, issue)
+                }
+                case (event: IssueEvent) => {
+                    @if(index > 0 && timeline(index-1).isInstanceOf[IssueEvent]) {
+                        @partial_event_timeline(event, project, issue, timeline(index-1).asInstanceOf[IssueEvent])
+                    } else {
+                        @partial_event_timeline(event, project, issue)
                     }
                 }
-                <span class="date"><a href="#event-@event.id">@utils.TemplateHelper.agoOrDateString(event.getDate())</a></span>
-            </li>
+            }
         }
     }
-    }
-}
-</ul>
+    </ul>
 }
 
app/views/issue/partial_event_timeline.scala.html (added)
+++ app/views/issue/partial_event_timeline.scala.html
@@ -0,0 +1,186 @@
+@**
+* Yona, 21st Century Project Hosting SW
+*
+* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
+* https://yona.io
+**@
+@(event: IssueEvent, project:Project, issue:Issue, previousEvent: IssueEvent = null)
+
+@import org.apache.commons.lang3.StringUtils
+@import utils.TemplateHelper._
+@import models.enumeration.EventType._
+
+@avatarByLoginId(loginId: String, loginName: String, sameTypeAsPrevious: Boolean = false) = {
+    <a href="@userInfo(loginId)" class="usf-group" data-toggle="tooltip" data-placement="top" title="@loginName">
+        <img src="@getUserAvatarUrl(User.findByLoginId(loginId), 32)" class="avatar-wrap small"></a>
+}
+
+@linkToUser(loginId: String, loginName: String, showAvatar: Boolean = true) = {
+    @loginId match {
+        case (loginId: String) => {
+            @if(showAvatar) {
+                @avatarByLoginId(loginId, loginName)
+            }
+            <a href="@userInfo(loginId)" class="usf-group" data-toggle="tooltip" data-placement="top" title="@loginId">
+                <strong>@loginName</strong></a>
+        }
+        case _ => { Anonymous }
+    }
+}
+
+@milestoneSpan(project: Project, milestone: Milestone) = {
+    <span class="bold font-blue">
+        <a href="@routes.MilestoneApp.milestone(project.owner, project.name, milestone.id)" data-toggle="tooltip" data-placement="bottom" title="@Messages("milestone")">
+        @milestone.title
+        </a>
+    </span>
+}
+
+@noMilestoneSpan() = {
+    <span class="bold">
+    @Messages("common.none")
+    </span>
+}
+
+@linkOfMilestone(milestoneId: String, project: Project) = @{
+    val milestone = Milestone.findById(Long.valueOf(milestoneId))
+
+    if(milestone == null || milestone.isNullMilestone) {
+        noMilestoneSpan
+    } else {
+        milestoneSpan(project, milestone)
+    }
+}
+
+@issueLabelBox(categoryAndName: String, project: Project) = @{
+    val splitedCategoryAndName = categoryAndName.split(" - ")
+    if(splitedCategoryAndName.length != 2) {
+        categoryAndName
+    } else {
+        var categoryName = splitedCategoryAndName(0).trim
+        var labelName = splitedCategoryAndName(1).trim
+        val issueLabel = IssueLabel.findByName(labelName, categoryName, project)
+        if(issueLabel != null) {
+            val labelColor = issueLabel.color
+            s"<div class='label issue-label' style='background-color: $labelColor'>$labelName</div>"
+        } else {
+            labelName
+        }
+    }
+}
+
+@assginedMesssage(newValue: String, user: User) = @{
+    val LoginId = user.loginId
+    newValue match {
+        case LoginId => "issue.event.assignedToMe"
+        case _: String => "issue.event.assigned"
+        case _ => "issue.event.unassigned"
+    }
+}
+
+@linkToPullRequest(pull: PullRequest) = {
+    <strong>@Messages("pullRequest")
+        -@pull.number <a href="@routes.PullRequestApp.pullRequest(pull.toProject.owner, pull.toProject.name, pull.number)" class="link">@pull.title</a></strong>
+}
+
+@linkToProject(owner: String, name: String) = {
+    <strong><a href="@routes.ProjectApp.project(owner, name)" class="link">@owner/@name</a></strong>
+}
+
+@linkToCommit(commitId: String) = {
+    <strong>@Messages("code.commits") <a href="@routes.CodeHistoryApp.show(project.owner, project.name, commitId)" class="link">@{
+        "@"
+    }@commitId</a></strong>
+}
+
+@isAddingEvent(event: IssueEvent) = @{
+    event != null && StringUtils.isBlank(event.oldValue) && StringUtils.isNotBlank(event.newValue)
+}
+
+@isDeletingEvent(event: IssueEvent) = @{
+    event != null && StringUtils.isBlank(event.newValue) && StringUtils.isNotBlank(event.oldValue)
+}
+
+@isSameEventTypeAsPrevious(event: IssueEvent, previousEvent: IssueEvent) = @{
+    previousEvent != null && event.eventType == previousEvent.eventType
+}
+
+@isSameEventTypeAndSameAction(event: IssueEvent, previousEvent: IssueEvent) = @{
+    isSameEventTypeAsPrevious(event, previousEvent) && (
+            isAddingEvent(event) && isAddingEvent(previousEvent)
+                    || isDeletingEvent(event) && isDeletingEvent(previousEvent)
+            )
+}
+
+@if(event.eventType != ISSUE_BODY_CHANGED) {
+    <li class="event" id="event-@event.id">
+        @defining(User.findByLoginId(event.senderLoginId)) { user =>
+            @event.eventType match {
+                case ISSUE_STATE_CHANGED => {
+                    <span class="state @event.newValue">@Messages("issue.state." + event.newValue)</span>
+                    @Html(Messages("issue.event." + event.newValue, linkToUser(user.loginId, user.getDisplayName)))
+                }
+                case ISSUE_ASSIGNEE_CHANGED => {
+                    <span class="state changed">@Messages("issue.state.assigned")</span>
+                    @Html(Messages(assginedMesssage(event.newValue, user), linkToUser(user.loginId, user.getDisplayName), linkToUser(event.newValue, User.findByLoginId(event.newValue).getDisplayName, true)))
+                }
+                case ISSUE_MILESTONE_CHANGED => {
+                    <span class="state milestone-changed">@Messages("issue.update.milestone.id")</span>
+                    @Html(Messages("issue.event.milestone.changed", linkToUser(user.loginId, user.getDisplayName), linkOfMilestone(event.newValue, project)))
+                }
+                case ISSUE_REFERRED_FROM_COMMIT => {
+                    <span class="state changed">@Messages("issue.event.referred.title")</span>
+                    @Html(Messages("issue.event.referred", linkToUser(user.loginId, user.getDisplayName), linkToCommit(event.newValue)))
+                }
+                case ISSUE_MOVED => {
+                    <span class="state changed">@Messages("issue.event.moved.title")</span>
+                    @Html(Messages("issue.event.moved", linkToUser(user.loginId, user.getDisplayName), linkToProject(event.oldValue.split("/")(0), event.oldValue.split("/")(1))))
+                }
+                case ISSUE_REFERRED_FROM_PULL_REQUEST => {
+                    <span class="state changed">@Messages("issue.event.referred.title")</span>
+                    @defining(PullRequest.findById(Long.valueOf(event.newValue))) { pull =>
+                        @Html(Messages("issue.event.referred", linkToUser(user.loginId, user.getDisplayName), linkToPullRequest(pull)))
+                    }
+                }
+                case ISSUE_SHARER_CHANGED => {
+                    @if(isAddingEvent(event)) {
+                        @if(isSameEventTypeAndSameAction(event, previousEvent)){
+                            <span class="state"></span>
+                        } else {
+                            <span class="state sharer-added">@Messages("issue.sharer")</span>
+                        }
+                        @Html(Messages("issue.event.sharer.added", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.newValue, User.findByLoginId(event.newValue).getPureNameOnly)))
+                    } else {
+                        @if(isSameEventTypeAndSameAction(event, previousEvent)){
+                            <span class="state"></span>
+                        } else {
+                            <span class="state sharer-deleted">@Messages("issue.event.sharer.deleted.title")</span>
+                        }
+                        @Html(Messages("issue.event.sharer.deleted", linkToUser(user.loginId, user.getPureNameOnly), linkToUser(event.oldValue, User.findByLoginId(event.oldValue).getPureNameOnly)))
+                    }
+                }
+                case ISSUE_LABEL_CHANGED => {
+                    @if(isAddingEvent(event)) {
+                        @if(isSameEventTypeAndSameAction(event, previousEvent)) {
+                            <span class="state"></span>
+                        } else {
+                            <span class="state label-added">@Messages("issue.event.label.added.title")</span>
+                        }
+                        @Html(Messages("issue.event.label.added", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.newValue, project)))
+                    } else {
+                        @if(isSameEventTypeAndSameAction(event, previousEvent)) {
+                            <span class="state"></span>
+                        } else {
+                            <span class="state label-deleted">@Messages("issue.event.label.deleted.title")</span>
+                        }
+                        @Html(Messages("issue.event.label.deleted", linkToUser(user.loginId, user.getPureNameOnly), issueLabelBox(event.oldValue, project)))
+                    }
+                }
+                case _ => {
+                    @event.newValue by @linkToUser(user.loginId, user.getDisplayName)
+                }
+            }
+        }
+        <span class="date"><a href="#event-@event.id">@utils.TemplateHelper.agoOrDateString(event.getDate())</a></span>
+    </li>
+}
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -116,7 +116,7 @@
 }
 }
 
-@conatinsCurrentUserInWatchers = @{issue.getWatchers(false).contains(UserApp.currentUser())}
+@conatinsCurrentUserInWatchers = @{Watch.isWatching(UserApp.currentUser(), issue.asResource())}
 
 @projectLayout(titleForOGTag, project, utils.MenuType.ISSUE){
 @projectMenu(project, utils.MenuType.ISSUE, "main-menu-only")
@@ -184,7 +184,7 @@
                         <div>
                             @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.WATCH)) {
                                 <button id="watch-button" type="button" class="ybtn @if(conatinsCurrentUserInWatchers) {ybtn-watching}"
-                                        data-toggle="button" data-watching="@conatinsCurrentUserInWatchers">
+                                        data-toggle="tooltip" data-placement="top" title="@Messages("issue.watch.description")" data-watching="@conatinsCurrentUserInWatchers">
                                 @if(conatinsCurrentUserInWatchers) {
                                     @Messages("issue.unwatch")
                                 } else {
@@ -567,6 +567,13 @@
             } else {
                 $yobi.notify('@Messages("site.features.error.clipboard")', 1500);
             }
+
+            // timeline label text color adjusting
+            $(".event > .label").each(function() {
+                var $this = $(this);
+                $this.removeClass("dimgray white")
+                        .addClass($yobi.getContrastColor($this.css('background-color')))
+            });
         });
 </script>
 }
app/views/layout.scala.html
--- app/views/layout.scala.html
+++ app/views/layout.scala.html
@@ -23,7 +23,6 @@
 <meta name="twitter:url" content="@play.mvc.Http.Context.current().request().path()" />
 <meta name="twitter:description" content="@{titleArray(titleArray.length-1)}" />
 <link rel="shortcut icon" type="image/x-icon" href="@routes.Assets.at("images/favicon.ico")">
-<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
 <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("bootstrap/css/bootstrap.css")">
 <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("stylesheets/yobicon/style.css")">
 <link rel="stylesheet" type="text/css" media="all" href="@routes.Assets.at("javascripts/lib/select2/select2.css")"/>
app/views/organization/group_pullrequest_list.scala.html
--- app/views/organization/group_pullrequest_list.scala.html
+++ app/views/organization/group_pullrequest_list.scala.html
@@ -17,7 +17,7 @@
 @searchFormAction(category: Category) = @{
     category match {
         case Category.CLOSED => {
-            routes.OrganizationApp.organizationPullRequests(organization.name, "closed")
+            routes.OrganizationApp.organizationClosedPullRequests(organization.name)
         }
         case Category.OPEN => {
             routes.OrganizationApp.organizationPullRequests(organization.name, "open")
app/views/site/siteMngLayout.scala.html
--- app/views/site/siteMngLayout.scala.html
+++ app/views/site/siteMngLayout.scala.html
@@ -59,9 +59,6 @@
                     <li class="@isActiveMenu(routes.SiteApp.massMail())">
                         <a href="@routes.SiteApp.massMail()">@Messages("site.sidebar.massMail")</a>
                     </li>
-                    <li class="@isActiveMenu(routes.SiteApp.data())">
-                        <a href="@routes.SiteApp.data()">@Messages("site.sidebar.data")</a>
-                    </li>
                     <li class="@isActiveMenu(routes.SiteApp.update())">
                         <a href="@routes.SiteApp.update()">@Messages("site.sidebar.update")
                         @if(YobiUpdate.versionToUpdate != null) { <span class="notification-badge">1</span> }
conf/messages
--- conf/messages
+++ conf/messages
@@ -279,6 +279,11 @@
 issue.event.assigned = {0} assigned this issue to {1}
 issue.event.assignedToMe = {0} assigned this issue to oneself
 issue.event.closed = {0} closed this issue
+issue.event.label.added = {0} added {1} label
+issue.event.label.added.title = Added
+issue.event.label.deleted = {0} removed {1} label
+issue.event.label.deleted.title = Removed
+issue.event.milestone.changed = {0} changed milestone to {1}
 issue.event.moved = {0} moved this issue from {1}
 issue.event.moved.title = moved
 issue.event.open = {0} reopened this issue
@@ -335,7 +340,8 @@
 issue.vote.description = Click here if you agree with this issue.
 issue.voters = People who agree with this
 issue.voters.more = and {0} others
-issue.watch = watch this issue
+issue.watch = Subscribe
+issue.watch.description = If subscribe, notify all new comments
 issue.watch.start =Now you will get notifications about this issue
 issue.watchers = Watchers
 issue.watchers.more = and {0} others
@@ -402,12 +408,14 @@
 notification = Notification
 notification.confirm.mail.will.be.sent = If you are trying first login, confirmation mail will be sent.
 notification.help = You will receive notification, when following events occur.
-notification.help.new = when new posts, new issues, new pull-requests or comments on pull-requests are added.
-notification.help.new.comment = when comments are added to post, issue, or code.
-notification.help.update.issue= when issue status or assignee is changed.
+notification.help.new = when new posts, new issues, new pull-requests are added.
+notification.help.new.comment = when comments are added to at your post, issue, or code.
+notification.help.update.issue= when the issue which you are author or assignee is changed.
 notification.help.update.pullrequest = when the status of pull request is changed.
 notification.issue.assigned = Issue has been assigned to {0}
 notification.issue.closed  = Issue has been closed
+notification.issue.label.added = Label {0} is added
+notification.issue.label.deleted = Label {0} is deleted
 notification.issue.reopened = Issue has been reopened
 notification.issue.unassigned = Issue has been unassigned
 notification.issue.sharer.added = Issue is shared with {0}
@@ -420,6 +428,7 @@
 notification.member.request.accept.title = Re: [{0}] {1} is accepted as a member
 notification.member.request.cancel.title = [{0}] {1} cancels the request to be a member.
 notification.member.request.title = [{0}] {1} wants to join your project
+notification.milestone.changed = Milestone is changed to {0}
 notification.none = No notification has been received.
 notification.off.settings = change settings at {0} if you want mute this.
 notification.off.unwatch = You can {0} or
@@ -450,11 +459,13 @@
 notification.type.issue.assignee.changed = Issue assignee changed.
 notification.type.issue.body.changed = Issue body changed
 notification.type.issue.is.moved = Issue moved
+notification.type.issue.label.changed = Issue label changed
 notification.type.issue.moved = Issue has been moved. From {0} To {1}
 notification.type.issue.referred.from.commit = Issue mentioned in commit
 notification.type.issue.referred.from.pullrequest = Issue mentioned in pull request
 notification.type.issue.state.changed = Issue status changed
 notification.type.member.enroll = Requests for joining projects
+notification.type.milestone.changed = Milestone changed
 notification.type.new.comment = New comment on post or issue added
 notification.type.new.commit = New commits on a project
 notification.type.new.issue = New issue added
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -279,6 +279,11 @@
 issue.event.assigned = {0}님이 {1}님을 이 이슈의 담당자로 지정하였습니다.
 issue.event.assignedToMe = {0}님이 자신을 이슈의 담당자로 지정하였습니다.
 issue.event.closed = {0}님이 이 이슈를 닫았습니다.
+issue.event.label.added = {0} 님이 {1} 라벨을 추가했습니다.
+issue.event.label.added.title = 추가
+issue.event.label.deleted = {0} 님이 {1} 라벨을 제거했습니다.
+issue.event.label.deleted.title = 삭제
+issue.event.milestone.changed = {0}님이 마일스톤을 {1}(으)로 변경했습니다.
 issue.event.moved = {0}님이 {1}로부터 이슈를 이동했습니다.
 issue.event.moved.title = 이동함
 issue.event.open = {0}님이 이 이슈를 다시 열었습니다.
@@ -336,7 +341,8 @@
 issue.voters = 이 이슈에 공감하는 사람들
 issue.voters.more = 외 {0}명
 issue.watch = 이슈 지켜보기
-issue.watch.start = 이제 이 이슈에 관한 알림을 받습니다
+issue.watch.description = 이 이슈의 모든 새 댓글을 알림으로 받습니다.
+issue.watch.start = 이제 이 이슈에 대한 모든 알림을 받습니다
 issue.watchers = Watchers
 issue.watchers.more = 외 {0} 명
 label = 라벨
@@ -402,12 +408,14 @@
 notification = 알림
 notification.confirm.mail.will.be.sent = 최초 로그인일 경우 확인 메일이 발송됩니다.
 notification.help = 다음 이벤트가 발생할 때 알림 메시지를 받습니다.
-notification.help.new = 새 글, 이슈, 코드 요청, 코드 요청에 댓글이 등록되었을 때
-notification.help.new.comment = 글, 이슈, 코드에 새 댓글이 등록되었을 때
-notification.help.update.issue= 이슈 상태가 변경되거나 담당자가 변경될 때
+notification.help.new = 새로운 이슈나 게시글, 코드 주고받기가 등록되었을 때