improve chat

This commit is contained in:
Thibault Duplessis 2014-01-03 20:17:09 +01:00
parent 687984724e
commit db4c499dc8
12 changed files with 204 additions and 101 deletions

View file

@ -155,25 +155,26 @@ data-accept-languages="@acceptLanguages.mkString(",")">
@ctx.chat.map { c =>
<div id="chat_wrap"@if(ctx.pref.chat.on) { class="on"}>
<div class="bar">
<a class="on">@trans.chat()</a>
<a class="friends">
<div class="left"><a class="on">@trans.chat()</a></div>
<div class="right friends">
@trans.onlineFriends() - <strong class="online"> </strong>/<span class="total"> </span>
</a>
</div>
<div class="center last"></div>
</div>
<div id="chat">
<a class="off"></a>
<a class="help">help</a>
<div class="chans"></div>
<div class="lists">
<div class="left chans"></div>
<div class="right lists">
<div class="tabs">
<a data-tab="friends" class="friends">@trans.onlineFriends()</a>
<a data-tab="teams" class="teams">@trans.teams()</a>
</div>
<div id="team_box" class="list_box teams" data-preload="@ctx.teams">
<div class="content list"></div>
<div class="content"></div>
</div>
<div id="friend_box" class="list_box friends" data-preload="@ctx.friends">
<div class="content list"></div>
<div class="content"></div>
<div class="nobody">
<span>@trans.noFriendsOnline()</span>
<a class="find button" href="@routes.Relation.suggest(me.username)">
@ -182,7 +183,7 @@ data-accept-languages="@acceptLanguages.mkString(",")">
</div>
</div>
</div>
<div class="room">
<div class="center room">
<div class="lines"></div>
<div class="controls">
<form action="#">

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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 _

View file

@ -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 = "<pre>" + 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 <chan> enter a chat room. Ex: /join en
/query <friend> start a private chat with a friend
/part quit the current room
/names show the users connected to the current room
_______________________ user commands ______________________
/msg <user> send a message to a user

View file

@ -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)

View file

@ -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('<span class="user">' + l.user + '</span> : <span class="text">' + l.html + '</span>');
}
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 '<div class="chan ' + color + (main ? ' main' : '') + ' clearfix">' +
return '<div data-chan="' + chan.key + '" class="chan ' + color + (main ? ' main' : '') + ' clearfix">' +
'<a class="close">x</a>' +
'<div class="check">' +
'<input type="checkbox"' + (active ? " checked" : "") + ' id="' + id + '" name="' + chan.key + '"/>' +
'<label for="' + id + '"></label>' +
'</div>' +
'<span data-chan="' + chan.key + '" class="name">' + chan.name + '</span>' +
'<span class="name">' + chan.name + '</span>' +
'</div>';
}).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'));
},

View file

@ -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 {

View file

@ -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;
}

5
todo
View file

@ -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
------