ADD possibilité d'afficher une bulle d'aide sur un coté determiné d'une boite
[euphorik.git] / js / pageMinichat.js
1 // coding: utf-8
2 // Copyright 2008 Grégory Burri
3 //
4 // This file is part of Euphorik.
5 //
6 // Euphorik is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // Euphorik is distributed in the hope that it will be useful,
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with Euphorik. If not, see <http://www.gnu.org/licenses/>.
18
19 function PageMinichat(client, formateur, util)
20 {
21 this.nom = "minichat"
22
23 this.client = client
24 this.formateur = formateur
25 this.util = util
26
27 // permet d'éviter d'envoyer plusieurs messages simultanément en pressant
28 // rapidement sur "enter" par exemple
29 this.envoieMessageEnCours = false
30
31 this.regexMessageTagMatch = /\{.*?\}>/g
32 this.regexMessageTagReplace = /^(.*?\{.*?\}>)*/
33 }
34
35 PageMinichat.prototype.contenu = function()
36 {
37 // le fait que tout soit collé est fait exprès, permet d'éviter d'avoir des espaces supplémentaires entre les spans'
38 var formulaireXHTML = '<form method="post" action="" id ="posterMessage">\
39 <p>\
40 <input class="captcha" name="captcha" type="text" size="8" maxlength="8"></input>\
41 <input class="pseudo" name="pseudo" type="text" maxlength="50" value="' + encodeURI(conf.nickDefaut) + '"></input>\
42 <span id="repondA"><span class="nb">0</span><span class="messages"></span></span>\
43 <input class="message" name="message" type="text" maxlength="500" value=""></input>\
44 <button class="smiles"></button>\
45 <button class="return"></button>\
46 </p>\
47 </form>'
48 var trollXHTML = '<div id="trollCourant">Troll de la semaine : <span class="troll"></span></div>'
49 //var titreXHTML = '<tr id="titres"></tr>'
50 //var messagesXHTML = '<tr id="messages"></tr>'
51 var conversationXHTML = '<table id="conversations"><tr></tr></table>'
52
53 if (this.client.chatOrder == "reverse")
54 return trollXHTML + formulaireXHTML + conversationXHTML
55 else
56 return trollXHTML + conversationXHTML + formulaireXHTML
57 }
58
59 PageMinichat.prototype.classes = function()
60 {
61 return this.client.chatOrder == "reverse" ? "orderReverse" : "orderChrono"
62 }
63
64 PageMinichat.prototype.charger = function()
65 {
66 thisPage = this
67
68 $("form input.pseudo").val(this.client.pseudo)
69
70 // cet appel ne doit pas être fait avant l'appel à 'charger'
71 this.conversations = new Conversations(this.client, this.formateur, this.util)
72
73 this.conversations.rafraichirMessages(true)
74
75 this.util.setCaretToEnd($("form input.message")[0])
76
77 // les outils de bannissement (uniquement pour les ekMaster)
78 if (this.client.ekMaster)
79 {
80 this.util.outilsBan = $(
81 '<span id="outilsBan">' +
82 '<form action=""><p><input id="raison" name="raison" type="text" size="10" maxlength="200"></input></p></form>' +
83 '<img id="ban" src="img/ban.gif" alt="Ban de 3 jours" />' +
84 '<img id="kick" src="img/kick.gif" alt="Ban de 15min" />' +
85 '<img id="slap" src="img/slap.gif" alt="Avertissement" />' +
86 '</span>'
87 )
88
89 this.util.infoBulle("Slap", $("#slap", this.util.outilsBan))
90 this.util.infoBulle("Kick (" + conf.tempsKick + "min)", $("#kick", this.util.outilsBan))
91 this.util.infoBulle("Ban (" + conf.tempsBan / 24 / 60 + " jours)", $("#ban", this.util.outilsBan))
92 this.util.infoBulle("La raison", $("input", this.util.outilsBan))
93 }
94
95 this.util.infoBulle("Ouvrir la conversation liée au troll de la semaine", $("#trollCourant .troll"))
96
97 this.util.infoBulle("Cliquer sur les messages pour les enlevers de la liste",
98 $("form#posterMessage #repondA").hover(
99 function() { thisPage.util.afficherBoite($(".messages", this), $(this), positionTypeX.centre, positionTypeY.bas) },
100 function() { $(".messages", this).hide() }
101 ).click(
102 function(e)
103 {
104 if ($(e.target).is(".nb"))
105 thisPage.conversations.enleverMessagesRepond()
106 }
107 ),
108 positionBulleType.droite
109 )
110
111 // <smiles>
112 $("body").append('<div id="smiles"></div>')
113 // affichage des smiles
114 $("#smiles").append(this.formateur.getSmilesHTML()).children().each(
115 function(i)
116 {
117 var opacityBase = $(this).css("opacity")
118 $(this).click(
119 function()
120 {
121 thisPage.util.replaceSelection($("form#posterMessage input.message")[0], thisPage.formateur.smiles[$(this).attr("class")][0].source.replace(/\\/g, ""))
122 }
123 ).hover(
124 function() { $(this).animate({opacity: 1}, 200) },
125 function() { $(this).animate({opacity: opacityBase}, 200) }
126 )
127 }
128 )
129 $("form button.smiles").hover(
130 // affichage de la boite présentant les smiles
131 function(e){ thisPage.util.afficherBoite($("#smiles"), $(e.target), positionTypeX.centre, positionTypeY.basRecouvrement) },
132 function(){}
133 )
134 $("#smiles").hover(
135 function(){},
136 function()
137 {
138 $("#smiles").hide()
139 }
140 )
141 // </smiles>
142
143 // événements
144 var nouveauMessage =
145 function()
146 {
147 // captcha anti bot
148 if ($("form input.captcha").val() != "") return
149
150 thisPage.envoyerMessage(
151 $("form input.pseudo").val(),
152 $("form input.message").val()
153 )
154
155 $("form input.message").focus()
156 }
157
158 $("form").keypress(
159 function(e)
160 {
161 if (e.which == 13) // return
162 nouveauMessage()
163 }
164 )
165
166 $("form button.return").click(nouveauMessage)
167
168 // interdiction de submiter le formulaire
169 $("form").submit(function(){ return false})
170
171 $("input.pseudo").click(
172 function()
173 {
174 var input = $("input.pseudo")[0]
175 if (input.value == conf.pseudoDefaut)
176 input.value = ""
177 }
178 )
179 }
180
181 PageMinichat.prototype.decharger = function()
182 {
183 this.conversations.pageEvent.stopAttenteCourante()
184
185 $("body #smiles").remove()
186 }
187
188 PageMinichat.prototype.getJSONMessage = function(pseudo, message)
189 {
190 var repondA = []
191 for (var id in this.conversations.messagesRepond)
192 repondA.push(parseInt(id)) // FIXME : une propriété ne peut pas être de type int ?
193
194 return {
195 "header" : { "action" : "put_message", "version" : conf.versionProtocole },
196 "cookie" : this.client.cookie,
197 "nick" : pseudo,
198 "content" : message,
199 "answer_to" : repondA
200 }
201 }
202
203 PageMinichat.prototype.envoyerMessage = function(pseudo, message)
204 {
205 var thisPageMinichat = this
206
207 // (un pseudo vide est autorisé)
208 pseudo = this.formateur.filtrerInputPseudo(pseudo)
209
210 if (pseudo == conf.nickDefaut)
211 {
212 this.util.messageDialogue("Le pseudo ne peut pas être " + conf.nickDefaut)
213 return
214 }
215
216 message = message.trim()
217 if (message == "")
218 {
219 this.util.messageDialogue("Le message est vide")
220 return
221 }
222
223 if (!this.client.authentifie())
224 if (!this.client.enregistrement())
225 {
226 this.util.messageDialogue("login impossible")
227 return
228 }
229
230 this.client.pseudo = pseudo
231
232 // évite le double post
233 if (this.envoieMessageEnCours)
234 {
235 this.util.messageDialogue("Message en cours d'envoie...")
236 return
237 }
238 this.envoieMessageEnCours = true
239
240 jQuery.ajax(
241 {
242 url : "request",
243 type: "POST",
244 data : this.util.jsonVersAction(this.getJSONMessage(pseudo, message)),
245 dataType : "json",
246 beforeSend : function(xmlHttpRequest)
247 {
248 // TODO : ça marche ça ??
249 xmlHttpRequest.setRequestHeader("X-Requested-With", "")
250 },
251 success : function(data, textStatus)
252 {
253 if(data["reply"] == "ok")
254 {
255 // met à jour la classe des messages auquel repond celui ci (c'est un peu de la triche) TODO : ya mieux ?
256 for (var messId in thisPageMinichat.conversations.messagesRepond)
257 {
258 for (var j = 0; j < thisPageMinichat.conversations.conversations.length; j++)
259 {
260 var mess = thisPageMinichat.conversations.conversations[j].messagesParId[messId]
261 if (mess != undefined)
262 mess.clientARepondu = true
263 }
264 // TODO : ca sert à qque chose ?
265 //$("#conversations div#" + thisPageMinichat.conversations.messagesRepond[messId].getId()).addClass("repondu")
266 }
267
268 $("form input.message").val("")
269 thisPageMinichat.conversations.enleverMessagesRepond()
270 }
271 else if (data["reply"] == "error")
272 {
273 thisPageMinichat.util.messageDialogue(data["error_message"])
274 }
275 thisPageMinichat.envoieMessageEnCours = false
276 },
277 error : function()
278 {
279 thisPageMinichat.envoieMessageEnCours = false
280 }
281 }
282 )
283 }
284
285 ///////////////////////////////////////////////////////////////////////////////////////////////////
286
287 function Reponse(id, pseudo, login)
288 {
289 this.id = id
290 this.pseudo = pseudo
291 this.login = login
292
293 if (this.pseudo == undefined)
294 this.pseudo = ""
295
296 if (this.login == undefined)
297 this.login = ""
298 }
299
300 ///////////////////////////////////////////////////////////////////////////////////////////////////
301
302 /**
303 * Représente un message.
304 */
305 function Message(client, formateur, element)
306 {
307 this.client = client
308 this.formateur = formateur
309
310 this.id = element["id"]
311 this.auteurId = element["user_id"]
312 this.racineId = element["root"]
313 this.date = element["date"]
314 this.pseudo = element["nick"]
315 this.login = element["login"]
316 this.contenu = element["content"]
317
318 this.appartientAuClient = element["owner"]
319 this.clientARepondu = element["answered"]
320 this.estUneReponse = element["is_a_reply"]
321 this.systeme = element["system"] // est-ce un message 'système' ?
322 this.setRepondA(element["answer_to"]) // un ensemble de reponse (voir Reponse) indexé par l'id du message de la reponses
323 this.ekMaster = element["ek_master"]
324 this.degreeOstentatoire = element["ostentatious_master"]
325 }
326
327 /**
328 * @param pre est un prefix permettant de créer un Id différent pour deux mêmes messages.
329 * Cela est utile pour afficher plusieurs mêmes messages au sein d'un document XHTML.
330 * voir également la fonction 'XHTML()'.
331 */
332 Message.prototype.getId = function(pre)
333 {
334 if (pre == undefined)
335 pre = ""
336 return pre + "mess" + this.id.toString(36)
337 }
338
339 /**
340 *
341 */
342 Message.prototype.setRepondA = function(repondAJSON)
343 {
344 var thisMessage = this
345 this.repondA = {}
346
347 for(var i = 0; i < repondAJSON.length; i++)
348 {
349 thisMessage.repondA[repondAJSON[i]["id"]] = new Reponse(
350 repondAJSON[i]["id"],
351 repondAJSON[i]["nick"],
352 repondAJSON[i]["login"]
353 )
354 }
355 }
356
357 /**
358 * Renvoie les messages faisant partie d'une conversation.
359 * @param messages l'ensemble des messages de la conversation
360 * @return les id des messages qui ont été mis en evidence sous la forme
361 * d'un hash (object) {id => 0 | 1 | 2 | 3}. 1 : proprietaire, 2 : reponse directe, 3 : message repondu
362 */
363 Message.prototype.getConversation = function(messages)
364 {
365 var thisMessage = this
366
367 // les messages faisant partie de la conversation
368 var messagesEnEvidence = {}
369
370 messagesEnEvidence[this.id] = 1
371
372 // recherche les réponses (O(n))
373 for (var i = 0; i < messages.messages.length; i++)
374 if (messages.messages[i].repondA.hasOwnProperty(this.id))
375 messagesEnEvidence[messages.messages[i].id] = 2
376
377 // parcours en
378 var f = function(tabIds, premierNiveau)
379 {
380 for(var id in tabIds)
381 {
382 // si le message (id) a déjà été traité
383 if (messagesEnEvidence[id] != undefined && !premierNiveau)
384 continue
385
386 var message = messages.messagesParId[id]
387 if (message != undefined)
388 {
389 messagesEnEvidence[id] = premierNiveau ? 3 : (message.auteurId == thisMessage.auteurId ? 1 : 0)
390 f (message.repondA, false)
391 }
392 }
393 }
394 f(this.repondA, true)
395
396 return messagesEnEvidence
397 }
398
399 /**
400 * Renvoie le message sous la forme de code XHTML (string) prêt à être inséré dans un document.
401 * Aucun callback n'est affecté.
402 */
403 Message.prototype.XHTML = function(messagePair, pre)
404 {
405 if (messagePair == undefined)
406 messagePair = true
407 if (pre == undefined)
408 pre = ""
409
410 // construit l'identifiant de la personne
411 var identifiant =
412 this.client.nickFormat == "nick" || this.login == "" ? this.formateur.traitementComplet(this.pseudo) :
413 (this.client.nickFormat == "login" ? this.formateur.traitementComplet(this.login) :
414 this.formateur.traitementComplet(this.pseudo) + "<span class=\"login\">(" + this.formateur.traitementComplet(this.login) +")</span>" )
415
416 var XHTMLrepondA = ""
417 var debut = true
418 for (var id in this.repondA)
419 {
420 if (!debut) XHTMLrepondA += ", "
421 XHTMLrepondA += this.formateur.traitementComplet(this.repondA[id].pseudo)
422 debut = false
423 }
424 if (XHTMLrepondA != "")
425 XHTMLrepondA = "<span class=\"repondA\">" + XHTMLrepondA + "</span><span class=\"delimitationRepondA\"></span>"
426
427 return "<div id=\"" + this.getId(pre) + "\" class=\"" + (messagePair ? "messagePair" : "messageImpair") + " message" +
428 (this.appartientAuClient ? " proprietaire" : "") +
429 (this.clientARepondu ? " repondu" : "") +
430 (this.estUneReponse ? " reponse" : "") +
431 (this.systeme ? " systeme" : "") +
432 (this.ekMaster ? " ekMaster" + this.degreeOstentatoire : "") +
433 "\">" +
434 "<div class=\"outilsMess\"><div class=\"extraire\"></div><div class=\"extraireCompletement\"></div></div><span class=\"entete\">" +
435 "<span class=\"dateComplete\">[<span class=\"date\">" + this.date + "</span>]</span>" +
436 "<span class=\"pseudo\"><span class=\"id\" style=\"display: none\">" + this.auteurId + "</span class=\"ident\">" + identifiant + "</span></span><span class=\"delimitationEntete\"></span>" +
437 XHTMLrepondA +
438 "<span class=\"contenu\">" + this.formateur.traitementComplet(this.contenu, this.pseudo) + "</span>" +
439 "</div>"
440 }
441
442 ///////////////////////////////////////////////////////////////////////////////////////////////////
443
444 /**
445 * Représente une conversation.
446 * Une conversation, au niveau XHTML, est formé de deux partie, le titre et les messages.
447 * Le titre comprend la navigation par page, un bouton pour la fermer, un bouton pour la plier, un bouton
448 * pour créer un lien ainsi que le premier message.
449 * @param conversations l'ensemble des conversations
450 * @param num le numéro de la conversation
451 */
452 function Conversation(conversations, num)
453 {
454 this.conversations = conversations
455 this.num = num // peut changer au cours de la vie de la conversation, n'est pas un id !
456 this.id = Math.floor(Math.random() * 1000000).toString(36)
457
458 this.util = this.conversations.util
459 this.formateur = this.conversations.formateur
460 this.client = this.conversations.client
461
462 this.idDernierMessageAffiche = 0
463 this.racine = undefined
464
465 this.messages = []
466 this.messagesParId = {}
467
468 this.nbMessageMax = conf.nbMessageAffiche // Le nombre de message affiché par page
469
470 var messagesXHTML = '<div class="messages"></div>'
471 var messageRacineXHTML = '<div class="messageRacine"></div>'
472 var reverse = this.client.chatOrder == "reverse"
473
474 var XHTML =
475 '<td class="conversation" id="' + this.getId() + '">' +
476 (reverse ? messagesXHTML : "") +
477 '<div class="titre">' +
478 (reverse ? messageRacineXHTML : "") +
479 '<div class="nav">' +
480 (num == 0 ? '' : '<div class="fermer"></div><div class="lien"></div><div class="reduire"></div>') +
481 '<span class="next">&lt;</span><span class="numPage">1</span><span class="prev">&gt;</span>' +
482 '</div>' +
483 (reverse ? "" : messageRacineXHTML) +
484 '</div>' +
485 (reverse ? "" : messagesXHTML) +
486 '</td>'
487
488 $("#conversations tr").append(XHTML)
489
490 this.util.infoBulle("Aller à la première page", $("#" + this.getId() + " .numPage"), positionBulleType.haut)
491 if (num != 0)
492 {
493 this.util.infoBulle("Créer un lien vers la conversation", $("#" + this.getId() + " .lien"))
494 this.util.infoBulle("Fermer la conversation", $("#" + this.getId() + " .fermer"))
495 }
496 }
497
498 /**
499 * @racine un message représentant la racine de la conversation, vaut undefined pour la conversation générale
500 */
501 Conversation.prototype.setRacine = function(racineElement)
502 {
503 this.racine = new Message(this.client, this.formateur, racineElement)
504 }
505
506 /**
507 * Met à jour la racine, décide de l'afficher ou non.
508 * On l'affiche uniquement si le message racine n'est pas déjà affiché sur la liste des messages.
509 */
510 Conversation.prototype.majRacine = function()
511 {
512 if (this.racine == undefined)
513 return
514
515 if (!(this.racine.id in this.messagesParId))
516 {
517 this.messagesParId[this.racine.id] = this.racine
518 var element = $(this.racine.XHTML(true, this.getId()))
519 this.attacherEventsSurMessage(element)
520 $("#" + this.getId() + " .titre .messageRacine").html(element)
521 }
522 else // si le message fait déjà parti de la conversation on n'affiche pas la racine
523 $("#" + this.getId() + " .titre .messageRacine").empty()
524 }
525
526 Conversation.prototype.enleverMiseEnEvidence = function()
527 {
528 $("#" + this.getId() + " .message").removeClass("cache")
529 }
530
531 Conversation.prototype.colorerEntetes = function()
532 {
533 var messagesReponse = ""
534 var messagesRepondu = ""
535 var messagesProprietaire = ""
536 for (var i = 0; i < this.messages.length; i++)
537 {
538 if (this.messages[i].appartientAuClient)
539 messagesProprietaire += "#" + this.messages[i].getId(this.getId()) + ","
540 else if (this.messages[i].clientARepondu)
541 messagesRepondu += "#" + this.messages[i].getId(this.getId()) + ","
542 else if (this.messages[i].estUneReponse)
543 messagesReponse += "#" + this.messages[i].getId(this.getId()) + ","
544 }
545 $(messagesReponse).addClass("reponse")
546 $(messagesRepondu).addClass("repondu")
547 $(messagesProprietaire).addClass("proprietaire")
548 }
549
550 Conversation.prototype.decolorerEntetes = function()
551 {
552 $("#" + this.getId() + " .messages .message")
553 .removeClass("reponse")
554 .removeClass("repondu")
555 .removeClass("proprietaire")
556 }
557
558 /**
559 * Défini la page courante et s'il l'on se trouve sur la dernière page.
560 * @pageCourante la page courante
561 * @dernierePage true si c'est la dernière page sinon false
562 */
563 Conversation.prototype.setPage = function(pageCourante, dernierePage)
564 {
565 $("#" + this.getId() + " .numPage").text(pageCourante)
566 $("#" + this.getId() + " .next").css("display", pageCourante == 1 ? "none" : "inline")
567 $("#" + this.getId() + " .prev").css("display", dernierePage ? "none" : "inline")
568 }
569
570 /**
571 * Evenement déclanché lors de l'insertion du lien de la conversation dans le message courant.
572 */
573 Conversation.prototype.eventLien = function(fun)
574 {
575 var thisConversation = this
576
577 $("#" + this.getId() + " .titre .lien").click(
578 function()
579 {
580 fun(thisConversation.num)
581 }
582 )
583 }
584
585 /**
586 * Evenement déclanché lors de la fermeture de la conversation.
587 */
588 Conversation.prototype.eventFermer = function(fun)
589 {
590 var thisConversation = this
591
592 $("#" + this.getId() + " .titre .fermer").click(
593 function()
594 {
595 fun(thisConversation.num)
596 }
597 )
598 }
599
600 /**
601 * @funNext appelé lorsque l'on passe à la page suivante (de 2 à 1 par exemple)
602 * @funPrev appelé lorsque l'on passe à la page précédente (de 1 à 2 par exemple)
603 * @funReset appelé lorsque l'on souhaite revenir à la page une
604 */
605 Conversation.prototype.setFunPage = function(funNext, funPrev, funReset)
606 {
607 var thisConversation = this
608
609 $("#" + this.getId() + " .next").click(
610 function() { funNext(thisConversation.num) }
611 )
612 $("#" + this.getId() + " .prev").click(
613 function() { funPrev(thisConversation.num) }
614 )
615 $("#" + this.getId() + " .numPage").click(
616 function() { funReset(thisConversation.num) }
617 )
618 }
619
620 /**
621 * Retourne l'id de la conversation sous la forme (par exemple) "conv3".
622 */
623 Conversation.prototype.getId = function()
624 {
625 return "conv" + this.id
626 }
627
628 Conversation.prototype.ajouterMessage = function(message)
629 {
630 this.messages.push(message)
631 this.messagesParId[message.id] = message
632 if (this.messages.length > this.nbMessageMax)
633 delete this.messagesParId[this.messages.shift().id]
634 }
635
636 /**
637 * FIXME : méthode très lourde. ne serait-ce pas mieux de virer d'un coup l'élément conversation et d'en remettre un vide ?
638 */
639 Conversation.prototype.viderMessages = function()
640 {
641 this.messages = []
642 this.messagesParId = {}
643 this.idDernierMessageAffiche = 0
644 $("#" + this.getId() + " .messages .message").remove()
645 }
646
647 Conversation.prototype.idMessageFromString = function(idString)
648 {
649 return parseInt(idString.substr(4 + this.getId().length), 36)
650 }
651
652 /**
653 * Après l'ajout d'un ou plusieurs message cette méthode est appelée afin
654 * d'afficher les messages non-affichés.
655 * FIXME : méthode super lourde, à optimiser.
656 */
657 Conversation.prototype.flush = function()
658 {
659 var thisConversation = this
660 var reverse = this.client.chatOrder == "reverse"
661
662 // est-ce que le prochain message est pair ? (permet d'alterner le style des messages)
663 var messagePair = (this.idDernierMessageAffiche == 0 ? true :
664 ($("#" + this.getId() + " .messages div:" + (reverse ? "first" : "last")).attr("class").search("messagePair") == -1)
665 )
666
667 // construction de l'XHTML des messages
668 var XHTML = ""
669 for (var i = 0; i < this.messages.length; i++)
670 if (this.messages[i].id > this.idDernierMessageAffiche)
671 {
672 XHTML += this.messages[i].XHTML(messagePair, this.getId())
673 messagePair = !messagePair
674 }
675
676 var DOM = $(XHTML)
677
678 // pour chaque nouveau message au niveau du document on crée ses événements
679 DOM.each(function() { thisConversation.attacherEventsSurMessage(this) })
680 if (reverse)
681 DOM.prependTo("#" + this.getId() + " .messages")
682 else
683 DOM.appendTo("#" + this.getId() + " .messages")
684
685 // enlève les messages exedentaires
686 var nbMessagesAffiche = $("#" + this.getId() + " .messages .message").size()
687 if (nbMessagesAffiche > this.nbMessageMax)
688 {
689 if (reverse)
690 $("#" + this.getId() + " .messages .message").slice(this.nbMessageMax, nbMessagesAffiche).remove()
691 else
692 $("#" + this.getId() + " .messages .message").slice(0, nbMessagesAffiche - this.nbMessageMax).remove()
693 }
694
695 if (this.messages.length > 0)
696 this.idDernierMessageAffiche = this.messages[this.messages.length-1].id
697
698 // met à jour la racine de la conversation
699 this.majRacine()
700 }
701
702 Conversation.prototype.attacherEventsSurMessage = function(element)
703 {
704 // l'id du message
705 var idMess = this.idMessageFromString($(element).attr("id"))
706
707 this.util.infoBulle("Extraction de la conversation à partir de ce message", $(".extraire", element))
708 this.util.infoBulle("Extraction de la conversation complète", $(".extraireCompletement", element))
709
710 var thisConversation = this
711 $(".lienConv", element).click(
712 function(event)
713 {
714 // FIXME : ya pas mieux ?
715 var racine = $(event.target).text()
716 thisConversation.conversations.ouvrirConversation(parseInt(idString.substring(1, racine.length - 1), 36))
717 return false
718 }
719 )
720
721 $(element).click(
722 function(event)
723 {
724 if ($(event.target).is("a") || $(event.target).parents("#outilsBan").length > 0) return
725
726 // extraction d'une conversation
727 if ($(event.target).is(".extraire"))
728 {
729 thisConversation.conversations.ouvrirConversation(idMess)
730 return
731 }
732
733 if ($(event.target).is(".extraireCompletement"))
734 {
735 thisConversation.conversations.ouvrirConversation(thisConversation.messagesParId[idMess].racineId)
736 return
737 }
738
739 // met ou enlève la mise en evidence du message
740 thisConversation.conversations.toggleMessageRepond(thisConversation.messagesParId[idMess])
741
742 // donne le focus à la ligne de saisie
743 $("form input.message").focus()
744 }
745 )
746
747 // mise en évidence de la conversation
748 $(".entete", element).hover(
749 function()
750 {
751 thisConversation.decolorerEntetes()
752 thisConversation.afficherConversation(idMess)
753 },
754 // quand on sort de l'entête du message la mise en évidence est enlevée
755 function()
756 {
757 thisConversation.enleverMiseEnEvidence()
758 thisConversation.decolorerEntetes()
759 thisConversation.colorerEntetes()
760 }
761 )
762
763 if (thisConversation.client.viewTimes)
764 $(".dateComplete", element).show()
765 else
766 $(".dateComplete", idMess).hide()
767
768 $("a[@rel*=lightbox]", idMess).lightBox()
769
770 // les outils de bannissement (uniquement pour les ekMaster)
771 if (thisConversation.client.ekMaster)
772 $(".pseudo", idMess).hover(
773 function(e)
774 {
775 var userId = parseInt($(".id", this).text())
776 var element = $(this)
777 var h = element.height()
778 var offset = element.offset()
779 thisConversation.util.outilsBan.css("top", offset.top - 2).css("left", offset.left - 2).height(h < 16 ? 16 : h).width(element.width() + 16 * 3 + 4 + 64).prependTo(this).show()
780 $("img", thisConversation.util.outilsBan).unbind("click")
781 $("#slap", thisConversation.util.outilsBan).click(
782 function()
783 {
784 thisConversation.client.slap(userId, $("#outilsBan input").val())
785 $("#outilsBan input").val("")
786 $("#outilsBan").hide()
787 }
788 )
789 $("#kick", thisConversation.util.outilsBan).click(
790 function()
791 {
792 thisConversation.client.kick(userId, $("#outilsBan input").val())
793 $("#outilsBan input").val("")
794 $("#outilsBan").hide()
795 }
796 )
797 $("#ban", thisConversation.util.outilsBan).click(
798 function()
799 {
800 thisConversation.client.ban(userId, $("#outilsBan input").val())
801 $("#outilsBan input").val("")
802 $("#outilsBan").hide()
803 }
804 )
805 },
806 function()
807 {
808 $("#outilsBan", this).hide()
809 }
810 )
811 }
812
813 /**
814 * Etablit une liste des messages à mettre en evidence et des messages à cacher.
815 * Puis applique un plan diabolique.
816 * @param id l'id du message
817 */
818 Conversation.prototype.afficherConversation = function(id)
819 {
820 var thisConversation = this
821
822 var message = this.messagesParId[id]
823 if (message == undefined) return
824
825 var mess = message.getConversation(this)
826
827 // FIXME : cet appel est très lent
828 $("#" + this.getId() + " .messages .message").each(
829 function()
830 {
831 var jq = $(this)
832 var statut = mess[thisConversation.idMessageFromString(jq.attr("id"))]
833 if (statut == undefined)
834 jq.addClass("cache")
835 else
836 {
837 jq.removeClass("cache")
838 switch (statut)
839 {
840 // "repondu" et "reponse" sont prioritaitres à "proprietaire"
841 // contrairement à la vue normale (sans mise en évidence d'une conversation)
842 case 3 :
843 jq.addClass("repondu")
844 break;
845 case 2 :
846 jq.addClass("reponse")
847 break;
848 case 1 :
849 jq.addClass("proprietaire")
850 break;
851 }
852 }
853 }
854 )
855 }
856
857 /**
858 * Supprime une conversation.
859 */
860 Conversation.prototype.supprimer = function()
861 {
862 $("#" + this.getId()).remove()
863 }
864
865 ///////////////////////////////////////////////////////////////////////////////////////////////////
866
867 /**
868 * Représente l'ensemble des conversations affichés.
869 */
870 function Conversations(client, formateur, util)
871 {
872 this.client = client
873 this.formateur = formateur
874 this.util = util
875
876 // un ensemble des messages (id) auquel l'utilisateur répond (vider après l'envoie du message courant)
877 this.messagesRepond = {}
878
879 this.conversations = new Array() // les conversations, la première représente la conversation principale
880
881 this.nouvelleConversation(0)
882
883 this.trollIdCourant = 0
884
885 this.pageEvent = new PageEvent("chat", this.util)
886 }
887
888 // les messages insérés dans le document XHTML on leur id prefixé par cette valeur
889 // cela permet de les distinguer des "vrais" messages
890 Conversations.prototype.prefixIdMessage = "rep"
891
892 /**
893 * Permet de définir un message comme étant ou n'étant plus un message auquel l'utilisateur
894 * répond.
895 */
896 Conversations.prototype.toggleMessageRepond = function(mess)
897 {
898 // est-ce que l'on répond déjà à ce message ? si oui alors on l'enlève de la liste
899 if (mess.id in this.messagesRepond)
900 {
901 this.enleverMessageRepond(mess)
902 return
903 }
904
905 this.ajouterMessageRepond(mess)
906 }
907
908 /**
909 * Enlève tous les messages auquel l'utilisateur souhaite répond.
910 */
911 Conversations.prototype.enleverMessagesRepond = function()
912 {
913 for (var messId in this.messagesRepond)
914 this.enleverMessageRepond(this.messagesRepond[messId])
915
916 // on réinitialise pour être sur que tout est bien enlevé
917 this.messagesRepond = {}
918 $("#conversations .message").removeClass("repondEnEvidence")
919 $("form#posterMessage #repondA .messages").empty()
920 }
921
922 /**
923 * Définit un message comme n'y répondant plus.
924 */
925 Conversations.prototype.enleverMessageRepond = function(mess)
926 {
927 $(this.exprIdsPotentiels(mess)).removeClass("repondEnEvidence")
928 $("#" + mess.getId(this.prefixIdMessage)).remove()
929 delete this.messagesRepond[mess.id]
930 this.rafraichireNombreMessagesRepond()
931 }
932
933 /**
934 * Définit un message comme y répondant.
935 */
936 Conversations.prototype.ajouterMessageRepond = function(mess)
937 {
938 var thisMessages = this
939
940 // est-ce que le message fait partie de la même conversation que les autres messages ?
941 // TODO : solution plus élégante pour prendre un mess parmis messagesRepond !?
942 var mess2
943 for(mess2 in this.messagesRepond){ break; }
944 mess2 = this.messagesRepond[mess2]
945
946 if (mess2 != undefined && mess2.racineId != mess.racineId)
947 {
948 this.util.messageDialogue("Impossible de répondre à deux messages ne faisant pas partie de la même conversation")
949 return
950 }
951
952 $("form#posterMessage #repondA .messages").append(mess.XHTML(undefined, this.prefixIdMessage))
953 this.messagesRepond[mess.id] = mess
954
955 // ajout la classe 'repondEnEvidence' au message. comme il peut se trouver potentiellement dans
956 // chaque conversation on construit tous les id potentiels
957 $(mess.getId(this.prefixIdMessage) + ", " + this.exprIdsPotentiels(mess)).addClass("repondEnEvidence")
958
959 $("#" + mess.getId(this.prefixIdMessage)).click(
960 function()
961 {
962 $(this).fadeOut("normal", function(){
963 thisMessages.enleverMessageRepond(mess)
964 $("form#posterMessage #repondA .messages").hide()
965 })
966 }
967 )
968 this.rafraichireNombreMessagesRepond()
969 }
970
971 /**
972 * Construit tous les id potentiels d'un message, renvoie par exemple :
973 * "conv9b28mess1h, conv9b2amess1h, conv9b32mess1h"
974 */
975 Conversations.prototype.exprIdsPotentiels = function(mess)
976 {
977 var exprMess = ""
978 for(var i = 0; i < this.conversations.length; i++)
979 {
980 exprMess += (mess != "" ? ", " : "") + "#" + mess.getId(this.conversations[i].getId())
981 }
982 return exprMess
983 }
984
985 /**
986 * Met à jour le nombre qui indique à l'utilisateur à combien de messages il répond.
987 */
988 Conversations.prototype.rafraichireNombreMessagesRepond = function()
989 {
990 // TODO : ya pas mieux pour trouver le nombre d'objet ?
991 var nb = 0
992 for (var m in this.messagesRepond)
993 nb += 1
994 $("#posterMessage #repondA .nb").text(nb)
995
996 var boite = $("#posterMessage #repondA")
997 if (nb > 0) boite.show()
998 else boite.hide()
999 }
1000
1001 /**
1002 * Affiche les messages auquel l'utilisateur souhaite répondre au sein des messages des conversations.
1003 * Utilisé lorsqu'une conversation est extraite.
1004 */
1005 Conversations.prototype.afficherMessagesRepondConversations = function()
1006 {
1007 var expr = ""
1008 for(var messId in this.messagesRepond)
1009 expr += "#" + this.messagesRepond[messId].getId() + ","
1010 $(expr).addClass("repondEnEvidence")
1011 }
1012
1013 /**
1014 * Crée un message JSON contenant le message demandant un rafraichissement.
1015 */
1016 Conversations.prototype.getJSONrafraichirMessages = function()
1017 {
1018 var mess = {
1019 "message_count" : conf.nbMessageAffiche,
1020 "main_page" : this.client.pagePrincipale,
1021 "conversations" : this.getJSONConversations(),
1022 "troll_id" : this.trollIdCourant
1023 }
1024
1025 if (this.client.cookie != null) mess["cookie"] = this.client.cookie
1026 mess["last_message_id"] = this.conversations[0].idDernierMessageAffiche
1027
1028 return mess
1029 }
1030
1031 Conversations.prototype.getJSONConversations = function()
1032 {
1033 var clientConv = []
1034
1035 for (var i = 0; i < this.client.conversations.length; i++)
1036 {
1037 clientConv.push(
1038 {
1039 root : this.client.conversations[i].root,
1040 page : this.client.conversations[i].page,
1041 last_message_id : this.conversations[i + 1] == undefined ? 0 : this.conversations[i + 1].idDernierMessageAffiche
1042 }
1043 )
1044 }
1045 return clientConv
1046 }
1047
1048 /**
1049 * Ajoute un ensemble de messages puis les affiches.
1050 * @param elements un tableau d'éléments JSON représentant les messages, voir protocole.txt
1051 * @param numConversation le numéro de la conversation auquel appartiennent les messages
1052 * @return true si les messages on été ajoutés, false si la conversation n'existe pas et qu'il n'y a pas de message
1053 */
1054 Conversations.prototype.ajouterMessages = function(elements, numConversation)
1055 {
1056 if (elements["messages"].length == 0)
1057 return this.conversations[numConversation] != undefined
1058
1059 for (var i = 0; i < elements["messages"].length; i++)
1060 // si une nouvelle conversation a été créée alors on lui donne la racine
1061 if(this.ajouterMessage(elements["messages"][i], numConversation))
1062 this.conversations[numConversation].setRacine(elements["first"])
1063
1064 this.flush(numConversation)
1065
1066 // renseigne la conversation sur la page courante et si c'est la dernière
1067 this.conversations[numConversation].setPage(
1068 numConversation == 0 ? this.client.pagePrincipale : this.client.conversations[numConversation - 1].page,
1069 elements["last_page"]
1070 )
1071
1072 return true
1073 }
1074
1075 /**
1076 * Création d'un nouveau message.
1077 * Les message sont données dans l'ordre de leur id.
1078 * @param element un element JSON représentant le message
1079 * @param numConversation le numéro de la conversation, 0 = principale
1080 * @return true si une nouvelle conversation a été créée sinon false
1081 */
1082 Conversations.prototype.ajouterMessage = function(element, numConversation)
1083 {
1084 var thisMessages = this
1085
1086 // pas d'utilisation de jquery pour des raisons de performance
1087 var message = new Message(
1088 this.client,
1089 this.formateur,
1090 element
1091 )
1092
1093 var nouvelleConversation = false
1094
1095 if (this.conversations[numConversation] == null)
1096 {
1097 nouvelleConversation = true
1098 this.nouvelleConversation(
1099 numConversation,
1100 function(num) // fermeture de la conversation
1101 {
1102 thisMessages.supprimerConversation(num)
1103 },
1104 function(num) // insertion du lien vers la conversation
1105 {
1106 thisPage.util.replaceSelection(
1107 $("form#posterMessage input.message")[0],
1108 "{" + thisMessages.client.conversations[num-1].root.toString(36) + "}"
1109 )
1110 }
1111 )
1112 }
1113
1114 this.conversations[numConversation].ajouterMessage(message)
1115 return nouvelleConversation
1116 }
1117
1118 Conversations.prototype.nouvelleConversation = function(num, funFermer, funLien)
1119 {
1120 var thisMessages = this
1121
1122 this.conversations[num] = new Conversation(this, num)
1123
1124 if (funFermer != undefined)
1125 this.conversations[num].eventFermer(funFermer)
1126 if (funLien != undefined)
1127 this.conversations[num].eventLien(funLien)
1128
1129 this.conversations[num].setFunPage(
1130 function(num) // page suivante
1131 {
1132 thisMessages.client.pageSuivante(num - 1)
1133 thisMessages.rafraichirMessages(true)
1134 },
1135 function(num) // page précédente
1136 {
1137 thisMessages.client.pagePrecedente(num - 1)
1138 thisMessages.rafraichirMessages(true)
1139 },
1140 function(num) // retour à la page une
1141 {
1142 if (thisMessages.client.goPremierePage(num - 1))
1143 thisMessages.rafraichirMessages(true)
1144 }
1145 )
1146
1147 this.ajusterLargeurConversations()
1148 }
1149
1150 /**
1151 * Enlève une conversation.
1152 */
1153 Conversations.prototype.supprimerConversation = function(num)
1154 {
1155 if (num <= 0 || num >= this.conversations.length) return // la numéro 0 ne peut être supprimé
1156 this.conversations[num].supprimer()
1157
1158 // décalage TODO : supprimer le dernier élément
1159 for (var i = num; i < this.conversations.length - 1; i++)
1160 {
1161 this.conversations[i] = this.conversations[i+1]
1162 this.conversations[i].num -= 1
1163 }
1164 this.conversations.pop()
1165 this.ajusterLargeurConversations()
1166
1167 this.client.supprimerConversation(num-1)
1168
1169 this.rafraichirMessages(true)
1170 }
1171
1172 /**
1173 * Ajuste la largeur des conversations en fonction de leur nombre. modifie l'attribut CSS 'width'.
1174 */
1175 Conversations.prototype.ajusterLargeurConversations = function()
1176 {
1177 // TODO : trouver mieux !
1178 var largeurPourcent = (100 / this.conversations.length)
1179 // obsolète !?
1180 // le "- 0.01" evite que IE se chie dessus lamentablement et affiche les conversations les unes au dessus des autres
1181 //if($.browser["msie"])
1182 // largeurPourcent -= 0.05
1183 $("#conversations td").css("width", largeurPourcent + "%")
1184 }
1185
1186 /**
1187 * Demande à toutes les conversations de se flusher (afficher les messages non-affichés).
1188 */
1189 Conversations.prototype.flushAll = function()
1190 {
1191 for (var i = 0; i < this.conversations.length; i++)
1192 this.flush(i)
1193 }
1194
1195 /**
1196 * Demande à une conversation de se flusher.
1197 */
1198 Conversations.prototype.flush = function(numConv)
1199 {
1200 this.conversations[numConv].flush()
1201 }
1202
1203 Conversations.prototype.ouvrirConversation = function(racine)
1204 {
1205 if (this.client.ajouterConversation(racine))
1206 this.rafraichirMessages(true)
1207 else
1208 this.util.messageDialogue("Cette conversation est déjà ouverte")
1209 }
1210
1211 Conversations.prototype.viderMessages = function()
1212 {
1213 for (var i = 0; i < this.conversations.length; i++)
1214 this.conversations[i].viderMessages()
1215 }
1216
1217 /**
1218 * Met à jour les messages de manière continue.
1219 * (AJAX-Comet-style proof)
1220 * @param vider vide tous les messages avant d'afficher les nouveaux
1221 */
1222 Conversations.prototype.rafraichirMessages = function(vider)
1223 {
1224 var thisMessages = this
1225
1226 if (vider == undefined)
1227 vider = false
1228
1229 if (vider)
1230 for (var i = 0; i < this.conversations.length; i++)
1231 this.conversations[i].idDernierMessageAffiche = 0
1232
1233 this.pageEvent.waitEvent(
1234 function() { return thisMessages.getJSONrafraichirMessages() },
1235 {
1236 "new_troll" :
1237 function(data)
1238 {
1239 thisMessages.trollIdCourant = data["troll_id"]
1240 $("#trollCourant .troll").html(thisMessages.formateur.traitementComplet(data["content"])).unbind("click").click(
1241 function()
1242 {
1243 thisMessages.ouvrirConversation(data["message_id"])
1244 }
1245 )
1246
1247 $("#trollCourant .troll a[@rel*=lightbox]").lightBox()
1248 },
1249 "new_messages" :
1250 function(data)
1251 {
1252 if (vider)
1253 thisMessages.viderMessages()
1254 // ajoute les messages reçus à leur conversation respective
1255 for (var numConv = 0; numConv < data["conversations"].length; numConv++)
1256 {
1257 if (!thisMessages.ajouterMessages(data["conversations"][numConv], numConv))
1258 {
1259 thisMessages.util.messageDialogue("La conversation {" + thisMessages.client.conversations[numConv -1].root.toString(36) + "} n'existe pas")
1260 thisMessages.client.supprimerConversation(numConv - 1)
1261 }
1262 }
1263 if (vider)
1264 thisMessages.afficherMessagesRepondConversations()
1265 vider = false
1266 }
1267 }
1268 )
1269 }