2018-12-08 15:06:14 -08:00
|
|
|
module Scylla.Views exposing (..)
|
|
|
|
import Scylla.Model exposing (..)
|
2018-12-08 17:15:35 -08:00
|
|
|
import Scylla.Sync exposing (..)
|
2018-12-08 19:09:20 -08:00
|
|
|
import Scylla.Route exposing (..)
|
2018-12-10 14:20:06 -08:00
|
|
|
import Scylla.Fnv as Fnv
|
2019-02-25 16:44:47 -08:00
|
|
|
import Scylla.Messages exposing (..)
|
2018-12-13 13:42:23 -08:00
|
|
|
import Scylla.Login exposing (Username)
|
2018-12-13 19:45:55 -08:00
|
|
|
import Scylla.Http exposing (fullMediaUrl)
|
|
|
|
import Scylla.Api exposing (ApiUrl)
|
2018-12-20 22:59:31 -08:00
|
|
|
import Html.Parser
|
|
|
|
import Html.Parser.Util
|
2018-12-10 16:16:39 -08:00
|
|
|
import Svg
|
|
|
|
import Svg.Attributes
|
2018-12-08 20:02:29 -08:00
|
|
|
import Url.Builder
|
2018-12-08 17:15:35 -08:00
|
|
|
import Json.Decode as Decode
|
2019-02-25 16:44:47 -08:00
|
|
|
import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, h3, table, td, tr, img, textarea, video, source, p)
|
2018-12-23 20:26:35 -08:00
|
|
|
import Html.Attributes exposing (type_, value, href, class, style, src, id, rows, controls, src)
|
2018-12-22 00:05:32 -08:00
|
|
|
import Html.Events exposing (onInput, onClick, preventDefaultOn)
|
2018-12-24 14:08:26 -08:00
|
|
|
import Dict exposing (Dict)
|
|
|
|
import Tuple
|
2018-12-08 15:06:14 -08:00
|
|
|
|
2018-12-27 00:12:48 -08:00
|
|
|
maybeHtml : List (Maybe (Html Msg)) -> List (Html Msg)
|
|
|
|
maybeHtml = List.filterMap (\i -> i)
|
|
|
|
|
2018-12-13 19:45:55 -08:00
|
|
|
contentRepositoryDownloadUrl : ApiUrl -> String -> String
|
|
|
|
contentRepositoryDownloadUrl apiUrl s =
|
|
|
|
let
|
|
|
|
lastIndex = Maybe.withDefault 6 <| List.head <| List.reverse <| String.indexes "/" s
|
|
|
|
authority = String.slice 6 lastIndex s
|
|
|
|
content = String.dropLeft (lastIndex + 1) s
|
|
|
|
in
|
|
|
|
(fullMediaUrl apiUrl) ++ "/download/" ++ authority ++ "/" ++ content
|
|
|
|
|
|
|
|
|
2018-12-10 14:20:06 -08:00
|
|
|
stringColor : String -> String
|
|
|
|
stringColor s =
|
|
|
|
let
|
|
|
|
hue = String.fromFloat <| (toFloat (Fnv.hash s)) / 4294967296 * 360
|
|
|
|
in
|
|
|
|
"hsl(" ++ hue ++ ", 82%, 71%)"
|
|
|
|
|
2018-12-08 17:15:35 -08:00
|
|
|
viewFull : Model -> List (Html Msg)
|
|
|
|
viewFull model =
|
|
|
|
let
|
2018-12-10 13:18:02 -08:00
|
|
|
room r = Maybe.map (\jr -> (r, jr))
|
|
|
|
<| Maybe.andThen (Dict.get r)
|
|
|
|
<| Maybe.andThen .join model.sync.rooms
|
2018-12-08 19:09:20 -08:00
|
|
|
core = case model.route of
|
|
|
|
Login -> loginView model
|
2018-12-10 13:18:02 -08:00
|
|
|
Base -> baseView model Nothing
|
|
|
|
Room r -> baseView model <| room r
|
2018-12-08 19:09:20 -08:00
|
|
|
_ -> div [] []
|
2018-12-08 17:15:35 -08:00
|
|
|
errorList = errorsView model.errors
|
|
|
|
in
|
|
|
|
[ errorList ] ++ [ core ]
|
|
|
|
|
|
|
|
errorsView : List String -> Html Msg
|
2018-12-23 00:23:48 -08:00
|
|
|
errorsView = div [ class "errors-wrapper" ] << List.indexedMap errorView
|
2018-12-08 17:15:35 -08:00
|
|
|
|
2018-12-23 00:23:48 -08:00
|
|
|
errorView : Int -> String -> Html Msg
|
|
|
|
errorView i s = div [ class "error-wrapper", onClick <| DismissError i ] [ iconView "alert-triangle", text s ]
|
2018-12-08 15:06:14 -08:00
|
|
|
|
2018-12-10 13:18:02 -08:00
|
|
|
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
|
|
|
|
baseView m jr =
|
|
|
|
let
|
2018-12-27 00:12:48 -08:00
|
|
|
roomView = Maybe.map (\(id, r) -> joinedRoomView m id r) jr
|
|
|
|
reconnect = reconnectView m
|
2018-12-10 13:18:02 -08:00
|
|
|
in
|
2018-12-27 00:12:48 -08:00
|
|
|
div [ class "base-wrapper" ] <| maybeHtml
|
|
|
|
[ Just <| roomListView m
|
|
|
|
, roomView
|
|
|
|
, reconnect
|
|
|
|
]
|
|
|
|
|
|
|
|
reconnectView : Model -> Maybe (Html Msg)
|
|
|
|
reconnectView m = if m.connected
|
|
|
|
then Nothing
|
|
|
|
else Just <| div [ class "reconnect-wrapper", onClick AttemptReconnect ] [ iconView "zap", text "Disconnected. Click here to reconnect." ]
|
2018-12-10 13:18:02 -08:00
|
|
|
|
|
|
|
roomListView : Model -> Html Msg
|
|
|
|
roomListView m =
|
|
|
|
let
|
2018-12-24 14:08:26 -08:00
|
|
|
rooms = Maybe.withDefault (Dict.empty)
|
|
|
|
<| Maybe.andThen .join
|
|
|
|
<| m.sync.rooms
|
|
|
|
groups = roomGroups
|
|
|
|
<| Dict.toList rooms
|
|
|
|
homeserverList = div [ class "homeservers-list" ]
|
|
|
|
<| List.map (\(k, v) -> homeserverView k v)
|
|
|
|
<| Dict.toList groups
|
2018-12-08 20:02:29 -08:00
|
|
|
in
|
2018-12-10 12:47:40 -08:00
|
|
|
div [ class "rooms-wrapper" ]
|
|
|
|
[ h2 [] [ text "Rooms" ]
|
2018-12-24 14:08:26 -08:00
|
|
|
, homeserverList
|
2018-12-10 12:47:40 -08:00
|
|
|
]
|
2018-12-08 20:02:29 -08:00
|
|
|
|
2018-12-24 14:08:26 -08:00
|
|
|
roomGroups : List (String, JoinedRoom) -> Dict String (List (String, JoinedRoom))
|
|
|
|
roomGroups jrs = groupBy (homeserver << Tuple.first) jrs
|
|
|
|
|
|
|
|
homeserverView : String -> List (String, JoinedRoom) -> Html Msg
|
|
|
|
homeserverView hs rs =
|
|
|
|
let
|
|
|
|
roomList = div [ class "rooms-list" ] <| List.map (\(rid, r) -> roomListElementView rid r) rs
|
|
|
|
in
|
|
|
|
div [ class "homeserver-wrapper" ] [ h3 [] [ text hs ], roomList ]
|
|
|
|
|
2018-12-10 13:18:02 -08:00
|
|
|
roomListElementView : String -> JoinedRoom -> Html Msg
|
|
|
|
roomListElementView s jr =
|
2018-12-08 20:02:29 -08:00
|
|
|
let
|
|
|
|
name = Maybe.withDefault "<No Name>" <| roomName jr
|
|
|
|
in
|
2018-12-13 17:47:58 -08:00
|
|
|
div [ class "room-link-wrapper" ]
|
|
|
|
[ a [ href <| roomUrl s ] [ text name ]
|
|
|
|
, roomNotificationCountView jr.unreadNotifications
|
|
|
|
]
|
|
|
|
|
|
|
|
roomNotificationCountView : Maybe UnreadNotificationCounts -> Html Msg
|
|
|
|
roomNotificationCountView ns =
|
|
|
|
let
|
|
|
|
spanNumber = case Maybe.andThen .notificationCount ns of
|
|
|
|
Nothing -> ""
|
|
|
|
Just 0 -> ""
|
|
|
|
Just i -> String.fromInt i
|
|
|
|
spanSuffix = case Maybe.andThen .highlightCount ns of
|
|
|
|
Nothing -> ""
|
|
|
|
Just 0 -> ""
|
|
|
|
Just i -> "!"
|
|
|
|
in
|
|
|
|
span [ class "notification-count" ] [ text (spanNumber ++ spanSuffix) ]
|
2018-12-08 15:06:14 -08:00
|
|
|
|
|
|
|
loginView : Model -> Html Msg
|
2018-12-10 12:21:08 -08:00
|
|
|
loginView m = div [ class "login-wrapper" ]
|
|
|
|
[ h2 [] [ text "Log In" ]
|
|
|
|
, input [ type_ "text", value m.loginUsername, onInput ChangeLoginUsername] []
|
2018-12-08 15:06:14 -08:00
|
|
|
, input [ type_ "password", value m.loginPassword, onInput ChangeLoginPassword ] []
|
|
|
|
, input [ type_ "text", value m.apiUrl, onInput ChangeApiUrl ] []
|
|
|
|
, button [ onClick AttemptLogin ] [ text "Log In" ]
|
|
|
|
]
|
2018-12-08 17:15:35 -08:00
|
|
|
|
2018-12-19 21:52:07 -08:00
|
|
|
joinedRoomView : Model -> RoomId -> JoinedRoom -> Html Msg
|
2018-12-09 23:38:43 -08:00
|
|
|
joinedRoomView m roomId jr =
|
2018-12-08 17:15:35 -08:00
|
|
|
let
|
|
|
|
events = Maybe.withDefault [] <| Maybe.andThen .events jr.timeline
|
2019-02-25 16:44:47 -08:00
|
|
|
renderedMessages = List.map (userMessagesView m) <| mergeMessages m <| extractMessageEvents events
|
|
|
|
messagesWrapper = messagesWrapperView m roomId renderedMessages
|
2018-12-13 16:28:13 -08:00
|
|
|
typing = List.map (displayName m) <| roomTypingUsers jr
|
|
|
|
typingText = String.join ", " typing
|
|
|
|
typingSuffix = case List.length typing of
|
|
|
|
0 -> ""
|
|
|
|
1 -> " is typing..."
|
|
|
|
_ -> " are typing..."
|
|
|
|
typingWrapper = div [ class "typing-wrapper" ] [ text <| typingText ++ typingSuffix ]
|
2018-12-10 13:18:02 -08:00
|
|
|
messageInput = div [ class "message-wrapper" ]
|
2018-12-22 00:05:32 -08:00
|
|
|
[ textarea
|
|
|
|
[ rows 1
|
2018-12-09 23:38:43 -08:00
|
|
|
, onInput <| ChangeRoomText roomId
|
2018-12-13 18:41:54 -08:00
|
|
|
, onEnterKey <| SendRoomText roomId
|
2018-12-09 23:38:43 -08:00
|
|
|
, value <| Maybe.withDefault "" <| Dict.get roomId m.roomText
|
|
|
|
] []
|
2018-12-20 17:03:26 -08:00
|
|
|
, button [ onClick <| SendFiles roomId ] [ iconView "file" ]
|
|
|
|
, button [ onClick <| SendImages roomId ] [ iconView "image" ]
|
2018-12-10 16:16:39 -08:00
|
|
|
, button [ onClick <| SendRoomText roomId ] [ iconView "send" ]
|
2018-12-09 23:38:43 -08:00
|
|
|
]
|
2018-12-08 17:15:35 -08:00
|
|
|
in
|
2018-12-10 13:18:02 -08:00
|
|
|
div [ class "room-wrapper" ]
|
|
|
|
[ h2 [] [ text <| Maybe.withDefault "<No Name>" <| roomName jr ]
|
2019-02-25 16:44:47 -08:00
|
|
|
, messagesWrapper
|
2018-12-13 16:28:13 -08:00
|
|
|
, typingWrapper
|
2018-12-10 13:18:02 -08:00
|
|
|
, messageInput
|
|
|
|
]
|
2018-12-08 17:15:35 -08:00
|
|
|
|
2018-12-13 18:41:54 -08:00
|
|
|
onEnterKey : Msg -> Attribute Msg
|
|
|
|
onEnterKey msg =
|
|
|
|
let
|
2018-12-22 00:05:32 -08:00
|
|
|
eventDecoder = Decode.map2 (\l r -> (l, r)) (Decode.field "keyCode" Decode.int) (Decode.field "shiftKey" Decode.bool)
|
|
|
|
msgFor (code, shift) = if code == 13 && not shift then Decode.succeed msg else Decode.fail "Not ENTER"
|
|
|
|
pairTrue v = (v, True)
|
|
|
|
decoder = Decode.map pairTrue <| Decode.andThen msgFor <| eventDecoder
|
2018-12-13 18:41:54 -08:00
|
|
|
in
|
2018-12-22 00:05:32 -08:00
|
|
|
preventDefaultOn "keydown" decoder
|
2018-12-13 18:41:54 -08:00
|
|
|
|
2018-12-10 16:16:39 -08:00
|
|
|
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) ] [] ]
|
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
messagesWrapperView : Model -> RoomId -> List (Html Msg) -> Html Msg
|
|
|
|
messagesWrapperView m rid es = div [ class "messages-wrapper", id "messages-wrapper" ]
|
2018-12-19 21:52:07 -08:00
|
|
|
[ a [ class "history-link", onClick <| History rid ] [ text "Load older messages" ]
|
2019-02-25 16:44:47 -08:00
|
|
|
, table [ class "messages-table" ] es
|
2018-12-19 21:52:07 -08:00
|
|
|
]
|
2018-12-08 17:15:35 -08:00
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
senderView : Model -> Username -> Html Msg
|
|
|
|
senderView m s =
|
|
|
|
span [ style "color" <| stringColor s, class "sender-wrapper" ] [ text <| displayName m s ]
|
|
|
|
|
|
|
|
userMessagesView : Model -> (Username, List Message) -> Html Msg
|
|
|
|
userMessagesView m (u, ms) =
|
2018-12-10 14:20:06 -08:00
|
|
|
let
|
2019-02-25 16:44:47 -08:00
|
|
|
wrap h = div [ class "message" ] [ h ]
|
2018-12-10 14:20:06 -08:00
|
|
|
in
|
2019-02-25 16:44:47 -08:00
|
|
|
tr []
|
|
|
|
[ td [] [ senderView m u ]
|
|
|
|
, td [] <| List.map wrap <| List.filterMap (messageView m) ms
|
|
|
|
]
|
2018-12-10 14:20:06 -08:00
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
messageView : Model -> Message -> Maybe (Html Msg)
|
|
|
|
messageView m msg = case msg of
|
|
|
|
SendingTextMessage t _ -> Just <| div [] [ text t ]
|
|
|
|
SyncMessage re -> roomEventView m re
|
2018-12-08 17:15:35 -08:00
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
roomEventView : Model -> RoomEvent -> Maybe (Html Msg)
|
|
|
|
roomEventView m re =
|
2018-12-08 17:15:35 -08:00
|
|
|
let
|
|
|
|
msgtype = Decode.decodeValue (Decode.field "msgtype" Decode.string) re.content
|
|
|
|
in
|
|
|
|
case msgtype of
|
2019-02-25 16:44:47 -08:00
|
|
|
Ok "m.text" -> roomEventTextView m re
|
|
|
|
Ok "m.image" -> roomEventImageView m re
|
|
|
|
Ok "m.file" -> roomEventFileView m re
|
|
|
|
Ok "m.video" -> roomEventVideoView m re
|
2018-12-08 17:15:35 -08:00
|
|
|
_ -> Nothing
|
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
roomEventTextView : Model -> RoomEvent -> Maybe (Html Msg)
|
|
|
|
roomEventTextView m re =
|
2018-12-08 17:15:35 -08:00
|
|
|
let
|
|
|
|
body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
|
2018-12-20 22:59:31 -08:00
|
|
|
customHtml = Maybe.map Html.Parser.Util.toVirtualDom
|
|
|
|
<| Maybe.andThen (Result.toMaybe << Html.Parser.run )
|
|
|
|
<| Result.toMaybe
|
|
|
|
<| Decode.decodeValue (Decode.field "formatted_body" Decode.string) re.content
|
2018-12-08 17:15:35 -08:00
|
|
|
in
|
2018-12-20 22:59:31 -08:00
|
|
|
case customHtml of
|
2019-02-25 16:44:47 -08:00
|
|
|
Just c -> Just <| div [] c
|
|
|
|
Nothing -> Maybe.map (p [] << List.singleton << text) <| Result.toMaybe body
|
2018-12-13 19:45:55 -08:00
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
roomEventImageView : Model -> RoomEvent -> Maybe (Html Msg)
|
|
|
|
roomEventImageView m re =
|
2018-12-13 19:45:55 -08:00
|
|
|
let
|
|
|
|
body = Decode.decodeValue (Decode.field "url" Decode.string) re.content
|
|
|
|
in
|
|
|
|
Maybe.map (\s -> img [ class "message-image", src s ] [])
|
|
|
|
<| Maybe.map (contentRepositoryDownloadUrl m.apiUrl)
|
|
|
|
<| Result.toMaybe body
|
2018-12-23 20:26:35 -08:00
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
roomEventFileView : Model -> RoomEvent -> Maybe (Html Msg)
|
|
|
|
roomEventFileView m re =
|
2018-12-23 20:26:35 -08:00
|
|
|
let
|
|
|
|
decoder = Decode.map2 (\l r -> (l, r)) (Decode.field "url" Decode.string) (Decode.field "body" Decode.string)
|
|
|
|
fileData = Decode.decodeValue decoder re.content
|
|
|
|
in
|
|
|
|
Maybe.map (\(url, name) -> a [ href url, class "file-wrapper" ] [ iconView "file", text name ])
|
|
|
|
<| Maybe.map (\(url, name) -> (contentRepositoryDownloadUrl m.apiUrl url, name))
|
|
|
|
<| Result.toMaybe fileData
|
|
|
|
|
2019-02-25 16:44:47 -08:00
|
|
|
roomEventVideoView : Model -> RoomEvent -> Maybe (Html Msg)
|
|
|
|
roomEventVideoView m re =
|
2018-12-23 20:26:35 -08:00
|
|
|
let
|
|
|
|
decoder = Decode.map2 (\l r -> (l, r))
|
|
|
|
(Decode.field "url" Decode.string)
|
|
|
|
(Decode.field "info" <| Decode.field "mimetype" Decode.string)
|
|
|
|
videoData = Decode.decodeValue decoder re.content
|
|
|
|
in
|
|
|
|
Maybe.map (\(url, t) -> video [ controls True ] [ source [ src url, type_ t ] [] ])
|
|
|
|
<| Maybe.map (\(url, type_) -> (contentRepositoryDownloadUrl m.apiUrl url, type_))
|
|
|
|
<| Result.toMaybe videoData
|