replace user list with user intmap + format

/*  A simple, one-room, scalable real-time web chat, with file sharing

    Copyright © 2012 Frederic Ye
    Copyright © 2010-2011 MLstate

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import stdlib.system

/** Constants **/

GITHUB_USER = "Aqua-Ye"
GITHUB_REPO = "OpaChat"
NB_LAST_MSGS = 10

/** Types **/

type user = { int id, string name }
type source = { system } or { user user }
type message = {
  source source,
  string text,
  Date.date date,
}
type media = {
  source source,
  string name,
  string src,
  string mimetype,
  Date.date date,
}
type client_channel = channel(void)
type network_msg =
   {message message}
or {media media}
or {(user, client_channel) connection}
or {user disconnection}
or {stats}

/** Database **/

database intmap(message) /history

exposed Network.network(network_msg) room = Network.cloud("room")
private reference(intmap(user)) users = ServerReference.create(IntMap.empty)
private launch_date = Date.now()

/** Page **/

watch_button =

fork_button =

function build_page(content) {

OpaChat

      {watch_button}
      {fork_button}

{content}

}

/** Connection **/

server function server_observe(message) {
  match (message) {
  case {connection:(user, client_channel)} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    Network.broadcast({stats}, room)
    Session.on_remove(client_channel, function() {
      server_observe({disconnection:user})
    })
  case {disconnection:user} :
    ServerReference.update(users, IntMap.remove(user.id, _))
    Network.broadcast({stats}, room)
  default: void
  }
}

_ = Network.observe(server_observe, room)

/** Stats **/

server function mem() {
  System.get_memory_usage()/(1024*1024)
}

server function compute_stats() {
  uptime_duration = Date.between(launch_date, Date.now())
  uptime = Date.of_duration(uptime_duration)
  uptime = Date.shift_backward(uptime, Date.to_duration(Date.milliseconds(3600000))) // 1 hour shift
  (uptime, mem())
}

client @async function update_stats((uptime, mem)) {
  #uptime = <>Uptime: {Date.to_string_time_only(uptime)}</>
  #memory = <>Memory: {mem} Mo</>
}

client @async function update_users(nb_users, users) {
  #users = <>Users: {nb_users}</>
  #user_list =
{users}

}

/** Conversation **/

function source_to_html(source) {
  match (source) {
  case {system} :
  case {~user} : {user.name}
  }
}

client @async function message_update(stats, list(message) messages) {
  update_stats(stats)
  List.iter(function(message) {
    date = Date.to_formatted_string(Date.default_printer, message.date)
    time = Date.to_string_time_only(message.date)
    line =

              {time}
              { source_to_html(message.source) }
              {message.text}

    #conversation =+ line
  }, messages)
  Dom.scroll_to_bottom(#conversation)
}

client @async function media_update(stats, list(media) medias) {
  update_stats(stats)
  List.iter(function(media) {
    date = Date.to_formatted_string(Date.default_printer, media.date)
    time = Date.to_string_time_only(media.date)
    media_parser = parser {
      case "image/" .*: {image}
      case "audio/" .*: {audio}
      case "video/" .*: {video}
    }
    line =

              {time}
              { source_to_html(media.source) }
              { match (Parser.try_parse(media_parser, media.mimetype)) {
                case {some:{image}}:

                case {some:{audio}}:
                  <audio src="{media.src}"
                         controls="controls"
                         type="{media.mimetype}"
                         preload="auto">
                    Your browser does not support the audio tag!
                  </audio>
                case {some:{video}}:
                  <video src="{media.src}"
                         controls="controls"
                         preload="auto"
                         type="{media.name}">
                    Your browser does not support the video tag!
                  </video>
                default:
                   is sharing a file :
                    {media.name}

                } }

    #conversation =+ line
  }, medias)
  Dom.scroll_to_bottom(#conversation)
}

exposed @async function server_broadcast(user, text) {
  message = {source:user, ~text, date:Date.now()}
  /history[?] <- message
  Network.broadcast({~message}, room)
}

client @async function broadcast(user, _) {
  _ = server_broadcast(user, Dom.get_value(#entry))
  Dom.clear_value(#entry)
}

server function client_observe(msg) {
  match (msg) {
  case {~message} :
    message_update(compute_stats(), [message])
  case {~media} :
    media_update(compute_stats(), [media])
  case {connection:(user, _)} :
    message = {
      source: {system},
      text : "{user.name} joined the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {disconnection:user} :
    message = {
      source: {system},
      text : "{user.name} left the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {stats} :
    update_stats(compute_stats())
    users = ServerReference.get(users)
            |> IntMap.To.val_list(_)
            |> List.sort_by(function(u){u.name}, _)
    users_html_list =
      List.fold(function(user, acc) {

{user.name}

        <+> acc
      }, users, <></>)
    update_users(List.length(users), users_html_list)
  default : void
  }
}

server function file_uploaded(user)(name, mimetype, key) {
  media = {
    source: {~user},
    ~name,
    src: "/file/{key}",
    ~mimetype,
    date: Date.now(),
  }
  Network.broadcast({~media}, room)
}

// Init the client from the server
server function init_client(user, client_channel) {
  // Observe client
  obs = Network.observe(client_observe, room)
  Network.broadcast({connection:(user, client_channel)}, room)
  // Observe disconnection
  Dom.bind_beforeunload_confirmation(function(_) {
    Network.broadcast({disconnection:user}, room)
    Network.unobserve(obs)
    none
  })
  // Send NB_LAST_MSGS messages
  history_list = IntMap.To.val_list(/history)
  len = List.length(history_list)
  history = List.drop(len-NB_LAST_MSGS, history_list)
  message_update(compute_stats(), history)
  // Initialize OpaShare
  OpaShare.init(file_uploaded(user))
}

server @async function enter_chat(user_name, client_channel) {
  user = {
    id: Random.int(max_int),
    name: user_name
  }
  send = broadcast({~user}, _)
  // #Body is the default body id in Opa
  #Body = build_page(

Users online

      {OpaShare.html()}

        <input id=#entry
               autofocus="autofocus"
               onready={function(_){Dom.give_focus(#entry)}}
               onnewline={send}
               x-webkit-speech="x-webkit-speech"/>

  )
}

client @async function join(_) {
  name = Dom.get_value(#name)
  client_channel = Session.make_callback(ignore)
  enter_chat(name, client_channel)
}

// Page headers
headers =
  Xhtml.of_string_unsafe("
") <+>
  <meta charset="utf-8"></meta>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
  <meta name="viewport" content="width=device-width,initial-scale=1"></meta>

// Start page
server function start() {
  page = build_page(

A real-time web chat built in Opa.

      <input id=#name
             placeholder="Name"
             autofocus="autofocus"
             onready={function(_){Dom.give_focus(#name)}}
             onnewline={join}/>
       <button class="btn primary"
               onclick={join}>Join</button>

  )
  Resource.full_page_with_doctype(
    "OpaChat - a real-time web chat built in Opa",
    {html5},
    page, headers, {success},
    []
  )
}

// Parse URL
url_parser = parser {
  case "/file/" key=Rule.integer:
    match (OpaShare.get(key)) {
    case {some:file}:
      Resource.binary(file.content, file.mimetype)
      |> Resource.add_header(_, {content_disposition:{attachment:file.name}})
    default: start()
    }
  case (.*): start()
}

// Start the server
Server.start(Server.http, [
  { resources : @static_resource_directory("resources") }, // include resources directory
  { register : [
      "/resources/css/bootstrap.min.css",
      "/resources/css/bootstrap-responsive.min.css",
      "/resources/css/style.css",
    ] }, // include CSS in headers
  { custom : url_parser } // URL parser
])

/*  A simple, one-room, scalable real-time web chat, with file sharing

    Copyright © 2012 Frederic Ye
    Copyright © 2010-2011 MLstate

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import stdlib.system

/** Constants **/

GITHUB_USER = "Aqua-Ye"
GITHUB_REPO = "OpaChat"
NB_LAST_MSGS = 10

/** Types **/

type user = { int id, string name }
type source = { system } or { user user }
type message = {
  source source,
  string text,
  Date.date date,
}
type media = {
  source source,
  string name,
  string src,
  string mimetype,
  Date.date date,
}
type client_channel = channel(void)
type network_msg =
   {message message}
or {(user, client_channel) connection}
or {user disconnection}
or {stats}
or {media media}

/** Database **/

database intmap(message) /history

exposed Network.network(network_msg) room = Network.cloud("room")
private reference(list(user)) users = ServerReference.create([])
private launch_date = Date.now()

/** Page **/

watch_button =

fork_button =

function build_page(content) {

OpaChat

      {watch_button}
      {fork_button}

{content}

}

/** Connection **/

server function server_observe(message) {
  match (message) {
  case {connection:(user, client_channel)} :
    ServerReference.update(users, List.add(user, _))
    Network.broadcast({stats}, room)
    Session.on_remove(client_channel, function() {
      server_observe({disconnection:user})
    })
  case {disconnection:user} :
    ServerReference.update(users, List.remove(user, _))
    Network.broadcast({stats}, room)
  default: void
  }
}

_ = Network.observe(server_observe, room)

/** Stats **/

server function mem() {
  System.get_memory_usage()/(1024*1024)
}

server function compute_stats() {
  uptime_duration = Date.between(launch_date, Date.now())
  uptime = Date.of_duration(uptime_duration)
  uptime = Date.shift_backward(uptime, Date.to_duration(Date.milliseconds(3600000))) // 1 hour shift
  (uptime, mem())
}

client @async function update_stats((uptime, mem)) {
  #uptime = <>Uptime: {Date.to_string_time_only(uptime)}</>
  #memory = <>Memory: {mem} Mo</>
}

client @async function update_users(nb_users, users) {
  #users = <>Users: {nb_users}</>
  #user_list =
{users}

}

/** Conversation **/

function source_to_html(source) {
  match (source) {
  case {system} :
  case {~user} : {user.name}
  }
}

client @async function message_update(stats, list(message) messages) {
  update_stats(stats)
  List.iter(function(message) {
    date = Date.to_formatted_string(Date.default_printer, message.date)
    time = Date.to_string_time_only(message.date)
    line =

              {time}
              { source_to_html(message.source) }
              {message.text}

    #conversation =+ line
  }, messages)
  Dom.scroll_to_bottom(#conversation)
}

client @async function media_update(stats, list(media) medias) {
  update_stats(stats)
  List.iter(function(media) {
    date = Date.to_formatted_string(Date.default_printer, media.date)
    time = Date.to_string_time_only(media.date)
    media_parser = parser {
      case "image/" .*: {image}
      case "audio/" .*: {audio}
      case "video/" .*: {video}
    }
    line =

              {time}
              { source_to_html(media.source) }
              { match (Parser.try_parse(media_parser, media.mimetype)) {
                case {some:{image}}:

                case {some:{audio}}:
                  <audio src="{media.src}"
                         controls="controls"
                         type="{media.mimetype}"
                         preload="auto">
                    Your browser does not support the audio tag!
                  </audio>
                case {some:{video}}:
                  <video src="{media.src}"
                         controls="controls"
                         preload="auto"
                         type="{media.name}">
                    Your browser does not support the video tag!
                  </video>
                default:
                   is sharing a file :
                    {media.name}

                } }

    #conversation =+ line
  }, medias)
  Dom.scroll_to_bottom(#conversation)
}

exposed @async function server_broadcast(user, text) {
  message = {source:user, ~text, date:Date.now()}
  /history[?] <- message
  Network.broadcast({~message}, room)
}

client @async function broadcast(user, _) {
  _ = server_broadcast(user, Dom.get_value(#entry))
  Dom.clear_value(#entry)
}

server function client_observe(msg) {
  match (msg) {
  case {~message} :
    message_update(compute_stats(), [message])
  case {stats} :
    update_stats(compute_stats())
    users = ServerReference.get(users)
    users_html_list = List.fold(function(elt, acc) {
                        <>
{elt.name}
{acc}</>
                      }, users, <></>)
    update_users(List.length(users), users_html_list)
  case {connection:(user, _)} :
    message = {
      source: {system},
      text : "{user.name} joined the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {disconnection:user} :
    message = {
      source: {system},
      text : "{user.name} left the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {~media} :
    media_update(compute_stats(), [media])
  default : void
  }
}

server function file_uploaded(user)(name, mimetype, key) {
  media = {
    source: {~user},
    ~name,
    src: "/file/{key}",
    ~mimetype,
    date: Date.now(),
  }
  Network.broadcast({~media}, room)
}

// Init the client from the server
server function init_client(user, client_channel) {
  // Observe client
  obs = Network.observe(client_observe, room)
  Network.broadcast({connection:(user, client_channel)}, room)
  // Observe disconnection
  Dom.bind_beforeunload_confirmation(function(_) {
    Network.broadcast({disconnection:user}, room)
    Network.unobserve(obs)
    none
  })
  // Send NB_LAST_MSGS messages
  history_list = IntMap.To.val_list(/history)
  len = List.length(history_list)
  history = List.drop(len-NB_LAST_MSGS, history_list)
  message_update(compute_stats(), history)
  // Initialize OpaShare
  OpaShare.init(file_uploaded(user))
}

server @async function enter_chat(user_name, client_channel) {
  user = {
    id: Random.int(max_int),
    name: user_name
  }
  send = broadcast({~user}, _)
  // #Body is the default body id in Opa
  #Body = build_page(

Users online

      {OpaShare.html()}

        <input id=#entry
               autofocus="autofocus"
               onready={function(_){Dom.give_focus(#entry)}}
               onnewline={send}
               x-webkit-speech="x-webkit-speech"/>

  )
}

client @async function join(_) {
  name = Dom.get_value(#name)
  client_channel = Session.make_callback(ignore)
  enter_chat(name, client_channel)
}

headers =
  Xhtml.of_string_unsafe("
") <+>
  <meta charset="utf-8"></meta>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
  <meta name="viewport" content="width=device-width,initial-scale=1"></meta>

// Start page
server function start() {
  page = build_page(

A real-time web chat built in Opa.

      <input id=#name
             placeholder="Name"
             autofocus="autofocus"
             onready={function(_){Dom.give_focus(#name)}}
             onnewline={join}/>
       <button class="btn primary"
               onclick={join}>Join</button>

  )
  Resource.full_page_with_doctype(
    "OpaChat - a real-time web chat built in Opa",
    {html5},
    page, headers, {success},
    []
  )
}

// Parse URL
url_parser = parser {
  case "/file/" key=Rule.integer:
    match (OpaShare.get(key)) {
      case {some:file}:
        Resource.binary(file.content, file.mimetype)
        |> Resource.add_header(_, {content_disposition:{attachment:file.name}})
      default: start()
    }
  case (.*): start()
}

// Start the server
Server.start(Server.http, [
  { resources : @static_resource_directory("resources") }, // include resources directory
  { register : [
      "/resources/css/bootstrap.min.css",
      "/resources/css/bootstrap-responsive.min.css",
      "/resources/css/style.css",
    ] }, // include CSS in headers
  { custom : url_parser } // URL parser
])


renamed app MindChat in the app itself

renamed app MindChat in the app itself

/*  A simple, one-room, scalable real-time web chat, with file sharing

    Copyright © 2010-2012 MLstate

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * @author David Rajchenbach-Teller
 * @author Henri Binsztok
 * @author Frederic Ye
 */

import stdlib.system
import stdlib.themes.bootstrap

/** Constants **/

GITHUB_USER = "MLstate"
GITHUB_REPO = "MindChat"
NB_LAST_MSGS = 10

/** Types **/

type user = { int id, string name, option(mindstate) mindwave }
type source = { system } or { string user }
type message = {
  source source,
  string text,
  Date.date date,
}
type media = {
  source source,
  string name,
  string src,
  string mimetype,
  Date.date date,
}
type client_channel = channel(void)
type network_msg =
   {message message}
or {media media}
or {(user, client_channel) connection}
or {user disconnection}
or {stats}
or {user mindstate}

/** Database **/

database intmap(message) /history

/** Top level values **/

exposed Network.network(network_msg) room = Network.cloud("room")
private reference(intmap(user)) users = ServerReference.create(IntMap.empty)
private launch_date = Date.now()

/** Connection **/

server function server_observe(message) {
  match (message) {
  case {connection:(user, client_channel)} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    Network.broadcast({stats}, room)
    Session.on_remove(client_channel, function() {
      server_observe({disconnection:user})
    })
  case {disconnection:user} :
    ServerReference.update(users, IntMap.remove(user.id, _))
    Network.broadcast({stats}, room)
  default: void
  }
}

_ = Network.observe(server_observe, room)

/** Stats **/

// Compute uptime and memory usage (MB)
server function compute_stats() {
  uptime = Date.between(launch_date, Date.now())
  mem = System.get_memory_usage()/(1024*1024)
  (uptime, mem)
}

client @async function update_stats((uptime, mem)) {
  #uptime = <>Uptime: {Duration.to_formatted_string(Duration.long_time_printer, uptime)}</>
  #memory = <>Memory: {mem} MB</>
}

/** Users **/

client @async function update_users(nb_users, users) {
  #users = <>Users: {nb_users}</>
  #user_list =
{users}

}

/** Conversation **/

// converts a source into HTML
function source_to_html(source) {
  match (source) {
  case {system} :
  case {~user} : {user}
  }
}

private client function generic_update(stats, elements, printer) {
  update_stats(stats)
  List.iter(function(element) {
    date = Date.to_formatted_string(Date.default_printer, element.date)
    time = Date.to_string_time_only(element.date)
    line =

              {time}
              {printer(element)}

    #conversation =+ line
  }, elements)
  Dom.scroll_to_bottom(#conversation)
}

client @async function message_update(stats, list(message) messages) {
  generic_update(stats, messages, function(message) {
    source_to_html(message.source) <+>
    {message.text}
  })
}

client @async function media_update(stats, list(media) medias) {
  media_parser = parser {
    case "image/" .*: {image}
    case "audio/" .*: {audio}
    case "video/" .*: {video}
  }
  generic_update(stats, medias, function(media) {
    source_to_html(media.source) <+>
    ( match (Parser.try_parse(media_parser, media.mimetype)) {
      case {some:{image}}:

      case {some:{audio}}:
        <audio src="{media.src}"
               controls="controls"
               type="{media.mimetype}"
               preload="auto">
          Your browser does not support the audio tag!
        </audio>
      case {some:{video}}:
        <video src="{media.src}"
               controls="controls"
               preload="auto"
               type="{media.name}">
          Your browser does not support the video tag!
        </video>
      default:
         is sharing a file :
          {media.name}

      } )
  })
}

server function file_uploaded(user)(name, mimetype, key) {
  media = {
    source: {user:user.name},
    ~name,
    src: "/file/{key}",
    ~mimetype,
    date: Date.now(),
  }
  Network.broadcast({~media}, room)
}

server function client_observe(msg) {
  match (msg) {
  case {~message} :
    message_update(compute_stats(), [message])
  case {~media} :
    media_update(compute_stats(), [media])
  case {connection:(user, _)} :
    message = {
      source: {system},
      text : "{user.name} joined the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {disconnection:user} :
    message = {
      source: {system},
      text : "{user.name} left the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {stats} :
    update_stats(compute_stats())
    users = ServerReference.get(users)
            |> IntMap.To.val_list(_)
            |> List.sort_by(function(u){u.name}, _)
    users_html_list =
      List.fold(function(user, acc) {
        mw = mindwave_to_html(user.mindwave)

{mw} {user.name}

        <+> acc
      }, users, <></>)
    update_users(List.length(users), users_html_list)
  case {mindstate:user} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    users = ServerReference.get(users) |> IntMap.To.val_list(_)
    waves_list =
      List.fold(function(user, acc) {
        (user.id, user.mindwave) +> acc
      }, users, [])
    update_mindwaves(waves_list)
  default : void
  }
}

/** Init **/

// Init the client from the server
server function init_client(user, client_channel, _) {
  // Observe client
  obs = Network.observe(client_observe, room)
  Network.broadcast({connection:(user, client_channel)}, room)
  // Observe disconnection
  Dom.bind_beforeunload_confirmation(function(_) {
    Network.broadcast({disconnection:user}, room)
    Network.unobserve(obs)
    none
  })
  // Send NB_LAST_MSGS messages
  history_list = IntMap.To.val_list(/history)
  len = List.length(history_list)
  history = List.drop(len-NB_LAST_MSGS, history_list)
  message_update(compute_stats(), history)
}

// Init various scheduling tasks
client function init_scheduling(user, _) {
  Scheduler.timer(MW_TIMER, function(){
    check_mindstate(user)
  })
}

client @async function send_message(broadcast, _) {
  void broadcast(Dom.get_value(#entry))
  Dom.clear_value(#entry)
}

exposed @async function enter_chat(user_name, client_channel) {
  user = {
    id: Random.int(max_int),
    name: user_name,
    mindwave: none,
  }
  function broadcast(text) {
    message = {source:{user:user_name}, ~text, date:Date.now()}
    /history[?] <- message
    Network.broadcast({~message}, room)
  }
  #main =

Users online

      {mindwave_flash}

        <input id=#entry
               autofocus="autofocus"
               onready={function(_){Dom.give_focus(#entry)}}
               onnewline={send_message(broadcast, _)}
               x-webkit-speech="x-webkit-speech"/>

}

client @async function join(_) {
  name = Dom.get_value(#name)
  client_channel = Session.make_callback(ignore)
  enter_chat(name, client_channel)
}

/** Server **/

watch_button =

fork_button =

function build_page(content) {

MindChat

      {watch_button}
      {fork_button}

{content}

}

// Page headers
headers =
  Xhtml.of_string_unsafe("
") <+>
  <meta charset="utf-8"></meta>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
  <meta name="viewport" content="width=device-width,initial-scale=1"></meta>

// Start page
server function start() {
  page = build_page(

The webchat that shows how you feel.

      <input id=#name
             placeholder="Name"
             autofocus="autofocus"
             onready={function(_){Dom.give_focus(#name)}}
             onnewline={join}/>
       <button class="btn primary"
               onclick={join}>Join</button>

  )
  Resource.full_page_with_doctype(
    "MindChat",
    {html5},
    page, headers, {success},
    []
  )
}

// Parse URL
url_parser = parser {
  case (.*): start()
}

Resource.register_external_js("/resources/neurosky/FlashToJs/api.js")

// Start the server
Server.start(Server.http, [
  { resources : @static_resource_directory("resources") }, // include resources directory
  { register : [
      "/resources/css/style.css",
      "/resources/neurosky/icons.css",
    ] }, // include CSS in headers
  { custom : url_parser } // URL parser
])

/*  A simple, one-room, scalable real-time web chat, with file sharing

    Copyright © 2010-2012 MLstate

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * @author David Rajchenbach-Teller
 * @author Henri Binsztok
 * @author Frederic Ye
 */

import stdlib.system
import stdlib.themes.bootstrap

/** Constants **/

GITHUB_USER = "MLstate"
GITHUB_REPO = "MindChat"
NB_LAST_MSGS = 10

/** Types **/

type user = { int id, string name, option(mindstate) mindwave }
type source = { system } or { string user }
type message = {
  source source,
  string text,
  Date.date date,
}
type media = {
  source source,
  string name,
  string src,
  string mimetype,
  Date.date date,
}
type client_channel = channel(void)
type network_msg =
   {message message}
or {media media}
or {(user, client_channel) connection}
or {user disconnection}
or {stats}
or {user mindstate}

/** Database **/

database intmap(message) /history

/** Top level values **/

exposed Network.network(network_msg) room = Network.cloud("room")
private reference(intmap(user)) users = ServerReference.create(IntMap.empty)
private launch_date = Date.now()

/** Connection **/

server function server_observe(message) {
  match (message) {
  case {connection:(user, client_channel)} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    Network.broadcast({stats}, room)
    Session.on_remove(client_channel, function() {
      server_observe({disconnection:user})
    })
  case {disconnection:user} :
    ServerReference.update(users, IntMap.remove(user.id, _))
    Network.broadcast({stats}, room)
  default: void
  }
}

_ = Network.observe(server_observe, room)

/** Stats **/

// Compute uptime and memory usage (MB)
server function compute_stats() {
  uptime = Date.between(launch_date, Date.now())
  mem = System.get_memory_usage()/(1024*1024)
  (uptime, mem)
}

client @async function update_stats((uptime, mem)) {
  #uptime = <>Uptime: {Duration.to_formatted_string(Duration.long_time_printer, uptime)}</>
  #memory = <>Memory: {mem} MB</>
}

/** Users **/

client @async function update_users(nb_users, users) {
  #users = <>Users: {nb_users}</>
  #user_list =
{users}

}

/** Conversation **/

// converts a source into HTML
function source_to_html(source) {
  match (source) {
  case {system} :
  case {~user} : {user}
  }
}

private client function generic_update(stats, elements, printer) {
  update_stats(stats)
  List.iter(function(element) {
    date = Date.to_formatted_string(Date.default_printer, element.date)
    time = Date.to_string_time_only(element.date)
    line =

              {time}
              {printer(element)}

    #conversation =+ line
  }, elements)
  Dom.scroll_to_bottom(#conversation)
}

client @async function message_update(stats, list(message) messages) {
  generic_update(stats, messages, function(message) {
    source_to_html(message.source) <+>
    {message.text}
  })
}

client @async function media_update(stats, list(media) medias) {
  media_parser = parser {
    case "image/" .*: {image}
    case "audio/" .*: {audio}
    case "video/" .*: {video}
  }
  generic_update(stats, medias, function(media) {
    source_to_html(media.source) <+>
    ( match (Parser.try_parse(media_parser, media.mimetype)) {
      case {some:{image}}:

      case {some:{audio}}:
        <audio src="{media.src}"
               controls="controls"
               type="{media.mimetype}"
               preload="auto">
          Your browser does not support the audio tag!
        </audio>
      case {some:{video}}:
        <video src="{media.src}"
               controls="controls"
               preload="auto"
               type="{media.name}">
          Your browser does not support the video tag!
        </video>
      default:
         is sharing a file :
          {media.name}

      } )
  })
}

server function file_uploaded(user)(name, mimetype, key) {
  media = {
    source: {user:user.name},
    ~name,
    src: "/file/{key}",
    ~mimetype,
    date: Date.now(),
  }
  Network.broadcast({~media}, room)
}

server function client_observe(msg) {
  match (msg) {
  case {~message} :
    message_update(compute_stats(), [message])
  case {~media} :
    media_update(compute_stats(), [media])
  case {connection:(user, _)} :
    message = {
      source: {system},
      text : "{user.name} joined the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {disconnection:user} :
    message = {
      source: {system},
      text : "{user.name} left the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {stats} :
    update_stats(compute_stats())
    users = ServerReference.get(users)
            |> IntMap.To.val_list(_)
            |> List.sort_by(function(u){u.name}, _)
    users_html_list =
      List.fold(function(user, acc) {
        mw = mindwave_to_html(user.mindwave)

{mw} {user.name}

        <+> acc
      }, users, <></>)
    update_users(List.length(users), users_html_list)
  case {mindstate:user} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    users = ServerReference.get(users) |> IntMap.To.val_list(_)
    waves_list =
      List.fold(function(user, acc) {
        (user.id, user.mindwave) +> acc
      }, users, [])
    update_mindwaves(waves_list)
  default : void
  }
}

/** Init **/

// Init the client from the server
server function init_client(user, client_channel, _) {
  // Observe client
  obs = Network.observe(client_observe, room)
  Network.broadcast({connection:(user, client_channel)}, room)
  // Observe disconnection
  Dom.bind_beforeunload_confirmation(function(_) {
    Network.broadcast({disconnection:user}, room)
    Network.unobserve(obs)
    none
  })
  // Send NB_LAST_MSGS messages
  history_list = IntMap.To.val_list(/history)
  len = List.length(history_list)
  history = List.drop(len-NB_LAST_MSGS, history_list)
  message_update(compute_stats(), history)
}

// Init various scheduling tasks
client function init_scheduling(user, _) {
  Scheduler.timer(MW_TIMER, function(){
    check_mindstate(user)
  })
}

client @async function send_message(broadcast, _) {
  void broadcast(Dom.get_value(#entry))
  Dom.clear_value(#entry)
}

exposed @async function enter_chat(user_name, client_channel) {
  user = {
    id: Random.int(max_int),
    name: user_name,
    mindwave: none,
  }
  function broadcast(text) {
    message = {source:{user:user_name}, ~text, date:Date.now()}
    /history[?] <- message
    Network.broadcast({~message}, room)
  }
  #main =

Users online

      {mindwave_flash}

        <input id=#entry
               autofocus="autofocus"
               onready={function(_){Dom.give_focus(#entry)}}
               onnewline={send_message(broadcast, _)}
               x-webkit-speech="x-webkit-speech"/>

}

client @async function join(_) {
  name = Dom.get_value(#name)
  client_channel = Session.make_callback(ignore)
  enter_chat(name, client_channel)
}

/** Server **/

watch_button =

fork_button =

function build_page(content) {

OpaChat

      {watch_button}
      {fork_button}

{content}

}

// Page headers
headers =
  Xhtml.of_string_unsafe("
") <+>
  <meta charset="utf-8"></meta>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
  <meta name="viewport" content="width=device-width,initial-scale=1"></meta>

// Start page
server function start() {
  page = build_page(

A real-time web chat built in Opa.

      <input id=#name
             placeholder="Name"
             autofocus="autofocus"
             onready={function(_){Dom.give_focus(#name)}}
             onnewline={join}/>
       <button class="btn primary"
               onclick={join}>Join</button>

  )
  Resource.full_page_with_doctype(
    "OpaChat - a real-time web chat built in Opa",
    {html5},
    page, headers, {success},
    []
  )
}

// Parse URL
url_parser = parser {
  case (.*): start()
}

Resource.register_external_js("/resources/neurosky/FlashToJs/api.js")

// Start the server
Server.start(Server.http, [
  { resources : @static_resource_directory("resources") }, // include resources directory
  { register : [
      "/resources/css/style.css",
      "/resources/neurosky/icons.css",
    ] }, // include CSS in headers
  { custom : url_parser } // URL parser
])


pull-left lost in merge

pull-left lost in merge

/*  A simple, one-room, scalable real-time web chat, with file sharing

    Copyright © 2010-2012 MLstate

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * @author David Rajchenbach-Teller
 * @author Henri Binsztok
 * @author Frederic Ye
 */

import stdlib.system
import stdlib.themes.bootstrap

/** Constants **/

GITHUB_USER = "MLstate"
GITHUB_REPO = "MindChat"
NB_LAST_MSGS = 10

/** Types **/

type user = { int id, string name, option(mindstate) mindwave }
type source = { system } or { string user }
type message = {
  source source,
  string text,
  Date.date date,
}
type media = {
  source source,
  string name,
  string src,
  string mimetype,
  Date.date date,
}
type client_channel = channel(void)
type network_msg =
   {message message}
or {media media}
or {(user, client_channel) connection}
or {user disconnection}
or {stats}
or {user mindstate}

/** Database **/

database intmap(message) /history

/** Top level values **/

exposed Network.network(network_msg) room = Network.cloud("room")
private reference(intmap(user)) users = ServerReference.create(IntMap.empty)
private launch_date = Date.now()

/** Connection **/

server function server_observe(message) {
  match (message) {
  case {connection:(user, client_channel)} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    Network.broadcast({stats}, room)
    Session.on_remove(client_channel, function() {
      server_observe({disconnection:user})
    })
  case {disconnection:user} :
    ServerReference.update(users, IntMap.remove(user.id, _))
    Network.broadcast({stats}, room)
  default: void
  }
}

_ = Network.observe(server_observe, room)

/** Stats **/

// Compute uptime and memory usage (MB)
server function compute_stats() {
  uptime = Date.between(launch_date, Date.now())
  mem = System.get_memory_usage()/(1024*1024)
  (uptime, mem)
}

client @async function update_stats((uptime, mem)) {
  #uptime = <>Uptime: {Duration.to_formatted_string(Duration.long_time_printer, uptime)}</>
  #memory = <>Memory: {mem} MB</>
}

/** Users **/

client @async function update_users(nb_users, users) {
  #users = <>Users: {nb_users}</>
  #user_list =
{users}

}

/** Conversation **/

// converts a source into HTML
function source_to_html(source) {
  match (source) {
  case {system} :
  case {~user} : {user}
  }
}

private client function generic_update(stats, elements, printer) {
  update_stats(stats)
  List.iter(function(element) {
    date = Date.to_formatted_string(Date.default_printer, element.date)
    time = Date.to_string_time_only(element.date)
    line =

              {time}
              {printer(element)}

    #conversation =+ line
  }, elements)
  Dom.scroll_to_bottom(#conversation)
}

client @async function message_update(stats, list(message) messages) {
  generic_update(stats, messages, function(message) {
    source_to_html(message.source) <+>
    {message.text}
  })
}

client @async function media_update(stats, list(media) medias) {
  media_parser = parser {
    case "image/" .*: {image}
    case "audio/" .*: {audio}
    case "video/" .*: {video}
  }
  generic_update(stats, medias, function(media) {
    source_to_html(media.source) <+>
    ( match (Parser.try_parse(media_parser, media.mimetype)) {
      case {some:{image}}:

      case {some:{audio}}:
        <audio src="{media.src}"
               controls="controls"
               type="{media.mimetype}"
               preload="auto">
          Your browser does not support the audio tag!
        </audio>
      case {some:{video}}:
        <video src="{media.src}"
               controls="controls"
               preload="auto"
               type="{media.name}">
          Your browser does not support the video tag!
        </video>
      default:
         is sharing a file :
          {media.name}

      } )
  })
}

server function file_uploaded(user)(name, mimetype, key) {
  media = {
    source: {user:user.name},
    ~name,
    src: "/file/{key}",
    ~mimetype,
    date: Date.now(),
  }
  Network.broadcast({~media}, room)
}

server function client_observe(msg) {
  match (msg) {
  case {~message} :
    message_update(compute_stats(), [message])
  case {~media} :
    media_update(compute_stats(), [media])
  case {connection:(user, _)} :
    message = {
      source: {system},
      text : "{user.name} joined the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {disconnection:user} :
    message = {
      source: {system},
      text : "{user.name} left the room",
      date : Date.now(),
    }
    message_update(compute_stats(), [message])
  case {stats} :
    update_stats(compute_stats())
    users = ServerReference.get(users)
            |> IntMap.To.val_list(_)
            |> List.sort_by(function(u){u.name}, _)
    users_html_list =
      List.fold(function(user, acc) {
        mw = mindwave_to_html(user.mindwave)

{mw} {user.name}

        <+> acc
      }, users, <></>)
    update_users(List.length(users), users_html_list)
  case {mindstate:user} :
    ServerReference.update(users, IntMap.add(user.id, user, _))
    users = ServerReference.get(users) |> IntMap.To.val_list(_)
    waves_list =
      List.fold(function(user, acc) {
        (user.id, user.mindwave) +> acc
      }, users, [])
    update_mindwaves(waves_list)
  default : void
  }
}

/** Init **/

// Init the client from the server
server function init_client(user, client_channel, _) {
  // Observe client
  obs = Network.observe(client_observe, room)
  Network.broadcast({connection:(user, client_channel)}, room)
  // Observe disconnection
  Dom.bind_beforeunload_confirmation(function(_) {
    Network.broadcast({disconnection:user}, room)
    Network.unobserve(obs)
    none
  })
  // Send NB_LAST_MSGS messages
  history_list = IntMap.To.val_list(/history)
  len = List.length(history_list)
  history = List.drop(len-NB_LAST_MSGS, history_list)
  message_update(compute_stats(), history)
}

// Init various scheduling tasks
client function init_scheduling(user, _) {
  Scheduler.timer(MW_TIMER, function(){
    check_mindstate(user)
  })
}

client @async function send_message(broadcast, _) {
  void broadcast(Dom.get_value(#entry))
  Dom.clear_value(#entry)
}

exposed @async function enter_chat(user_name, client_channel) {
  user = {
    id: Random.int(max_int),
    name: user_name,
    mindwave: none,
  }
  function broadcast(text) {
    message = {source:{user:user_name}, ~text, date:Date.now()}
    /history[?] <- message
    Network.broadcast({~message}, room)
  }
  #main =

Users online

      {mindwave_flash}

        <input id=#entry
               autofocus="autofocus"
               onready={function(_){Dom.give_focus(#entry)}}
               onnewline={send_message(broadcast, _)}
               x-webkit-speech="x-webkit-speech"/>

}

client @async function join(_) {
  name = Dom.get_value(#name)
  client_channel = Session.make_callback(ignore)
  enter_chat(name, client_channel)
}

/** Server **/

watch_button =

fork_button =

function build_page(content) {

OpaChat

      {watch_button}
      {fork_button}

{content}

}

// Page headers
headers =
  Xhtml.of_string_unsafe("
") <+>
  <meta charset="utf-8"></meta>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta>
  <meta name="viewport" content="width=device-width,initial-scale=1"></meta>

// Start page
server function start() {
  page = build_page(

A real-time web chat built in Opa.

      <input id=#name
             placeholder="Name"
             autofocus="autofocus"
             onready={function(_){Dom.give_focus(#name)}}
             onnewline={join}/>
       <button class="btn primary"
               onclick={join}>Join</button>

  )
  Resource.full_page_with_doctype(
    "OpaChat - a real-time web chat built in Opa",
    {html5},
    page, headers, {success},
    []
  )
}

// Parse URL
url_parser = parser {
  case (.*): start()
}

Resource.register_external_js("/resources/neurosky/FlashToJs/api.js")

// Start the server
Server.start(Server.http, [
  { resources : @static_resource_directory("resources") }, // include resources directory
  { register : [
      "/resources/css/style.css",
      "/resources/neurosky/icons.css",
    ] }, // include CSS in headers
  { custom : url_parser } // URL parser
])


Enhance server disconnection user message

When the web application can't join the server, an error message is displayed to
the user. This patch uses the Notifications.error helper to display it

/*
 * PEPS is a modern collaboration server
 * Copyright (C) 2015 MLstate
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mlstate.webmail.view

// Global alias used in all views
WB = WBootstrap

module Keyboard {

  client function Dom.event_propagation propagation_handler(Dom.event e) {
    propagation = {stop_propagation : false, prevent_default : false};
    if (not(Dom.is_empty(Dom.select_raw_unsafe("input:focus, textarea:focus"))))
      propagation
    else
      match (e) {
      case { key_code : { some : 47 }, ... }:
        {propagation with prevent_default : true}
      case { key_code : { some : 114 }, key_modifiers : [], ... }:
        {propagation with prevent_default : true}
      default: propagation
      }
  }

  client function handler(Dom.event e) {
    if (not(Dom.is_empty(Dom.select_raw_unsafe("input:focus, textarea:focus"))))
      void
    else {
      match (e) {
      case { key_code : { some : 99 }, key_modifiers : [], ... }:
        ComposeView.new("", e)
      case { key_code : { some : 117 }, key_modifiers : [], ... }:
        UploadView.show(e)
      case { key_code : { some : 109 }, key_modifiers : [], ... }:
        MessageView.mark_read(e)
      // case { key_code : { some : 105 }, key_modifiers : [], ... }:
      //   Dom.trigger(#info_toggle, {click})
      case { key_code : { some : 47 }, ... }: SearchView.focus(e)
      // case { key_code : { some : 114 }, key_modifiers : [], ... }:
      //   Dom.trigger(#quick_reply, {click})
      case { key_code : { some : 115 }, key_modifiers : [], ... }:
        Dom.trigger(#star_message, {click})
      case { key_code : { some : 106 }, key_modifiers : [], ... }:
        MessageView.Snippet.next(e)
      case { key_code : { some : 107 }, key_modifiers : [], ... }:
        MessageView.Snippet.prev(e)
      default: void
      }
    }
  }

}

client logout_timer_time = ClientReference.create(60)
client logout_timer_ctxt = ClientReference.create(option(ThreadContext.client) {none})

module Content {

    log = Log.notice("Content", _)
    error = Log.error("Content", _)

    // // FIXME: move to automated JS
    // function choose_selected_box(anchor) {
    //     function act(anchor) {
    //         Dom.remove_class(Dom.select_raw_unsafe("li"), "selected");
    //         Dom.add_class(Dom.select_class(anchor), "selected");
    //         void
    //     }
    //     update({messages: {inbox}}, anchor)
    // }

    // client function update_items(anchor) {
    //     Content.choose_selected_box(anchor);
    //     void
    // }

    function insert_loading() {
        Log.info("Loading", "content...");
        if (Dom.is_empty(#loading-content)) {
          loading =

;
          #content = loading
        } else void
    }

    function make_alert(string title, xhtml description) {
      WBootstrap.Alert.make({
        block: ~{ title, description },
        actions: none,
        closable: false
      }, {error})
    }

    login_please =
      make_alert(
        @intl("Your session has expired!"),
        <>{@intl("Please log in to access this resource")}</>
      )

    non_existent_resource =
      make_alert(
        @intl("Oops!"),
        <>{@intl("Looks like the resource you are looking for does not exist ...")}</>
      )

    not_allowed_message =
      make_alert(
        @intl("Sorry!"),
        XmlConvert.of_alpha(AppText.not_allowed_message())
      )

    not_allowed_resource =
      make_alert(
        "Sorry!",
        XmlConvert.of_alpha(AppText.not_allowed_resource())
      )

    exposed function logout_and_reload(ctx) {
      state = Login.get_state()
      log("logout_and_reload: unlogging {state}")
      key = state.key
      error("INACTIVE LOGOUT {key}")
      Login.logout()
      ClientEvent.remove_inactivity_delay(ctx)
      Client.do_reload(false)
    }

    client function do_confirm(ctx, btn_id, confirmed, _) {
      Button.reset(#{btn_id})
      Modal.hide(#modal_logout_confirm)
      stop_logout_timer()
      if (confirmed) {
        logout_and_reload(ctx)
      }
    }

    recursive private client function logout_timer_function() {
      time = ClientReference.get(logout_timer_time)
      if (time > 0) {
        time = time - 1
        ClientReference.set(logout_timer_time, time)
        #logout_confirm_time = AppText.logout_timer(time)
      } else {
        logout_timer.stop()
        match (ClientReference.get(logout_timer_ctxt)) {
        case {some:ctx}: logout_and_reload({some:ctx});
        case {none}: void;
        }
      }
    }

    recursive private client logout_timer = Scheduler.make_timer(1000, logout_timer_function)

    client function start_logout_timer(ctx) {
      ClientReference.set(logout_timer_time, AdminController.get_grace_period())
      ClientReference.set(logout_timer_ctxt, ctx)
      logout_timer.start()
    }

    client function stop_logout_timer() {
      ClientReference.set(logout_timer_ctxt, {none})
      logout_timer.stop()
    }

    function logout_confirm(ctx) {
      Modal.make(
       "modal_logout_confirm",
       <>{AppText.session_expired()}</>,
       <>
{AppText.logout_confirm()}

{AppText.logout_timer(AdminController.get_grace_period())}

       </>,
       (WBootstrap.Button.make({button:<> {AppText.yes()}</>,
                        callback:do_confirm(ctx, "confirm_yes", true, _)}, [{success}])
        |> Xhtml.add_id(some("confirm_yes"), _)) <+>
       (WBootstrap.Button.make({button:<> {AppText.no()}</>,
                        callback:do_confirm(ctx, "confirm_no", false, _)}, [{primary}])
        |> Xhtml.add_id(some("confirm_no"), _)),
       {Modal.default_options with backdrop : false, static : false, keyboard : false})
    }

    // FIXME: not here
    exposed function init_server(channel) {
      state = Login.get_state()
      key = state.key
      if (not(Login.is_logged(state))) void
      else {
        ctx = ThreadContext.Client.get_opt({current})
        timeout = Duration.min(AdminController.get_timeout())
        _ = ClientEvent.set_inactivity_delay(ctx, timeout)
        clek =
            ClientEvent.set_on_inactive_client(function(ctx) {
              start_logout_timer({some:ctx})
              Modal.show(#modal_logout_confirm)
            })
        Dom.bind_beforeunload_confirmation(function(_) {
          Notification.unregister(key, channel)
          ClientEvent.remove_event(clek)
          ClientEvent.remove_inactivity_delay(ctx)
          none
        })
        Intl.setLocale(User.getLocale(key))
        Notification.register(key, channel)
        // Update profile picture.
        match (Option.bind(RawFile.get_thumbnail, User.get_picture(state.key))) {
          case {some: picture}:
            #profile_picture =
          default: void
        }
      }
    }

    xhtml drop_file =
        <>

         {@intl("Drop files here")}
        </>

    // client function clear_modal_errors() {
    //   Dom.remove_class(Dom.select_raw_unsafe("#modal_compose .form-group"), "error")
    // }

    client function clear_tooltips() {
      Dom.remove(Dom.select_class("tooltip"))
    }

    exposed function do_setup_admin(pass) {
      // Build admin user.
      if (Admin.undefined()) {
        Admin.create(pass)
        Login.login("admin", pass)
      }
    }
    client function setup_admin(_evt) {
      pass = Dom.get_value(#pass) |> String.trim
      passrepeat = Dom.get_value(#passrepeat) |> String.trim

      if (pass != passrepeat || pass == "") Notifications.error("Error", <>{@intl("Invalid password")}</>)
      else do_setup_admin(pass)
    }

    protected function signin(state) {
      <>

{AppText.sign_in_to_mailbox()}

        {Login.build(state)}

      { if (not(Admin.only_admin_can_register())) {

            {@intl("Don't have an account? Sign up")}

        }
        else <></>
      }
      </>
    }

    protected function signup(state) {
      AdminView.register(state, none) <+>

        {@intl("Already have an account? Sign in")}

    }

    /** Log in window, if user is not connected. */
    @expand protected function loginbox(state) {

{Admin.logo()}

              {signin(state)}

    }

    @expand protected function passwordbox(state) {

{Admin.logo()}

{@intl("Setup your PEPS in two steps")}

            <form role="form" class="form-simple">

                {@intl("Admin Password")}
                <input id="pass" type="password" class="form-control" placeholder="{@intl("Password")}"/>

                {@intl("Repeat")}
                <input id="passrepeat" type="password" class="form-control" placeholder="{@intl("Password repeat")}"/>

                <button type="button" class="btn btn-block btn-primary" onclick={setup_admin}>{@intl("Next")}</button>

            </form>

    }

    /**
     * Function called when the main page is ready.
     * URN is some if the user is logged in, else none.
     */
    client function init_client(option(URN.t) urn, _) {
      errTitle = @intl("Connection lost")
      errBody =
{@intl("Please check your network connectivity and then refresh the page")}

      PingClient.add_connection_lost_handler(function() {
        Notifications.error(errTitle, errBody)
      });
      Dom.bind_with_options(
        Dom.select_document(),
        {keypress}, Keyboard.handler,
        [ {propagation_handler : Keyboard.propagation_handler} ]
      ) |> ignore
      Collapse.init(Dom.select_class("collapse"))
      Dropdown.init(Dom.select_class("dropdown-toggle"))

      channel = Session.make_callback(Notifications.handler)
      init_server(channel)
      // TODO: preserve last mode and path?
      log("init_client: {urn}")
      Option.iter(update(_, true), urn)
    }

    protected function build(Login.state state, URN.t urn) {
      unprotected = Mode.unprotected(urn.mode) // Give access to unprotected share links.
      logged = Login.is_logged(state)

      (view, _, _, _) = SettingsController.get_user_preferences(state.key)
      cl = if (view == {folders}) " narrow" else ""
      init = if (logged || unprotected) init_client(some(urn), _) else init_client(none, _)
      modals =
        if (Mode.is_share(urn.mode))
          <></>
        else
          logout_confirm(ThreadContext.Client.get_opt({current})) <+>
          FolderView.build_form(state, none) <+>
          SearchView.build_form(state)

{
        if (Admin.undefined() && not(unprotected))
          passwordbox(state)
        else if (not(logged || unprotected))
          loginbox(state)
        // Condition shown iff active licensing.
        else

 <+>

 <+>
          modals
      }

    }

  protected function selector(state, URN.t urn) {
    match (urn.mode) {
      case {messages: box}:   MessageView.build_box(state, box, urn.path, none)
      case {files: mode}:     FileView.build(state, mode, urn.path)
      case {people: mode}:    PeopleView.build(state, mode, urn.path)
      case {admin: mode}:     AdminView.build(state, mode, urn.path)
      // TODO for Norman
      case ~{app, active: _}: AppView.build(state, app, urn.path)
      case {share: link}:     FileView.build_shared(link, urn.path)
      case {settings: mode}:  SettingsView.build(state, mode, urn.path)
      case {dashboard: mode}: Dashboard.build(state, mode, urn.path)
      case {error}:           non_existent_resource
    }
  }

  // function to verify view requirements
  // should be called by every view main function to ensure consistency
  function ensure(req, state, resource) {
    if ((req=={logged} || req=={admin} || req=={super}) && not(Login.is_logged(state))) { login_please }
    else if ((req=={admin} || req=={super}) && not(Login.is_admin(state))) { not_allowed_resource }
    else if (req=={super} && not(Login.is_super_admin(state))) { not_allowed_resource }
    else { resource }
  }

  /** check_admin state */
  check_admin = ensure({admin}, _, _)
  check_super = ensure({super}, _, _)

  function onboarding(state) {
    if (SettingsController.onboarding(state.key)) {
      match (OnboardView.build(state)) {
        case {some: html}:
          Notifications.warning("Welcome to PEPS", html)
          Dom.add_class(#notification_area, "notify-static")
        default:
          Dom.remove_class(#notification_area, "notify-static")
      }
    } else {
      Dom.remove_class(#notification_area, "notify-static")
    }
  }

  /**
   * Update the client state.
   * Grouping the updates like this help reducing the number of client calls, and optimes the
   * slicing delay.
   */
  client function upload(urn, content, sidebar, badges) {
    // Clear current notifications.
    URN.change(urn)
    TopbarView.activate(urn.mode)
    Notifications.clear()
    Content.clear_tooltips()
    // Updates contents.
    #content = content
    #sidebar = sidebar
    // Insert badges.
    Notifications.Badge.insert(badges)
  }

  @async
  function change_urn(state, URN.t urn, force) {
    current = URN.get()
    if (current == urn && not(force))
      // Add reloading calls to subviews.
      void
    else {
      // Always reload the content.
      content = Content.selector(state, urn)
      sidebar = SidebarView.content(state, urn.mode)
      badges = Notifications.Badge.fetch(urn.mode)
      upload(urn, content, sidebar, badges)
      // onboarding(state)
    }
  }

  function update_callback(URN.t urn, _) {
    update(urn, false)
  }

  @async
  exposed function update(URN.t urn, bool force) {
    // Get a new state. CHECK
    state = Login.get_state()
    change_urn(state, urn, force)
  }

  /** Return to the main view. */
  function reset() {
    update(URN.init, true)
  }

  /** Refresh the view. */
  function refresh() {
    update(URN.get(), true)
  }

  /** Toggle an app sidebar. */
  client function toggleSidebar(Mode.t mode, _evt) {
    name = Mode.class(mode)
    if (Dom.has_class(#{name}, "active")) hideSidebar(mode)
    else showSidebar(mode)
  }

  /** Display an app sidebar. */
  client function showSidebar(Mode.t mode) {
    name = Mode.class(mode)
    Dom.show(#appsidebar)
    // Close open sidebars.
    Dom.iter(function (sidebar) {
      Parser.parse(parser {
        case "sidebar_" id=Rule.ident:
          if (id != name) { // Spare the mode to load.
            Dom.remove_class(#{id}, "active") // Toggle the topbar icon.
            Dom.remove_class(sidebar, "app-visible") // remove active class.
            Dom.hide(sidebar) // Hide the sidebar (do not destroy it: keep the state).
          }
        case .*: void
      }, Dom.get_id(sidebar))
    }, Dom.select_class("app-sidebar"))
    // Look up mode sidebar.
    sidebar = #{"sidebar_{name}"}
    if (Dom.is_empty(sidebar)) #appsidebar += buildSidebar(mode) // Load the sidebar.
    else Dom.show(sidebar) // Toggle the existing sidebar.
    // Activate topbar icon.
    Dom.add_class(#{name}, "active")
    Dom.add_class(#{"sidebar_{name}"}, "app-visible")
    // Reset badges.
    Notifications.Badge.reset(mode)
  }

  /** Hide an app's sidebar. */
  client function hideSidebar(Mode.t mode) {
    name = Mode.class(mode)
    Dom.iter(Dom.hide, #{"sidebar_{name}"})
    Dom.hide(#appsidebar)
    Dom.remove_class(#{name}, "active")
    Dom.remove_class(#{"sidebar_{name}"}, "app-visible")
  }

  /** Build a sidebar. */
  exposed function buildSidebar(Mode.t mode) {
    state = Login.get_state()
    name = Mode.class(mode)
    if (not(Login.is_logged(state))) login_please
    else

        {selector(state, URN.make(mode, []))}

  }

}

/*
 * PEPS is a modern collaboration server
 * Copyright (C) 2015 MLstate
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mlstate.webmail.view

// Global alias used in all views
WB = WBootstrap

module Keyboard {

  client function Dom.event_propagation propagation_handler(Dom.event e) {
    propagation = {stop_propagation : false, prevent_default : false};
    if (not(Dom.is_empty(Dom.select_raw_unsafe("input:focus, textarea:focus"))))
      propagation
    else
      match (e) {
      case { key_code : { some : 47 }, ... }:
        {propagation with prevent_default : true}
      case { key_code : { some : 114 }, key_modifiers : [], ... }:
        {propagation with prevent_default : true}
      default: propagation
      }
  }

  client function handler(Dom.event e) {
    if (not(Dom.is_empty(Dom.select_raw_unsafe("input:focus, textarea:focus"))))
      void
    else {
      match (e) {
      case { key_code : { some : 99 }, key_modifiers : [], ... }:
        ComposeView.new("", e)
      case { key_code : { some : 117 }, key_modifiers : [], ... }:
        UploadView.show(e)
      case { key_code : { some : 109 }, key_modifiers : [], ... }:
        MessageView.mark_read(e)
      // case { key_code : { some : 105 }, key_modifiers : [], ... }:
      //   Dom.trigger(#info_toggle, {click})
      case { key_code : { some : 47 }, ... }: SearchView.focus(e)
      // case { key_code : { some : 114 }, key_modifiers : [], ... }:
      //   Dom.trigger(#quick_reply, {click})
      case { key_code : { some : 115 }, key_modifiers : [], ... }:
        Dom.trigger(#star_message, {click})
      case { key_code : { some : 106 }, key_modifiers : [], ... }:
        MessageView.Snippet.next(e)
      case { key_code : { some : 107 }, key_modifiers : [], ... }:
        MessageView.Snippet.prev(e)
      default: void
      }
    }
  }

}

client logout_timer_time = ClientReference.create(60)
client logout_timer_ctxt = ClientReference.create(option(ThreadContext.client) {none})

module Content {

    log = Log.notice("Content", _)
    error = Log.error("Content", _)

    // // FIXME: move to automated JS
    // function choose_selected_box(anchor) {
    //     function act(anchor) {
    //         Dom.remove_class(Dom.select_raw_unsafe("li"), "selected");
    //         Dom.add_class(Dom.select_class(anchor), "selected");
    //         void
    //     }
    //     update({messages: {inbox}}, anchor)
    // }

    // client function update_items(anchor) {
    //     Content.choose_selected_box(anchor);
    //     void
    // }

    function insert_loading() {
        Log.info("Loading", "content...");
        if (Dom.is_empty(#loading-content)) {
          loading =

;
          #content = loading
        } else void
    }

    function make_alert(string title, xhtml description) {
      WBootstrap.Alert.make({
        block: ~{ title, description },
        actions: none,
        closable: false
      }, {error})
    }

    login_please =
      make_alert(
        @intl("Your session has expired!"),
        <>{@intl("Please log in to access this resource")}</>
      )

    non_existent_resource =
      make_alert(
        @intl("Oops!"),
        <>{@intl("Looks like the resource you are looking for does not exist ...")}</>
      )

    not_allowed_message =
      make_alert(
        @intl("Sorry!"),
        XmlConvert.of_alpha(AppText.not_allowed_message())
      )

    not_allowed_resource =
      make_alert(
        "Sorry!",
        XmlConvert.of_alpha(AppText.not_allowed_resource())
      )

    exposed function logout_and_reload(ctx) {
      state = Login.get_state()
      log("logout_and_reload: unlogging {state}")
      key = state.key
      error("INACTIVE LOGOUT {key}")
      Login.logout()
      ClientEvent.remove_inactivity_delay(ctx)
      Client.do_reload(false)
    }

    client function do_confirm(ctx, btn_id, confirmed, _) {
      Button.reset(#{btn_id})
      Modal.hide(#modal_logout_confirm)
      stop_logout_timer()
      if (confirmed) {
        logout_and_reload(ctx)
      }
    }

    recursive private client function logout_timer_function() {
      time = ClientReference.get(logout_timer_time)
      if (time > 0) {
        time = time - 1
        ClientReference.set(logout_timer_time, time)
        #logout_confirm_time = AppText.logout_timer(time)
      } else {
        logout_timer.stop()
        match (ClientReference.get(logout_timer_ctxt)) {
        case {some:ctx}: logout_and_reload({some:ctx});
        case {none}: void;
        }
      }
    }

    recursive private client logout_timer = Scheduler.make_timer(1000, logout_timer_function)

    client function start_logout_timer(ctx) {
      ClientReference.set(logout_timer_time, AdminController.get_grace_period())
      ClientReference.set(logout_timer_ctxt, ctx)
      logout_timer.start()
    }

    client function stop_logout_timer() {
      ClientReference.set(logout_timer_ctxt, {none})
      logout_timer.stop()
    }

    function logout_confirm(ctx) {
      Modal.make(
       "modal_logout_confirm",
       <>{AppText.session_expired()}</>,
       <>
{AppText.logout_confirm()}

{AppText.logout_timer(AdminController.get_grace_period())}

       </>,
       (WBootstrap.Button.make({button:<> {AppText.yes()}</>,
                        callback:do_confirm(ctx, "confirm_yes", true, _)}, [{success}])
        |> Xhtml.add_id(some("confirm_yes"), _)) <+>
       (WBootstrap.Button.make({button:<> {AppText.no()}</>,
                        callback:do_confirm(ctx, "confirm_no", false, _)}, [{primary}])
        |> Xhtml.add_id(some("confirm_no"), _)),
       {Modal.default_options with backdrop : false, static : false, keyboard : false})
    }

    // FIXME: not here
    exposed function init_server(channel) {
      state = Login.get_state()
      key = state.key
      if (not(Login.is_logged(state))) void
      else {
        ctx = ThreadContext.Client.get_opt({current})
        timeout = Duration.min(AdminController.get_timeout())
        _ = ClientEvent.set_inactivity_delay(ctx, timeout)
        clek =
            ClientEvent.set_on_inactive_client(function(ctx) {
              start_logout_timer({some:ctx})
              Modal.show(#modal_logout_confirm)
            })
        Dom.bind_beforeunload_confirmation(function(_) {
          Notification.unregister(key, channel)
          ClientEvent.remove_event(clek)
          ClientEvent.remove_inactivity_delay(ctx)
          none
        })
        Intl.setLocale(User.getLocale(key))
        Notification.register(key, channel)
        // Update profile picture.
        match (Option.bind(RawFile.get_thumbnail, User.get_picture(state.key))) {
          case {some: picture}:
            #profile_picture =
          default: void
        }
      }
    }

    xhtml drop_file =
        <>

         {@intl("Drop files here")}
        </>

    // client function clear_modal_errors() {
    //   Dom.remove_class(Dom.select_raw_unsafe("#modal_compose .form-group"), "error")
    // }

    client function clear_tooltips() {
      Dom.remove(Dom.select_class("tooltip"))
    }

    exposed function do_setup_admin(pass) {
      // Build admin user.
      if (Admin.undefined()) {
        Admin.create(pass)
        Login.login("admin", pass)
      }
    }
    client function setup_admin(_evt) {
      pass = Dom.get_value(#pass) |> String.trim
      passrepeat = Dom.get_value(#passrepeat) |> String.trim

      if (pass != passrepeat || pass == "") Notifications.error("Error", <>{@intl("Invalid password")}</>)
      else do_setup_admin(pass)
    }

    protected function signin(state) {
      <>

{AppText.sign_in_to_mailbox()}

        {Login.build(state)}

      { if (not(Admin.only_admin_can_register())) {

            {@intl("Don't have an account? Sign up")}

        }
        else <></>
      }
      </>
    }

    protected function signup(state) {
      AdminView.register(state, none) <+>

        {@intl("Already have an account? Sign in")}

    }

    /** Log in window, if user is not connected. */
    @expand protected function loginbox(state) {

{Admin.logo()}

              {signin(state)}

    }

    @expand protected function passwordbox(state) {

{Admin.logo()}

{@intl("Setup your PEPS in two steps")}

            <form role="form" class="form-simple">

                {@intl("Admin Password")}
                <input id="pass" type="password" class="form-control" placeholder="{@intl("Password")}"/>

                {@intl("Repeat")}
                <input id="passrepeat" type="password" class="form-control" placeholder="{@intl("Password repeat")}"/>

                <button type="button" class="btn btn-block btn-primary" onclick={setup_admin}>{@intl("Next")}</button>

            </form>

    }

    /**
     * Function called when the main page is ready.
     * URN is some if the user is logged in, else none.
     */
    client function init_client(option(URN.t) urn, _) {
      Dom.bind_with_options(
        Dom.select_document(),
        {keypress}, Keyboard.handler,
        [ {propagation_handler : Keyboard.propagation_handler} ]
      ) |> ignore
      Collapse.init(Dom.select_class("collapse"))
      Dropdown.init(Dom.select_class("dropdown-toggle"))

      channel = Session.make_callback(Notifications.handler)
      init_server(channel)
      // TODO: preserve last mode and path?
      log("init_client: {urn}")
      Option.iter(update(_, true), urn)
    }

    protected function build(Login.state state, URN.t urn) {
      unprotected = Mode.unprotected(urn.mode) // Give access to unprotected share links.
      logged = Login.is_logged(state)

      (view, _, _, _) = SettingsController.get_user_preferences(state.key)
      cl = if (view == {folders}) " narrow" else ""
      init = if (logged || unprotected) init_client(some(urn), _) else init_client(none, _)
      modals =
        if (Mode.is_share(urn.mode))
          <></>
        else
          logout_confirm(ThreadContext.Client.get_opt({current})) <+>
          FolderView.build_form(state, none) <+>
          SearchView.build_form(state)

{
        if (Admin.undefined() && not(unprotected))
          passwordbox(state)
        else if (not(logged || unprotected))
          loginbox(state)
        // Condition shown iff active licensing.
        else

 <+>

 <+>
          modals
      }

    }

  protected function selector(state, URN.t urn) {
    match (urn.mode) {
      case {messages: box}:   MessageView.build_box(state, box, urn.path, none)
      case {files: mode}:     FileView.build(state, mode, urn.path)
      case {people: mode}:    PeopleView.build(state, mode, urn.path)
      case {admin: mode}:     AdminView.build(state, mode, urn.path)
      // TODO for Norman
      case ~{app, active: _}: AppView.build(state, app, urn.path)
      case {share: link}:     FileView.build_shared(link, urn.path)
      case {settings: mode}:  SettingsView.build(state, mode, urn.path)
      case {dashboard: mode}: Dashboard.build(state, mode, urn.path)
      case {error}:           non_existent_resource
    }
  }

  // function to verify view requirements
  // should be called by every view main function to ensure consistency
  function ensure(req, state, resource) {
    if ((req=={logged} || req=={admin} || req=={super}) && not(Login.is_logged(state))) { login_please }
    else if ((req=={admin} || req=={super}) && not(Login.is_admin(state))) { not_allowed_resource }
    else if (req=={super} && not(Login.is_super_admin(state))) { not_allowed_resource }
    else { resource }
  }

  /** check_admin state */
  check_admin = ensure({admin}, _, _)
  check_super = ensure({super}, _, _)

  function onboarding(state) {
    if (SettingsController.onboarding(state.key)) {
      match (OnboardView.build(state)) {
        case {some: html}:
          Notifications.warning("Welcome to PEPS", html)
          Dom.add_class(#notification_area, "notify-static")
        default:
          Dom.remove_class(#notification_area, "notify-static")
      }
    } else {
      Dom.remove_class(#notification_area, "notify-static")
    }
  }

  /**
   * Update the client state.
   * Grouping the updates like this help reducing the number of client calls, and optimes the
   * slicing delay.
   */
  client function upload(urn, content, sidebar, badges) {
    // Clear current notifications.
    URN.change(urn)
    TopbarView.activate(urn.mode)
    Notifications.clear()
    Content.clear_tooltips()
    // Updates contents.
    #content = content
    #sidebar = sidebar
    // Insert badges.
    Notifications.Badge.insert(badges)
  }

  @async
  function change_urn(state, URN.t urn, force) {
    current = URN.get()
    if (current == urn && not(force))
      // Add reloading calls to subviews.
      void
    else {
      // Always reload the content.
      content = Content.selector(state, urn)
      sidebar = SidebarView.content(state, urn.mode)
      badges = Notifications.Badge.fetch(urn.mode)
      upload(urn, content, sidebar, badges)
      // onboarding(state)
    }
  }

  function update_callback(URN.t urn, _) {
    update(urn, false)
  }

  @async
  exposed function update(URN.t urn, bool force) {
    // Get a new state. CHECK
    state = Login.get_state()
    change_urn(state, urn, force)
  }

  /** Return to the main view. */
  function reset() {
    update(URN.init, true)
  }

  /** Refresh the view. */
  function refresh() {
    update(URN.get(), true)
  }

  /** Toggle an app sidebar. */
  client function toggleSidebar(Mode.t mode, _evt) {
    name = Mode.class(mode)
    if (Dom.has_class(#{name}, "active")) hideSidebar(mode)
    else showSidebar(mode)
  }

  /** Display an app sidebar. */
  client function showSidebar(Mode.t mode) {
    name = Mode.class(mode)
    Dom.show(#appsidebar)
    // Close open sidebars.
    Dom.iter(function (sidebar) {
      Parser.parse(parser {
        case "sidebar_" id=Rule.ident:
          if (id != name) { // Spare the mode to load.
            Dom.remove_class(#{id}, "active") // Toggle the topbar icon.
            Dom.remove_class(sidebar, "app-visible") // remove active class.
            Dom.hide(sidebar) // Hide the sidebar (do not destroy it: keep the state).
          }
        case .*: void
      }, Dom.get_id(sidebar))
    }, Dom.select_class("app-sidebar"))
    // Look up mode sidebar.
    sidebar = #{"sidebar_{name}"}
    if (Dom.is_empty(sidebar)) #appsidebar += buildSidebar(mode) // Load the sidebar.
    else Dom.show(sidebar) // Toggle the existing sidebar.
    // Activate topbar icon.
    Dom.add_class(#{name}, "active")
    Dom.add_class(#{"sidebar_{name}"}, "app-visible")
    // Reset badges.
    Notifications.Badge.reset(mode)
  }

  /** Hide an app's sidebar. */
  client function hideSidebar(Mode.t mode) {
    name = Mode.class(mode)
    Dom.iter(Dom.hide, #{"sidebar_{name}"})
    Dom.hide(#appsidebar)
    Dom.remove_class(#{name}, "active")
    Dom.remove_class(#{"sidebar_{name}"}, "app-visible")
  }

  /** Build a sidebar. */
  exposed function buildSidebar(Mode.t mode) {
    state = Login.get_state()
    name = Mode.class(mode)
    if (not(Login.is_logged(state))) login_please
    else

        {selector(state, URN.make(mode, []))}

  }

}