vertical-align: top;\r
}
+/* voir pour l'astuce css "float left" des conversations : http://www.quirksmode.org/css/clearing.html */
+#page.minichat #conversations {
+ overflow: hidden;
+ width: 100%
+}
+
#page.minichat #conversations .conversation {
- border-width: 2px;
+ border-width: 0px;
border-style: solid;
- border-color: white
+ border-color: white;
+ float: left;
+ width: 100%;
}\r
-#page.minichat #conversations div.message { \r
+#page.minichat #conversations div.message {\r
border-left-width: 5px;\r
border-left-style: solid;\r
border-color: transparent;
text-align: left;
- padding-right: 8px;
+ padding-right: 5px;
padding-left: 4px;\r
cursor: pointer;
}
#page.minichat #conversations .extraire:hover {
background-color: #818c27
+}
+
+#page.minichat #conversations .titre {
+ padding-right: 8px;
+ background-color: #4b4215
+}
+
+#page.minichat #conversations .titre .fermer {
+ float: right;
+ padding-right: 5px;
+ padding-left: 5px;
+ background-color: #c90000;
+ cursor: pointer;
}\r
-\r
+
+#page.minichat #conversations .titre .fermer:hover {
+ background-color: #c95656
+}
+ \r
#page.minichat #pages {\r
margin-top: 10px;\r
}\r
\r
#page.minichat #pages span {\r
- padding-right : 5px;\r
- padding-left: 5px;\r
+ padding-right : 4px;\r
+ padding-left: 4px;\r
color: #7664ff;\r
cursor:pointer;\r
}\r
* Profiling pour améliorer les performances (client et serveur)
* traitementComplet() de euphorik.js est très très lent à executer
* Traiter les tags TODO dans le code
+* Compatage des js lors de la mise en production (afin d'optimiser la bande passante lors de l'accès au site)
+* Elaborer une stratégie de mise à jour de la structure de la BD quand celle ci est modifié (voir euphorik_bd:vers_version())
== Bugs ==
[ok] On ne peut pas réponde aux messages du système
[ok] Apparement les process liés aux connexions ne sont jamais terminé même quand l'utilisateur coupe la connexion à cause de minichat:attends_nouveau_messages()
+[1] Lors de l'extraction d'une conv il arrive que la conv extracté soit bien créée mais vide, le bouton ne ferme pas la conv (très étrange, bug de firefox?)
[3] Amélioration des requêtes MNESIA, voir : http://mail.google.com/mail/#label/Erlang+mailing-list/117f688280569a58
[2] Quand on revient en arrière dans firefox le message en rédaction est perdu
[2] En changeant de page puis en revenant sur la page principale les smiles ne sont plus highlightés lorsque le curseur les survol
Rafraichissement:
* Le client envoie une demande au serveur avec l'id du dernier message (via XMLHttpRequest ou un function de JQuery)
* Le serveur maintient la connexion bloqué si le client est à jour.
- * Dès qclientu'un nouveau message arrive, le serveurs débloque la connexion est envoie le ou les messages manquants.
+ * Dès qu'un nouveau message arrive, le serveurs débloque la connexion est envoie le ou les messages manquants.
C. Protocole
----------
c -> s
-#1
<action name="login">
<login>Paul</login>
<password>IJKJDHHSAD9081238</password>
</action>
+
+ou
+<action name="login">
+ <cookie>LKJDLAKSJBFLKASN</cookie>
+</action>
ou
-#2 (<login> et <password> peuvent être omis)
<action name="register">
<login>Paul</login>
<password>IJKJDHHSAD9081238</password>
</action>
s -> c
-#3 (<information> et <pseudo> pas obligatoire)
+(<information> et <pseudo> pas obligatoire)
<reponse name="login">
<statut>enregistre|identifie|erreur</statut>
<cookie>LKJDLAKSJBFLKASN</cookie>
<login>paul49</login>
<email>paul@pierre.com</email>
<css>css/lite.css</css>
+ <pagePrincipale>1</pagePrincipale> <!-- facultatif -->
<!-- L'ordre des conversations est le même que lors de la sauvegarde du profile -->
- <!-- Comprend également la conversation principal, dans ce cas la racine est à 0 -->
<conversation>
- <racine>4</racine>
+ <racine>4F</racine>
<page>1</page>
</conversation>
<!-- [..] -->
<pseudo>Paul</pseudo>
<email>paul@pierre.com</email>
<css>css/dark.css</css>
+ <pagePrincipale>1</pagePrincipale> <!-- facultatif -->
<conversation>
- <racine>4</racine>
+ <racine>4F</racine>
<page>1</page>
</conversation>
<!-- [..] -->
* Extraction d'une conversation
a) Conversation.click
- b) Messages.ExtraireConversation(numMess)
- c) User.ajouterConversation(numMess)
+ b) Client.ajouterConversation(idMess)
+ c) Client.flush(false) // mise à jour du profile de manière synchrone
d) Messages.rafraichirMessages(true)
* Suppression d'une conversation
m5 -> m3
m6 -> m3
m7
-
+m8 -> m7
+m9 -> m7
{\r
if(typeof XMLSerializer != "undefined")
this.serializer = new XMLSerializer()
- \r
- // fermeture des dialogues d
+
jQuery("#info .fermer").click(function(){
jQuery("#info").slideUp(50)
})
this.email = ""\r
this.css = jQuery("link#cssPrincipale").attr("href")
+ this.pagePrincipale = 1
+
// les conversations, une conversation est un objet possédant les attributs suivants :
// - racine (entier)
// - page (entier)
* Ajoute une conversation à la vue de l'utilisateur.
* Le profile de l'utilisateur est directement sauvegardé sur le serveur.
* @param racines la racine de la conversation
+ * @return true si la conversation a été créée sinon false (par exemple si la conv existe déjà)
*/
Client.prototype.ajouterConversation = function(racine)
{
+ // vérification s'il elle n'existe pas déjà
+ for (var i = 0; i < this.conversations.length; i++)
+ if (this.conversations[i].racine == racine)
+ return false
+
this.conversations.push({racine : racine, page : 1})
- this.flush()
+ this.flush(false)
+ return true
+}
+
+Client.prototype.supprimerConversation = function(num)
+{
+ if (num < 0 || num >= this.conversations.length) return
+
+ // décalage TODO : supprimer le dernier élément
+ for (var i = num; i < this.conversations.length - 1; i++)
+ this.conversations[i] = this.conversations[i+1]
+ this.conversations.pop()
+
+ this.flush(false)
}\r
Client.prototype.getXMLlogin = function(login, password)
nodeCSS.appendChild(XMLDocument.createTextNode(this.css))
XMLDocument.documentElement.appendChild(nodeCSS)
+ var nodePagePrincipale = XMLDocument.createElement("pagePrincipale")
+ nodePagePrincipale.appendChild(XMLDocument.createTextNode(this.pagePrincipale))
+ XMLDocument.documentElement.appendChild(nodePagePrincipale)
+
// mémorise les conversations affichées
- if (this.conversations.length > 0)
+ for (var i = 0; i < this.conversations.length; i++)
{
- var nodeConversations = XMLDocument.createElement("conversations")
- XMLDocument.documentElement.appendChild(nodeConversations)
- for (var i = 0; i < this.conversations.length; i++)
- {
- var nodeConv = XMLDocument.createElement("conversation")
- nodeConversations.appendChild(nodeConv)
-
- var nodeRacine = XMLDocument.createElement("racine")
- nodeRacine.appendChild(XMLDocument.createTextNode(this.conversations[i].racine))
- nodeConv.appendChild(nodeRacine)
-
- var nodePage = XMLDocument.createElement("page")
- nodePage.appendChild(XMLDocument.createTextNode(this.conversations[i].page))
- nodeConv.appendChild(nodePage)
- }
+ var nodeConv = XMLDocument.createElement("conversation")
+ XMLDocument.documentElement.appendChild(nodeConv)
+
+ var nodeRacine = XMLDocument.createElement("racine")
+ nodeRacine.appendChild(XMLDocument.createTextNode(this.conversations[i].racine))
+ nodeConv.appendChild(nodeRacine)
+
+ var nodePage = XMLDocument.createElement("page")
+ nodePage.appendChild(XMLDocument.createTextNode(this.conversations[i].page))
+ nodeConv.appendChild(nodePage)
}
return XMLDocument
Client.prototype.connexion = function(action)
{
+ //action.action.dump()
thisClient = this
jQuery.ajax(
{
success:
function(data)
{
+ //thisClient.util.serializer.serializeToString(data).dump()
thisClient.chargerDonnees(data)
}
}
Client.prototype.chargerDonnees = function(data)
{
+ var thisClient = this
+
this.setStatut(jQuery("statut", data.documentElement).text())
if (this.identifie())
this.pseudo = jQuery("pseudo", data.documentElement).text()\r
this.email = jQuery("email", data.documentElement).text()\r
this.css = jQuery("css", data.documentElement).text()
+
+ // la page de la conversation principale
+ var tmp = jQuery("pagePrincipale", data.documentElement)
+ this.pagePrincipale = tmp.length < 1 ? 1 : tmp.text()
+
// met à jour la css
if (this.css != "")
{
jQuery("link#cssPrincipale").attr("href", this.css)
this.majMenu()
}
+ // les conversations
+ this.conversations = new Array()
+ jQuery("conversation", data.documentElement).each(
+ function(i)
+ {
+ thisClient.conversations.push( { racine : jQuery("racine", this).text(), page : jQuery("page", this).text() } )
+ }
+ )
}
this.dernierMessageErreur = jQuery("information", data.documentElement).text()
}
/**
* Met à jour les données personne sur serveur.
+ * @param async de manière asynchrone ? défaut = true
*/
-Client.prototype.flush = function()
+Client.prototype.flush = function(async)
{
+ if (async == undefined)
+ async = true
+
thisClient = this
//thisClient.util.log(this.util.xmlVersAction(this.getXMLProfile()).action)
jQuery.ajax(
{
- async: true,
+ async: async,
type: "POST",
url: "request",
dataType: "xml",
for (var i = 0; i < repondA.length; i++)
{
jQuery("#conversation div#" + repondA[i]).addClass("repondu")
- for (var m = 0; m < this.messages.messages.length; m++)
- this.messages.messages[m].clientARepondu = true
+ for (var c = 0; c < this.messages.conversations.length; c++)
+ for (var m = 0; m < this.messages.conversations[c].messages.length; m++)
+ this.messages.conversations[c].messages[m].clientARepondu = true
}
}\r
\r
* @param numConv le numéro (appelé id) de la conversation
* @param formateur outil permettant la mise en forme du texte des messages
*/
-function Conversation(numConv, formateur, util)
+function Conversation(num, util, formateur, funFermer)
{
+ //alert("ok")
var thisConversation = this
- this.id = numConv
+ this.num = num
+ this.id = Math.random()*100000000000000000
this.messageOver = null // le message sur lequel se trouve le curseur
- this.formateur = formateur
this.util = util
+ this.formateur = formateur
this.messages = new Array()
this.messagesParId = new Object()
-
- this.idDernierMesssage = null
this.page = 1 // par défaut on se trouve sur la première page
jQuery("#conversations").append(
- "<div id=\"" + this.getId() + "\" class=\"conversation\"></div>"
+ '<div id="' + this.getId() + '" class="conversation">\
+ <div class="titre">' +
+ (funFermer == undefined ? '' : '<div class="fermer">x</div>') +
+ '< 1 >\
+ </div>\
+ </div>'
)
// enlève la mise en évidence pour la conversation
thisConversation.messageOver = null
}
)
+
+ // suppression de la conversation
+ if (funFermer != undefined)
+ jQuery("#conversations #" + this.getId() + " .titre .fermer").click(
+ function()
+ {
+ funFermer(thisConversation.num)
+ }
+ )
}
/**
Conversation.prototype.viderMessages = function()
{
this.messages = new Array()
- jQuery("#conversations #" + this.getId()).empty()
+ jQuery("#conversations #" + this.getId() + " .message").remove()
}
/**
)
}
+/**
+ * Supprime une conversation.
+ */
+Conversation.prototype.supprimer = function()
+{
+ jQuery("#conversations #" + this.getId()).remove()
+}
+
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
this.util = util
this.conversations = new Array() // les conversations, la première représente la conversation principale
- this.conversations[0] = new Conversation(0, this.formateur, this.util)
+ this.conversations[0] = new Conversation(0, this.util, this.formateur)
- this.idDernierMesssage = null // l'id du dernier message connu
+ this.idDernierMessage = null // l'id du dernier message connu
// l'objet XMLHttpRequest représentant la connexion d'attente
this.attenteCourante = null
XMLDocument.documentElement.appendChild(nodeCookie)
}
- if (this.idDernierMesssage != null)
+ if (this.idDernierMessage != null)
{
var nodeDernierMessageId = XMLDocument.createElement("dernierMessageId")
- nodeDernierMessageId.appendChild(XMLDocument.createTextNode(this.idDernierMesssage))
+ nodeDernierMessageId.appendChild(XMLDocument.createTextNode(this.idDernierMessage))
XMLDocument.documentElement.appendChild(nodeDernierMessageId)
}
XMLDocument.documentElement.appendChild(nodePage)
// les conversations
+ /* Obsolète, le serveur les recupères directement auprès des informations utilisateurs
for (var i = 0; i < this.client.conversations.length; i++)
{
var nodeConversation = XMLDocument.createElement("conversation")
var nodePageConv = XMLDocument.createElement("page")
nodePageConv.appendChild(XMLDocument.createTextNode(this.client.conversations[i].page))
nodeConversation.appendChild(nodePageConv)
- }
+ }*/
return XMLDocument;
}
*/
Messages.prototype.ajouterMessage = function(element, numConversation)
{
+ var thisMessages = this
+
// pas d'utilisation de jquery pour des raisons de performance
- this.idDernierMesssage = element.getAttribute("id")
+ var id = element.getAttribute("id")
+ //alert(parseInt(null, 36) +" " + parseInt(this.idDernierMessage, 36))
+ if (this.idDernierMessage == null || parseInt(id, 36) > parseInt(this.idDernierMessage, 36))
+ this.idDernierMessage = id
+ //alert(this.idDernierMessage)
var message = new Message(
- this.idDernierMesssage, \r
+ id, \r
jQuery("date", element).text(),\r
jQuery("pseudo", element).text(),\r
jQuery("contenu", element).text()
message.setRepondA(jQuery("repondA", element))
if (this.conversations[numConversation] == null)
- this.conversations[numConversation] = new Conversation(numConversation, this.formateur)
+ {
+ this.conversations[numConversation] = new Conversation(numConversation, this.util, this.formateur,
+ function(num) // fermeture de la conversation
+ {
+ thisMessages.supprimerConversation(num)
+ }
+ )
+
+ this.ajusterLargeurConversations()
+ }
this.conversations[numConversation].ajouterMessage(message)
}
+/**
+ * Enlève une conversation.
+ */
+Messages.prototype.supprimerConversation = function(num)
+{
+ if (num <= 0 || num >= this.conversations.length) return // la numéro 0 ne peut être supprimé
+ this.conversations[num].supprimer()
+
+ // décalage TODO : supprimer le dernier élément
+ for (var i = num; i < this.conversations.length - 1; i++)
+ {
+ this.conversations[i] = this.conversations[i+1]
+ this.conversations[i].num -= 1
+ }
+ this.conversations.pop()
+ this.ajusterLargeurConversations()
+
+ this.client.supprimerConversation(num-1)
+
+ this.rafraichirMessages(true)
+}
+
+
+/**
+ * Ajuste la largeur des conversations en fonction de leur nombre. modifie l'attribut CSS 'width'.
+ */
+Messages.prototype.ajusterLargeurConversations = function()
+{
+ jQuery("#conversations .conversation").css("width", 100 / this.conversations.length + "%")
+}
+
/**
* Demande à toutes les conversations de se flusher (afficher les messages non-affichés).
*/
this.conversations[numConv].flush
(
- // fonction appellée lors de la demande d'extraction d'une conversation
+ // fonction appelée lors de la demande d'extraction d'une conversation
function(idMess)
{
- thisMessages.client.ajouterConversation(idMess)
- thisMessages.rafraichirMessages(true)
+ if (thisMessages.client.ajouterConversation(idMess))
+ thisMessages.rafraichirMessages(true)
}
)
}
Messages.prototype.viderMessages = function()
{
- this.idDernierMesssage = null
+ this.idDernierMessage = null
for (var i = 0; i < this.conversations.length; i++)
this.conversations[i].viderMessages()
if (vider == undefined)
vider = false
+ var thisMessages = this // caisupair javacrypte
+
if (vider)
- {
- this.idDernierMesssage = null
- this.messages = new Array()
- }
-
- var thisMessages = this // caisupair javacrypte\r
+ {
+ this.idDernierMessage = null
+ }\r
- //this.util.log(this.util.serializer.serializeToString(this.getXMLrafraichirMessages()))
+ this.util.xmlVersAction(this.getXMLrafraichirMessages()).action.dump()
//alert(this.util.xmlVersAction(this.getXMLrafraichirMessages()).action)
this.attenteCourante = jQuery.ajax({
type: "POST",
success:
function(data)
{ \r
- //thisMessages.util.log(thisMessages.util.serializer.serializeToString(data)) \r
+ thisMessages.util.serializer.serializeToString(data).dump()\r
if (vider)
+ {
thisMessages.viderMessages()
+ /* Obsolète : pas besoin de supprimer les conversations
+ for (var i = 1; i < this.conversations.length; i++)
+ this.supprimerConversation(1)
+ */
+ }
//thisMessages.MAJPages(parseInt(jQuery("nbPage", data.documentElement).text()))
messages.push(this)
}
)
+ //alert("ajoutMess : " + messages.length + " " + numConv)
thisMessages.ajouterMessages(messages, numConv)
numConv += 1
}
mnesia:delete_schema([node()]),\r
mnesia:create_schema([node()]), % nécessaire pour les tables sur disc\r
mnesia:start(),\r
- create_tables().\r
-\r
+ create_tables().
+ \r
create_tables() ->\r
mnesia:create_table(counter, [\r
end,
record_info(fields, user),
user
+ );
+% Ajout du numéro de page de la conv principale
+vers_version(5) ->
+ mnesia:transform_table(
+ user,
+ fun({user, Id, Cookie, Pseudo, Login, Password, Email, Date_creation, Date_derniere_connexion, Css, Indice_flood, Conversations}) ->
+ {user, Id, Cookie, Pseudo, Login, Password, Email, Date_creation, Date_derniere_connexion, Css, Indice_flood, 1, Conversations}
+ end,
+ record_info(fields, user),
+ user
).
+
% exemple de peuplage de la BD, utilisé pour les tests
peupler() ->
mnesia:transaction(
est_une_reponse_a_user/2,
% set :\r
update_pseudo_user/2,
- set_profile/6,
+ set_profile/8,
update_date_derniere_connexion/1,
nouveau_user/2,
nouveau_user/3,
% Mise à par Cookie les autres peuvent être undefined ce qui veut dire qu'ils ne seront pas modifié.\r
-set_profile(Cookie, Login, Password, Pseudo, Email, Css) ->
+set_profile(Cookie, Login, Password, Pseudo, Email, Css, Page_principale, Conversations) ->
resultat_transaction(mnesia:transaction(
fun() ->
case user_by_cookie(Cookie) of
{ok, U} when U#user.id =/= User#user.id ->\r
login_deja_pris;\r
_ -> \r
- User_modifie = User#user{\r
+ User_modifie = User#user{
+ % TODO : pourquoi ne pas tester avec la valeur "undefined" plutôt qu'avec "is_list" ?\r
login = if is_list(Login) -> Login; true -> User#user.login end,\r
password = if is_list(Password) -> Password; true -> User#user.password end,\r
pseudo = if is_list(Pseudo) -> Pseudo; true -> User#user.pseudo end,\r
email = if is_list(Email) -> Email; true -> User#user.email end,\r
- css = if is_list(Css) -> Css; true -> User#user.css end\r
+ css = if is_list(Css) -> Css; true -> User#user.css end,
+ page_principale = if is_list(Page_principale) -> Page_principale; true -> User#user.page_principale end,
+ conversations = if is_list(Conversations) -> Conversations; true -> User#user.conversations end\r
},\r
mnesia:write(User_modifie),\r
ok\r
Pseudo = case xmerl_xpath:string("pseudo", Action) of [#xmlElement{content = [#xmlText{value = P2}]}] -> P2; _ -> Login end,
Email = case xmerl_xpath:string("email", Action) of [#xmlElement{content = [#xmlText{value = E}]}] -> E; _ -> undefined end,
Css = case xmerl_xpath:string("css", Action) of [#xmlElement{content = [#xmlText{value = C}]}] -> C; _ -> undefined end,
- Conversations = case xmerl_xpath:string("conversations", Action) of
- Conversations ->
- % extraction de chaque conversation
-
- _ -> []
- end,
- case euphorik_minichat:set_profile(Cookie, Login, Password, Pseudo, Email, Css) of
+ Page_principale = case xmerl_xpath:string("pagePrincipale", Action) of [#xmlElement{content = [#xmlText{value = P3}]}] -> list_to_integer(P3); _ -> undefined end,
+ Conversations = lists:map(
+ fun(Conv) ->
+ [#xmlElement{content = [#xmlText{value = Id_racine_str}]}] = xmerl_xpath:string("racine", Conv),
+ [#xmlElement{content = [#xmlText{value = Page_conv_str}]}] = xmerl_xpath:string("page", Conv),
+ {erlang:list_to_integer(Id_racine_str, 36), list_to_integer(Page_conv_str)}
+ end,
+ xmerl_xpath:string("conversation", Action)
+ ),
+ case euphorik_minichat:set_profile(Cookie, Login, Password, Pseudo, Email, Css, Page_principale, Conversations) of
ok ->
xml_reponse_profile_ok();\r
login_deja_pris ->\r
_ -> inconnu
end,
% extraction des conversations en [{id, page}, ..]
- Conversations = lists:map(
- fun(Conv) ->
- [#xmlElement{content = [#xmlText{value = Id_racine_str}]}] = xmerl_xpath:string("racine", Conv),
- [#xmlElement{content = [#xmlText{value = Page_conv_str}]}] = xmerl_xpath:string("page", Conv),
- {erlang:list_to_integer(Id_racine_str, 36), erlang:list_to_integer(Page_conv_str)}
- end,
- xmerl_xpath:string("conversation", Action)
- ),\r
+ % Obsolète : obtenu depuis la table 'user'
+ %~ Conversations = lists:map(
+ %~ fun(Conv) ->
+ %~ [#xmlElement{content = [#xmlText{value = Id_racine_str}]}] = xmerl_xpath:string("racine", Conv),
+ %~ [#xmlElement{content = [#xmlText{value = Page_conv_str}]}] = xmerl_xpath:string("page", Conv),
+ %~ {erlang:list_to_integer(Id_racine_str, 36), erlang:list_to_integer(Page_conv_str)}
+ %~ end,
+ %~ xmerl_xpath:string("conversation", Action)
+ %~ ),\r
% accrochez-vous ca va siouxer ;)
[{reponse, [{name, "refreshMessages"}],\r
lists:map(\r
]
}\r
end,
- euphorik_minichat_conversation:conversations(Conversations, Nb_message, Dernier_id, Page)\r
+ euphorik_minichat_conversation:conversations(User#user.conversations, Nb_message, Dernier_id, Page)\r
)
}];
_ ->
{pseudo, [User#user.pseudo]},
{login, [User#user.login]},
{email, [User#user.email]},
- {css, [User#user.css]}
- ]
+ {css, [User#user.css]},
+ {pagePrincipale, [integer_to_list(User#user.page_principale)]}
+ ] ++
+ lists:map(
+ fun(C) ->
+ {conversation,
+ [
+ {racine, [erlang:integer_to_list(element(1, C), 36)]},
+ {page, [integer_to_list(element(2, C))]}
+ ]
+ }
+ end,
+ User#user.conversations
+ )
}].
%~ traiter_action([#xmlAttribute{value="loginCaptcha"}], XML) ->\r
%~ euphorik_protocole:nouveau_user_captcha(XML); \r
\r
-
+
% un client s'enregistre (pseudo + password)
traiter_action([#xmlAttribute{value="register"}], XML) ->
euphorik_protocole:nouveau_user_login(XML); \r
date_derniere_connexion, % erlang:now(), est mis à jour lors de n'importe quelle activitée (envoie de message par exemple)\r
css = [], % string()
indice_flood = 0, % integer() est incrémenté lorsque l'utilisateur envoie trop rapidement des messages.
+ page_principale = 1, % la page de la conversation principale
conversations = [] % [{integer(), integer()}], la liste des messages correspondant au conversation ainsi que la page affichée\r
}).