diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 9be49058..3c976233 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -19,4 +19,5 @@
//= require lib/backbone
//= require backbone/kandan
//= require_tree .
-
+//= require lib/jquery.atwho
+//= require lib/jquery.caret
\ No newline at end of file
diff --git a/app/assets/javascripts/backbone/kandan.js.coffee.erb b/app/assets/javascripts/backbone/kandan.js.coffee.erb
index 0f268f1a..24323672 100644
--- a/app/assets/javascripts/backbone/kandan.js.coffee.erb
+++ b/app/assets/javascripts/backbone/kandan.js.coffee.erb
@@ -28,6 +28,7 @@ window.Kandan =
registerPlugins: ->
plugins = [
"UserList"
+ ,"Mentions"
,"Notifications"
,"MusicPlayer"
,"YouTubeEmbed"
@@ -119,7 +120,8 @@ window.Kandan =
Kandan.initTabs()
Kandan.Widgets.initAll()
Kandan.Helpers.Channels.scrollToLatestMessage()
-
+ Kandan.Plugins.Mentions.initUsersMentions(Kandan.Helpers.ActiveUsers.all())
+ return
setCurrentUser: ()->
template = JST['current_user']
diff --git a/app/assets/javascripts/backbone/plugins/mentions.coffee b/app/assets/javascripts/backbone/plugins/mentions.coffee
new file mode 100644
index 00000000..1912b497
--- /dev/null
+++ b/app/assets/javascripts/backbone/plugins/mentions.coffee
@@ -0,0 +1,25 @@
+# The mentions plugin takes care of highlighting the @useranme and passing the users to the atwho plugin.
+# The show_activities addMessage method is the responsible of changing the look of a message body when a user is mentioned
+class Kandan.Plugins.Mentions
+ @options:
+ regex: /@\S*/g
+
+ template: _.template '''<%= mention %>'''
+
+ @init: ()->
+ Kandan.Data.ActiveUsers.registerCallback "change", (data)=>
+ @initUsersMentions(data.extra.active_users)
+
+ Kandan.Modifiers.register @options.regex, (message, state) =>
+ for mention in message.content.match(@options.regex)
+ replacement = @options.template({mention: mention})
+ message.content = message.content.replace(mention, replacement)
+
+ return Kandan.Helpers.Activities.buildFromMessageTemplate(message)
+
+ @initUsersMentions: (activeUsers)->
+ users = _.map activeUsers, (user)->
+ user.username
+
+ $(".chat-input").atwho("@", {data: users});
+ return
diff --git a/app/assets/javascripts/backbone/views/show_activity.js.coffee b/app/assets/javascripts/backbone/views/show_activity.js.coffee
index c3a229c6..a34a9d53 100644
--- a/app/assets/javascripts/backbone/views/show_activity.js.coffee
+++ b/app/assets/javascripts/backbone/views/show_activity.js.coffee
@@ -16,9 +16,16 @@ class Kandan.Views.ShowActivity extends Backbone.View
@compiledTemplate = Kandan.Helpers.Activities.buildFromMessageTemplate activity
$(@el).data("activity-id", activity.id)
- if activity.action == "message" && activity.user.id == Kandan.Helpers.Users.currentUser().id
- $(@el).addClass("current_user")
+ if activity.action == "message"
+ user_mention_regex = new RegExp("@#{Kandan.Helpers.Users.currentUser().username}\\b")
+ all_mention_regex = new RegExp("@all")
+
+ if activity.user.id == Kandan.Helpers.Users.currentUser().id
+ $(@el).addClass("current_user")
+
+ if user_mention_regex.test(@compiledTemplate) || all_mention_regex.test(@compiledTemplate)
+ $(@el).addClass("mentioned_user")
if activity.id == undefined
$(@el).attr("id", "activity-c#{activity.cid}")
diff --git a/app/assets/javascripts/lib/jquery.atwho.js b/app/assets/javascripts/lib/jquery.atwho.js
new file mode 100644
index 00000000..5928761d
--- /dev/null
+++ b/app/assets/javascripts/lib/jquery.atwho.js
@@ -0,0 +1,562 @@
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+
+(function() {
+
+ (function(factory) {
+ if (typeof define === 'function' && define.amd) {
+ return define(['jquery'], factory);
+ } else {
+ return factory(window.jQuery);
+ }
+ })(function($) {
+ var Controller, DEFAULT_CALLBACKS, DEFAULT_TPL, KEY_CODE, Mirror, View;
+ Mirror = (function() {
+
+ Mirror.prototype.css_attr = ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom", 'fontFamily', 'borderStyle', 'borderWidth', 'wordWrap', 'fontSize', 'lineHeight', 'overflowX'];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.copy_inputor_css = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000,
+ 'white-space': 'pre-wrap'
+ };
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('
');
+ this.$mirror.css(this.copy_inputor_css());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.get_flag_rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("span#flag");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ bottom: $flag.height() + pos.top
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+ })();
+ KEY_CODE = {
+ DOWN: 40,
+ UP: 38,
+ ESC: 27,
+ TAB: 9,
+ ENTER: 13
+ };
+ DEFAULT_CALLBACKS = {
+ data_refactor: function(data) {
+ return $.map(data, function(item, k) {
+ if (!$.isPlainObject(item)) {
+ item = {
+ name: item
+ };
+ }
+ return item;
+ });
+ },
+ matcher: function(flag, subtext) {
+ var match, matched, regexp;
+ regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
+ match = regexp.exec(subtext);
+ matched = null;
+ if (match) {
+ matched = match[2] ? match[2] : match[1];
+ }
+ return matched;
+ },
+ filter: function(query, data, search_key) {
+ var _this = this;
+ return $.map(data, function(item, i) {
+ var name;
+ name = $.isPlainObject(item) ? item[search_key] : item;
+ if (name.toLowerCase().indexOf(query) >= 0) {
+ return item;
+ }
+ });
+ },
+ remote_filter: function(params, url, render_view) {
+ return $.ajax(url, {
+ data: params,
+ success: function(data) {
+ return render_view(data);
+ }
+ });
+ },
+ sorter: function(query, items, search_key) {
+ var item, results, text, _i, _len;
+ if (!query) {
+ items;
+
+ }
+ results = [];
+ for (_i = 0, _len = items.length; _i < _len; _i++) {
+ item = items[_i];
+ text = item[search_key];
+ item.order = text.toLowerCase().indexOf(query);
+ results.push(item);
+ }
+ return results.sort(function(a, b) {
+ return a.order - b.order;
+ });
+ },
+ tpl_eval: function(tpl, map) {
+ var el;
+ try {
+ return el = tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+ return map[key];
+ });
+ } catch (error) {
+ return "";
+ }
+ },
+ highlighter: function(li, query) {
+ if (!query) {
+ return li;
+ }
+ return li.replace(new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'), function(str, $1, $2, $3) {
+ return '> ' + $1 + '' + $2 + '' + $3 + ' <';
+ });
+ },
+ selector: function($li) {
+ if ($li.length > 0) {
+ return this.replace_str($li.data("value") || "");
+ }
+ }
+ };
+ Controller = (function() {
+
+ function Controller(inputor) {
+ this.settings = {};
+ this.common_settings = {};
+ this.pos = 0;
+ this.flags = null;
+ this.current_flag = null;
+ this.query = null;
+ this.$inputor = $(inputor);
+ this.mirror = new Mirror(this.$inputor);
+ this.common_settings = $.extend({}, $.fn.atwho["default"]);
+ this.view = new View(this, this.$el);
+ this.listen();
+ }
+
+ Controller.prototype.listen = function() {
+ var _this = this;
+ return this.$inputor.on('keyup.atwho', function(e) {
+ return _this.on_keyup(e);
+ }).on('keydown.atwho', function(e) {
+ return _this.on_keydown(e);
+ }).on('scroll.atwho', function(e) {
+ return _this.view.hide();
+ }).on('blur.atwho', function(e) {
+ return _this.view.hide(_this.get_opt("display_timeout"));
+ });
+ };
+
+ Controller.prototype.reg = function(flag, settings) {
+ var current_settings, data;
+ current_settings = {};
+ current_settings = $.isPlainObject(flag) ? this.common_settings = $.extend({}, this.common_settings, flag) : !this.settings[flag] ? this.settings[flag] = $.extend({}, settings) : this.settings[flag] = $.extend({}, this.settings[flag], settings);
+ data = current_settings["data"];
+ if (typeof data === "string") {
+ current_settings["data"] = data;
+ } else if (data) {
+ current_settings["data"] = this.callbacks("data_refactor").call(this, data);
+ }
+ return this;
+ };
+
+ Controller.prototype.trigger = function(name, data) {
+ data || (data = []);
+ data.push(this);
+ return this.$inputor.trigger("" + name + ".atwho", data);
+ };
+
+ Controller.prototype.data = function() {
+ return this.get_opt("data");
+ };
+
+ Controller.prototype.callbacks = function(func_name) {
+ var func;
+ if (!(func = this.get_opt("callbacks", {})[func_name])) {
+ func = this.common_settings["callbacks"][func_name];
+ }
+ return func;
+ };
+
+ Controller.prototype.get_opt = function(key, default_value) {
+ var value;
+ try {
+ if (this.current_flag) {
+ value = this.settings[this.current_flag][key];
+ }
+ if (value === void 0) {
+ value = this.common_settings[key];
+ }
+ return value = value === void 0 ? default_value : value;
+ } catch (e) {
+ return value = default_value === void 0 ? null : default_value;
+ }
+ };
+
+ Controller.prototype.rect = function() {
+ var $inputor, Sel, at_rect, bottom, format, html, offset, start_range, x, y;
+ $inputor = this.$inputor;
+ if (document.selection) {
+ Sel = document.selection.createRange();
+ x = Sel.boundingLeft + $inputor.scrollLeft();
+ y = Sel.boundingTop + $(window).scrollTop() + $inputor.scrollTop();
+ bottom = y + Sel.boundingHeight;
+ return {
+ top: y - 2,
+ left: x - 2,
+ bottom: bottom - 2
+ };
+ }
+ format = function(value) {
+ return value.replace(//g, '>').replace(/`/g, '`').replace(/"/g, '"').replace(/\r\n|\r|\n/g, "
");
+ };
+ /* 克隆完inputor后将原来的文本内容根据
+ @的位置进行分块,以获取@块在inputor(输入框)里的position
+ */
+
+ start_range = $inputor.val().slice(0, this.pos - 1);
+ html = "" + format(start_range) + "";
+ html += "?";
+ /*
+ 将inputor的 offset(相对于document)
+ 和@在inputor里的position相加
+ 就得到了@相对于document的offset.
+ 当然,还要加上行高和滚动条的偏移量.
+ */
+
+ offset = $inputor.offset();
+ at_rect = this.mirror.create(html).get_flag_rect();
+ x = offset.left + at_rect.left - $inputor.scrollLeft();
+ y = offset.top - $inputor.scrollTop();
+ bottom = y + at_rect.bottom;
+ y += at_rect.top;
+ return {
+ top: y,
+ left: x,
+ bottom: bottom + 2
+ };
+ };
+
+ Controller.prototype.catch_query = function() {
+ var caret_pos, content, end, query, start, subtext,
+ _this = this;
+ content = this.$inputor.val();
+ caret_pos = this.$inputor.caretPos();
+ /* 向在插入符前的的文本进行正则匹配
+ * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个
+ */
+
+ subtext = content.slice(0, caret_pos);
+ query = null;
+ $.each(this.settings, function(flag, settings) {
+ query = _this.callbacks("matcher").call(_this, flag, subtext);
+ if (query != null) {
+ _this.current_flag = flag;
+ return false;
+ }
+ });
+ if (typeof query === "string" && query.length <= 20) {
+ start = caret_pos - query.length;
+ end = start + query.length;
+ this.pos = start;
+ query = {
+ 'text': query.toLowerCase(),
+ 'head_pos': start,
+ 'end_pos': end
+ };
+ this.trigger("matched", [this.current_flag, query.text]);
+ } else {
+ this.view.hide();
+ }
+ return this.query = query;
+ };
+
+ Controller.prototype.replace_str = function(str) {
+ var $inputor, flag_len, source, start_str, text;
+ $inputor = this.$inputor;
+ source = $inputor.val();
+ flag_len = this.get_opt("display_flag") ? 0 : this.current_flag.length;
+ start_str = source.slice(0, (this.query['head_pos'] || 0) - flag_len);
+ text = "" + start_str + str + " " + (source.slice(this.query['end_pos'] || 0));
+ $inputor.val(text);
+ $inputor.caretPos(start_str.length + str.length + 1);
+ return $inputor.change();
+ };
+
+ Controller.prototype.on_keyup = function(e) {
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ this.view.hide();
+ break;
+ case KEY_CODE.DOWN:
+ case KEY_CODE.UP:
+ $.noop();
+ break;
+ default:
+ this.look_up();
+ }
+ return e.stopPropagation();
+ };
+
+ Controller.prototype.on_keydown = function(e) {
+ if (!this.view.visible()) {
+ return;
+ }
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ this.view.hide();
+ break;
+ case KEY_CODE.UP:
+ e.preventDefault();
+ this.view.prev();
+ break;
+ case KEY_CODE.DOWN:
+ e.preventDefault();
+ this.view.next();
+ break;
+ case KEY_CODE.TAB:
+ case KEY_CODE.ENTER:
+ if (!this.view.visible()) {
+ return;
+ }
+ e.preventDefault();
+ this.view.choose();
+ break;
+ default:
+ $.noop();
+ }
+ return e.stopPropagation();
+ };
+
+ Controller.prototype.render_view = function(data) {
+ var search_key;
+ search_key = this.get_opt("search_key");
+ data = this.callbacks("sorter").call(this, this.query.text, data, search_key);
+ data = data.splice(0, this.get_opt('limit'));
+ return this.view.render(data);
+ };
+
+ Controller.prototype.look_up = function() {
+ var data, origin_data, params, query, search_key;
+ query = this.catch_query();
+ if (!query) {
+ return false;
+ }
+ origin_data = this.get_opt("data");
+ search_key = this.get_opt("search_key");
+ if (typeof origin_data === "string") {
+ params = {
+ q: query.text,
+ limit: this.get_opt("limit")
+ };
+ this.callbacks('remote_filter').call(this, params, origin_data, $.proxy(this.render_view, this));
+ } else if ((data = this.callbacks('filter').call(this, query.text, origin_data, search_key))) {
+ this.render_view(data);
+ } else {
+ this.view.hide();
+ }
+ return $.noop();
+ };
+
+ return Controller;
+
+ })();
+ View = (function() {
+
+ function View(controller) {
+ this.controller = controller;
+ this.id = this.controller.get_opt("view_id", "at-view");
+ this.timeout_id = null;
+ this.$el = $("#" + this.id);
+ this.create_view();
+ }
+
+ View.prototype.create_view = function() {
+ var $menu, tpl,
+ _this = this;
+ if (this.exist()) {
+ return;
+ }
+ tpl = "";
+ $("body").append(tpl);
+ this.$el = $("#" + this.id);
+ $menu = this.$el.find('ul');
+ return $menu.on('mouseenter.view', 'li', function(e) {
+ $menu.find('.cur').removeClass('cur');
+ return $(e.currentTarget).addClass('cur');
+ }).on('click', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ return _this.$el.data("_view").choose();
+ });
+ };
+
+ View.prototype.exist = function() {
+ return $("#" + this.id).length > 0;
+ };
+
+ View.prototype.visible = function() {
+ return this.$el.is(":visible");
+ };
+
+ View.prototype.choose = function() {
+ var $li;
+ $li = this.$el.find(".cur");
+ this.controller.callbacks("selector").call(this.controller, $li);
+ this.controller.trigger("choose", [$li]);
+ return this.hide();
+ };
+
+ View.prototype.reposition = function() {
+ var offset, rect;
+ rect = this.controller.rect();
+ if (rect.bottom + this.$el.height() - $(window).scrollTop() > $(window).height()) {
+ rect.bottom = rect.top - this.$el.height();
+ }
+ offset = {
+ left: rect.left,
+ top: rect.bottom
+ };
+ this.$el.offset(offset);
+ return this.controller.trigger("reposition", [offset]);
+ };
+
+ View.prototype.next = function() {
+ var cur, next;
+ cur = this.$el.find('.cur').removeClass('cur');
+ next = cur.next();
+ if (!next.length) {
+ next = $(this.$el.find('li')[0]);
+ }
+ return next.addClass('cur');
+ };
+
+ View.prototype.prev = function() {
+ var cur, prev;
+ cur = this.$el.find('.cur').removeClass('cur');
+ prev = cur.prev();
+ if (!prev.length) {
+ prev = this.$el.find('li').last();
+ }
+ return prev.addClass('cur');
+ };
+
+ View.prototype.show = function() {
+ if (!this.visible()) {
+ this.$el.show();
+ }
+ return this.reposition();
+ };
+
+ View.prototype.hide = function(time) {
+ var callback,
+ _this = this;
+ if (isNaN(time)) {
+ if (this.visible()) {
+ return this.$el.hide();
+ }
+ } else {
+ callback = function() {
+ return _this.hide();
+ };
+ clearTimeout(this.timeout_id);
+ return this.timeout_id = setTimeout(callback, time);
+ }
+ };
+
+ View.prototype.clear = function() {
+ return this.$el.find('ul').empty();
+ };
+
+ View.prototype.render = function(list) {
+ var $ul, tpl,
+ _this = this;
+ if (!$.isArray(list)) {
+ return false;
+ }
+ if (list.length <= 0) {
+ this.hide();
+ return true;
+ }
+ this.clear();
+ this.$el.data("_view", this);
+ $ul = this.$el.find('ul');
+ tpl = this.controller.get_opt('tpl', DEFAULT_TPL);
+ $.each(list, function(i, item) {
+ var $li, li;
+ li = _this.controller.callbacks("tpl_eval").call(_this.controller, tpl, item);
+ $li = $(_this.controller.callbacks("highlighter").call(_this.controller, li, _this.controller.query.text));
+ $li.data("info", item);
+ return $ul.append($li);
+ });
+ this.show();
+ return $ul.find("li:eq(0)").addClass("cur");
+ };
+
+ return View;
+
+ })();
+ DEFAULT_TPL = "${name}";
+ $.fn.atwho = function(flag, options) {
+ return this.filter('textarea, input').each(function() {
+ var $this, data;
+ $this = $(this);
+ data = $this.data("atwho");
+ if (!data) {
+ $this.data('atwho', (data = new Controller(this)));
+ }
+ return data.reg(flag, options);
+ });
+ };
+ $.fn.atwho.Controller = Controller;
+ $.fn.atwho.View = View;
+ $.fn.atwho.Mirror = Mirror;
+ return $.fn.atwho["default"] = {
+ data: null,
+ search_key: "name",
+ callbacks: DEFAULT_CALLBACKS,
+ limit: 5,
+ display_flag: true,
+ display_timeout: 300,
+ tpl: DEFAULT_TPL
+ };
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/jquery.caret.js b/app/assets/javascripts/lib/jquery.caret.js
new file mode 100644
index 00000000..93694a28
--- /dev/null
+++ b/app/assets/javascripts/lib/jquery.caret.js
@@ -0,0 +1,128 @@
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+
+(function() {
+
+ (function(factory) {
+ if (typeof exports === 'object') {
+ return factory(require('jquery'));
+ } else if (typeof define === 'function' && define.amd) {
+ return define(['jquery']);
+ } else {
+ return factory(window.jQuery);
+ }
+ })(function($) {
+ var getCaretPos, setCaretPos;
+ getCaretPos = function(inputor) {
+ var end, endRange, len, normalizedValue, pos, range, start, textInputRange;
+ if (document.selection) {
+ /*
+ #assume we select "HATE" in the inputor such as textarea -> { }.
+ * start end-point.
+ * /
+ * < I really [HATE] IE > between the brackets is the selection range.
+ * \
+ * end end-point.
+ */
+
+ range = document.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ /* SOMETIME !!!
+ "/r/n" is counted as two char.
+ one line is two, two will be four. balalala.
+ so we have to using the normalized one's length.;
+ */
+
+ len = normalizedValue.length;
+ /*
+ <[ I really HATE IE ]>:
+ the whole content in the inputor will be the textInputRange.
+ */
+
+ textInputRange = inputor.createTextRange();
+ /* _here must be the position of bookmark.
+ /
+ <[ I really [HATE] IE ]>
+ [---------->[ ] : this is what moveToBookmark do.
+ < I really [[HATE] IE ]> : here is result.
+ \ two brackets in should be in line.
+ */
+
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ /* [--------------------->[] : if set false all end-point goto end.
+ < I really [[HATE] IE []]>
+ */
+
+ endRange.collapse(false);
+ /*
+ ___VS____
+ / \
+ < I really [[HATE] IE []]>
+ \_endRange end-point.
+
+ " > -1" mean the start end-point will be the same or right to the end end-point
+ * simplelly, all in the end.
+ */
+
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ start = end = len;
+ } else {
+ /*
+ I really |HATE] IE ]>
+ <-|
+ I really[ [HATE] IE ]>
+ <-[
+ I reall[y [HATE] IE ]>
+
+ will return how many unit have moved.
+ */
+
+ start = -textInputRange.moveStart("character", -len);
+ end = -textInputRange.moveEnd("character", -len);
+ }
+ }
+ } else {
+ start = inputor.selectionStart;
+ }
+ return start;
+ };
+ setCaretPos = function(inputor, pos) {
+ var range;
+ if (document.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ return range.select();
+ } else {
+ return inputor.setSelectionRange(pos, pos);
+ }
+ };
+ return $.fn.caretPos = function(pos) {
+ var inputor;
+ inputor = this[0];
+ inputor.focus();
+ if (pos) {
+ return setCaretPos(inputor, pos);
+ } else {
+ return getCaretPos(inputor);
+ }
+ };
+ });
+
+}).call(this);
diff --git a/app/assets/stylesheets/_activity.sass b/app/assets/stylesheets/_activity.sass
index d99ed04d..21f8700d 100644
--- a/app/assets/stylesheets/_activity.sass
+++ b/app/assets/stylesheets/_activity.sass
@@ -42,6 +42,12 @@
margin: 0 5px
vertical-align: bottom
border: 1px solid gray
+ .mention
+ border: 1px solid #bbb
+ background-color: #bcd3e8
+ color: black
+ padding: 2px 3px
+ border-radius: 5px
.emoticon-embed
vertical-align: bottom
.emoticon-embed.small
@@ -74,8 +80,14 @@
.current_user
background-color: #F5F5F5
.posted_at
- background: #E6EAEB
- color: #A9B1B6
+ background: #bcd3e8
+ color: #6F7477
+
+.mentioned_user
+ background-color: #E2ECF5
+ .posted_at
+ background: #bcd3e8
+ color: #6F7477
img.image-embed
max-width: 300px
diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass
index ae087027..766666f4 100644
--- a/app/assets/stylesheets/application.css.sass
+++ b/app/assets/stylesheets/application.css.sass
@@ -9,4 +9,5 @@
@import sidebar
@import plugins/attachments
@import plugins/user_list
-@import plugins/notification_list
+@import lib/jquery.atwho
+@import plugins/notification_list
\ No newline at end of file
diff --git a/app/assets/stylesheets/lib/jquery.atwho.css b/app/assets/stylesheets/lib/jquery.atwho.css
new file mode 100644
index 00000000..a6904abc
--- /dev/null
+++ b/app/assets/stylesheets/lib/jquery.atwho.css
@@ -0,0 +1,48 @@
+#at-view {
+ position:absolute;
+ top: 0;
+ left: 0;
+ display: none;
+ margin-top: 18px;
+ background: white;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ box-shadow: 0 0 5px rgba(0,0,0,0.1);
+ min-width: 120px;
+ z-index: 601;
+}
+
+#at-view .cur {
+ background: #3366FF;
+ color: white;
+}
+#at-view .cur small {
+ color: white;
+}
+#at-view strong {
+ color: #3366FF;
+}
+#at-view .cur strong {
+ color: white;
+ font:bold;
+}
+#at-view ul {
+ /* width: 100px; */
+ list-style:none;
+ padding:0;
+ margin:auto;
+}
+#at-view ul li {
+ display: block;
+ padding: 5px 10px;
+ border-bottom: 1px solid #DDD;
+ cursor: pointer;
+ /* border-top: 1px solid #C8C8C8; */
+}
+#at-view small {
+ font-size: smaller;
+ color: #777;
+ font-weight: normal;
+}
+
+
diff --git a/app/models/activity_observer.rb b/app/models/activity_observer.rb
index 41ac8ac4..ba745dab 100644
--- a/app/models/activity_observer.rb
+++ b/app/models/activity_observer.rb
@@ -11,7 +11,7 @@ def after_save(activity)
def message_broadcast_data(activity)
faye_channel = "/channels/#{activity.channel.to_param}"
broadcast_data = activity.attributes.merge({
- :user => activity.user.as_json(:only => [:id, :ido_id, :email, :first_name, :last_name, :gravatar_hash, :active, :locale]),
+ :user => activity.user.as_json(:only => [:id, :ido_id, :email, :first_name, :last_name, :gravatar_hash, :active, :locale, :username]),
:channel => activity.channel.attributes
})
[faye_channel, broadcast_data]
@@ -22,7 +22,7 @@ def upload_broadcast_data(activity)
broadcast_data = {
:event => "attachment#upload",
:entity => activity.attributes.merge({
- :user => activity.user.as_json(:only => [:id, :ido_id, :email, :first_name, :last_name, :gravatar_hash, :active, :locale]),
+ :user => activity.user.as_json(:only => [:id, :ido_id, :email, :first_name, :last_name, :gravatar_hash, :active, :locale, :username]),
:channel => activity.channel.attributes
}),
:extra => {