diff --git a/app/views/base/layout.scala.html b/app/views/base/layout.scala.html index 3aeb0a91f8..c7b7a9699e 100644 --- a/app/views/base/layout.scala.html +++ b/app/views/base/layout.scala.html @@ -155,25 +155,26 @@ data-accept-languages="@acceptLanguages.mkString(",")"> @ctx.chat.map { c =>
- @trans.chat() - + +
@trans.onlineFriends() - / - +
+
help -
-
+
+ -
+
diff --git a/modules/chat/src/main/Api.scala b/modules/chat/src/main/Api.scala index 83d72a506c..d720ab5ad0 100644 --- a/modules/chat/src/main/Api.scala +++ b/modules/chat/src/main/Api.scala @@ -14,6 +14,8 @@ private[chat] final class Api( getTeamIds: String ⇒ Fu[List[String]], netDomain: String) { + private val NB_LINES = 30 + def join(user: User, chat: ChatHead, chan: Chan): ChatHead = { chanVoter(user.id, chan.key) truncate(user, chat join chan, chan.key) @@ -58,7 +60,7 @@ private[chat] final class Api( def populate(head: ChatHead, user: User): Fu[Chat] = namer.chans(head.chans, user) zip { relationApi blocking user.id flatMap { - LineRepo.find(head.activeChanKeys, user.troll, _, 20) flatMap { + LineRepo.find(head.activeChanKeys, user.troll, _, NB_LINES) flatMap { _.map(namer.line).sequenceFu } } diff --git a/modules/chat/src/main/Chan.scala b/modules/chat/src/main/Chan.scala index d13d91e4ac..52c008e34e 100644 --- a/modules/chat/src/main/Chan.scala +++ b/modules/chat/src/main/Chan.scala @@ -76,11 +76,11 @@ object LangChan { } case class UserChan(u1: String, u2: String) extends IdChan("user", false) { - val id = List(u1, u2).sorted mkString "-" + val id = List(u1, u2).sorted mkString "/" def contains(userId: String) = u1 == userId || u2 == userId } object UserChan { - def apply(id: String): Option[UserChan] = id.split("-") match { + def apply(id: String): Option[UserChan] = id.split("/") match { case Array(u1, u2) ⇒ UserChan(u1, u2).some case _ ⇒ none } diff --git a/modules/chat/src/main/Chat.scala b/modules/chat/src/main/Chat.scala index 8083e70c3b..d67fb5a6d2 100644 --- a/modules/chat/src/main/Chat.scala +++ b/modules/chat/src/main/Chat.scala @@ -40,6 +40,8 @@ case class ChatHead( def join(c: Chan) = setChan(c, true).setActiveChanKey(c.key, true).setMainChanKey(c.key.some) + def part(c: Chan) = setChan(c, false) + def inactiveChanKeys = chanKeys filterNot activeChanKeys.contains def updatePref(pref: ChatPref) = ChatPref( @@ -48,6 +50,8 @@ case class ChatHead( activeChans = activeChanKeys filterNot Chan.autoActive, mainChan = (mainChanKey filterNot Chan.autoActive) orElse pref.mainChan) + def sees(line: Line) = (chanKeys contains line.chan.key) && (activeChanKeys contains line.chan.key) + def sorted = copy(chans = chans.sorted) } diff --git a/modules/chat/src/main/ChatActor.scala b/modules/chat/src/main/ChatActor.scala index fcaa597a85..19a8377e54 100644 --- a/modules/chat/src/main/ChatActor.scala +++ b/modules/chat/src/main/ChatActor.scala @@ -51,7 +51,12 @@ private[chat] final class ChatActor( saveAndReload(member) } - case Say(chan, member, text) => api.write(chan.key, member.userId, text) foreach { _ foreach self.! } + case Part(member, chan) ⇒ { + member setHead (member.head part chan) + save(member) + } + + case Say(chan, member, text) ⇒ api.write(chan.key, member.userId, text) foreach { _ foreach self.! } case Activate(member, chan) ⇒ api.active(member, chan) foreach { head ⇒ member setHead head @@ -134,8 +139,10 @@ private[chat] final class ChatActor( } } + private def save(member: ChatMember) = prefApi.setChatPref(member.userId, member.head.updatePref) + private def saveAndReload(member: ChatMember) { - prefApi.setChatPref(member.userId, member.head.updatePref) >>- reload(member) + save(member) >>- reload(member) } private def reload(m: ChatMember) { diff --git a/modules/chat/src/main/ChatMember.scala b/modules/chat/src/main/ChatMember.scala index 7aa99f418f..4a86ef444e 100644 --- a/modules/chat/src/main/ChatMember.scala +++ b/modules/chat/src/main/ChatMember.scala @@ -16,9 +16,7 @@ private[chat] final class ChatMember( def wants(line: Line) = line.chan match { case c: UserChan if c.contains(userId) ⇒ true - case _ ⇒ (troll || !line.troll) && - (head.activeChanKeys contains line.chan.key) && - !blocks(line.userId) + case _ ⇒ (troll || !line.troll) && (head sees line) && !blocks(line.userId) } def hasActiveChan = head.activeChanKeys contains _ diff --git a/modules/chat/src/main/Commander.scala b/modules/chat/src/main/Commander.scala index c64f6153f9..ec37d29d26 100644 --- a/modules/chat/src/main/Commander.scala +++ b/modules/chat/src/main/Commander.scala @@ -20,21 +20,27 @@ private[chat] final class Commander( namer: Namer, getTeamIds: String ⇒ Fu[List[String]]) extends Actor { - val chat = context.parent + private val chat = context.parent + + private val aliases = Map( + "leave" -> "part") def receive = { - case command @ Command(chanOption, member, text) ⇒ text.split(' ').toList match { + case command@Command(chanOption, member, text) ⇒ text.split(' ').toList match { - case "help" :: "tutorial" :: _ ⇒ flash(member, tutorial) - case "help" :: "mod" :: _ ⇒ flash(member, modHelp) - case "help" :: _ ⇒ flash(member, help) + case aliased :: words if aliases.contains(aliased) ⇒ self ! command.copy( + text = ((aliases.get(aliased) | aliased) :: words) mkString " ") - case "open" :: _ ⇒ chat ! SetOpen(member, true) - case "close" :: _ ⇒ chat ! SetOpen(member, false) + case "help" :: "tutorial" :: Nil ⇒ flash(member, tutorial) + case "help" :: "mod" :: Nil ⇒ flash(member, modHelp) + case "help" :: Nil ⇒ flash(member, help) - case "query" :: username :: _ ⇒ chat ! Query(member, username.toLowerCase) + case "open" :: Nil ⇒ chat ! SetOpen(member, true) + case "close" :: Nil ⇒ chat ! SetOpen(member, false) - case "join" :: chanKey :: _ ⇒ Chan parse chanKey match { + case "query" :: username :: Nil ⇒ chat ! Query(member, username.toLowerCase) + + case "join" :: chanKey :: Nil ⇒ Chan parse chanKey match { case Some(chan@TeamChan(teamId)) ⇒ getTeamIds(member.userId) foreach { case teamIds if teamIds.contains(teamId) ⇒ chat ! Join(member, chan) case _ ⇒ flash(member, s"You are not a member of this team.") @@ -42,18 +48,18 @@ private[chat] final class Commander( case Some(chan) ⇒ chat ! Join(member, chan) case None ⇒ flash(member, s"The channel $chanKey does not exist.") } - case "show" :: chanKey :: _ ⇒ Chan parse chanKey foreach { chan ⇒ - chat ! Activate(member, chan) - } - case "hide" :: chanKey :: _ ⇒ Chan parse chanKey foreach { chan ⇒ - chat ! DeActivate(member, chan) - } - case "say" :: words ⇒ chanOption foreach { chan ⇒ chat ! Say(chan, member, words mkString " ") } + case "show" :: chanKey :: Nil ⇒ withChan(chanKey) { chan ⇒ chat ! Activate(member, chan) } + case "hide" :: chanKey :: Nil ⇒ withChan(chanKey) { chan ⇒ chat ! DeActivate(member, chan) } + + case "part" :: chanKey :: Nil ⇒ withChan(chanKey) { chan ⇒ chat ! Part(member, chan) } + case "part" :: Nil ⇒ member.head.mainChanKey flatMap Chan.parse foreach { chan ⇒ chat ! Part(member, chan) } + + case "say" :: words ⇒ chanOption foreach { chan ⇒ chat ! Say(chan, member, words mkString " ") } case escaped :: _ if escaped.startsWith("/") ⇒ self ! command.copy(text = "say " + text) - case "names" :: _ ⇒ chanOption foreach { chan ⇒ + case "names" :: Nil ⇒ chanOption foreach { chan ⇒ userOf(member) foreach { user ⇒ namer.chan(chan, user) foreach { named ⇒ chat ! WithChanNicks(chan.key, { nicks ⇒ @@ -63,10 +69,10 @@ private[chat] final class Commander( } } - case ("rematch" | "resign" | "abort" | "takeback") :: _ ⇒ gameOnlyCommand(member) - case MoveRegex(orig, dest) :: _ ⇒ gameOnlyCommand(member) + case ("rematch" | "resign" | "abort" | "takeback") :: Nil ⇒ gameOnlyCommand(member) + case MoveRegex(orig, dest) :: Nil ⇒ gameOnlyCommand(member) - case "troll" :: username :: _ ⇒ Secure(member, _.MarkTroll) { me ⇒ + case "troll" :: username :: Nil ⇒ Secure(member, _.MarkTroll) { me ⇒ modApi.troll(me.id, username) foreach { troll ⇒ flash(member, s"User $username is ${troll.fold("now", "no longer")} a troll.") } @@ -97,6 +103,10 @@ private[chat] final class Commander( private def userOf(member: ChatMember): Fu[User] = UserRepo byId member.userId flatten s"No such user: $member.userId" + private def withChan(key: String)(f: Chan ⇒ Unit) { + Chan parse key foreach f + } + val tutorial = "
" + escapeXml("""
 _______________________ lichess chat _______________________
 The text input at the bottom can be used to command lichess!
@@ -108,6 +118,7 @@ For instance, try and send /help to see available commands.
 _______________________ chat commands ______________________
 /join             enter a chat room. Ex: /join en
 /query          start a private chat with a friend
+/part                   quit the current room
 /names                  show the users connected to the current room
 _______________________ user commands ______________________
 /msg              send a message to a user
diff --git a/modules/chat/src/main/actorApi.scala b/modules/chat/src/main/actorApi.scala
index a1d4bb8fe2..8ea574239f 100644
--- a/modules/chat/src/main/actorApi.scala
+++ b/modules/chat/src/main/actorApi.scala
@@ -11,6 +11,7 @@ case class SetOpen(member: ChatMember, value: Boolean)
 case class Query(member: ChatMember, username: String)
 
 case class Join(member: ChatMember, chan: Chan)
+case class Part(member: ChatMember, chan: Chan)
 
 case class Say(chan: Chan, member: ChatMember, text: String)
 
diff --git a/public/javascripts/big.js b/public/javascripts/big.js
index 893570a152..4241e1de23 100644
--- a/public/javascripts/big.js
+++ b/public/javascripts/big.js
@@ -857,7 +857,6 @@ var storage = {
       self.$invite = self.$form.find('.invite');
       self.$input = self.$form.find('input');
 
-      self.on = _lc_.on;
       self.systemUsername = 'Lichess';
       self.reload(_lc_);
 
@@ -868,8 +867,11 @@ var storage = {
         self._tell();
         return false;
       });
+      self.$chans.on('click', '.close', function() {
+        self._part($(this).parent().data('chan'));
+      });
       self.$chans.on('click', '.name', function() {
-        self._join($(this).data('chan'));
+        self._join($(this).parent().data('chan'));
       });
       self.$chans.on('click', 'input', function() {
         self._show($(this).attr('name'), $(this).prop('checked'));
@@ -877,7 +879,7 @@ var storage = {
       self.element.find('.help').click(function() {
         self._send('/help tutorial');
       });
-      self.$bar.find('> *').click(function() {
+      self.$bar.click(function() {
         self._send('/open');
       });
       self.element.find('> .off').click(function() {
@@ -913,17 +915,20 @@ var storage = {
       this.head = c.head;
       if (!this._exists(this.head.mainChan)) this.head.mainChan = null;
       this.lines = c.lines;
-      this._renderChans();
-      this._renderLines();
-      this._renderForm();
+      this._renderAll();
     },
     line: function(l) {
       var self = this;
       self.lines.push(l);
+      var rendered = self._renderLine(l);
       if (l.chan.type == "user" && !self._isActive(l.chan.key)) {
         self._send('/show ' + l.chan.key);
       }
-      self.$lines.append(self._renderLine(l)).scrollTop(999999);
+      if (!self.$wrap.hasClass('on') && self._isActive(l.chan.key)) {
+        self.$bar.find('.last').html('' + l.user + ' : ' + l.html + '');
+      }
+      self.$lines.append(rendered);
+      self._scrollLines();
     },
     flash: function(html) {
       var self = this;
@@ -958,6 +963,9 @@ var storage = {
       else if (text == '/open') {
         self.$wrap.addClass('on');
         self._joinFirst();
+        self._scrollLines();
+      } else if (text == '/part' || text == '/leave') {
+        if (self.head.mainChan) self._part(self.head.mainChan);
       } else if (text.match(/^\/msg\s(\w+)/)) {
         location.href = '/inbox/new?username=' + text.match(/^\/msg\s(\w+)/)[1];
         return;
@@ -990,8 +998,19 @@ var storage = {
     },
     _join: function(chan) {
       this._disablePlaceholder();
+      if (this._isActive(chan)) { // immediate rendering
+        this.head.mainChan = chan;
+        this._renderAll();
+      }
       this._send('/join ' + chan);
     },
+    _part: function(chan) {
+      delete this.head.chans[chan];
+      this.head.activeChans = _.difference(this.head.activeChans, [chan]);
+      if (this.head.mainChan == chan) this.head.mainChan = null;
+      this._renderAll();
+      this._send('/part ' + chan);
+    },
     _chanIndex: function(key) {
       return _.keys(this.head.chans).indexOf(key);
     },
@@ -1004,6 +1023,11 @@ var storage = {
     _isActive: function(chan) {
       return _.contains(this.head.activeChans, chan);
     },
+    _renderAll: function() {
+      this._renderForm();
+      this._renderChans();
+      this._renderLines();
+    },
     _renderLine: function(line) {
       var self = this;
       if (!self._isActive(line.chan.key)) return '';
@@ -1023,7 +1047,11 @@ var storage = {
       var self = this;
       self.$lines.html(_.map(self.lines, function(line) {
         return self._renderLine(line);
-      }).join('')).scrollTop(999999);
+      }).join(''));
+      self._scrollLines();
+    },
+    _scrollLines: function() {
+      this.$lines.scrollTop(999999);
     },
     _renderChans: function() {
       var self = this;
@@ -1032,12 +1060,13 @@ var storage = {
         var active = self.head.activeChans.indexOf(chan.key) != -1;
         var main = self.head.mainChan == chan.key;
         var color = self._colorClass(self._chanIndex(chan.key));
-        return '
' + + return '
' + + 'x' + '
' + '' + '' + '
' + - '' + chan.name + '' + + '' + chan.name + '' + '
'; }).join('')); }, @@ -1062,7 +1091,7 @@ var storage = { $.widget("lichess.teams", { _create: function() { var self = this; - self.$list = self.element.find("div.list"); + self.$list = self.element.find("div.content"); self.$list.on('click', 'a', function() { $('#chat').onechat('team', $(this).data('id')); }); @@ -1086,7 +1115,7 @@ var storage = { self.$title = self.options.$title; self.$nbOnline = self.$title.find('.online'); self.$nbTotal = self.$title.find('.total'); - self.$list = self.element.find("div.list"); + self.$list = self.element.find("div.content"); self.$nobody = self.element.find("div.nobody"); self.set(self.element.data('preload')); }, diff --git a/public/stylesheets/common.css b/public/stylesheets/common.css index 0b3a008c0d..5937859a18 100644 --- a/public/stylesheets/common.css +++ b/public/stylesheets/common.css @@ -437,49 +437,66 @@ div.footer div.right { position: fixed; bottom: 0; } +#chat_wrap div.left { + float: left; + width: 220px; + border-right: 1px solid #ccc; +} +#chat_wrap div.right { + float: right; + width: 220px; + border-left: 1px solid #ccc; +} +#chat_wrap div.center { + margin: 0 221px; +} #chat_wrap > .bar { width: 100%; height: 32px; - border-top: 1px solid #444; - background: rgba(20, 20, 20, 0.9); + border-top: 1px solid #ccc; + background: rgba(230, 230, 230, 0.7); position: relative; + cursor: pointer; } #chat_wrap.on > .bar { display: none; } -#chat_wrap > .bar > * { - position: absolute; - top: 0; - display:inline-block; +#chat_wrap > .bar:hover { + background: #ccc; +} +#chat_wrap > .bar > div > * { height: 32px; line-height: 32px; text-decoration: none; - text-transform: uppercase; } -#chat_wrap > .bar > *:hover { - background-color: #0a0a0a; -} -#chat_wrap > .bar > .on { - left: 0; +#chat_wrap > .bar .on { + display:block; background: url(/assets/images/open32.png) top left no-repeat; background-position: +10px +1px; font-size: 1.2em; padding: 0 10px 0 46px; + text-transform: uppercase; } -#chat_wrap > .bar > .friends { - right: 0; - width: 220px; +#chat_wrap > .bar .last { + display: block; +} +#chat_wrap > .bar .last > span { + display: inline-block; + margin-left: 10px; + color: #505050; +} +#chat_wrap > .bar .friends { text-transform: capitalize; text-align: center; - border-left: 1px solid #444; } #chat { width: 100%; height: 195px; - border-top: 1px solid #444; + border-top: 1px solid #ccc; position: relative; display: none; - background: rgba(20, 20, 20, 0.9); + background: #f0f0f0; + box-shadow: 0 0 15px #888; } #chat_wrap.on #chat { display: block; @@ -499,7 +516,7 @@ div.footer div.right { -webkit-transform: rotate(180deg); } #chat > .off:hover { - background-color: #0a0a0a; + background-color: #ccc; } #chat > .help { display: inline-block; @@ -509,16 +526,13 @@ div.footer div.right { height: 16px; line-height: 16px; padding: 0 8px; - border-top: 1px solid #444; - border-right: 1px solid #444; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; text-decoration: none; - background: rgba(20, 20, 20, 0.9); + background: #f0f0f0; } #chat > .chans { - float: left; - width: 220px; height: 100%; - border-right: 1px solid #444; } #chat .chan { display: block; @@ -526,11 +540,11 @@ div.footer div.right { line-height: 27px; height: 27px; padding: 0px 10px; - border-bottom: 1px solid #3a3a3a; + border-bottom: 1px solid #ccc; cursor: pointer; } #chat .chan.main { - background: #030303; + background: #ccc; } #chat .chan > .name { display: block; @@ -538,6 +552,21 @@ div.footer div.right { white-space: nowrap; overflow: hidden; } +#chat .chan > .close { + text-decoration: none; + float: left; + display: none; + height: 17px; + line-height: 17px; + margin: 3px 0 0 -7px; + padding: 2px 8px; +} +#chat .chan:hover > .close { + display: block; +} +#chat a.close:hover { + background-color: #ccc; +} #chat div.check { float: right; width: 20px; @@ -557,8 +586,8 @@ div.footer div.right { border-radius: 4px; } #chat .chan:hover div.check label { - background: linear-gradient(to bottom, #222 0%, #45484d 100%); - box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, .4); + background: #c5c8cd; + background: linear-gradient(to bottom, #ccc 0%, #a5a8ad 100%); } #chat div.check label:after { opacity: 0; @@ -569,7 +598,7 @@ div.footer div.right { background: transparent; top: 5px; left: 4px; - border: 3px solid #fcfff4; + border: 3px solid #0c0f04; border-top: none; border-right: none; transform: rotate(-45deg); @@ -584,14 +613,10 @@ div.footer div.right { #chat .chan:hover div.check input[type=checkbox]:checked + label:after { opacity: 1; } -#chat > .room { - margin-left: 221px; - margin-right: 221px; -} #chat > .room > .lines { height: 167px; overflow: hidden; - border-bottom: 1px solid #444; + border-bottom: 1px solid #ccc; } #chat > .room > .lines:hover { overflow-y: auto; @@ -601,7 +626,7 @@ div.footer div.right { padding: 1px 10px; position: relative; opacity: 0.7; - color: #b0b0b0; + color: #505050; } #chat .line.main { opacity: 1; @@ -620,20 +645,17 @@ div.footer div.right { text-decoration: none; } #chat .line > .text { - display: inline-block; + display: inline; } #chat .line pre { margin: 0; } #chat > .lists { - float: right; - width: 220px; - height: 100%; - border-left: 1px solid #444; + height: 195px; overflow: hidden; } #chat > .lists > .tabs { - border-bottom: 1px solid #444; + border-bottom: 1px solid #ccc; display: block; } #chat > .lists > .tabs > a { @@ -644,12 +666,13 @@ div.footer div.right { padding: 0 10px; } #chat > .lists > .tabs > a.active { - background-color: #202020; + background-color: #ccc; } #chat .list_box { display: none; - height: 100%; + height: 175px; width: 100%; + overflow: hidden; } #chat .list_box:hover { overflow-y: auto; @@ -662,7 +685,7 @@ div.footer div.right { overflow: hidden; } #chat .list_box .content a:hover { - background-color: #202020; + background-color: #ccc; } #friend_box .content .s16 { background-position: 0 -208px; @@ -700,13 +723,13 @@ div.footer div.right { line-height: 27px; position: relative; margin-right: 12px; - background: #0a0a0a; + background: #ccc; } #chat .controls > form .invite:after { content:""; border-top: 14px solid transparent; border-bottom: 14px solid transparent; - border-left: 14px solid #0a0a0a; + border-left: 14px solid #ccc; position: absolute; right: -14px; top: 0; @@ -718,7 +741,7 @@ div.footer div.right { height: 19px; padding: 4px 10px; border: none; - color: #b0b0b0; + color: #505050; background: none; } #chat .color0 { diff --git a/public/stylesheets/dark.css b/public/stylesheets/dark.css index 7fa794dd83..5f17f7b794 100644 --- a/public/stylesheets/dark.css +++ b/public/stylesheets/dark.css @@ -156,9 +156,37 @@ body.dark a#sound_state.sound_state_on span { body.dark .s16.ddown { background-position: right -368px; } -body.dark #chat .chan { - border-color: #2a2a2a; +body.dark #chat { + box-shadow: 0 0 15px #000; } +body.dark #chat_wrap > .bar { + background: rgba(20, 20, 20, 0.9); +} +body.dark #chat, body.dark #chat > .help { + background: #101010; +} +body.dark #chat > .lists > .tabs, body.dark #chat > .room > .lines, body.dark #chat, body.dark #chat .chan, body.dark #chat_wrap > .bar, body.dark #chat > .help, body.dark #chat_wrap div.left, body.dark #chat_wrap div.right, body.dark #chat_wrap div.left { + border-color: #303030; +} +body.dark #chat .controls > form .invite, body.dark #chat .list_box .content a:hover, body.dark #chat > .lists > .tabs > a.active, body.dark #chat .chan.main, body.dark #chat a.close:hover, body.dark #chat_wrap > .bar:hover, body.dark #chat > .off:hover { + background-color: #0a0a0a; +} +body.dark #chat .controls > form .invite:after { + border-left-color: #0a0a0a; +} +body.dark #chat .chan:hover div.check label { + background-color: #45484d; +} +body.dark #chat div.check label:after { + border-color: #fcfff4; +} +body.dark #chat_wrap > .bar .last > span, body.dark #chat .controls > form .input, body.dark #chat .line { + color: #b0b0b0; +} +body.dark #chat .line.sys { + color: #808080; +} + body.dark ::-webkit-input-placeholder { color: #666; } diff --git a/todo b/todo index 0f2aee8574..8637c58d68 100644 --- a/todo +++ b/todo @@ -106,11 +106,8 @@ time pie chart colors http://en.lichess.org/52ede6hu/stats full page recent forum posts en_GB, en_US, pt_PT, pt_BR = localization /takeback to accept (or sys message) -chat multiline badges out of screen chat click user to chat -try to switch chan without server -chat links = new tab moretime button position (>1hour) move command sys message abort system message @@ -119,6 +116,8 @@ copy rematch chats chat logs maybe no chat transparency? indicator when someone talks to you but the chat is closed +user badge is too large; also add a link to profile to it +chat visited link color deploy ------