ADD avancement sur la possibilité de réduire une conversation (pas fini)
[euphorik.git] / js / euphorik.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 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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 /**
20 * Contient la base javascript pour le site euphorik.ch.
21 * Chaque page possède son propre fichier js nommé "page<nom de la page>.js".
22 * Auteur : GBurri
23 * Date : 6.11.2007
24 */
25
26
27 /**
28 * La configuration.
29 * Normalement 'const' à la place de 'var' mais non supporté par IE7.
30 */
31 var conf = {
32 versionProtocole : 3, // version du protcole
33 nickDefaut : "<nick>",
34 nbMessageAffiche : 10, // (par page)
35 pseudoDefaut : "<nick>",
36 tempsAffichageMessageDialogue : 4000, // en ms
37 tempsKick : 15, // en minute
38 tempsBan : 60 * 24 * 3, // en minutes (3jours)
39 smiles : {
40 "smile" : [/:\)/g, /:-\)/g],
41 "bigsmile" : [/:D/g, /:-D/g],
42 "clin" : [/;\)/g, /;-\)/g],
43 "cool" : [/8\)/g, /8-\)/g],
44 "eheheh" : [/:P/g, /:-P/g],
45 "lol" : [/\[-lol\]/g],
46 "spliff" : [/\[-spliff\]/g],
47 "oh" : [/:o/g, /:O/g],
48 "heink" : [/\[-heink\]/g],
49 "hum" : [/\[-hum\]/g],
50 "boh" : [/\[-boh\]/g],
51 "sniff" : [/:\(/g, /:-\(/g],
52 "triste" : [/\[-triste\]/g],
53 "pascontent" : [/>\(/g, /&gt;\(/g],
54 "argn" : [/\[-argn\]/g],
55 "redface" : [/\[-redface\]/g],
56 "bunny" : [/\[-lapin\]/g],
57 "chat" : [/\[-chat\]/g],
58 "renne" : [/\[-renne\]/g],
59 "star" : [/\[-star\]/g],
60 "kirby" : [/\[-kirby\]/g],
61 "slurp" : [/\[-slurp\]/g],
62 "agreed" : [/\[-agreed\]/g],
63 "dodo" : [/\[-dodo\]/g],
64 "bn" : [/\[-bn\]/g]
65 }
66 }
67
68 ///////////////////////////////////////////////////////////////////////////////////////////////////
69
70 String.prototype.trim = function()
71 {
72 return jQuery.trim(this) // anciennement : this.replace(/^\s+|\s+$/g, "");
73 }
74
75 String.prototype.ltrim = function()
76 {
77 return this.replace(/^\s+/, "");
78 }
79
80 String.prototype.rtrim = function()
81 {
82 return this.replace(/\s+$/, "");
83 }
84
85 ///////////////////////////////////////////////////////////////////////////////////////////////////
86
87 /**
88 * Cette classe regroupe des fonctions utilitaires (helpers).
89 * @formateur est permet de formater les messages affichés à l'aide de messageDialogue (facultatif)
90 */
91 function Util(formateur)
92 {
93 $("#info .fermer").click(function(){
94 $("#info").slideUp(50)
95 })
96
97 $("body").append('<div id="flecheBulle"></div>').append('<div id="messageBulle"><p></p></div>')
98
99 this.formateur = formateur
100 this.bulleActive = true
101 }
102
103 var messageType = {informatif: 0, question: 1, erreur: 2}
104
105 /**
106 * Affiche une boite de dialogue avec un message à l'intérieur.
107 * @param message le message (string)
108 * @param type voir 'messageType'. par défaut messageType.informatif
109 * @param les boutons sous la forme d'un objet ou les clefs sont les labels des boutons
110 * et les valeurs les fonctions executées lorsqu'un bouton est activé.
111 * @param formate faut-il formaté le message ? true par défaut
112 */
113 Util.prototype.messageDialogue = function(message, type, boutons, formate)
114 {
115 var thisUtil = this
116
117 if (type == undefined)
118 type = messageType.informatif
119
120 if (formate == undefined)
121 formate = true
122
123 if (this.timeoutMessageDialogue != undefined)
124 clearTimeout(this.timeoutMessageDialogue)
125
126 var fermer = function(){$("#info").slideUp(100)}
127 fermer()
128
129 $("#info .message").html(thisUtil.formateur == undefined || !formate ? message : thisUtil.formateur.traitementComplet(message))
130 switch(type)
131 {
132 case messageType.informatif : $("#info #icone").attr("class", "information"); break
133 case messageType.question : $("#info #icone").attr("class", "interrogation"); break
134 case messageType.erreur : $("#info #icone").attr("class", "exclamation"); break
135 }
136 $("#info .boutons").html("")
137 for (var b in boutons)
138 $("#info .boutons").append("<div>" + b + "</div>").find("div:last").click(boutons[b]).click(fermer)
139
140 $("#info").slideDown(200)
141 this.timeoutMessageDialogue = setTimeout(fermer, conf.tempsAffichageMessageDialogue)
142 }
143
144 var positionTypeX = {gauche: 0, gaucheRecouvrement: 1, centre: 2, droiteRecouvrement: 3, droite: 4}
145 var positionTypeY = {haut: 0, hautRecouvrement: 1, centre: 2, basRecouvrement: 3, bas: 4}
146
147 /**
148 * Afficher une boite flottante (élément) par rapport à une cible.
149 * La boite est affichée de manière à ne pas dépasser de la fenêtre.
150 * @boite l'élément à afficher
151 * @cible l'élément sur lequel est affiché la boite
152 * @positionX de type positionTypeX
153 * @positionY de type positionTypeY
154 */
155 Util.prototype.afficherBoite = function(boite, cible, positionX, positionY)
156 {
157 var positionCible = cible.offset()
158 var positionBoite =
159 {
160 left : positionX == positionTypeX.gauche ? positionCible.left - boite.width() :
161 (positionX == positionTypeX.gaucheRecouvrement ? positionCible.left - boite.width() + cible.width() :
162 (positionX == positionTypeX.droitelsRecouvrement ? positionCible.left :
163 (positionX == positionTypeX.droite ? positionCible.left + cible.width() :
164 positionCible.left + cible.width() / 2 - boite.width() / 2 ))), // centre
165 top : positionY == positionTypeY.haut ? positionCible.top - boite.height() :
166 (positionY == positionTypeY.hautRecouvrement ? positionCible.top - boite.height() + cible.height() :
167 (positionY == positionTypeY.basRecouvrement ? positionCible.top :
168 (positionY == positionTypeY.bas ? positionCible.top + cible.height() :
169 positionCible.top + cible.height() / 2 - boite.height() / 2 ))) // centre
170 }
171
172 // calcul les décalages en x et en y pour éviter que la boite ne sorte de la fenêtre, tient compte de la position des barres de défilement
173 var marge = 10
174 positionBoite.left = positionBoite.left < marge + window.pageXOffset ? marge + window.pageXOffset :
175 (boite.width() - $(window).width() + (positionBoite.left - window.pageXOffset) + marge > 0 ? $(window).width() - boite.width() - marge + window.pageXOffset : positionBoite.left)
176 positionBoite.top = positionBoite.top < marge + window.pageYOffset ? marge + window.pageYOffset :
177 (boite.height() - $(window).height() + (positionBoite.top - window.pageYOffset) + marge > 0 ? $(window).height() - boite.height() - marge + window.pageYOffset : positionBoite.top)
178
179 boite.css("top", positionBoite.top).css("left", positionBoite.left).show()
180 }
181
182 /**
183 * Affiche un info bulle lorsque le curseur survole l'élément donné.
184 * FIXME : le width de element ne tient pas compte du padding !?
185 */
186 Util.prototype.infoBulle = function(message, element)
187 {
188 var thisUtil = this
189
190 var cacherBulle = function()
191 {
192 $("#flecheBulle").hide()
193 $("#messageBulle").hide()
194 }
195
196 element.hover(
197 function()
198 {
199 if (!thisUtil.bulleActive)
200 return
201
202 var m = $("#messageBulle")
203 var f = $("#flecheBulle")
204
205 // remplie le paragraphe de la bulle avec le message
206 $("p", m).html(message)
207
208 // réinitialise la position, évite le cas ou la boite est collé à droite et remplie avec un texte la faisant dépassé
209 // dans ce cas la hauteur n'est pas calculé correctement
210 m.css("top", 0).css("left", 0)
211
212 var positionFleche = {
213 left : element.offset().left + element.innerWidth() / 2 - f.width() / 2,
214 top : element.offset().top - f.height()
215 }
216 var positionMessage = {
217 left : element.offset().left + element.width() / 2 - m.width() / 2,
218 top : element.offset().top - f.height() - m.height()
219 }
220 var depassementDroit = (positionMessage.left + m.width()) - $("body").width()
221 if (depassementDroit > 0)
222 positionMessage.left -= depassementDroit
223 else
224 {
225 if (positionMessage.left < 0)
226 positionMessage.left = 0
227 }
228
229 m.css("top", positionMessage.top).css("left", positionMessage.left).show()
230 f.css("top", positionFleche.top).css("left", positionFleche.left).show()
231 },
232 cacherBulle
233 ).click(cacherBulle)
234 }
235
236 /**
237 * Utilisé pour l'envoie de donnée avec la méthode ajax de jQuery.
238 */
239 Util.prototype.jsonVersAction = function(json)
240 {
241 return {action : JSON.stringify(json) }
242 }
243
244 Util.prototype.md5 = function(chaine)
245 {
246 return hex_md5(chaine)
247 }
248
249 // pompé de http://www.faqts.com/knowledge_base/view.phtml/aid/13562/fid/130
250 Util.prototype.setSelectionRange = function(input, selectionStart, selectionEnd)
251 {
252 if (input.setSelectionRange)
253 {
254 input.focus()
255 input.setSelectionRange(selectionStart, selectionEnd)
256 }
257 else if (input.createTextRange)
258 {
259 var range = input.createTextRange()
260 range.collapse(true)
261 range.moveEnd('character', selectionEnd)
262 range.moveStart('character', selectionStart)
263 range.select()
264 }
265 }
266
267 Util.prototype.setCaretToEnd = function(input)
268 {
269 this.setSelectionRange(input, input.value.length, input.value.length)
270 }
271 Util.prototype.setCaretToBegin = function(input)
272 {
273 this.setSelectionRange(input, 0, 0)
274 }
275 Util.prototype.setCaretToPos = function(input, pos)
276 {
277 this.setSelectionRange(input, pos, pos)
278 }
279 Util.prototype.selectString = function(input, string)
280 {
281 var match = new RegExp(string, "i").exec(input.value)
282 if (match)
283 {
284 this.setSelectionRange (input, match.index, match.index + match[0].length)
285 }
286 }
287 Util.prototype.replaceSelection = function(input, replaceString) {
288 if (input.setSelectionRange)
289 {
290 var selectionStart = input.selectionStart
291 var selectionEnd = input.selectionEnd
292 input.value = input.value.substring(0, selectionStart) + replaceString + input.value.substring(selectionEnd)
293
294 if (selectionStart != selectionEnd) // has there been a selection
295 this.setSelectionRange(input, selectionStart, selectionStart + replaceString.length)
296 else // set caret
297 this.setCaretToPos(input, selectionStart + replaceString.length)
298 }
299 else if (document.selection)
300 {
301 input.focus()
302 var range = document.selection.createRange()
303 if (range.parentElement() == input)
304 {
305 var isCollapsed = range.text == ''
306 range.text = replaceString
307 if (!isCollapsed)
308 {
309 range.moveStart('character', -replaceString.length);
310 }
311 }
312 }
313 }
314
315 Util.prototype.rot13 = function(chaine)
316 {
317 var ACode = 'A'.charCodeAt(0)
318 var aCode = 'a'.charCodeAt(0)
319 var MCode = 'M'.charCodeAt(0)
320 var mCode = 'm'.charCodeAt(0)
321 var ZCode = 'Z'.charCodeAt(0)
322 var zCode = 'z'.charCodeAt(0)
323
324 var f = function(ch, pos) {
325 if (pos == ch.length)
326 return ""
327
328 var c = ch.charCodeAt(pos);
329 return String.fromCharCode(
330 c +
331 (c >= ACode && c <= MCode || c >= aCode && c <= mCode ? 13 :
332 (c > MCode && c <= ZCode || c > mCode && c <= zCode ? -13 : 0))
333 ) + f(ch, pos + 1)
334 }
335 return f(chaine, 0)
336 }
337
338 ///////////////////////////////////////////////////////////////////////////////////////////////////
339
340 function Pages()
341 {
342 this.pageCourante = null
343 this.pages = {}
344 }
345
346 /**
347 * Accepte soit un objet soit un string.
348 * un string correspond au nom de la page, par exemple : "page" -> "page.html"
349 */
350 Pages.prototype.ajouterPage = function(page)
351 {
352 if (typeof page == "string")
353 {
354 this.pages[page] = page
355 }
356 else
357 {
358 page.pages = this // la magie des langages dynamiques : le foutoire
359 this.pages[page.nom] = page
360 }
361 }
362
363 Pages.prototype.afficherPage = function(nomPage, forcerChargement)
364 {
365 if (forcerChargement == undefined) forcerChargement = false
366
367 var page = this.pages[nomPage]
368 if (page == undefined || (!forcerChargement && page == this.pageCourante)) return
369
370 if (this.pageCourante != null && this.pageCourante.decharger)
371 this.pageCourante.decharger()
372
373 $("#menu li").removeClass("courante")
374 $("#menu li." + nomPage).addClass("courante")
375
376 this.pageCourante = page
377 var contenu = ""
378 if (typeof page == "string")
379 $.ajax({async: false, url: "pages/" + page + ".html", success : function(page) { contenu += page }})
380 else
381 contenu += this.pageCourante.contenu()
382 $("#page").html(contenu).removeClass().addClass(this.pageCourante.nom)
383
384 if (this.pageCourante.charger)
385 this.pageCourante.charger()
386 }
387
388 ///////////////////////////////////////////////////////////////////////////////////////////////////
389
390 /**
391 * Classe permettant de formater du texte par exemple pour la substitution des liens dans les
392 * message par "[url]".
393 * TODO : améliorer l'efficacité des méthods notamment lié au smiles.
394 */
395 function Formateur()
396 {
397 this.smiles = conf.smiles
398 this.protocoles = "http|https|ed2k"
399
400 this.regexUrl = new RegExp("(?:(?:" + this.protocoles + ")://|www\\.)[^ ]*", "gi")
401 this.regexImg = new RegExp("^.*?\\.(gif|jpg|png|jpeg|bmp|tiff)$", "i")
402 this.regexDomaine = new RegExp("^(?:(?:" + this.protocoles + ")://|www\\.).*?([^/.]+\\.[^/.]+)(?:$|/).*$", "i")
403 this.regexTestProtocoleExiste = new RegExp("^(?:" + this.protocoles + ")://.*$", "i")
404 this.regexNomProtocole = new RegExp("^(.*?)://")
405 }
406
407 /**
408 * Formate un pseudo saise par l'utilisateur.
409 * @param pseudo le pseudo brut
410 * @return le pseudo filtré
411 */
412 Formateur.prototype.filtrerInputPseudo = function(pseudo)
413 {
414 return pseudo.replace(/{|}/g, "").trim()
415 }
416
417 Formateur.prototype.getSmilesHTML = function()
418 {
419 var XHTML = ""
420 for (var sNom in this.smiles)
421 {
422 XHTML += "<img class=\"" + sNom + "\" src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />"
423 }
424 return XHTML
425 }
426
427 /**
428 * Formatage complet d'un texte.
429 * @M le message
430 * @pseudo facultatif, permet de contruire le label des images sous la forme : "<Pseudo> : <Message>"
431 */
432 Formateur.prototype.traitementComplet = function(M, pseudo)
433 {
434 return this.traiterLiensConv(this.traiterSmiles(this.traiterURL(this.traiterWikiSyntaxe(this.remplacerBalisesHTML(M)), pseudo)))
435 }
436
437 /**
438 * Transforme les liens en entités clickables.
439 * Un lien vers une conversation permet d'ouvrire celle ci, elle se marque comme ceci dans un message :
440 * "{5F}" ou 5F est la racine de la conversation.
441 * Ce lien sera transformer en <span class="lienConv">{5F}</span> pouvant être clické pour créer la conv 5F.
442 */
443 Formateur.prototype.traiterLiensConv = function(M)
444 {
445 return M.replace(
446 /\{\w+\}/g,
447 function(lien)
448 {
449 return "<span class=\"lienConv\">" + lien + "</span>"
450 }
451 )
452 }
453
454 /**
455 * FIXME : Cette méthode est attrocement lourde ! A optimiser.
456 * moyenne sur échantillon : 234ms
457 */
458 Formateur.prototype.traiterSmiles = function(M)
459 {
460 for (var sNom in this.smiles)
461 {
462 ss = this.smiles[sNom]
463 for (var i = 0; i < ss.length; i++)
464 M = M.replace(ss[i], "<img src=\"img/smileys/" + sNom + ".gif\" alt =\"" + sNom + "\" />")
465 }
466 return M
467 }
468
469 Formateur.prototype.remplacerBalisesHTML = function(M)
470 {
471 return M.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
472 }
473
474 Formateur.prototype.traiterURL = function(M, pseudo)
475 {
476 thisFormateur = this
477
478 var traitementUrl = function(url)
479 {
480 // si ya pas de protocole on rajoute "http://"
481 if (!thisFormateur.regexTestProtocoleExiste.test(url))
482 url = "http://" + url
483 var extension = thisFormateur.getShort(url)
484 return "<a " + (extension[1] ? "title=\"" + (pseudo == undefined ? "" : thisFormateur.traiterPourFenetreLightBox(pseudo, url) + ": ") + thisFormateur.traiterPourFenetreLightBox(M, url) + "\"" + " rel=\"lightbox\"" : "") + " href=\"" + url + "\" >[" + extension[0] + "]</a>"
485 }
486 return M.replace(this.regexUrl, traitementUrl)
487 }
488
489 /**
490 * Formatage en utilisant un sous-ensemble des règles de mediwiki.
491 * par exemple ''italic'' devient <i>italic</i>
492 */
493 Formateur.prototype.traiterWikiSyntaxe = function(M)
494 {
495 return M.replace(
496 /'''(.*?)'''/g,
497 function(texte, capture)
498 {
499 return "<b>" + capture + "</b>"
500 }
501 ).replace(
502 /''(.*?)''/g,
503 function(texte, capture)
504 {
505 return "<i>" + capture + "</i>"
506 }
507 )
508 }
509
510 /**
511 * Renvoie une version courte de l'url.
512 * par exemple : http://en.wikipedia.org/wiki/Yakov_Smirnoff devient wikipedia.org
513 */
514 Formateur.prototype.getShort = function(url)
515 {
516 var estUneImage = false
517 var versionShort = null
518 var rechercheImg = this.regexImg.exec(url)
519
520 if (rechercheImg != null)
521 {
522 versionShort = rechercheImg[1].toLowerCase()
523 if (versionShort == "jpeg") versionShort = "jpg" // jpeg -> jpg
524 estUneImage = true
525 }
526 else
527 {
528 var rechercheDomaine = this.regexDomaine.exec(url)
529 if (rechercheDomaine != null && rechercheDomaine.length >= 2)
530 versionShort = rechercheDomaine[1]
531 else
532 {
533 var nomProtocole = this.regexNomProtocole.exec(url)
534 if (nomProtocole != null && nomProtocole.length >= 2)
535 versionShort = nomProtocole[1]
536 }
537 }
538
539 return [versionShort == null ? "url" : versionShort, estUneImage]
540 }
541
542 /**
543 * Traite les pseudo et messages à être affiché dans le titre d'une image visualisé avec lightbox.
544 */
545 Formateur.prototype.traiterPourFenetreLightBox = function(M, urlCourante)
546 {
547 thisFormateur = this
548 var traitementUrl = function(url)
549 {
550 return "[" + thisFormateur.getShort(url)[0] + (urlCourante == url ? "*" : "") + "]"
551 }
552
553 return this.remplacerBalisesHTML(M).replace(this.regexUrl, traitementUrl)
554 }
555
556
557 ///////////////////////////////////////////////////////////////////////////////////////////////////
558
559 // les statuts possibes du client
560 var statutType = {
561 // mode enregistré, peut poster des messages et modifier son profile
562 auth_registered : 0,
563 // mode identifié, peut poster des messages mais n'a pas accès au profile
564 auth_not_registered : 1,
565 // mode déconnecté, ne peut pas poster de message
566 deconnected : 2
567 }
568
569 function Client(util)
570 {
571 this.util = util
572
573 this.cookie = null
574 this.regexCookie = new RegExp("^cookie=([^;]*)")
575
576 // données personnels
577 this.resetDonneesPersonnelles()
578
579 this.setStatut(statutType.deconnected)
580
581 // si true alors chaque modification du client est mémorisé sur le serveur
582 this.autoflush = $.browser["opera"]
583 }
584
585 Client.prototype.resetDonneesPersonnelles = function()
586 {
587 this.id = 0
588 this.pseudo = conf.pseudoDefaut
589 this.login = ""
590 this.password = ""
591 this.email = ""
592 this.css = $("link#cssPrincipale").attr("href")
593 this.nickFormat = "nick"
594 this.viewTimes = true
595 this.viewTooltips = true
596 this.cookie = undefined
597
598 this.pagePrincipale = 1
599 this.ekMaster = false
600 this.ostentatiousMaster = "light"
601
602 // les conversations, une conversation est un objet possédant les attributs suivants :
603 // - root (entier)
604 // - page (entier)
605 this.conversations = new Array()
606 }
607
608 Client.prototype.setCss = function(css)
609 {
610 if (this.css == css || css == "")
611 return
612
613 this.css = css
614 $("link#cssPrincipale").attr("href", this.css)
615 if (this.autoflush) this.flush(true)
616 }
617
618 Client.prototype.pageSuivante = function(numConv)
619 {
620 if (numConv < 0 && this.pagePrincipale > 1)
621 this.pagePrincipale -= 1
622 else if (this.conversations[numConv].page > 1)
623 this.conversations[numConv].page -= 1
624 }
625
626 Client.prototype.pagePrecedente = function(numConv)
627 {
628 if (numConv < 0)
629 this.pagePrincipale += 1
630 else
631 this.conversations[numConv].page += 1
632 }
633
634 /**
635 * Définit la première page pour la conversation donnée.
636 * @return true si la page a changé sinon false
637 */
638 Client.prototype.goPremierePage = function(numConv)
639 {
640 if (numConv < 0)
641 {
642 if (this.pagePrincipale == 1)
643 return false
644 this.pagePrincipale = 1
645 }
646 else
647 {
648 if (this.conversations[numConv].page == 1)
649 return false
650 this.conversations[numConv].page = 1
651 }
652 return true
653 }
654
655 /**
656 * Ajoute une conversation à la vue de l'utilisateur.
657 * Le profile de l'utilisateur est directement sauvegardé sur le serveur.
658 * @param racines la racine de la conversation (integer)
659 * @return true si la conversation a été créée sinon false (par exemple si la conv existe déjà)
660 */
661 Client.prototype.ajouterConversation = function(racine)
662 {
663 // vérification s'il elle n'existe pas déjà
664 for (var i = 0; i < this.conversations.length; i++)
665 if (this.conversations[i].root == racine)
666 return false
667
668 this.conversations.push({root : racine, page : 1})
669 if (this.autoflush) this.flush(true)
670
671 return true
672 }
673
674 Client.prototype.supprimerConversation = function(num)
675 {
676 if (num < 0 || num >= this.conversations.length) return
677
678 // décalage TODO : supprimer le dernier élément
679 for (var i = num; i < this.conversations.length - 1; i++)
680 this.conversations[i] = this.conversations[i+1]
681 this.conversations.pop()
682
683 if (this.autoflush) this.flush(true)
684 }
685
686 Client.prototype.getJSONLogin = function(login, password)
687 {
688 return {
689 "header" : { "action" : "authentification", "version" : conf.versionProtocole },
690 "login" : login,
691 "password" : password
692 }
693 }
694
695 Client.prototype.getJSONLoginCookie = function()
696 {
697 return {
698 "header" : { "action" : "authentification", "version" : conf.versionProtocole },
699 "cookie" : this.cookie
700 }
701 }
702
703 /**
704 * le couple (login, password) est facultatif. S'il n'est pas fournit alors il ne sera pas possible
705 * de s'autentifier avec (login, password).
706 */
707 Client.prototype.getJSONEnregistrement = function(login, password)
708 {
709 var mess = { "header" : { "action" : "register", "version" : conf.versionProtocole }}
710
711 if (login != undefined && password != undefined)
712 {
713 mess["login"] = login
714 mess["password"] = password
715 }
716
717 return mess;
718 }
719
720 Client.prototype.getJSONConversations = function()
721 {
722 var conversations = new Array()
723 for (var i = 0; i < this.conversations.length; i++)
724 conversations.push({root : this.conversations[i].root, minimized : this.conversations[i].reduit})
725 return conversations
726 }
727
728 Client.prototype.getJSONProfile = function()
729 {
730 return {
731 "header" : { "action" : "set_profile", "version" : conf.versionProtocole },
732 "cookie" : this.cookie,
733 "login" : this.login,
734 "password" : this.password,
735 "nick" : this.pseudo,
736 "email" : this.email,
737 "css" : this.css,
738 "nick_format" : this.nickFormat,
739 "view_times" : this.viewTimes,
740 "view_tooltips" : this.viewTooltips,
741 "conversations" : this.getJSONConversations(),
742 "ostentatious_master" : this.ostentatiousMaster
743 }
744 }
745
746 /**
747 * Renvoie null si pas définit.
748 */
749 Client.prototype.getCookie = function()
750 {
751 var cookie = this.regexCookie.exec(document.cookie)
752 if (cookie == null) this.cookie = null
753 else this.cookie = cookie[1]
754 }
755
756 Client.prototype.delCookie = function()
757 {
758 document.cookie = "cookie=; max-age=0"
759 }
760
761 Client.prototype.setCookie = function()
762 {
763 if (this.cookie == null || this.cookie == undefined)
764 return
765
766 // ne fonctionne pas sous IE....
767 /*document.cookie = "cookie=" + this.cookie + "; max-age=" + (60 * 60 * 24 * 365) */
768
769 document.cookie =
770 "cookie="+this.cookie+"; expires=" + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365).toUTCString()
771 }
772
773 Client.prototype.authentifie = function()
774 {
775 return this.statut == statutType.auth_registered || this.statut == statutType.auth_not_registered
776 }
777
778 Client.prototype.setStatut = function(statut)
779 {
780 // conversation en "enum" si en "string"
781 if (typeof(statut) == "string")
782 {
783 statut =
784 statut == "auth_registered" ?
785 statutType.auth_registered :
786 (statut == "auth_not_registered" ? statutType.auth_not_registered : statutType.deconnected)
787 }
788
789 if (statut == this.statut) return
790
791 this.statut = statut
792 this.majMenu()
793 this.majLogo()
794 }
795
796 /**
797 * Effectue la connexion vers le serveur.
798 * Cette fonction est bloquante tant que la connexion n'a pas été établie.
799 * S'il existe un cookie en local on s'authentifie directement avec lui.
800 * Si il n'est pas possible de s'authentifier alors on affiche un captcha anti-bot.
801 */
802 Client.prototype.connexionCookie = function()
803 {
804 this.getCookie()
805 if (this.cookie == null) return false;
806 return this.connexion(this.getJSONLoginCookie())
807 }
808
809 Client.prototype.connexionLogin = function(login, password)
810 {
811 return this.connexion(this.getJSONLogin(login, password))
812 }
813
814 Client.prototype.enregistrement = function(login, password)
815 {
816 if (this.authentifie())
817 {
818 this.login = login
819 this.password = password
820 if(this.flush())
821 {
822 this.setStatut(statutType.auth_registered)
823 return true
824 }
825 return false
826 }
827 else
828 {
829 return this.connexion(this.getJSONEnregistrement(login, password))
830 }
831 }
832
833 Client.prototype.connexion = function(messageJson)
834 {
835 thisClient = this
836 jQuery.ajax(
837 {
838 async: false,
839 type: "POST",
840 url: "request",
841 dataType: "json",
842 data: this.util.jsonVersAction(messageJson),
843 success:
844 function(data)
845 {
846 if (data["reply"] == "error")
847 thisClient.util.messageDialogue(data["error_message"])
848 else
849 thisClient.chargerDonnees(data)
850 }
851 }
852 )
853 return this.authentifie()
854 }
855
856 Client.prototype.deconnexion = function()
857 {
858 this.flush(true)
859 this.delCookie()
860 this.resetDonneesPersonnelles()
861 this.setStatut(statutType.deconnected) // deconnexion
862 }
863
864 Client.prototype.chargerDonnees = function(data)
865 {
866 // la modification du statut qui suit met à jour le menu, le menu dépend (page admin)
867 // de l'état ekMaster
868 this.ekMaster = data["ek_master"] != undefined ? data["ek_master"] : false
869
870 this.setStatut(data["status"])
871
872 if (this.authentifie())
873 {
874 this.cookie = data["cookie"]
875 this.setCookie()
876
877 this.id = data["id"]
878 this.login = data["login"]
879 this.pseudo = data["nick"]
880 this.email = data["email"]
881 this.setCss(data["css"])
882 this.nickFormat = data["nick_format"]
883 this.viewTimes = data["view_times"]
884 this.viewTooltips = data["view_tooltips"]
885 this.ostentatiousMaster = data["ostentatious_master"]
886
887 // la page de la conversation principale
888 this.pagePrincipale = 1
889
890 // les conversations
891 this.conversations = data["conversations"]
892 for (var i = 0; i < this.conversations.length; i++)
893 this.conversations[i] = {root : this.conversations[i].root, page : 1, reduit : this.conversations[i].minimized}
894
895 this.majBulle()
896 this.majCssSelectionee()
897 //this.majLogo()
898 }
899 }
900
901 /**
902 * Met à jour les données personne sur serveur.
903 * @param async de manière asynchrone ? défaut = true
904 * @return false si le flush n'a pas pû se faire sinon true
905 */
906 Client.prototype.flush = function(async)
907 {
908 if (async == undefined)
909 async = false
910
911 if (!this.authentifie())
912 return false
913
914 var thisClient = this
915 var ok = true
916
917 jQuery.ajax(
918 {
919 async: async,
920 type: "POST",
921 url: "request",
922 dataType: "json",
923 data: this.util.jsonVersAction(this.getJSONProfile()),
924 success:
925 function(data)
926 {
927 if (data["reply"] == "error")
928 {
929 thisClient.util.messageDialogue(data["error_message"])
930 ok = false
931 }
932 else
933 {
934 thisClient.majBulle()
935 }
936 }
937 }
938 )
939
940 return ok
941 }
942
943 Client.prototype.majMenu = function()
944 {
945 displayType = "block"
946
947 $("#menu .admin").css("display", this.ekMaster ? displayType : "none")
948
949 // met à jour le menu
950 if (this.statut == statutType.auth_registered)
951 {
952 $("#menu .profile").css("display", displayType).text("profile")
953 $("#menu .logout").css("display", displayType)
954 $("#menu .register").css("display", "none")
955 }
956 else if (this.statut == statutType.auth_not_registered)
957 {
958 $("#menu .profile").css("display", "none")
959 $("#menu .logout").css("display", displayType)
960 $("#menu .register").css("display", displayType)
961 }
962 else
963 {
964 $("#menu .profile").css("display", displayType).text("login")
965 $("#menu .logout").css("display", "none")
966 $("#menu .register").css("display", displayType)
967 }
968 }
969
970 /**
971 * Met à jour l'affichage des infos bulles en fonction du profile.
972 */
973 Client.prototype.majBulle = function()
974 {
975 this.util.bulleActive = this.viewTooltips
976 }
977
978 /**
979 * Met à jour la css sélectionnée, lors du chargement des données.
980 */
981 Client.prototype.majCssSelectionee = function()
982 {
983 // extraction du numéro de la css courante
984 var numCssCourante = this.css.match(/^.*?\/(\d)\/.*$/)
985 if (numCssCourante != null && numCssCourante[1] != undefined)
986 {
987 $("#menuCss option").removeAttr("selected")
988 $("#menuCss option[value=" + numCssCourante[1]+ "]").attr("selected", "selected")
989 }
990 }
991
992 /**
993 * Change la "class" du logo en fonction du statut de ekMaster.
994 */
995 Client.prototype.majLogo = function()
996 {
997 if (this.ekMaster)
998 $("#logo").addClass("ekMaster")
999 else
1000 $("#logo").removeClass("ekMaster")
1001 }
1002
1003
1004 Client.prototype.slap = function(userId, raison)
1005 {
1006 var thisClient = this
1007
1008 jQuery.ajax({
1009 type: "POST",
1010 url: "request",
1011 dataType: "json",
1012 data: this.util.jsonVersAction(
1013 {
1014 "header" : { "action" : "slap", "version" : conf.versionProtocole },
1015 "cookie" : thisClient.cookie,
1016 "user_id" : userId,
1017 "reason" : raison
1018 }),
1019 success:
1020 function(data)
1021 {
1022 if (data["reply"] == "error")
1023 thisClient.util.messageDialogue(data["error_message"])
1024 }
1025 })
1026 }
1027
1028 Client.prototype.ban = function(userId, raison, minutes)
1029 {
1030 var thisClient = this
1031
1032 // par défaut un ban correspond à 3 jours
1033 if (typeof(minutes) == "undefined")
1034 minutes = conf.tempsBan;
1035
1036 jQuery.ajax({
1037 type: "POST",
1038 url: "request",
1039 dataType: "json",
1040 data: this.util.jsonVersAction(
1041 {
1042 "header" : { "action" : "ban", "version" : conf.versionProtocole },
1043 "cookie" : thisClient.cookie,
1044 "duration" : minutes,
1045 "user_id" : userId,
1046 "reason" : raison
1047 }),
1048 success:
1049 function(data)
1050 {
1051 if (data["reply"] == "error")
1052 thisClient.util.messageDialogue(data["error_message"])
1053 }
1054 })
1055 }
1056
1057 Client.prototype.kick = function(userId, raison)
1058 {
1059 this.ban(userId, raison, conf.tempsKick)
1060 }
1061
1062 ///////////////////////////////////////////////////////////////////////////////////////////////////
1063
1064 /**
1065 * classe permettant de gérer les événements (push serveur).
1066 * l'information envoyé est sous la forme :
1067 * {
1068 * "header" : {"action" : "wait_event", "version" : <v> },
1069 * "page" : <page>
1070 * [..]
1071 * }
1072 * l'information reçu est sous la forme :
1073 * {
1074 * "reply" : <reply>
1075 * }
1076 * @page la page
1077 */
1078 function PageEvent(page, util)
1079 {
1080 this.page = page
1081 this.util = util
1082
1083 // l'objet JSONHttpRequest représentant la connexion d'attente
1084 this.attenteCourante = null
1085
1086 // le multhreading du pauvre, merci javascript de m'offrire autant de primitives pour la gestion de la concurrence...
1087 this.stop = false
1088 }
1089
1090 /**
1091 * Arrête l'attente courante s'il y en a une.
1092 */
1093 PageEvent.prototype.stopAttenteCourante = function()
1094 {
1095 this.stop = true
1096
1097 if (this.attenteCourante != null)
1098 {
1099 this.attenteCourante.abort()
1100 }
1101 }
1102
1103 /**
1104 * Attend un événement lié à la page.
1105 * @funSend une fonction renvoyant les données json à envoyer
1106 * @funsReceive est un objet comprenant les fonctions à appeler en fonction du "reply"
1107 * les fonctions acceptent un paramètre correspondant au données reçues.
1108 * exemple : {"new_message" : function(data){ ... }}
1109 */
1110 PageEvent.prototype.waitEvent = function(funSend, funsReceive)
1111 {
1112 this.stopAttenteCourante()
1113
1114 this.stop = false
1115
1116 var thisPageEvent = this
1117
1118 // on doit conserver l'ordre des valeurs de l'objet JSON (le serveur les veut dans l'ordre définit dans le protocole)
1119 // TODO : ya pas mieux ?
1120 var dataToSend =
1121 {
1122 "header" : { "action" : "wait_event", "version" : conf.versionProtocole },
1123 "page" : this.page
1124 }
1125 var poulpe = funSend()
1126 for (v in poulpe)
1127 dataToSend[v] = poulpe[v]
1128
1129 this.attenteCourante = jQuery.ajax({
1130 type: "POST",
1131 url: "request",
1132 dataType: "json",
1133 // TODO : doit disparaitre
1134 timeout: 180000, // timeout de 3min. Gros HACK pas beau. FIXME problème décrit ici : http://groups.google.com/group/jquery-en/browse_thread/thread/8724e64af3333a76
1135 data: this.util.jsonVersAction(dataToSend),
1136 success:
1137 function(data)
1138 {
1139 funsReceive[data["reply"]](data)
1140
1141 // rappel de la fonction dans 100 ms
1142 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 100)
1143 },
1144 error:
1145 function(XMLHttpRequest, textStatus, errorThrown)
1146 {
1147 ;; console.log("Connexion perdue dans waitEvent")
1148 setTimeout(function(){ thisPageEvent.waitEvent2(funSend, funsReceive) }, 1000)
1149 }
1150 })
1151 }
1152
1153 /**
1154 * Si un stopAttenteCourante survient un peu n'importe quand il faut imédiatement arreter de boucler.
1155 */
1156 PageEvent.prototype.waitEvent2 = function(funSend, funsReceive)
1157 {
1158 if (this.stop)
1159 return
1160 this.waitEvent(funSend, funsReceive)
1161 }
1162
1163 ///////////////////////////////////////////////////////////////////////////////////////////////////
1164
1165 function initialiserListeStyles(client)
1166 {
1167 $("#menuCss").change(
1168 function()
1169 {
1170 client.setCss("css/" + $("option:selected", this).attr("value") + "/euphorik.css")
1171 }
1172 )
1173 }
1174
1175 // charge dynamiquement le script de debug
1176 ;; jQuery.ajax({async : false, url : "js/debug.js", dataType : "script"})
1177
1178 // le main
1179 $(document).ready(
1180 function()
1181 {
1182 var formateur = new Formateur()
1183 var util = new Util(formateur)
1184 var client = new Client(util)
1185 var pages = new Pages()
1186
1187 // connexion vers le serveur (utilise un cookie qui traine)
1188 client.connexionCookie()
1189
1190 initialiserListeStyles(client)
1191
1192 // FIXME : ne fonctionne pas sous opera
1193 // voir : http://dev.jquery.com/ticket/2892#preview
1194 $(window).unload(function(){client.flush()})
1195
1196 $("#menu .minichat").click(function(){ pages.afficherPage("minichat") })
1197 $("#menu .admin").click(function(){ pages.afficherPage("admin") })
1198 $("#menu .profile").click(function(){ pages.afficherPage("profile") })
1199 $("#menu .logout").click(function(){
1200 util.messageDialogue("Êtes-vous sur de vouloir vous délogger ?", messageType.question,
1201 {"Oui" : function()
1202 {
1203 client.deconnexion();
1204 pages.afficherPage("minichat", true)
1205 },
1206 "Non" : function(){}
1207 }
1208 )
1209 })
1210 $("#menu .register").click(function(){ pages.afficherPage("register") })
1211 $("#menu .about").click(function(){ pages.afficherPage("about") })
1212
1213 // TODO : simplifier et pouvoir créer des liens par exemple : <span class="lien" href="conditions">Conditions d'utilisation</span>
1214 $("#footer .conditions").click(function(){ pages.afficherPage("conditions_utilisation") })
1215
1216 pages.ajouterPage(new PageMinichat(client, formateur, util))
1217 pages.ajouterPage(new PageAdmin(client, formateur, util))
1218 pages.ajouterPage(new PageProfile(client, formateur, util))
1219 pages.ajouterPage(new PageRegister(client, formateur, util))
1220 pages.ajouterPage(new PageAbout(client, formateur, util))
1221 pages.ajouterPage("conditions_utilisation")
1222
1223 pages.afficherPage("minichat")
1224 }
1225 )