Compare commits
No commits in common. "6ea55241c8398a5784b7f574bc87eec5bc37a44e" and "490d0eff2cb93bff3e73f401d0aa2a122edb648a" have entirely different histories.
6ea55241c8
...
490d0eff2c
24
README.md
24
README.md
|
@ -1,24 +0,0 @@
|
||||||
# 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
21
index.html
|
@ -1,21 +0,0 @@
|
||||||
<!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>
|
|
Binary file not shown.
Before Width: | Height: | Size: 111 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
19
src/Main.elm
19
src/Main.elm
|
@ -12,7 +12,6 @@ import Scylla.UserData exposing (..)
|
||||||
import Scylla.Notification exposing (..)
|
import Scylla.Notification exposing (..)
|
||||||
import Scylla.Storage exposing (..)
|
import Scylla.Storage exposing (..)
|
||||||
import Scylla.Markdown exposing (..)
|
import Scylla.Markdown exposing (..)
|
||||||
import Scylla.AccountData exposing (..)
|
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
import Url.Parser exposing (parse)
|
import Url.Parser exposing (parse)
|
||||||
import Url.Builder
|
import Url.Builder
|
||||||
|
@ -93,10 +92,6 @@ update msg model = case msg of
|
||||||
SendImageResponse _ -> (model, Cmd.none)
|
SendImageResponse _ -> (model, Cmd.none)
|
||||||
SendFileResponse _ -> (model, Cmd.none)
|
SendFileResponse _ -> (model, Cmd.none)
|
||||||
ReceiveMarkdown md -> updateMarkdown model md
|
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 : Model -> MarkdownResponse -> (Model, Cmd Msg)
|
||||||
updateMarkdown m { roomId, text, markdown } =
|
updateMarkdown m { roomId, text, markdown } =
|
||||||
|
@ -114,11 +109,8 @@ updateFileUploadComplete m rid mime ur =
|
||||||
command = case ur of
|
command = case ur of
|
||||||
Ok u -> sendFileMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
Ok u -> sendFileMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
||||||
_ -> Cmd.none
|
_ -> Cmd.none
|
||||||
newErrors = case ur of
|
|
||||||
Err e -> [ "Error uploading file. Please check your internet connection and try again." ]
|
|
||||||
_ -> []
|
|
||||||
in
|
in
|
||||||
({ m | errors = newErrors ++ m.errors, transactionId = m.transactionId + 1}, command)
|
({ m | transactionId = m.transactionId + 1}, command)
|
||||||
|
|
||||||
updateImageUploadComplete : Model -> RoomId -> String -> (Result Http.Error String) -> (Model, Cmd Msg)
|
updateImageUploadComplete : Model -> RoomId -> String -> (Result Http.Error String) -> (Model, Cmd Msg)
|
||||||
updateImageUploadComplete m rid mime ur =
|
updateImageUploadComplete m rid mime ur =
|
||||||
|
@ -126,9 +118,6 @@ updateImageUploadComplete m rid mime ur =
|
||||||
command = case ur of
|
command = case ur of
|
||||||
Ok u -> sendImageMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
Ok u -> sendImageMessage m.apiUrl (Maybe.withDefault "" m.token) m.transactionId rid mime u
|
||||||
_ -> Cmd.none
|
_ -> Cmd.none
|
||||||
newErrors = case ur of
|
|
||||||
Err e -> [ "Error uploading image. Please check your internet connection and try again." ]
|
|
||||||
_ -> []
|
|
||||||
in
|
in
|
||||||
({ m | transactionId = m.transactionId + 1}, command)
|
({ m | transactionId = m.transactionId + 1}, command)
|
||||||
|
|
||||||
|
@ -151,7 +140,7 @@ updateHistoryResponse m r hr =
|
||||||
in
|
in
|
||||||
case hr of
|
case hr of
|
||||||
Ok h -> ({ m | sync = appendHistoryResponse m.sync r h }, newUsersCmd h)
|
Ok h -> ({ m | sync = appendHistoryResponse m.sync r h }, newUsersCmd h)
|
||||||
Err _ -> ({ m | errors = "Unable to load older history from server"::m.errors }, Cmd.none)
|
Err _ -> (m, Cmd.none)
|
||||||
|
|
||||||
updateHistory : Model -> RoomId -> (Model, Cmd Msg)
|
updateHistory : Model -> RoomId -> (Model, Cmd Msg)
|
||||||
updateHistory m r =
|
updateHistory m r =
|
||||||
|
@ -236,7 +225,7 @@ updateViewportAfterMessage m vr =
|
||||||
updateUserData : Model -> String -> Result Http.Error UserData -> (Model, Cmd Msg)
|
updateUserData : Model -> String -> Result Http.Error UserData -> (Model, Cmd Msg)
|
||||||
updateUserData m s r = case r of
|
updateUserData m s r = case r of
|
||||||
Ok ud -> ({ m | userData = Dict.insert s ud m.userData }, Cmd.none)
|
Ok ud -> ({ m | userData = Dict.insert s ud m.userData }, Cmd.none)
|
||||||
Err e -> ({ m | errors = ("Failed to retrieve user data for user " ++ s)::m.errors }, userData m.apiUrl (Maybe.withDefault "" m.token) s)
|
Err e -> (m, userData m.apiUrl (Maybe.withDefault "" m.token) s)
|
||||||
|
|
||||||
updateSendRoomText : Model -> RoomId -> (Model, Cmd Msg)
|
updateSendRoomText : Model -> RoomId -> (Model, Cmd Msg)
|
||||||
updateSendRoomText m r =
|
updateSendRoomText m r =
|
||||||
|
@ -267,7 +256,7 @@ updateLoginResponse model a r = case r of
|
||||||
<| encodeLoginInfo
|
<| encodeLoginInfo
|
||||||
<| LoginInfo lr.accessToken model.apiUrl lr.userId model.transactionId)
|
<| LoginInfo lr.accessToken model.apiUrl lr.userId model.transactionId)
|
||||||
] )
|
] )
|
||||||
Err e -> ({ model | errors = "Failed to log in. Are your username and password correct?"::model.errors }, Cmd.none)
|
Err e -> (model, Cmd.none)
|
||||||
|
|
||||||
updateSyncResponse : Model -> Result Http.Error SyncResponse -> Bool -> (Model, Cmd Msg)
|
updateSyncResponse : Model -> Result Http.Error SyncResponse -> Bool -> (Model, Cmd Msg)
|
||||||
updateSyncResponse model r notify =
|
updateSyncResponse model r notify =
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
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,7 +63,6 @@ type Msg =
|
||||||
| SendImageResponse (Result Http.Error ())
|
| SendImageResponse (Result Http.Error ())
|
||||||
| SendFileResponse (Result Http.Error ())
|
| SendFileResponse (Result Http.Error ())
|
||||||
| ReceiveMarkdown MarkdownResponse
|
| ReceiveMarkdown MarkdownResponse
|
||||||
| DismissError Int
|
|
||||||
|
|
||||||
displayName : Model -> Username -> String
|
displayName : Model -> Username -> String
|
||||||
displayName m s = Maybe.withDefault (senderName s) <| Maybe.andThen .displayName <| Dict.get s m.userData
|
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 Svg.Attributes
|
||||||
import Url.Builder
|
import Url.Builder
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, table, td, tr, img, textarea, video, source)
|
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, controls, src)
|
import Html.Attributes exposing (type_, value, href, class, style, src, id, rows)
|
||||||
import Html.Events exposing (onInput, onClick, preventDefaultOn)
|
import Html.Events exposing (onInput, onClick, preventDefaultOn)
|
||||||
import Dict
|
import Dict
|
||||||
|
|
||||||
|
@ -50,10 +50,10 @@ viewFull model =
|
||||||
[ errorList ] ++ [ core ]
|
[ errorList ] ++ [ core ]
|
||||||
|
|
||||||
errorsView : List String -> Html Msg
|
errorsView : List String -> Html Msg
|
||||||
errorsView = div [ class "errors-wrapper" ] << List.indexedMap errorView
|
errorsView = div [] << List.map errorView
|
||||||
|
|
||||||
errorView : Int -> String -> Html Msg
|
errorView : String -> Html Msg
|
||||||
errorView i s = div [ class "error-wrapper", onClick <| DismissError i ] [ iconView "alert-triangle", text s ]
|
errorView s = div [] [ text s ]
|
||||||
|
|
||||||
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
|
baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
|
||||||
baseView m jr =
|
baseView m jr =
|
||||||
|
@ -194,8 +194,6 @@ messageView m re =
|
||||||
case msgtype of
|
case msgtype of
|
||||||
Ok "m.text" -> messageTextView m re
|
Ok "m.text" -> messageTextView m re
|
||||||
Ok "m.image" -> messageImageView m re
|
Ok "m.image" -> messageImageView m re
|
||||||
Ok "m.file" -> messageFileView m re
|
|
||||||
Ok "m.video" -> messageVideoView m re
|
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
|
messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
|
||||||
|
@ -220,25 +218,3 @@ messageImageView m re =
|
||||||
Maybe.map (\s -> img [ class "message-image", src s ] [])
|
Maybe.map (\s -> img [ class "message-image", src s ] [])
|
||||||
<| Maybe.map (contentRepositoryDownloadUrl m.apiUrl)
|
<| Maybe.map (contentRepositoryDownloadUrl m.apiUrl)
|
||||||
<| Result.toMaybe body
|
<| 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,8 +5,6 @@ $primary-color-light: #9FDBFB;
|
||||||
$active-input-color: white;
|
$active-input-color: white;
|
||||||
$background-color: #fafafa;
|
$background-color: #fafafa;
|
||||||
$background-color-dark: darken($background-color, 4%);
|
$background-color-dark: darken($background-color, 4%);
|
||||||
$error-color: #f01d43;
|
|
||||||
$error-color-dark: darken(#f01d43, 10%);
|
|
||||||
$transition-duration: .125s;
|
$transition-duration: .125s;
|
||||||
|
|
||||||
$inactive-input-color: darken($active-input-color, 3%);
|
$inactive-input-color: darken($active-input-color, 3%);
|
||||||
|
@ -72,48 +70,6 @@ h2 {
|
||||||
margin-bottom: 3px;
|
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
|
* Login Screen
|
||||||
*/
|
*/
|
||||||
|
@ -221,11 +177,6 @@ table.events-table {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
video {
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:nth-child(1) {
|
td:nth-child(1) {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user