diff options
author | Jakob Odersky <jakob@inpher.io> | 2019-10-09 17:10:43 -0400 |
---|---|---|
committer | Jakob Odersky <jakob@inpher.io> | 2019-10-09 20:33:16 -0400 |
commit | 0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7 (patch) | |
tree | 2df0258f81050e6fed51d38e217c4f6256518e12 /shared | |
parent | faed28c54900fc0b359700873367095f51425794 (diff) | |
download | scala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.tar.gz scala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.tar.bz2 scala-triad-0ceee5ed4bae240b8c8e94d2fd7424d9d0b67ec7.zip |
Migrate build to mill
Diffstat (limited to 'shared')
-rw-r--r-- | shared/ApiProtocol.scala | 12 | ||||
-rw-r--r-- | shared/Message.scala | 42 | ||||
-rw-r--r-- | shared/Templates.scala | 54 | ||||
-rw-r--r-- | shared/TextTemplates.scala | 64 | ||||
-rw-r--r-- | shared/http/Backend.scala | 8 | ||||
-rw-r--r-- | shared/http/Request.scala | 9 | ||||
-rw-r--r-- | shared/http/Response.scala | 6 |
7 files changed, 195 insertions, 0 deletions
diff --git a/shared/ApiProtocol.scala b/shared/ApiProtocol.scala new file mode 100644 index 0000000..5d0e00a --- /dev/null +++ b/shared/ApiProtocol.scala @@ -0,0 +1,12 @@ +package triad + +import java.time.Instant +import spray.json.{DerivedJsonProtocol, JsNumber, JsValue, JsonFormat} + +object ApiProtocol extends DerivedJsonProtocol { + implicit val timestampFormat: JsonFormat[Instant] = new JsonFormat[Instant] { + def read(js: JsValue) = Instant.ofEpochMilli(js.convertTo[Long]) + def write(i: Instant) = JsNumber(i.toEpochMilli) + } + implicit val messageFormat = jsonFormat[Message] +} diff --git a/shared/Message.scala b/shared/Message.scala new file mode 100644 index 0000000..84c733e --- /dev/null +++ b/shared/Message.scala @@ -0,0 +1,42 @@ +package triad + +import java.security.MessageDigest +import java.time.Instant + +case class Message(content: String, + author: String, + timestamp: Instant = Instant.now()) { + + lazy val id: String = { + val digest = MessageDigest.getInstance("SHA-256") + digest.update(content.getBytes) + digest.update(author.getBytes) + digest.update((timestamp.getEpochSecond & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 8) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 16) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 24) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 32) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 40) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 48) & 0xff).toByte) + digest.update(((timestamp.getEpochSecond >> 56) & 0xff).toByte) + Message.bytesToHex(digest.digest()) + } + + def hashTags: Seq[String] = + content.split("\\s").filter(_.startsWith("#")).map(_.drop(1)) + +} + +object Message { + private def bytesToHex(hash: Array[Byte]): String = { + val hexString = new StringBuffer(hash.length * 2) + var i = 0 + while (i < hash.length) { + val hex = Integer.toHexString(0xff & hash(i)) + if (hex.length == 1) hexString.append('0') + hexString.append(hex) + i += 1 + } + hexString.toString + } +} diff --git a/shared/Templates.scala b/shared/Templates.scala new file mode 100644 index 0000000..10ee116 --- /dev/null +++ b/shared/Templates.scala @@ -0,0 +1,54 @@ +package triad + +class Templates[Builder, Output <: FragT, FragT]( + val bundle: scalatags.generic.Bundle[Builder, Output, FragT]) { + import bundle.all._ + + val colorStyles = List( + "bg-primary", + "bg-secondary", + "bg-success", + "bg-danger", + "bg-warning", + "bg-info", + "bg-dark" + ) + // pick a "random" style by computing a hash of arbitrary data + def dataStyle(data: String) = { + val dataHash = data.foldLeft(7) { + case (hash, char) => + (hash * 31 + char.toInt) + } + colorStyles( + ((dataHash % colorStyles.length) + colorStyles.length) % colorStyles.length) + } + + def message(msg: Message) = { + val tags = msg.hashTags.map( + hashTag => + span(`class` := "badge badge-light float-right ml-1")( + hashTag + )) + div(`class` := "col-xs-12 col-sm-6 col-md-3 col-lg-2")( + div(`class` := s"card text-white mb-3 ${dataStyle(msg.author)}")( + div(`class` := "card-header")( + msg.author, + tags + ), + div(`class` := "card-body")( + div(`class` := "card-text")( + msg.content + ) + ) + ) + ) + } + + def conversation(messages: Seq[Message]): Tag = + div(`class` := "container-fluid")( + div(id := "conversation", `class` := "row")( + for (msg <- messages.sortBy(_.timestamp)) yield message(msg) + ) + ) + +} diff --git a/shared/TextTemplates.scala b/shared/TextTemplates.scala new file mode 100644 index 0000000..0dd45c5 --- /dev/null +++ b/shared/TextTemplates.scala @@ -0,0 +1,64 @@ +package triad + +object TextTemplates extends Templates(scalatags.Text) { + import bundle.all._ + + def scripts(js: Boolean = true) = + if (js) + Seq( + div(id := "scalajs-error", style := "display: none;")( + "ScalaJS raised an exception. See the log for more information." + ), + script(`type` := "text/javascript", + src := "/out.js"), + script(`type` := "text/javascript")( + raw( + """|document.addEventListener("DOMContentLoaded", function(event) { + | try { + | // root element that will contain the ScalaJS application + | var root = document.getElementById("conversation"); + | + | // clear any existing content + | while (root.firstChild) { + | root.removeChild(root.firstChild); + | } + | + | // run ScalaJS application + | console.info("Starting ScalaJS application...") + | triad.Main().main(root) + | } catch(ex) { + | // display warning message in case of exception + | document.getElementById("scalajs-error").style.display = "block"; + | throw ex; + | } + |}); + |""".stripMargin + ) + ) + ) + else Seq.empty + + def page(messages: Seq[Message], js: Boolean = true) = html( + head( + link(rel := "stylesheet", + `type` := "text/css", + href := "/assets/lib/bootstrap-4.1.0/css/bootstrap-reboot.min.css"), + link(rel := "stylesheet", + `type` := "text/css", + href := "/assets/lib/bootstrap-4.1.0/css/bootstrap-grid.min.css"), + link(rel := "stylesheet", + `type` := "text/css", + href := "/assets/lib/bootstrap-4.1.0/css/bootstrap.min.css"), + link(rel := "stylesheet", + `type` := "text/css", + href := "/assets/main.css"), + meta(name := "viewport", + content := "width=device-width, initial-scale=1, shrink-to-fit=no") + ), + body( + conversation(messages), + scripts(js) + ) + ) + +} diff --git a/shared/http/Backend.scala b/shared/http/Backend.scala new file mode 100644 index 0000000..f3ce5f8 --- /dev/null +++ b/shared/http/Backend.scala @@ -0,0 +1,8 @@ +package triad +package http + +import scala.concurrent.Future + +trait Backend { + def send(request: Request): Future[Response] +} diff --git a/shared/http/Request.scala b/shared/http/Request.scala new file mode 100644 index 0000000..ec7d28d --- /dev/null +++ b/shared/http/Request.scala @@ -0,0 +1,9 @@ +package triad +package http + +case class Request( + method: String, + url: String, + headers: Map[String, String] = Map.empty, + body: Array[Byte] = Array.empty +) diff --git a/shared/http/Response.scala b/shared/http/Response.scala new file mode 100644 index 0000000..4ba2342 --- /dev/null +++ b/shared/http/Response.scala @@ -0,0 +1,6 @@ +package triad +package http + +case class Response(statusCode: Int, + headers: Map[String, String], + body: Array[Byte]) |