Compare commits
9 Commits
b76e4bdf7d
...
42126c1489
Author | SHA1 | Date | |
---|---|---|---|
42126c1489 | |||
d855467f14 | |||
56a2212410 | |||
8261ae402a | |||
d15cc437b7 | |||
27634bf766 | |||
cee113b0dd | |||
16b83511d6 | |||
4b2c0d2ae8 |
1
elm.json
1
elm.json
|
@ -12,6 +12,7 @@
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0",
|
||||||
"elm/http": "2.0.0",
|
"elm/http": "2.0.0",
|
||||||
"elm/json": "1.1.2",
|
"elm/json": "1.1.2",
|
||||||
|
"elm/svg": "1.0.1",
|
||||||
"elm/url": "1.0.0"
|
"elm/url": "1.0.0"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
|
|
9
src/Scylla/Fnv.elm
Normal file
9
src/Scylla/Fnv.elm
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Scylla.Fnv exposing (..)
|
||||||
|
import Bitwise
|
||||||
|
|
||||||
|
hash : String -> Int
|
||||||
|
hash = String.foldl hashChar 2166136261
|
||||||
|
|
||||||
|
hashChar : Char -> Int -> Int
|
||||||
|
hashChar char h = modBy 4294967295
|
||||||
|
<| (Bitwise.xor h <| Char.toCode char) * 16777619
|
|
@ -2,23 +2,42 @@ module Scylla.Views exposing (..)
|
||||||
import Scylla.Model exposing (..)
|
import Scylla.Model exposing (..)
|
||||||
import Scylla.Sync exposing (..)
|
import Scylla.Sync exposing (..)
|
||||||
import Scylla.Route exposing (..)
|
import Scylla.Route exposing (..)
|
||||||
|
import Scylla.Fnv as Fnv
|
||||||
|
import Svg
|
||||||
|
import Svg.Attributes
|
||||||
import Url.Builder
|
import Url.Builder
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Html exposing (Html, div, input, text, button, div, span, a)
|
import Html exposing (Html, div, input, text, button, div, span, a, h2, table, td, tr)
|
||||||
import Html.Attributes exposing (type_, value, href)
|
import Html.Attributes exposing (type_, value, href, class, style)
|
||||||
import Html.Events exposing (onInput, onClick)
|
import Html.Events exposing (onInput, onClick)
|
||||||
import Dict
|
import Dict
|
||||||
|
|
||||||
|
stringColor : String -> String
|
||||||
|
stringColor s =
|
||||||
|
let
|
||||||
|
hue = String.fromFloat <| (toFloat (Fnv.hash s)) / 4294967296 * 360
|
||||||
|
in
|
||||||
|
"hsl(" ++ hue ++ ", 82%, 71%)"
|
||||||
|
|
||||||
|
senderName : String -> String
|
||||||
|
senderName s =
|
||||||
|
let
|
||||||
|
colonIndex = Maybe.withDefault -1
|
||||||
|
<| List.head
|
||||||
|
<| String.indexes ":" s
|
||||||
|
in
|
||||||
|
String.slice 1 colonIndex s
|
||||||
|
|
||||||
viewFull : Model -> List (Html Msg)
|
viewFull : Model -> List (Html Msg)
|
||||||
viewFull model =
|
viewFull model =
|
||||||
let
|
let
|
||||||
|
room r = Maybe.map (\jr -> (r, jr))
|
||||||
|
<| Maybe.andThen (Dict.get r)
|
||||||
|
<| Maybe.andThen .join model.sync.rooms
|
||||||
core = case model.route of
|
core = case model.route of
|
||||||
Login -> loginView model
|
Login -> loginView model
|
||||||
Base -> baseView model
|
Base -> baseView model Nothing
|
||||||
Room r -> Maybe.withDefault (div [] [])
|
Room r -> baseView model <| room r
|
||||||
<| Maybe.map (joinedRoomView model r)
|
|
||||||
<| Maybe.andThen (Dict.get r)
|
|
||||||
<| Maybe.andThen .join model.sync.rooms
|
|
||||||
_ -> div [] []
|
_ -> div [] []
|
||||||
errorList = errorsView model.errors
|
errorList = errorsView model.errors
|
||||||
in
|
in
|
||||||
|
@ -30,23 +49,40 @@ errorsView = div [] << List.map errorView
|
||||||
errorView : String -> Html Msg
|
errorView : String -> Html Msg
|
||||||
errorView s = div [] [ text s ]
|
errorView s = div [] [ text s ]
|
||||||
|
|
||||||
baseView : Model -> Html Msg
|
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
|
||||||
baseView m =
|
baseView m jr =
|
||||||
let
|
let
|
||||||
rooms = Maybe.withDefault (Dict.empty) <| Maybe.andThen .join <| m.sync.rooms
|
roomView = case jr of
|
||||||
|
Just (id, r) -> joinedRoomView m id r
|
||||||
|
Nothing -> div [] []
|
||||||
in
|
in
|
||||||
div [] <| Dict.values <| Dict.map roomListView rooms
|
div [ class "base-wrapper" ]
|
||||||
|
[ roomListView m
|
||||||
|
, roomView
|
||||||
|
]
|
||||||
|
|
||||||
roomListView : String -> JoinedRoom -> Html Msg
|
roomListView : Model -> Html Msg
|
||||||
roomListView s jr =
|
roomListView m =
|
||||||
|
let
|
||||||
|
rooms = Maybe.withDefault (Dict.empty) <| Maybe.andThen .join <| m.sync.rooms
|
||||||
|
roomList = div [ class "rooms-list" ] <| Dict.values <| Dict.map roomListElementView rooms
|
||||||
|
in
|
||||||
|
div [ class "rooms-wrapper" ]
|
||||||
|
[ h2 [] [ text "Rooms" ]
|
||||||
|
, roomList
|
||||||
|
]
|
||||||
|
|
||||||
|
roomListElementView : String -> JoinedRoom -> Html Msg
|
||||||
|
roomListElementView s jr =
|
||||||
let
|
let
|
||||||
name = Maybe.withDefault "<No Name>" <| roomName jr
|
name = Maybe.withDefault "<No Name>" <| roomName jr
|
||||||
in
|
in
|
||||||
a [ href <| Url.Builder.absolute [ "room", s ] [] ] [ text name ]
|
a [ href <| Url.Builder.absolute [ "room", s ] [] ] [ text name ]
|
||||||
|
|
||||||
loginView : Model -> Html Msg
|
loginView : Model -> Html Msg
|
||||||
loginView m = div []
|
loginView m = div [ class "login-wrapper" ]
|
||||||
[ input [ type_ "text", value m.loginUsername, onInput ChangeLoginUsername] []
|
[ h2 [] [ text "Log In" ]
|
||||||
|
, input [ type_ "text", value m.loginUsername, onInput ChangeLoginUsername] []
|
||||||
, input [ type_ "password", value m.loginPassword, onInput ChangeLoginPassword ] []
|
, input [ type_ "password", value m.loginPassword, onInput ChangeLoginPassword ] []
|
||||||
, input [ type_ "text", value m.apiUrl, onInput ChangeApiUrl ] []
|
, input [ type_ "text", value m.apiUrl, onInput ChangeApiUrl ] []
|
||||||
, button [ onClick AttemptLogin ] [ text "Log In" ]
|
, button [ onClick AttemptLogin ] [ text "Log In" ]
|
||||||
|
@ -57,25 +93,51 @@ joinedRoomView m roomId jr =
|
||||||
let
|
let
|
||||||
events = Maybe.withDefault [] <| Maybe.andThen .events jr.timeline
|
events = Maybe.withDefault [] <| Maybe.andThen .events jr.timeline
|
||||||
renderedEvents = List.filterMap (eventView m) events
|
renderedEvents = List.filterMap (eventView m) events
|
||||||
eventContainer = eventContainerView m renderedEvents
|
eventWrapper = eventWrapperView m renderedEvents
|
||||||
messageInput = div []
|
messageInput = div [ class "message-wrapper" ]
|
||||||
[ input
|
[ input
|
||||||
[ type_ "text"
|
[ type_ "text"
|
||||||
, onInput <| ChangeRoomText roomId
|
, onInput <| ChangeRoomText roomId
|
||||||
, value <| Maybe.withDefault "" <| Dict.get roomId m.roomText
|
, value <| Maybe.withDefault "" <| Dict.get roomId m.roomText
|
||||||
] []
|
] []
|
||||||
, button [ onClick <| SendRoomText roomId ] [ text "Send" ]
|
, button [ onClick <| SendRoomText roomId ] [ iconView "send" ]
|
||||||
]
|
]
|
||||||
in
|
in
|
||||||
div [] [ eventContainer, messageInput ]
|
div [ class "room-wrapper" ]
|
||||||
|
[ h2 [] [ text <| Maybe.withDefault "<No Name>" <| roomName jr ]
|
||||||
|
, eventWrapper
|
||||||
|
, messageInput
|
||||||
|
]
|
||||||
|
|
||||||
eventContainerView : Model -> List (Html Msg) -> Html Msg
|
iconView : String -> Html Msg
|
||||||
eventContainerView m = div []
|
iconView name =
|
||||||
|
let
|
||||||
|
url = Url.Builder.absolute [ "static", "svg", "feather-sprite.svg" ] []
|
||||||
|
in
|
||||||
|
Svg.svg
|
||||||
|
[ Svg.Attributes.class "feather-icon"
|
||||||
|
] [ Svg.use [ Svg.Attributes.xlinkHref (url ++ "#" ++ name) ] [] ]
|
||||||
|
|
||||||
|
eventWrapperView : Model -> List (Html Msg) -> Html Msg
|
||||||
|
eventWrapperView m es = div [ class "events-wrapper" ] [ table [ class "events-table" ] es ]
|
||||||
|
|
||||||
eventView : Model -> RoomEvent -> Maybe (Html Msg)
|
eventView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||||
eventView m re = case re.type_ of
|
eventView m re =
|
||||||
"m.room.message" -> messageView m re
|
let
|
||||||
_ -> Nothing
|
viewFunction = case re.type_ of
|
||||||
|
"m.room.message" -> Just messageView
|
||||||
|
_ -> Nothing
|
||||||
|
createRow mhtml = tr []
|
||||||
|
[ td [] [ eventSenderView re.sender ]
|
||||||
|
, td [] [ mhtml ]
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Maybe.map createRow
|
||||||
|
<| Maybe.andThen (\f -> f m re) viewFunction
|
||||||
|
|
||||||
|
eventSenderView : String -> Html Msg
|
||||||
|
eventSenderView s =
|
||||||
|
span [ style "background-color" <| stringColor s, class "sender-wrapper" ] [ text <| senderName s ]
|
||||||
|
|
||||||
messageView : Model -> RoomEvent -> Maybe (Html Msg)
|
messageView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||||
messageView m re =
|
messageView m re =
|
||||||
|
@ -90,9 +152,6 @@ messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||||
messageTextView m re =
|
messageTextView m re =
|
||||||
let
|
let
|
||||||
body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
|
body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
|
||||||
wrap mtext = div []
|
wrap mtext = span [] [ text mtext ]
|
||||||
[ span [] [ text re.sender ]
|
|
||||||
, span [] [ text mtext ]
|
|
||||||
]
|
|
||||||
in
|
in
|
||||||
Maybe.map wrap <| Result.toMaybe body
|
Maybe.map wrap <| Result.toMaybe body
|
||||||
|
|
170
static/scss/style.scss
Normal file
170
static/scss/style.scss
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Open+Sans');
|
||||||
|
$primary-color: #53C0FA;
|
||||||
|
$primary-color-highlight: #4298C7;
|
||||||
|
$primary-color-light: #9FDBFB;
|
||||||
|
$active-input-color: white;
|
||||||
|
$transition-duration: .125s;
|
||||||
|
|
||||||
|
$inactive-input-color: darken($active-input-color, 3%);
|
||||||
|
$active-input-border-color: $primary-color;
|
||||||
|
$inactive-input-border-color: darken($inactive-input-color, 10%);
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
margin: 0px;
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin input-common {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
transition: background-color $transition-duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include input-common();
|
||||||
|
background-color: $inactive-input-color;
|
||||||
|
color: black;
|
||||||
|
border: .5px solid $inactive-input-border-color;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: $active-input-color;
|
||||||
|
border-color: $active-input-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include input-common();
|
||||||
|
border: none;
|
||||||
|
background-color: $primary-color;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
background-color: $primary-color-highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $primary-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color-highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Login Screen
|
||||||
|
*/
|
||||||
|
div.login-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
input, button {
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Base View
|
||||||
|
*/
|
||||||
|
div.base-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
div {
|
||||||
|
padding: 3px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-bottom: 1px solid $inactive-input-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The list of rooms
|
||||||
|
*/
|
||||||
|
div.rooms-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The current room, if any.
|
||||||
|
*/
|
||||||
|
div.room-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The message input and send button.
|
||||||
|
*/
|
||||||
|
div.message-wrapper {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 9;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.events-wrapper {
|
||||||
|
overflow-y: scroll;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.events-table {
|
||||||
|
td {
|
||||||
|
padding-left: 5px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.sender-wrapper {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icons
|
||||||
|
*/
|
||||||
|
.feather-icon {
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
1
static/svg/feather-sprite.svg
Normal file
1
static/svg/feather-sprite.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 54 KiB |
Loading…
Reference in New Issue
Block a user