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/http": "2.0.0",
 | 
			
		||||
            "elm/json": "1.1.2",
 | 
			
		||||
            "elm/svg": "1.0.1",
 | 
			
		||||
            "elm/url": "1.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "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.Sync exposing (..)
 | 
			
		||||
import Scylla.Route exposing (..)
 | 
			
		||||
import Scylla.Fnv as Fnv
 | 
			
		||||
import Svg
 | 
			
		||||
import Svg.Attributes
 | 
			
		||||
import Url.Builder
 | 
			
		||||
import Json.Decode as Decode
 | 
			
		||||
import Html exposing (Html, div, input, text, button, div, span, a)
 | 
			
		||||
import Html.Attributes exposing (type_, value, href)
 | 
			
		||||
import Html exposing (Html, div, input, text, button, div, span, a, h2, table, td, tr)
 | 
			
		||||
import Html.Attributes exposing (type_, value, href, class, style)
 | 
			
		||||
import Html.Events exposing (onInput, onClick)
 | 
			
		||||
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 = 
 | 
			
		||||
    let
 | 
			
		||||
        room r = Maybe.map (\jr -> (r, jr))
 | 
			
		||||
            <| Maybe.andThen (Dict.get r)
 | 
			
		||||
            <| Maybe.andThen .join model.sync.rooms
 | 
			
		||||
        core = case model.route of
 | 
			
		||||
            Login -> loginView model 
 | 
			
		||||
            Base -> baseView model
 | 
			
		||||
            Room r -> Maybe.withDefault (div [] [])
 | 
			
		||||
                <| Maybe.map (joinedRoomView model r)
 | 
			
		||||
                <| Maybe.andThen (Dict.get r)
 | 
			
		||||
                <| Maybe.andThen .join model.sync.rooms
 | 
			
		||||
            Base -> baseView model Nothing
 | 
			
		||||
            Room r -> baseView model <| room r
 | 
			
		||||
            _ -> div [] []
 | 
			
		||||
        errorList = errorsView model.errors
 | 
			
		||||
    in
 | 
			
		||||
@ -30,23 +49,40 @@ errorsView = div [] << List.map errorView
 | 
			
		||||
errorView : String -> Html Msg
 | 
			
		||||
errorView s = div [] [ text s ]
 | 
			
		||||
 | 
			
		||||
baseView : Model -> Html Msg
 | 
			
		||||
baseView m =
 | 
			
		||||
    let 
 | 
			
		||||
        rooms = Maybe.withDefault (Dict.empty) <| Maybe.andThen .join <| m.sync.rooms
 | 
			
		||||
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
 | 
			
		||||
baseView m jr = 
 | 
			
		||||
    let
 | 
			
		||||
        roomView = case jr of
 | 
			
		||||
            Just (id, r) -> joinedRoomView m id r
 | 
			
		||||
            Nothing -> div [] []
 | 
			
		||||
    in
 | 
			
		||||
        div [] <| Dict.values <| Dict.map roomListView rooms
 | 
			
		||||
        div [ class "base-wrapper" ]
 | 
			
		||||
        [ roomListView m
 | 
			
		||||
        , roomView
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
roomListView : String -> JoinedRoom -> Html Msg
 | 
			
		||||
roomListView s jr =
 | 
			
		||||
roomListView : Model -> Html Msg
 | 
			
		||||
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
 | 
			
		||||
        name = Maybe.withDefault "<No Name>"  <| roomName jr
 | 
			
		||||
    in
 | 
			
		||||
        a [ href <| Url.Builder.absolute [ "room", s ] [] ] [ text name ]
 | 
			
		||||
 | 
			
		||||
loginView : Model -> Html Msg
 | 
			
		||||
loginView m = div []
 | 
			
		||||
    [ input [ type_ "text", value m.loginUsername, onInput ChangeLoginUsername] []
 | 
			
		||||
loginView m = div [ class "login-wrapper" ]
 | 
			
		||||
    [ h2 [] [ text "Log In" ]
 | 
			
		||||
    , input [ type_ "text", value m.loginUsername, onInput ChangeLoginUsername] []
 | 
			
		||||
    , input [ type_ "password", value m.loginPassword, onInput ChangeLoginPassword ] []
 | 
			
		||||
    , input [ type_ "text", value m.apiUrl, onInput ChangeApiUrl ] []
 | 
			
		||||
    , button [ onClick AttemptLogin ] [ text "Log In" ]
 | 
			
		||||
@ -57,25 +93,51 @@ joinedRoomView m roomId jr =
 | 
			
		||||
    let
 | 
			
		||||
        events = Maybe.withDefault [] <| Maybe.andThen .events jr.timeline
 | 
			
		||||
        renderedEvents = List.filterMap (eventView m) events
 | 
			
		||||
        eventContainer = eventContainerView m renderedEvents
 | 
			
		||||
        messageInput = div []
 | 
			
		||||
        eventWrapper = eventWrapperView m renderedEvents
 | 
			
		||||
        messageInput = div [ class "message-wrapper" ]
 | 
			
		||||
            [ input
 | 
			
		||||
                [ type_ "text"
 | 
			
		||||
                , onInput <| ChangeRoomText roomId
 | 
			
		||||
                , value <| Maybe.withDefault "" <| Dict.get roomId m.roomText
 | 
			
		||||
                ]  []
 | 
			
		||||
            , button [ onClick <| SendRoomText roomId ] [ text "Send" ]
 | 
			
		||||
            , button [ onClick <| SendRoomText roomId ] [ iconView "send" ]
 | 
			
		||||
            ]
 | 
			
		||||
    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
 | 
			
		||||
eventContainerView m = div []
 | 
			
		||||
iconView : String -> Html Msg
 | 
			
		||||
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 m re = case re.type_ of
 | 
			
		||||
    "m.room.message" -> messageView m re
 | 
			
		||||
    _ -> Nothing
 | 
			
		||||
eventView m re = 
 | 
			
		||||
    let
 | 
			
		||||
        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 m re =
 | 
			
		||||
@ -90,9 +152,6 @@ messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
 | 
			
		||||
messageTextView m re =
 | 
			
		||||
    let
 | 
			
		||||
        body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
 | 
			
		||||
        wrap mtext = div []
 | 
			
		||||
            [ span [] [ text re.sender ]
 | 
			
		||||
            , span [] [ text mtext ]
 | 
			
		||||
            ]
 | 
			
		||||
        wrap mtext = span [] [ text mtext ]
 | 
			
		||||
    in
 | 
			
		||||
        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