Scylla/src/Scylla/Views.elm

326 lines
13 KiB
Elm
Raw Normal View History

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
import Scylla.Messages exposing (..)
2018-12-13 13:42:23 -08:00
import Scylla.Login exposing (Username)
import Scylla.UserData exposing (UserData)
2018-12-13 19:45:55 -08:00
import Scylla.Http exposing (fullMediaUrl)
import Scylla.Api exposing (ApiUrl)
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
import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, h3, table, td, tr, img, textarea, video, source, p)
2019-05-19 13:23:16 -07:00
import Html.Attributes exposing (type_, placeholder, value, href, class, style, src, id, rows, controls, src, classList)
2018-12-22 00:05:32 -08:00
import Html.Events exposing (onInput, onClick, preventDefaultOn)
import Html.Lazy exposing (lazy6)
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
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
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
baseView : Model -> Maybe (RoomId, 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
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." ]
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" ]
2019-05-15 20:27:06 -07:00
<| List.map (\(k, v) -> homeserverView m k v)
2018-12-24 14:08:26 -08:00
<| Dict.toList groups
2018-12-08 20:02:29 -08:00
in
div [ class "rooms-wrapper" ]
[ h2 [] [ text "Rooms" ]
2019-05-19 13:42:22 -07:00
, input
[ class "room-search"
, type_ "text"
, placeholder "Search chats..."
, onInput UpdateSearchText
, value m.searchText
] []
2018-12-24 14:08:26 -08:00
, homeserverList
]
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
2019-05-15 20:27:06 -07:00
homeserverView : Model -> String -> List (String, JoinedRoom) -> Html Msg
homeserverView m hs rs =
2018-12-24 14:08:26 -08:00
let
2019-05-15 20:48:31 -07:00
roomList = div [ class "rooms-list" ]
<| List.map (\(rid, r) -> roomListElementView m rid r)
<| List.sortBy (\(rid, r) -> roomDisplayName m.roomNames rid) rs
2018-12-24 14:08:26 -08:00
in
div [ class "homeserver-wrapper" ] [ h3 [] [ text hs ], roomList ]
2019-08-31 23:03:57 -07:00
roomListElementView : Model -> RoomId -> JoinedRoom -> Html Msg
roomListElementView m rid jr =
2018-12-08 20:02:29 -08:00
let
name = roomDisplayName m.roomNames rid
2019-05-19 13:42:22 -07:00
isVisible = m.searchText == "" || (String.contains (String.toLower m.searchText) <| String.toLower name)
2019-05-19 13:23:16 -07:00
isCurrentRoom = case currentRoomId m of
Nothing -> False
2019-08-31 23:03:57 -07:00
Just cr -> cr == rid
2018-12-08 20:02:29 -08:00
in
2019-05-19 13:23:16 -07:00
div [ classList
[ ("room-link-wrapper", True)
, ("active", isCurrentRoom)
2019-05-19 13:42:22 -07:00
, ("hidden", not isVisible)
2019-05-19 13:23:16 -07:00
]
]
2019-05-19 15:01:02 -07:00
<| roomNotificationCountView jr.unreadNotifications ++
2019-08-31 23:03:57 -07:00
[ a [ href <| roomUrl rid ] [ text name ] ]
2018-12-13 17:47:58 -08:00
2019-05-19 15:01:02 -07:00
roomNotificationCountView : Maybe UnreadNotificationCounts -> List (Html Msg)
2018-12-13 17:47:58 -08:00
roomNotificationCountView ns =
let
2019-05-19 15:01:02 -07:00
wrap b = span
[ classList
[ ("notification-count", True)
, ("bright", b)
]
]
getCount f = Maybe.withDefault 0 << Maybe.andThen f
2018-12-13 17:47:58 -08:00
in
2019-05-19 15:01:02 -07:00
case (getCount .notificationCount ns, getCount .highlightCount ns) of
(0, 0) -> []
(i, 0) -> [ wrap False [ iconView "bell", text <| String.fromInt i ] ]
(i, j) -> [ wrap True [ iconView "alert-circle", text <| String.fromInt i ] ]
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", placeholder "Username", value m.loginUsername, onInput ChangeLoginUsername] []
, input [ type_ "password", placeholder "Password", value m.loginPassword, onInput ChangeLoginPassword ] []
, input [ type_ "text", placeholder "Homeserver URL", value m.apiUrl, onInput ChangeApiUrl ] []
2018-12-08 15:06:14 -08:00
, button [ onClick AttemptLogin ] [ text "Log In" ]
]
2018-12-08 17:15:35 -08:00
joinedRoomView : Model -> RoomId -> JoinedRoom -> Html Msg
joinedRoomView m roomId jr =
2018-12-08 17:15:35 -08:00
let
typing = List.map (displayName m.userData) <| roomTypingUsers jr
2018-12-13 16:28:13 -08:00
typingText = String.join ", " typing
typingSuffix = case List.length typing of
0 -> ""
1 -> " is typing..."
_ -> " are typing..."
typingWrapper = div [ class "typing-wrapper" ] [ text <| typingText ++ typingSuffix ]
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
2019-05-19 01:32:39 -07:00
, placeholder "Type your message here..."
2018-12-09 23:38:43 -08:00
, value <| Maybe.withDefault "" <| Dict.get roomId m.roomText
] []
, 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
div [ class "room-wrapper" ]
[ h2 [] [ text <| roomDisplayName m.roomNames roomId ]
, lazy6 lazyMessagesView m.userData roomId jr m.apiUrl m.loginUsername m.sending
, messageInput
2019-05-19 01:32:39 -07:00
, typingWrapper
]
2018-12-08 17:15:35 -08:00
lazyMessagesView : Dict String UserData -> RoomId -> JoinedRoom -> ApiUrl -> Username -> Dict Int (RoomId, SendingMessage) -> Html Msg
lazyMessagesView ud rid jr au lu snd =
let
roomReceived = receivedMessagesRoom
<| Maybe.withDefault []
<| Maybe.andThen .events jr.timeline
roomSending = sendingMessagesRoom rid snd
renderedMessages = List.map (userMessagesView ud au)
<| mergeMessages lu
<| roomReceived ++ roomSending
in
messagesWrapperView rid renderedMessages
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) ] [] ]
messagesWrapperView : RoomId -> List (Html Msg) -> Html Msg
messagesWrapperView 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" ]
, table [ class "messages-table" ] es
2018-12-19 21:52:07 -08:00
]
2018-12-08 17:15:35 -08:00
senderView : Dict String UserData -> Username -> Html Msg
senderView ud s =
span [ style "color" <| stringColor s, class "sender-wrapper" ] [ text <| displayName ud s ]
userMessagesView : Dict String UserData -> ApiUrl -> (Username, List Message) -> Html Msg
userMessagesView ud apiUrl (u, ms) =
2018-12-10 14:20:06 -08:00
let
wrap h = div [ class "message" ] [ h ]
2018-12-10 14:20:06 -08:00
in
tr []
[ td [] [ senderView ud u ]
2019-09-08 15:00:52 -07:00
, td [] <| List.map wrap <| List.filterMap (messageView ud apiUrl) ms
]
2018-12-10 14:20:06 -08:00
2019-09-08 15:00:52 -07:00
messageView : Dict String UserData -> ApiUrl -> Message -> Maybe (Html Msg)
messageView ud apiUrl msg = case msg of
Sending t -> Just <| sendingMessageView t
2019-09-08 15:00:52 -07:00
Received re -> roomEventView ud apiUrl re
sendingMessageView : SendingMessage -> Html Msg
sendingMessageView msg = case msg.body of
TextMessage t -> span [ class "sending"] [ text t ]
2018-12-08 17:15:35 -08:00
2019-09-08 15:00:52 -07:00
roomEventView : Dict String UserData -> ApiUrl -> RoomEvent -> Maybe (Html Msg)
roomEventView ud apiUrl re =
2018-12-08 17:15:35 -08:00
let
msgtype = Decode.decodeValue (Decode.field "msgtype" Decode.string) re.content
in
case msgtype of
Ok "m.text" -> roomEventTextView re
2019-09-08 15:00:52 -07:00
Ok "m.notice" -> roomEventNoticeView re
Ok "m.emote" -> roomEventEmoteView ud re
Ok "m.image" -> roomEventImageView apiUrl re
Ok "m.file" -> roomEventFileView apiUrl re
Ok "m.video" -> roomEventVideoView apiUrl re
2018-12-08 17:15:35 -08:00
_ -> Nothing
2019-09-08 15:00:52 -07:00
roomEventFormattedContent : RoomEvent -> Maybe (List (Html Msg))
roomEventFormattedContent re = 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
roomEventContent : (List (Html Msg) -> Html Msg) -> RoomEvent -> Maybe (Html Msg)
roomEventContent f re =
2018-12-08 17:15:35 -08:00
let
body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
2019-09-08 15:00:52 -07:00
customHtml = roomEventFormattedContent re
2018-12-08 17:15:35 -08:00
in
case customHtml of
2019-09-08 15:00:52 -07:00
Just c -> Just <| f c
Nothing -> Maybe.map (f << List.singleton << text) <| Result.toMaybe body
roomEventEmoteView : Dict String UserData -> RoomEvent -> Maybe (Html Msg)
roomEventEmoteView ud re =
let
emoteText = "* " ++ displayName ud re.sender ++ " "
in
roomEventContent (\cs -> span [] (text emoteText :: cs)) re
roomEventNoticeView : RoomEvent -> Maybe (Html Msg)
roomEventNoticeView = roomEventContent (span [ class "message-notice" ])
roomEventTextView : RoomEvent -> Maybe (Html Msg)
roomEventTextView = roomEventContent (span [])
2018-12-13 19:45:55 -08:00
roomEventImageView : ApiUrl -> RoomEvent -> Maybe (Html Msg)
roomEventImageView apiUrl 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 apiUrl)
2018-12-13 19:45:55 -08:00
<| Result.toMaybe body
2018-12-23 20:26:35 -08:00
roomEventFileView : ApiUrl -> RoomEvent -> Maybe (Html Msg)
roomEventFileView apiUrl 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 apiUrl url, name))
2018-12-23 20:26:35 -08:00
<| Result.toMaybe fileData
roomEventVideoView : ApiUrl -> RoomEvent -> Maybe (Html Msg)
roomEventVideoView apiUrl 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 apiUrl url, type_))
2018-12-23 20:26:35 -08:00
<| Result.toMaybe videoData