Compare commits
7 Commits
490d0eff2c
...
6ea55241c8
Author | SHA1 | Date | |
---|---|---|---|
6ea55241c8 | |||
12e5fdfbf1 | |||
525a6dd878 | |||
859023942e | |||
2d133167ed | |||
c08ef14832 | |||
50701e1885 |
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Scylla
|
||||
A minimalist client for the Matrix chat protocol.
|
||||
|
||||
## Screenshots
|
||||
![Main View](/screenshots/screenshot-1.png)
|
||||
![Login View](/screenshots/screenshot-2.png)
|
||||
![Error](/screenshots/screenshot-3.png)
|
||||
|
||||
## Why?
|
||||
Riot, the flagship Matrix client, is slow. Switching rooms has a noticable delay (probably around several hundred milliseconds).
|
||||
History loading causes strange jumps in the scroll position. Scylla aims to be faster and more responsive,
|
||||
while still maintaining all the necessary features for a chat client.
|
||||
|
||||
## What can it do?
|
||||
Scylla currently supports the following features:
|
||||
* Sending and receiving messages
|
||||
* Sending and receiving images and files
|
||||
* Converting Markdown into HTML for specially formatted messages
|
||||
* Loading old history in a room
|
||||
|
||||
## Should I use Scylla?
|
||||
Maybe. Scylla aims for a more minimalistic experience, remeniscent of IRC. It doesn't strive to have the rich chat client features
|
||||
that Riot goes for, and attempts to polish the common tasks. If you would like a more advanced chat client, stick with Riot. However,
|
||||
if you prefer polished minimalism, Scylla might be the client for you.
|
21
index.html
Normal file
21
index.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<script src="/static/js/elm.js"></script>
|
||||
<script src="/static/js/notifications.js"></script>
|
||||
<script src="/static/js/storage.js"></script>
|
||||
<script src="/static/js/markdown.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.5.2/marked.min.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
app = Elm.Main.init({ "flags" : { "token" : null } });
|
||||
setupNotificationPorts(app);
|
||||
setupStorage(app);
|
||||
setupMarkdownPorts(app);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
screenshots/screenshot-1.png
Normal file
BIN
screenshots/screenshot-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
BIN
screenshots/screenshot-2.png
Normal file
BIN
screenshots/screenshot-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
screenshots/screenshot-3.png
Normal file
BIN
screenshots/screenshot-3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
19
src/Main.elm
19
src/Main.elm
|
@ -12,6 +12,7 @@ import Scylla.UserData exposing (..)
|
|||
import Scylla.Notification exposing (..)
|
||||
import Scylla.Storage exposing (..)
|
||||
import Scylla.Markdown exposing (..)
|
||||
import Scylla.AccountData exposing (..)
|
||||
import Url exposing (Url)
|
||||
import Url.Parser exposing (parse)
|
||||
import Url.Builder
|
||||
|
@ -92,6 +93,10 @@ update msg model = case msg of
|
|||
SendImageResponse _ -> (model, Cmd.none)
|
||||
SendFileResponse _ -> (model, Cmd.none)
|
||||
ReceiveMarkdown md -> updateMarkdown model md
|
||||
DismissError i -> updateDismissError model i
|
||||
|
||||
updateDismissError : Model -> Int -> (Model, Cmd Msg)
|
||||
updateDismissError m i = ({ m | errors = (List.take i m.errors) ++ (List.drop (i+1) m.errors)}, Cmd.none)
|
||||
|
||||
updateMarkdown : Model -> MarkdownResponse -> (Model, Cmd Msg)
|
||||
updateMarkdown m { roomId, text, markdown } =
|
||||
|
@ -109,8 +114,11 @@ updateFileUploadComplete m rid mime ur =
|
|||
command = case ur of
|
||||
Ok u -> sendFileMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
||||
_ -> Cmd.none
|
||||
newErrors = case ur of
|
||||
Err e -> [ "Error uploading file. Please check your internet connection and try again." ]
|
||||
_ -> []
|
||||
in
|
||||
({ m | transactionId = m.transactionId + 1}, command)
|
||||
({ m | errors = newErrors ++ m.errors, transactionId = m.transactionId + 1}, command)
|
||||
|
||||
updateImageUploadComplete : Model -> RoomId -> String -> (Result Http.Error String) -> (Model, Cmd Msg)
|
||||
updateImageUploadComplete m rid mime ur =
|
||||
|
@ -118,6 +126,9 @@ updateImageUploadComplete m rid mime ur =
|
|||
command = case ur of
|
||||
Ok u -> sendImageMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
||||
_ -> Cmd.none
|
||||
newErrors = case ur of
|
||||
Err e -> [ "Error uploading image. Please check your internet connection and try again." ]
|
||||
_ -> []
|
||||
in
|
||||
({ m | transactionId = m.transactionId + 1}, command)
|
||||
|
||||
|
@ -140,7 +151,7 @@ updateHistoryResponse m r hr =
|
|||
in
|
||||
case hr of
|
||||
Ok h -> ({ m | sync = appendHistoryResponse m.sync r h }, newUsersCmd h)
|
||||
Err _ -> (m, Cmd.none)
|
||||
Err _ -> ({ m | errors = "Unable to load older history from server"::m.errors }, Cmd.none)
|
||||
|
||||
updateHistory : Model -> RoomId -> (Model, Cmd Msg)
|
||||
updateHistory m r =
|
||||
|
@ -225,7 +236,7 @@ updateViewportAfterMessage m vr =
|
|||
updateUserData : Model -> String -> Result Http.Error UserData -> (Model, Cmd Msg)
|
||||
updateUserData m s r = case r of
|
||||
Ok ud -> ({ m | userData = Dict.insert s ud m.userData }, Cmd.none)
|
||||
Err e -> (m, userData m.apiUrl (Maybe.withDefault "" m.token) s)
|
||||
Err e -> ({ m | errors = ("Failed to retrieve user data for user " ++ s)::m.errors }, userData m.apiUrl (Maybe.withDefault "" m.token) s)
|
||||
|
||||
updateSendRoomText : Model -> RoomId -> (Model, Cmd Msg)
|
||||
updateSendRoomText m r =
|
||||
|
@ -256,7 +267,7 @@ updateLoginResponse model a r = case r of
|
|||
<| encodeLoginInfo
|
||||
<| LoginInfo lr.accessToken model.apiUrl lr.userId model.transactionId)
|
||||
] )
|
||||
Err e -> (model, Cmd.none)
|
||||
Err e -> ({ model | errors = "Failed to log in. Are your username and password correct?"::model.errors }, Cmd.none)
|
||||
|
||||
updateSyncResponse : Model -> Result Http.Error SyncResponse -> Bool -> (Model, Cmd Msg)
|
||||
updateSyncResponse model r notify =
|
||||
|
|
22
src/Scylla/AccountData.elm
Normal file
22
src/Scylla/AccountData.elm
Normal file
|
@ -0,0 +1,22 @@
|
|||
module Scylla.AccountData exposing (..)
|
||||
import Scylla.Sync exposing (AccountData, JoinedRoom, roomAccountData)
|
||||
import Json.Decode as Decode
|
||||
|
||||
type NotificationSetting = Normal | MentionsOnly | None
|
||||
|
||||
notificationSettingDecoder : Decode.Decoder NotificationSetting
|
||||
notificationSettingDecoder =
|
||||
let
|
||||
checkString s = case s of
|
||||
"Normal" -> Decode.succeed Normal
|
||||
"MentionsOnly" -> Decode.succeed MentionsOnly
|
||||
"None" -> Decode.succeed None
|
||||
_ -> Decode.fail "Not a valid notification setting"
|
||||
in
|
||||
Decode.andThen checkString Decode.string
|
||||
|
||||
roomNotificationSetting : JoinedRoom -> NotificationSetting
|
||||
roomNotificationSetting jr = Maybe.withDefault Normal
|
||||
<| Maybe.andThen Result.toMaybe
|
||||
<| Maybe.map (Decode.decodeValue notificationSettingDecoder)
|
||||
<| roomAccountData jr "com.danilafe.scylla.notifications"
|
|
@ -63,6 +63,7 @@ type Msg =
|
|||
| SendImageResponse (Result Http.Error ())
|
||||
| SendFileResponse (Result Http.Error ())
|
||||
| ReceiveMarkdown MarkdownResponse
|
||||
| DismissError Int
|
||||
|
||||
displayName : Model -> Username -> String
|
||||
displayName m s = Maybe.withDefault (senderName s) <| Maybe.andThen .displayName <| Dict.get s m.userData
|
||||
|
|
|
@ -12,8 +12,8 @@ import Svg
|
|||
import Svg.Attributes
|
||||
import Url.Builder
|
||||
import Json.Decode as Decode
|
||||
import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, table, td, tr, img, textarea)
|
||||
import Html.Attributes exposing (type_, value, href, class, style, src, id, rows)
|
||||
import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, table, td, tr, img, textarea, video, source)
|
||||
import Html.Attributes exposing (type_, value, href, class, style, src, id, rows, controls, src)
|
||||
import Html.Events exposing (onInput, onClick, preventDefaultOn)
|
||||
import Dict
|
||||
|
||||
|
@ -50,10 +50,10 @@ viewFull model =
|
|||
[ errorList ] ++ [ core ]
|
||||
|
||||
errorsView : List String -> Html Msg
|
||||
errorsView = div [] << List.map errorView
|
||||
errorsView = div [ class "errors-wrapper" ] << List.indexedMap errorView
|
||||
|
||||
errorView : String -> Html Msg
|
||||
errorView s = div [] [ text s ]
|
||||
errorView : Int -> String -> Html Msg
|
||||
errorView i s = div [ class "error-wrapper", onClick <| DismissError i ] [ iconView "alert-triangle", text s ]
|
||||
|
||||
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
|
||||
baseView m jr =
|
||||
|
@ -194,6 +194,8 @@ messageView m re =
|
|||
case msgtype of
|
||||
Ok "m.text" -> messageTextView m re
|
||||
Ok "m.image" -> messageImageView m re
|
||||
Ok "m.file" -> messageFileView m re
|
||||
Ok "m.video" -> messageVideoView m re
|
||||
_ -> Nothing
|
||||
|
||||
messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||
|
@ -218,3 +220,25 @@ messageImageView m re =
|
|||
Maybe.map (\s -> img [ class "message-image", src s ] [])
|
||||
<| Maybe.map (contentRepositoryDownloadUrl m.apiUrl)
|
||||
<| Result.toMaybe body
|
||||
|
||||
messageFileView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||
messageFileView m re =
|
||||
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
|
||||
|
||||
messageVideoView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||
messageVideoView m re =
|
||||
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
|
||||
|
|
|
@ -5,6 +5,8 @@ $primary-color-light: #9FDBFB;
|
|||
$active-input-color: white;
|
||||
$background-color: #fafafa;
|
||||
$background-color-dark: darken($background-color, 4%);
|
||||
$error-color: #f01d43;
|
||||
$error-color-dark: darken(#f01d43, 10%);
|
||||
$transition-duration: .125s;
|
||||
|
||||
$inactive-input-color: darken($active-input-color, 3%);
|
||||
|
@ -70,6 +72,48 @@ h2 {
|
|||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
a.file-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.feather-icon {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div.errors-wrapper {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.error-wrapper {
|
||||
pointer-events: auto;
|
||||
width: 400px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, .25);
|
||||
padding: 5px;
|
||||
background-color: $error-color;
|
||||
border: 1px solid $error-color-dark;
|
||||
color: white;
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.feather-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Login Screen
|
||||
*/
|
||||
|
@ -177,6 +221,11 @@ table.events-table {
|
|||
max-height: 400px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 90%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
td:nth-child(1) {
|
||||
width: 10%;
|
||||
max-width: 100px;
|
||||
|
|
Loading…
Reference in New Issue
Block a user