Scylla/src/Main.elm

379 lines
16 KiB
Elm
Raw Permalink Normal View History

2018-12-08 20:02:29 -08:00
import Browser exposing (application, UrlRequest(..))
import Browser.Navigation as Nav
import Browser.Dom exposing (Viewport, setViewportOf)
2019-09-10 23:24:47 -07:00
import Scylla.Room exposing (OpenRooms, applySync)
2018-12-08 13:49:30 -08:00
import Scylla.Sync exposing (..)
2019-09-10 18:15:28 -07:00
import Scylla.Sync.Events exposing (toMessageEvent, getType, getSender, getUnsigned)
2019-09-10 23:24:47 -07:00
import Scylla.Sync.AccountData exposing (..)
import Scylla.Sync.Push exposing (..)
2019-09-10 23:24:47 -07:00
import Scylla.ListUtils exposing (..)
2019-02-25 19:54:54 -08:00
import Scylla.Messages exposing (..)
2018-12-08 15:06:14 -08:00
import Scylla.Login exposing (..)
import Scylla.Api exposing (..)
2018-12-08 13:49:30 -08:00
import Scylla.Model exposing (..)
import Scylla.Http exposing (..)
2018-12-08 15:06:14 -08:00
import Scylla.Views exposing (viewFull)
2018-12-17 19:56:50 -08:00
import Scylla.Route exposing (Route(..), RoomId)
2018-12-13 13:42:23 -08:00
import Scylla.Notification exposing (..)
import Scylla.Storage exposing (..)
2018-12-20 22:01:09 -08:00
import Scylla.Markdown exposing (..)
2019-09-10 23:24:47 -07:00
import Scylla.Room exposing (..)
import Url exposing (Url)
2018-12-08 19:09:20 -08:00
import Url.Parser exposing (parse)
import Url.Builder
import Json.Encode
import Json.Decode
2018-12-17 19:56:50 -08:00
import Time exposing (every)
2018-12-08 13:49:30 -08:00
import Html exposing (div, text)
2018-12-20 19:22:51 -08:00
import File exposing (File)
import File.Select as Select
2018-12-08 15:06:14 -08:00
import Http
2018-12-09 23:38:43 -08:00
import Dict
import Task
2018-12-17 19:56:50 -08:00
syncTimeout = 10000
typingTimeout = 2000
2018-12-17 22:07:27 -08:00
init : () -> Url -> Nav.Key -> (Model, Cmd Msg)
init _ url key =
let
model =
{ key = key
2018-12-08 19:09:20 -08:00
, route = Maybe.withDefault Unknown <| parse Scylla.Route.route url
2018-12-17 22:07:27 -08:00
, token = Nothing
2018-12-08 15:06:14 -08:00
, loginUsername = ""
, loginPassword = ""
2018-12-08 13:49:30 -08:00
, apiUrl = "https://matrix.org"
2019-09-11 00:52:42 -07:00
, nextBatch = ""
, accountData = { events = Just [] }
2018-12-08 17:15:35 -08:00
, errors = []
2018-12-09 23:38:43 -08:00
, roomText = Dict.empty
, sending = Dict.empty
2018-12-09 23:38:43 -08:00
, transactionId = 0
2018-12-27 00:12:48 -08:00
, connected = True
2019-05-19 13:42:22 -07:00
, searchText = ""
2019-09-10 23:24:47 -07:00
, rooms = emptyOpenRooms
}
2018-12-17 22:07:27 -08:00
cmd = getStoreValuePort "scylla.loginInfo"
in
(model, cmd)
view : Model -> Browser.Document Msg
view m =
2019-03-15 17:44:54 -07:00
let
2019-09-11 00:52:42 -07:00
notificationString = getTotalNotificationCountString m.rooms
2019-03-15 17:44:54 -07:00
titleString = case notificationString of
Nothing -> "Scylla"
Just s -> s ++ " Scylla"
in
{ title = titleString
, body = viewFull m
}
update : Msg -> Model -> (Model, Cmd Msg)
2018-12-08 15:06:14 -08:00
update msg model = case msg of
ChangeApiUrl u -> ({ model | apiUrl = u }, Cmd.none)
ChangeLoginUsername u -> ({ model | loginUsername = u }, Cmd.none)
ChangeLoginPassword p -> ({ model | loginPassword = p }, Cmd.none)
AttemptLogin -> (model, Scylla.Http.login model.apiUrl model.loginUsername model.loginPassword) -- TODO
2018-12-08 20:02:29 -08:00
TryUrl urlRequest -> updateTryUrl model urlRequest
2018-12-13 14:06:15 -08:00
OpenRoom s -> (model, Nav.pushUrl model.key <| roomUrl s)
2018-12-15 20:56:17 -08:00
ChangeRoute r -> updateChangeRoute model r
ViewportAfterMessage v -> updateViewportAfterMessage model v
ViewportChangeComplete _ -> (model, Cmd.none)
ReceiveLoginResponse a r -> updateLoginResponse model a r
ReceiveFirstSyncResponse r -> updateSyncResponse model r False
ReceiveSyncResponse r -> updateSyncResponse model r True
ReceiveUserData s r -> (model, Cmd.none)
2018-12-17 19:56:50 -08:00
ChangeRoomText r t -> updateChangeRoomText model r t
2018-12-09 23:38:43 -08:00
SendRoomText r -> updateSendRoomText model r
SendRoomTextResponse t r -> updateSendRoomTextResponse model t r
ReceiveCompletedReadMarker r -> (model, Cmd.none)
ReceiveCompletedTypingIndicator r -> (model, Cmd.none)
ReceiveStoreData d -> updateStoreData model d
2018-12-17 19:56:50 -08:00
TypingTick _ -> updateTypingTick model
2018-12-19 21:52:07 -08:00
History r -> updateHistory model r
ReceiveHistoryResponse r hr -> updateHistoryResponse model r hr
2019-03-15 18:56:17 -07:00
SendImages rid -> (model, Select.files [ "image/jpeg", "image/png", "image/gif" ] <| ImagesSelected rid)
2018-12-20 19:22:51 -08:00
SendFiles rid -> (model, Select.files [ "application/*" ] <| FilesSelected rid)
ImagesSelected rid f fs -> updateUploadSelected model rid f fs (ImageUploadComplete rid)
FilesSelected rid f fs -> updateUploadSelected model rid f fs (FileUploadComplete rid)
ImageUploadComplete rid mime ur -> updateImageUploadComplete model rid mime ur
FileUploadComplete rid mime ur -> updateFileUploadComplete model rid mime ur
2018-12-20 19:22:51 -08:00
SendImageResponse _ -> (model, Cmd.none)
SendFileResponse _ -> (model, Cmd.none)
2018-12-20 22:01:09 -08:00
ReceiveMarkdown md -> updateMarkdown model md
2018-12-23 00:23:48 -08:00
DismissError i -> updateDismissError model i
2018-12-27 00:12:48 -08:00
AttemptReconnect -> ({ model | connected = True }, firstSync model.apiUrl (Maybe.withDefault "" model.token))
2019-05-19 13:42:22 -07:00
UpdateSearchText s -> ({ model | searchText = s }, Cmd.none)
2018-12-23 00:23:48 -08:00
2019-02-25 20:16:06 -08:00
requestScrollCmd : Cmd Msg
requestScrollCmd = Task.attempt ViewportAfterMessage (Browser.Dom.getViewportOf "messages-wrapper")
updateSendRoomTextResponse : Model -> Int -> Result Http.Error String -> (Model, Cmd Msg)
updateSendRoomTextResponse m t r =
let
updateFunction newId msg = case msg of
Just (rid, { body, id }) -> Just (rid, { body = body, id = Just newId })
Nothing -> Nothing
in
case r of
Ok s -> ({ m | sending = Dict.update t (updateFunction s) m.sending }, Cmd.none)
Err e -> ({ m | sending = Dict.remove t m.sending }, Cmd.none)
2018-12-23 00:23:48 -08:00
updateDismissError : Model -> Int -> (Model, Cmd Msg)
updateDismissError m i = ({ m | errors = (List.take i m.errors) ++ (List.drop (i+1) m.errors)}, Cmd.none)
2018-12-20 22:01:09 -08:00
updateMarkdown : Model -> MarkdownResponse -> (Model, Cmd Msg)
updateMarkdown m { roomId, text, markdown } =
let
storeValueCmd = setStoreValuePort ("scylla.loginInfo", Json.Encode.string
<| encodeLoginInfo
<| LoginInfo (Maybe.withDefault "" m.token) m.apiUrl m.loginUsername (m.transactionId + 1))
sendMessageCmd = sendMarkdownMessage m.apiUrl (Maybe.withDefault "" m.token) (m.transactionId + 1) roomId text markdown
2019-02-25 19:54:54 -08:00
newModel =
{ m | transactionId = m.transactionId + 1
, sending = Dict.insert (m.transactionId + 1) (roomId, { body = TextMessage text, id = Nothing }) m.sending
2019-02-25 19:54:54 -08:00
}
2018-12-20 22:01:09 -08:00
in
2019-02-25 20:16:06 -08:00
(newModel, Cmd.batch [ storeValueCmd, sendMessageCmd, requestScrollCmd ])
2018-12-20 19:22:51 -08:00
updateFileUploadComplete : Model -> RoomId -> File -> (Result Http.Error String) -> (Model, Cmd Msg)
updateFileUploadComplete m rid mime ur =
2018-12-20 19:22:51 -08:00
let
command = case ur of
Ok u -> sendFileMessage m.apiUrl (Maybe.withDefault "" m.token) (m.transactionId + 1) rid mime u
2018-12-20 19:22:51 -08:00
_ -> Cmd.none
2018-12-23 00:39:20 -08:00
newErrors = case ur of
Err e -> [ "Error uploading file. Please check your internet connection and try again." ]
_ -> []
2018-12-20 19:22:51 -08:00
in
2018-12-23 00:39:20 -08:00
({ m | errors = newErrors ++ m.errors, transactionId = m.transactionId + 1}, command)
2018-12-20 19:22:51 -08:00
updateImageUploadComplete : Model -> RoomId -> File -> (Result Http.Error String) -> (Model, Cmd Msg)
updateImageUploadComplete m rid mime ur =
2018-12-20 19:22:51 -08:00
let
command = case ur of
Ok u -> sendImageMessage m.apiUrl (Maybe.withDefault "" m.token) (m.transactionId + 1) rid mime u
2018-12-20 19:22:51 -08:00
_ -> Cmd.none
2018-12-23 00:39:20 -08:00
newErrors = case ur of
Err e -> [ "Error uploading image. Please check your internet connection and try again." ]
_ -> []
2018-12-20 19:22:51 -08:00
in
({ m | transactionId = m.transactionId + 1}, command)
updateUploadSelected : Model -> RoomId -> File -> List File -> (File -> Result Http.Error String -> Msg) -> (Model, Cmd Msg)
2018-12-20 19:22:51 -08:00
updateUploadSelected m rid f fs msg =
let
uploadCmds = List.map (uploadMediaFile m.apiUrl (Maybe.withDefault "" m.token) msg) (f::fs)
in
(m, Cmd.batch uploadCmds)
2018-12-17 19:56:50 -08:00
2018-12-19 21:52:07 -08:00
updateHistoryResponse : Model -> RoomId -> Result Http.Error HistoryResponse -> (Model, Cmd Msg)
2018-12-20 16:39:10 -08:00
updateHistoryResponse m r hr =
case hr of
Ok h -> ({ m | rooms = applyHistoryResponse r h m.rooms }, Cmd.none)
Err _ -> ({ m | errors = "Unable to load older history from server"::m.errors }, Cmd.none)
2018-12-19 21:52:07 -08:00
updateHistory : Model -> RoomId -> (Model, Cmd Msg)
updateHistory m r =
let
2019-09-11 00:52:42 -07:00
prevBatch = Dict.get r m.rooms
|> Maybe.andThen (.prevHistoryBatch)
2018-12-19 21:52:07 -08:00
command = case prevBatch of
Just pv -> getHistory m.apiUrl (Maybe.withDefault "" m.token) r pv
Nothing -> Cmd.none
in
(m, command)
2018-12-17 19:56:50 -08:00
updateChangeRoomText : Model -> RoomId -> String -> (Model, Cmd Msg)
updateChangeRoomText m roomId text =
let
typingIndicator = case (text, Dict.get roomId m.roomText) of
("", _) -> Just False
(_, Just "") -> Just True
2018-12-17 20:30:11 -08:00
(_, Nothing) -> Just True
2018-12-17 19:56:50 -08:00
_ -> Nothing
command = case typingIndicator of
Just b -> sendTypingIndicator m.apiUrl (Maybe.withDefault "" m.token) roomId m.loginUsername b typingTimeout
_ -> Cmd.none
in
({ m | roomText = Dict.insert roomId text m.roomText}, command)
updateTypingTick : Model -> (Model, Cmd Msg)
updateTypingTick m =
let
command = case currentRoomId m of
Just rid -> sendTypingIndicator m.apiUrl (Maybe.withDefault "" m.token) rid m.loginUsername True typingTimeout
Nothing -> Cmd.none
in
(m, command)
updateStoreData : Model -> Json.Encode.Value -> (Model, Cmd Msg)
updateStoreData m d = case (Json.Decode.decodeValue storeDataDecoder d) of
Ok { key, value } -> case key of
"scylla.loginInfo" -> updateLoginInfo m value
_ -> (m, Cmd.none)
Err _ -> (m, Cmd.none)
updateLoginInfo : Model -> Json.Encode.Value -> (Model, Cmd Msg)
updateLoginInfo m s = case Json.Decode.decodeValue (Json.Decode.map decodeLoginInfo Json.Decode.string) s of
2018-12-17 20:30:11 -08:00
Ok (Just { token, apiUrl, username, transactionId }) ->
(
{ m | token = Just token
, apiUrl = apiUrl
, loginUsername = username
, transactionId = transactionId
}
, firstSync apiUrl token
)
_ -> (m, Nav.pushUrl m.key <| Url.Builder.absolute [ "login" ] [])
2018-12-09 23:38:43 -08:00
2018-12-15 20:56:17 -08:00
updateChangeRoute : Model -> Route -> (Model, Cmd Msg)
updateChangeRoute m r =
let
joinedRoom = case r of
2019-09-11 00:52:42 -07:00
Room rid -> Dict.get rid m.rooms
2018-12-15 20:56:17 -08:00
_ -> Nothing
2019-09-11 00:52:42 -07:00
lastMessage = Maybe.map .messages joinedRoom
|> Maybe.andThen (findLastEvent (((==) "m.room.message") << .type_))
2018-12-15 20:56:17 -08:00
readMarkerCmd = case (r, lastMessage) of
(Room rid, Just re) -> setReadMarkers m.apiUrl (Maybe.withDefault "" m.token) rid re.eventId <| Just re.eventId
_ -> Cmd.none
in
({ m | route = r }, readMarkerCmd)
updateViewportAfterMessage : Model -> Result Browser.Dom.Error Viewport -> (Model, Cmd Msg)
updateViewportAfterMessage m vr =
let
cmd vp = if vp.scene.height - (vp.viewport.y + vp.viewport.height ) < 100
then Task.attempt ViewportChangeComplete <| setViewportOf "messages-wrapper" vp.viewport.x vp.scene.height
else Cmd.none
in
(m, Result.withDefault Cmd.none <| Result.map cmd vr)
2018-12-17 20:30:11 -08:00
updateSendRoomText : Model -> RoomId -> (Model, Cmd Msg)
2018-12-09 23:38:43 -08:00
updateSendRoomText m r =
let
token = Maybe.withDefault "" m.token
message = Maybe.andThen (\s -> if s == "" then Nothing else Just s)
<| Dict.get r m.roomText
2018-12-17 20:30:11 -08:00
combinedCmd = case message of
Nothing -> Cmd.none
Just s -> Cmd.batch
2018-12-20 22:01:09 -08:00
[ requestMarkdownPort { roomId = r, text = s }
2018-12-17 20:30:11 -08:00
, sendTypingIndicator m.apiUrl token r m.loginUsername False typingTimeout
]
2018-12-09 23:38:43 -08:00
in
2018-12-20 22:01:09 -08:00
({ m | roomText = Dict.insert r "" m.roomText }, combinedCmd)
2018-12-08 20:02:29 -08:00
updateTryUrl : Model -> Browser.UrlRequest -> (Model, Cmd Msg)
updateTryUrl m ur = case ur of
Internal u -> (m, Nav.pushUrl m.key (Url.toString u))
_ -> (m, Cmd.none)
2018-12-08 15:06:14 -08:00
updateLoginResponse : Model -> ApiUrl -> Result Http.Error LoginResponse -> (Model, Cmd Msg)
updateLoginResponse model a r = case r of
Ok lr -> ( { model | token = Just lr.accessToken, loginUsername = lr.userId, apiUrl = a }, Cmd.batch
2018-12-08 19:09:20 -08:00
[ firstSync model.apiUrl lr.accessToken
, Nav.pushUrl model.key <| Url.Builder.absolute [] []
2018-12-17 20:30:11 -08:00
, setStoreValuePort ("scylla.loginInfo", Json.Encode.string
<| encodeLoginInfo
<| LoginInfo lr.accessToken model.apiUrl lr.userId model.transactionId)
2018-12-08 19:09:20 -08:00
] )
2018-12-23 00:39:20 -08:00
Err e -> ({ model | errors = "Failed to log in. Are your username and password correct?"::model.errors }, Cmd.none)
2018-12-08 15:06:14 -08:00
updateSyncResponse : Model -> Result Http.Error SyncResponse -> Bool -> (Model, Cmd Msg)
updateSyncResponse model r notify =
2018-12-09 00:35:07 -08:00
let
token = Maybe.withDefault "" model.token
2019-09-11 00:52:42 -07:00
nextBatch = Result.withDefault model.nextBatch
<| Result.map .nextBatch r
2018-12-19 21:52:07 -08:00
syncCmd = sync model.apiUrl token nextBatch
notification sr =
getPushRuleset model.accountData
|> Maybe.map (\rs -> getNotificationEvents rs sr)
|> Maybe.withDefault []
|> findFirstBy
(\(s, e) -> e.originServerTs)
(\(s, e) -> e.sender /= model.loginUsername)
2018-12-14 00:04:41 -08:00
notificationCmd sr = if notify
then Maybe.withDefault Cmd.none
<| Maybe.map (\(s, e) -> sendNotificationPort
{ name = roomLocalDisplayName model s e.sender
, text = getText e
2018-12-14 00:04:41 -08:00
, room = s
}) <| notification sr
else Cmd.none
room = currentRoomId model
roomMessages sr = case room of
Just rid -> List.filter (((==) "m.room.message") << .type_)
<| Maybe.withDefault []
2019-09-10 18:15:28 -07:00
<| Maybe.map (List.filterMap (toMessageEvent))
<| Maybe.andThen .events
<| Maybe.andThen .timeline
<| Maybe.andThen (Dict.get rid)
<| Maybe.andThen .join
<| sr.rooms
Nothing -> []
2018-12-14 00:04:41 -08:00
setScrollCmd sr = if List.isEmpty
<| roomMessages sr
then Cmd.none
2019-02-25 20:16:06 -08:00
else requestScrollCmd
setReadReceiptCmd sr = case (room, List.head <| List.reverse <| roomMessages sr) of
(Just rid, Just re) -> setReadMarkers model.apiUrl token rid re.eventId <| Just re.eventId
_ -> Cmd.none
receivedEvents sr = List.map Just <| allTimelineEventIds sr
2019-09-10 18:15:28 -07:00
receivedTransactions sr = List.filterMap (Maybe.andThen .transactionId << getUnsigned)
2019-09-08 14:22:08 -07:00
<| allTimelineEvents sr
sending sr = Dict.filter (\tid (rid, { body, id }) -> not <| List.member (String.fromInt tid) <| receivedTransactions sr) model.sending
newModel sr =
2019-09-11 00:52:42 -07:00
{ model | nextBatch = nextBatch
, sending = sending sr
2019-09-10 23:24:47 -07:00
, rooms = applySync sr model.rooms
2019-09-11 00:52:42 -07:00
, accountData = applyAccountData sr.accountData model.accountData
}
2018-12-09 00:35:07 -08:00
in
case r of
Ok sr -> (newModel sr
, Cmd.batch
2018-12-13 13:42:23 -08:00
[ syncCmd
2018-12-14 00:04:41 -08:00
, notificationCmd sr
, setScrollCmd sr
, setReadReceiptCmd sr
2018-12-13 13:42:23 -08:00
])
2018-12-27 00:12:48 -08:00
_ -> ({ model | connected = False }, Cmd.none)
subscriptions : Model -> Sub Msg
2018-12-17 19:56:50 -08:00
subscriptions m =
let
2018-12-17 20:30:11 -08:00
currentText = Maybe.withDefault ""
<| Maybe.andThen (\rid -> Dict.get rid m.roomText)
<| currentRoomId m
typingTimer = case currentText of
2018-12-17 19:56:50 -08:00
"" -> Sub.none
_ -> every typingTimeout TypingTick
in
Sub.batch
[ onNotificationClickPort OpenRoom
, receiveStoreValuePort ReceiveStoreData
, typingTimer
2018-12-20 22:01:09 -08:00
, receiveMarkdownPort ReceiveMarkdown
2018-12-17 19:56:50 -08:00
]
onUrlRequest : Browser.UrlRequest -> Msg
onUrlRequest = TryUrl
onUrlChange : Url -> Msg
2018-12-08 19:09:20 -08:00
onUrlChange = ChangeRoute << Maybe.withDefault Unknown << parse Scylla.Route.route
main = application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlRequest = onUrlRequest
, onUrlChange = onUrlChange
}