Compare commits

...

8 Commits

17 changed files with 493 additions and 112 deletions

77
elm-dependencies.nix Normal file
View File

@@ -0,0 +1,77 @@
{
"NoRedInk/elm-json-decode-pipeline" = {
sha256 = "0y25xn0yx1q2xlg1yx1i0hg4xq1yxx6yfa99g272z8162si75hnl";
version = "1.0.0";
};
"elm/browser" = {
sha256 = "1zlmx672glg7fdgkvh5jm47y85pv7pdfr5mkhg6x7ar6k000vyka";
version = "1.0.1";
};
"elm/core" = {
sha256 = "1l0qdbczw91kzz8sx5d5zwz9x662bspy7p21dsr3f2rigxiix2as";
version = "1.0.2";
};
"elm/file" = {
sha256 = "15vw1ilbg0msimq2k9magwigp8lwqrgvz3vk6qia6b3ldahvw8jr";
version = "1.0.1";
};
"elm/html" = {
sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
version = "1.0.0";
};
"elm/http" = {
sha256 = "008bs76mnp48b4dw8qwjj4fyvzbxvlrl4xpa2qh1gg2kfwyw56v1";
version = "2.0.0";
};
"elm/json" = {
sha256 = "1a107nmm905dih4w4mjjkkpdcjbgaf5qjvr7fl30kkpkckfjjnrw";
version = "1.1.2";
};
"elm/svg" = {
sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
version = "1.0.1";
};
"elm/time" = {
sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
version = "1.0.0";
};
"elm/url" = {
sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
version = "1.0.0";
};
"hecrj/html-parser" = {
sha256 = "0pla6hswsl9piwrj3yl4pc4nfs5adc4g4c93644j4xila7bqqg8a";
version = "2.0.0";
};
"elm/bytes" = {
sha256 = "040d7irrawcbnq9jxhzx8p9qacdlw5bncy6lgndd6inm53rvvwbp";
version = "1.0.7";
};
"elm/parser" = {
sha256 = "0a3cxrvbm7mwg9ykynhp7vjid58zsw03r63qxipxp3z09qks7512";
version = "1.1.0";
};
"elm/virtual-dom" = {
sha256 = "0q1v5gi4g336bzz1lgwpn5b1639lrn63d8y6k6pimcyismp2i1yg";
version = "1.0.2";
};
"rtfeldman/elm-hex" = {
sha256 = "1y0aa16asvwdqmgbskh5iba6psp43lkcjjw9mgzj3gsrg33lp00d";
version = "1.0.0";
};
}

View File

@@ -3,7 +3,7 @@
"source-directories": [
"src"
],
"elm-version": "0.19.0",
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
@@ -29,4 +29,4 @@
"direct": {},
"indirect": {}
}
}
}

56
elm.nix Normal file
View File

@@ -0,0 +1,56 @@
{ lib, stdenv, elm, fetchElmDeps, uglify-js, sass }:
let
mkDerivation =
{ srcs ? ./elm-dependencies.nix
, src
, name
, srcdir ? "./src"
, targets ? []
, registryDat ? ./registry.dat
, outputJavaScript ? false
}:
stdenv.mkDerivation {
inherit name src;
buildInputs = [ elm sass ]
++ lib.optional outputJavaScript uglify-js;
buildPhase = fetchElmDeps {
elmPackages = import srcs;
elmVersion = "0.19.1";
inherit registryDat;
};
installPhase = let
elmfile = module: "${srcdir}/${builtins.replaceStrings ["."] ["/"] module}.elm";
extension = if outputJavaScript then "js" else "html";
in ''
${lib.concatStrings (map (module: ''
echo "compiling ${elmfile module}"
elm make ${elmfile module} --optimize --output $out/${module}.${extension}
${lib.optionalString outputJavaScript ''
echo "minifying ${elmfile module}"
uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
| uglifyjs --mangle --output $out/${module}.min.${extension}
''}
'') targets)}
# Custom logic for Scylla in particular
mkdir $out/static $out/static/js $out/static/css $out/static/svg
cp $src/index.html $out/index.html
cp $out/Main.min.js $out/static/js/elm.js
cp $src/static/js/*.js $out/static/js
cp $src/static/svg/*.svg $out/static/svg
sass $src/static/scss/style.scss $out/static/css/style.css
'';
};
in mkDerivation {
name = "Scylla-0.1.0";
srcs = ./elm-dependencies.nix;
src = ./.;
targets = ["Main"];
srcdir = "./src";
outputJavaScript = true;
}

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766736597,
"narHash": "sha256-BASnpCLodmgiVn0M1MU2Pqyoz0aHwar/0qLkp7CjvSQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f560ccec6b1116b22e6ed15f4c510997d99d5852",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

23
flake.nix Normal file
View File

@@ -0,0 +1,23 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
Scylla = import ./elm.nix {
inherit (pkgs) lib stdenv sass;
inherit (pkgs.elmPackages) fetchElmDeps elm;
inherit (pkgs.nodePackages) uglify-js;
};
in
{
packages = { inherit Scylla; };
defaultPackage = Scylla;
}
);
}

BIN
registry.dat Normal file

Binary file not shown.

View File

@@ -1,3 +1,4 @@
module Main exposing (..)
import Browser exposing (application, UrlRequest(..))
import Browser.Navigation as Nav
import Browser.Dom exposing (Viewport, setViewportOf)
@@ -5,6 +6,7 @@ import Scylla.Room exposing (OpenRooms, applySync)
import Scylla.Sync exposing (..)
import Scylla.Sync.Events exposing (toMessageEvent, getType, getSender, getUnsigned)
import Scylla.Sync.AccountData exposing (..)
import Scylla.Sync.Push exposing (..)
import Scylla.ListUtils exposing (..)
import Scylla.Messages exposing (..)
import Scylla.Login exposing (..)
@@ -13,7 +15,6 @@ import Scylla.Model exposing (..)
import Scylla.Http exposing (..)
import Scylla.Views exposing (viewFull)
import Scylla.Route exposing (Route(..), RoomId)
import Scylla.UserData exposing (..)
import Scylla.Notification exposing (..)
import Scylla.Storage exposing (..)
import Scylla.Markdown exposing (..)
@@ -50,8 +51,6 @@ init _ url key =
, roomText = Dict.empty
, sending = Dict.empty
, transactionId = 0
, userData = Dict.empty
, roomNames = Dict.empty
, connected = True
, searchText = ""
, rooms = emptyOpenRooms
@@ -86,7 +85,7 @@ update msg model = case msg of
ReceiveLoginResponse a r -> updateLoginResponse model a r
ReceiveFirstSyncResponse r -> updateSyncResponse model r False
ReceiveSyncResponse r -> updateSyncResponse model r True
ReceiveUserData s r -> updateUserData model s r
ReceiveUserData s r -> (model, Cmd.none)
ChangeRoomText r t -> updateChangeRoomText model r t
SendRoomText r -> updateSendRoomText model r
SendRoomTextResponse t r -> updateSendRoomTextResponse model t r
@@ -112,12 +111,6 @@ update msg model = case msg of
requestScrollCmd : Cmd Msg
requestScrollCmd = Task.attempt ViewportAfterMessage (Browser.Dom.getViewportOf "messages-wrapper")
newUsersCmd : Model -> List Username -> Cmd Msg
newUsersCmd m us = m.token
|> Maybe.map (\t -> List.map (userData m.apiUrl t) us)
|> Maybe.withDefault []
|> Cmd.batch
updateSendRoomTextResponse : Model -> Int -> Result Http.Error String -> (Model, Cmd Msg)
updateSendRoomTextResponse m t r =
let
@@ -179,16 +172,9 @@ updateUploadSelected m rid f fs msg =
updateHistoryResponse : Model -> RoomId -> Result Http.Error HistoryResponse -> (Model, Cmd Msg)
updateHistoryResponse m r hr =
let
userDataCmd h = newUsersCmd m
<| newUsers m
<| uniqueBy identity
<| List.map getSender
<| h.chunk
in
case hr of
Ok h -> ({ m | rooms = applyHistoryResponse r h m.rooms }, userDataCmd h)
Err _ -> ({ m | errors = "Unable to load older history from server"::m.errors }, Cmd.none)
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)
updateHistory : Model -> RoomId -> (Model, Cmd Msg)
updateHistory m r =
@@ -268,11 +254,6 @@ updateViewportAfterMessage m vr =
in
(m, Result.withDefault Cmd.none <| Result.map cmd 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 | errors = ("Failed to retrieve user data for user " ++ s)::m.errors }, Cmd.none)
updateSendRoomText : Model -> RoomId -> (Model, Cmd Msg)
updateSendRoomText m r =
let
@@ -311,18 +292,18 @@ updateSyncResponse model r notify =
nextBatch = Result.withDefault model.nextBatch
<| Result.map .nextBatch r
syncCmd = sync model.apiUrl token nextBatch
userDataCmd sr = newUsersCmd model
<| newUsers model
<| allUsers sr
notification sr = findFirstBy
(\(s, e) -> e.originServerTs)
(\(s, e) -> e.sender /= model.loginUsername)
<| joinedRoomNotificationEvents sr
notification sr =
getPushRuleset model.accountData
|> Maybe.map (\rs -> getNotificationEvents rs sr)
|> Maybe.withDefault []
|> findFirstBy
(\(s, e) -> e.originServerTs)
(\(s, e) -> e.sender /= model.loginUsername)
notificationCmd sr = if notify
then Maybe.withDefault Cmd.none
<| Maybe.map (\(s, e) -> sendNotificationPort
{ name = getDisplayName model.userData e.sender
, text = notificationText e
{ name = roomLocalDisplayName model s e.sender
, text = getText e
, room = s
}) <| notification sr
else Cmd.none
@@ -359,7 +340,6 @@ updateSyncResponse model r notify =
Ok sr -> (newModel sr
, Cmd.batch
[ syncCmd
, userDataCmd sr
, notificationCmd sr
, setScrollCmd sr
, setReadReceiptCmd sr

View File

@@ -138,8 +138,8 @@ login apiUrl username password = request
, tracker = Nothing
}
userData : ApiUrl -> ApiToken -> Username -> Cmd Msg
userData apiUrl token username = request
getUserData : ApiUrl -> ApiToken -> Username -> Cmd Msg
getUserData apiUrl token username = request
{ method = "GET"
, headers = authenticatedHeaders token
, url = (fullClientUrl apiUrl) ++ "/profile/" ++ username

View File

@@ -16,13 +16,13 @@ type Message
= Sending SendingMessage
| Received MessageEvent
messageUsername : Username -> Message -> Username
messageUsername u msg = case msg of
getUsername : Username -> Message -> Username
getUsername u msg = case msg of
Sending _ -> u
Received re -> re.sender
mergeMessages : Username -> List Message -> List (Username, List Message)
mergeMessages du xs =
groupMessages : Username -> List Message -> List (Username, List Message)
groupMessages du xs =
let
initialState = (Nothing, [], [])
appendNamed mu ms msl = case mu of
@@ -30,19 +30,19 @@ mergeMessages du xs =
Nothing -> msl
foldFunction msg (pu, ms, msl) =
let
nu = Just <| messageUsername du msg
nu = Just <| getUsername du msg
in
if pu == nu then (pu, ms ++ [msg], msl) else (nu, [msg], appendNamed pu ms msl)
(fmu, fms, fmsl) = List.foldl foldFunction initialState xs
in
appendNamed fmu fms fmsl
receivedMessagesRoom : RoomData -> List Message
receivedMessagesRoom rd = rd.messages
getReceivedMessages : RoomData -> List Message
getReceivedMessages rd = rd.messages
|> List.filter (\e -> e.type_ == "m.room.message")
|> List.map Received
sendingMessagesRoom : RoomId -> Dict Int (RoomId, SendingMessage) -> List Message
sendingMessagesRoom rid ms = List.map (\(tid, (_, sm)) -> Sending sm)
getSendingMessages : RoomId -> Dict Int (RoomId, SendingMessage) -> List Message
getSendingMessages rid ms = List.map (\(tid, (_, sm)) -> Sending sm)
<| List.filter (\(_, (nrid, _)) -> nrid == rid)
<| Dict.toList ms

View File

@@ -1,12 +1,14 @@
module Scylla.Model exposing (..)
import Scylla.Api exposing (..)
import Scylla.Room exposing (getLocalDisplayName)
import Scylla.Sync exposing (SyncResponse, HistoryResponse)
import Scylla.ListUtils exposing (findFirst)
import Scylla.Room exposing (OpenRooms)
import Scylla.UserData exposing (UserData, getSenderName)
import Scylla.Sync.Rooms exposing (JoinedRoom)
import Scylla.Sync.Push exposing (Ruleset)
import Scylla.Sync.AccountData exposing (AccountData, directMessagesDecoder)
import Scylla.Login exposing (LoginResponse, Username, Password)
import Scylla.UserData exposing (UserData)
import Scylla.Route exposing (Route(..), RoomId)
import Scylla.Messages exposing (..)
import Scylla.Storage exposing (..)
@@ -35,8 +37,6 @@ type alias Model =
, roomText : Dict RoomId String
, sending : Dict Int (RoomId, SendingMessage)
, transactionId : Int
, userData : Dict Username UserData
, roomNames : Dict RoomId String
, connected : Bool
, searchText : String
, rooms : OpenRooms
@@ -84,10 +84,13 @@ roomUrl s = Url.Builder.absolute [ "room", s ] []
loginUrl : String
loginUrl = Url.Builder.absolute [ "login" ] []
newUsers : Model -> List Username -> List Username
newUsers m lus = List.filter (\u -> not <| Dict.member u m.userData) lus
currentRoomId : Model -> Maybe RoomId
currentRoomId m = case m.route of
Room r -> Just r
_ -> Nothing
roomLocalDisplayName : Model -> RoomId -> Username -> String
roomLocalDisplayName m rid u =
case Dict.get rid m.rooms of
Just rd -> getLocalDisplayName rd u
_ -> getSenderName u

View File

@@ -1,6 +1,7 @@
port module Scylla.Notification exposing (..)
import Scylla.Sync exposing (SyncResponse, joinedRoomsTimelineEvents)
import Scylla.Sync.Events exposing (RoomEvent, MessageEvent, toMessageEvent)
import Scylla.Sync.Push exposing (Ruleset, getEventNotification)
import Json.Decode as Decode exposing (string, field)
import Dict
@@ -13,18 +14,19 @@ type alias Notification =
port sendNotificationPort : Notification -> Cmd msg
port onNotificationClickPort : (String -> msg) -> Sub msg
notificationText : MessageEvent -> String
notificationText re = case (Decode.decodeValue (field "msgtype" string) re.content) of
getText : MessageEvent -> String
getText re = case (Decode.decodeValue (field "msgtype" string) re.content) of
Ok "m.text" -> Result.withDefault "" <| (Decode.decodeValue (field "body" string) re.content)
_ -> ""
joinedRoomNotificationEvents : SyncResponse -> List (String, MessageEvent)
joinedRoomNotificationEvents s =
let
applyPair k = List.map (\v -> (k, v))
in
List.sortBy (\(k, v) -> v.originServerTs)
<| List.filterMap (\(k, e) -> Maybe.map (\me -> (k, me)) <| toMessageEvent e)
<| Dict.foldl (\k v a -> a ++ applyPair k v) []
<| joinedRoomsTimelineEvents s
getNotificationEvents : Ruleset -> SyncResponse -> List (String, MessageEvent)
getNotificationEvents rs s = s.rooms
|> Maybe.andThen .join
|> Maybe.map (Dict.map (\k v -> v.timeline
|> Maybe.andThen .events
|> Maybe.map (List.filter <| getEventNotification rs k)
|> Maybe.map (List.filterMap <| toMessageEvent)
|> Maybe.withDefault []))
|> Maybe.withDefault Dict.empty
|> Dict.toList
|> List.concatMap (\(k, vs) -> List.map (\v -> (k, v)) vs)

View File

@@ -2,7 +2,7 @@ module Scylla.Room exposing (..)
import Scylla.Route exposing (RoomId)
import Scylla.Sync exposing (SyncResponse)
import Scylla.Login exposing (Username)
import Scylla.UserData exposing (UserData, getDisplayName)
import Scylla.UserData exposing (getSenderName)
import Scylla.Sync exposing (HistoryResponse)
import Scylla.Sync.Events exposing (MessageEvent, StateEvent, toStateEvent, toMessageEvent)
import Scylla.Sync.AccountData exposing (AccountData, getDirectMessages, applyAccountData)
@@ -86,7 +86,10 @@ changeRoomData jr rd =
, messages = changeTimeline jr rd.messages
, ephemeral = changeEphemeral jr rd.ephemeral
, unreadNotifications = changeNotifications jr rd.unreadNotifications
, prevHistoryBatch = Maybe.andThen .prevBatch jr.timeline
, prevHistoryBatch =
case rd.prevHistoryBatch of
Nothing -> Maybe.andThen .prevBatch jr.timeline
Just _ -> rd.prevHistoryBatch
}
updateRoomData : JoinedRoom -> Maybe RoomData -> Maybe RoomData
@@ -130,8 +133,8 @@ getRoomTypingUsers : RoomData -> List String
getRoomTypingUsers = Maybe.withDefault []
<< getEphemeralData "m.typing" (field "user_ids" (list string))
getRoomName : AccountData -> Dict Username UserData -> RoomId -> RoomData -> String
getRoomName ad ud rid rd =
getRoomName : AccountData -> RoomId -> RoomData -> String
getRoomName ad rid rd =
let
customName = getStateData ("m.room.name", "") (field "name" (string)) rd
direct = getDirectMessages ad
@@ -139,11 +142,16 @@ getRoomName ad ud rid rd =
in
case (customName, direct) of
(Just cn, _) -> cn
(_, Just d) -> getDisplayName ud d
(_, Just d) -> getLocalDisplayName rd d
_ -> rid
getRoomNotificationCount : RoomData -> (Int, Int)
getRoomNotificationCount rd =
getLocalDisplayName : RoomData -> Username -> String
getLocalDisplayName rd u =
getStateData ("m.room.member", u) (field "displayname" string) rd
|> Maybe.withDefault (getSenderName u)
getNotificationCount : RoomData -> (Int, Int)
getNotificationCount rd =
( Maybe.withDefault 0 rd.unreadNotifications.notificationCount
, Maybe.withDefault 0 rd.unreadNotifications.highlightCount
)
@@ -153,7 +161,7 @@ getTotalNotificationCount =
let
sumTuples (x1, y1) (x2, y2) = (x1+x2, y1+y2)
in
Dict.foldl (\_ -> sumTuples << getRoomNotificationCount) (0, 0)
Dict.foldl (\_ -> sumTuples << getNotificationCount) (0, 0)
getTotalNotificationCountString : OpenRooms -> Maybe String
getTotalNotificationCountString or =

View File

@@ -2,7 +2,8 @@ module Scylla.Sync.AccountData exposing (..)
import Scylla.ListUtils exposing (..)
import Scylla.Sync.DecodeTools exposing (maybeDecode)
import Scylla.Sync.Events exposing (Event, eventDecoder)
import Json.Decode as Decode exposing (Decoder, list, decodeValue)
import Scylla.Sync.Push exposing (Ruleset, rulesetDecoder)
import Json.Decode as Decode exposing (Decoder, list, field, decodeValue)
import Dict exposing (Dict)
type alias AccountData =
@@ -48,3 +49,6 @@ getAccountData key d ad = ad.events
getDirectMessages : AccountData -> Maybe DirectMessages
getDirectMessages = getAccountData "m.direct" directMessagesDecoder
getPushRuleset : AccountData -> Maybe Ruleset
getPushRuleset = getAccountData "m.push_rules" (field "global" rulesetDecoder)

View File

@@ -38,8 +38,8 @@ type RoomEvent
roomEventDecoder : Decoder RoomEvent
roomEventDecoder = oneOf
[ Decode.map MessageRoomEvent messageEventDecoder
, Decode.map StateRoomEvent stateEventDecoder
[ Decode.map StateRoomEvent stateEventDecoder
, Decode.map MessageRoomEvent messageEventDecoder
]
type alias MessageEvent =
@@ -124,6 +124,12 @@ getType re =
StateRoomEvent e -> e.type_
MessageRoomEvent e -> e.type_
getContent : RoomEvent -> Decode.Value
getContent re =
case re of
StateRoomEvent e -> e.content
MessageRoomEvent e -> e.content
toStateEvent : RoomEvent -> Maybe StateEvent
toStateEvent re =
case re of

167
src/Scylla/Sync/Push.elm Normal file
View File

@@ -0,0 +1,167 @@
module Scylla.Sync.Push exposing (..)
import Scylla.Sync.DecodeTools exposing (maybeDecode)
import Scylla.Sync.Events exposing (RoomEvent, getSender, getContent, getType)
import Scylla.Route exposing (RoomId)
import Json.Decode as Decode exposing (Decoder, string, int, field, value, bool, list)
import Json.Decode.Pipeline exposing (required, optional)
type Condition
= EventMatch String String
| ContainsDisplayName
| RoomMemberCount Int
| SenderNotificationPermission String
conditionDecoder : Decoder Condition
conditionDecoder =
let
eventMatchDecoder =
Decode.succeed EventMatch
|> required "key" string
|> required "pattern" string
containsDisplayNameDecoder =
Decode.succeed ContainsDisplayName
roomMemberCountDecoder =
Decode.succeed RoomMemberCount
|> required "is"
(Decode.map (Maybe.withDefault 0 << String.toInt) string)
senderNotifPermissionDecoder =
Decode.succeed SenderNotificationPermission
|> required "key" string
dispatchDecoder k =
case k of
"event_match" -> eventMatchDecoder
"contains_display_name" -> containsDisplayNameDecoder
"room_member_count" -> roomMemberCountDecoder
"sender_notification_permission" -> senderNotifPermissionDecoder
_ -> Decode.fail "Unknown condition code"
in
field "kind" string
|> Decode.andThen dispatchDecoder
type Action
= Notify
| DontNotify
| Coalesce
| SetTweak String (Maybe Decode.Value)
actionDecoder : Decoder Action
actionDecoder =
let
dispatchStringDecoder s =
case s of
"notify" -> Decode.succeed Notify
"dont_notify" -> Decode.succeed DontNotify
"coalesce" -> Decode.succeed Coalesce
_ -> Decode.fail "Unknown action string"
objectDecoder =
Decode.succeed SetTweak
|> required "set_tweak" string
|> maybeDecode "value" value
in
Decode.oneOf
[ string |> Decode.andThen dispatchStringDecoder
, objectDecoder
]
type alias Rule =
{ ruleId : String
, default : Bool
, enabled : Bool
, conditions : List Condition
, actions : List Action
}
ruleDecoder : Decoder Rule
ruleDecoder =
let
patternDecoder = Decode.oneOf
[ field "pattern" string
|> Decode.andThen
(\p -> Decode.succeed <| \r ->
{ r | conditions = (EventMatch "content.body" p)::r.conditions })
, Decode.succeed identity
]
basicRuleDecoder = Decode.succeed Rule
|> required "rule_id" string
|> optional "default" bool True
|> optional "enabled" bool False
|> optional "conditions" (list conditionDecoder) []
|> required "actions" (list actionDecoder)
in
patternDecoder
|> Decode.andThen (\f -> Decode.map f basicRuleDecoder)
type alias Ruleset =
{ content : List Rule
, override : List Rule
, room : List Rule
, sender : List Rule
, underride : List Rule
}
rulesetDecoder : Decoder Ruleset
rulesetDecoder = Decode.succeed Ruleset
|> optional "content" (list ruleDecoder) []
|> optional "override" (list ruleDecoder) []
|> optional "room" (list ruleDecoder) []
|> optional "sender" (list ruleDecoder) []
|> optional "underride" (list ruleDecoder) []
checkCondition : RoomEvent -> Condition -> Bool
checkCondition re c =
let
pathDecoder xs p =
Decode.at xs string
|> Decode.map (String.contains p << String.toLower)
matchesPattern xs p =
case Decode.decodeValue (pathDecoder xs p) (getContent re) of
Ok True -> True
_ -> False
in
case c of
EventMatch k p ->
case String.split "." k of
"content"::xs -> matchesPattern xs p
"type"::[] -> String.contains p <| getType re
_ -> False
ContainsDisplayName -> False
RoomMemberCount _ -> False
SenderNotificationPermission _ -> False
applyAction : Action -> List Action -> List Action
applyAction a as_ =
case a of
Notify -> Notify :: List.filter (\a_ -> a_ /= DontNotify) as_
DontNotify -> DontNotify :: List.filter (\a_ -> a_ /= Notify) as_
Coalesce -> Coalesce :: List.filter (\a_ -> a_ /= DontNotify) as_
a_ -> a_ :: as_
applyActions : List Action -> List Action -> List Action
applyActions l r = List.foldl applyAction r l
updatePushRuleActions : Rule -> RoomEvent -> List Action -> List Action
updatePushRuleActions r re as_ =
if List.all (checkCondition re) r.conditions
then applyActions r.actions as_
else as_
updatePushActions : List Rule -> RoomEvent -> List Action -> List Action
updatePushActions rs re as_ =
List.filter .enabled rs
|> List.foldl (\r -> updatePushRuleActions r re) as_
getPushActions : Ruleset -> RoomId -> RoomEvent -> List Action
getPushActions rs rid re =
let
roomRules = List.filter (((==) rid) << .ruleId) rs.room
senderRules = List.filter (((==) <| getSender re) << .ruleId) rs.sender
in
updatePushActions rs.underride re []
|> updatePushActions senderRules re
|> updatePushActions roomRules re
|> updatePushActions rs.override re
getEventNotification : Ruleset -> RoomId -> RoomEvent -> Bool
getEventNotification rs rid re =
getPushActions rs rid re
|> List.member Notify

View File

@@ -15,11 +15,6 @@ userDataDecoder =
|> optional "displayname" (Decode.map Just string) Nothing
|> optional "avatar_url" (Decode.map Just string) Nothing
getDisplayName : Dict Username UserData -> Username -> String
getDisplayName ud s = Dict.get s ud
|> Maybe.andThen .displayName
|> Maybe.withDefault (getSenderName s)
getSenderName : Username -> String
getSenderName s =
let

View File

@@ -3,12 +3,11 @@ import Scylla.Model exposing (..)
import Scylla.Sync exposing (..)
import Scylla.Sync.Events exposing (..)
import Scylla.Sync.Rooms exposing (..)
import Scylla.Room exposing (RoomData, emptyOpenRooms, getHomeserver, getRoomName, getRoomTypingUsers)
import Scylla.Room exposing (RoomData, emptyOpenRooms, getHomeserver, getRoomName, getRoomTypingUsers, getLocalDisplayName)
import Scylla.Route exposing (..)
import Scylla.Fnv as Fnv
import Scylla.Messages exposing (..)
import Scylla.Login exposing (Username)
import Scylla.UserData exposing (UserData, getDisplayName)
import Scylla.Http exposing (fullMediaUrl)
import Scylla.Api exposing (ApiUrl)
import Scylla.ListUtils exposing (groupBy)
@@ -21,7 +20,7 @@ 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)
import Html.Attributes exposing (type_, placeholder, value, href, class, style, src, id, rows, controls, src, classList)
import Html.Events exposing (onInput, onClick, preventDefaultOn)
import Html.Lazy exposing (lazy6)
import Html.Lazy exposing (lazy5)
import Dict exposing (Dict)
import Tuple
@@ -111,14 +110,14 @@ homeserverView m hs rs =
let
roomList = div [ class "rooms-list" ]
<| List.map (\(rid, r) -> roomListElementView m rid r)
<| List.sortBy (\(rid, r) -> getRoomName m.accountData m.userData rid r) rs
<| List.sortBy (\(rid, r) -> getRoomName m.accountData rid r) rs
in
div [ class "homeserver-wrapper" ] [ h3 [] [ text hs ], roomList ]
roomListElementView : Model -> RoomId -> RoomData -> Html Msg
roomListElementView m rid rd =
let
name = getRoomName m.accountData m.userData rid rd
name = getRoomName m.accountData rid rd
isVisible = m.searchText == "" || (String.contains (String.toLower m.searchText) <| String.toLower name)
isCurrentRoom = case currentRoomId m of
Nothing -> False
@@ -161,7 +160,7 @@ loginView m = div [ class "login-wrapper" ]
joinedRoomView : Model -> RoomId -> RoomData -> Html Msg
joinedRoomView m roomId rd =
let
typing = List.map (getDisplayName m.userData) <| getRoomTypingUsers rd
typing = List.map (getLocalDisplayName rd) <| getRoomTypingUsers rd
typingText = String.join ", " typing
typingSuffix = case List.length typing of
0 -> ""
@@ -182,19 +181,19 @@ joinedRoomView m roomId rd =
]
in
div [ class "room-wrapper" ]
[ h2 [] [ text <| getRoomName m.accountData m.userData roomId rd ]
, lazy6 lazyMessagesView m.userData roomId rd m.apiUrl m.loginUsername m.sending
[ h2 [] [ text <| getRoomName m.accountData roomId rd ]
, lazy5 lazyMessagesView roomId rd m.apiUrl m.loginUsername m.sending
, messageInput
, typingWrapper
]
lazyMessagesView : Dict String UserData -> RoomId -> RoomData -> ApiUrl -> Username -> Dict Int (RoomId, SendingMessage) -> Html Msg
lazyMessagesView ud rid rd au lu snd =
lazyMessagesView : RoomId -> RoomData -> ApiUrl -> Username -> Dict Int (RoomId, SendingMessage) -> Html Msg
lazyMessagesView rid rd au lu snd =
let
roomReceived = receivedMessagesRoom rd
roomSending = sendingMessagesRoom rid snd
renderedMessages = List.map (userMessagesView ud au)
<| mergeMessages lu
roomReceived = getReceivedMessages rd
roomSending = getSendingMessages rid snd
renderedMessages = List.map (userMessagesView rd au)
<| groupMessages lu
<| roomReceived ++ roomSending
in
messagesWrapperView rid renderedMessages
@@ -224,38 +223,38 @@ messagesWrapperView rid es = div [ class "messages-wrapper", id "messages-wrappe
, table [ class "messages-table" ] es
]
senderView : Dict String UserData -> Username -> Html Msg
senderView ud s =
span [ style "color" <| stringColor s, class "sender-wrapper" ] [ text <| getDisplayName ud s ]
senderView : RoomData -> Username -> Html Msg
senderView rd s =
span [ style "color" <| stringColor s, class "sender-wrapper" ] [ text <| getLocalDisplayName rd s ]
userMessagesView : Dict String UserData -> ApiUrl -> (Username, List Message) -> Html Msg
userMessagesView ud apiUrl (u, ms) =
userMessagesView : RoomData -> ApiUrl -> (Username, List Message) -> Html Msg
userMessagesView rd apiUrl (u, ms) =
let
wrap h = div [ class "message" ] [ h ]
in
tr []
[ td [] [ senderView ud u ]
, td [] <| List.map wrap <| List.filterMap (messageView ud apiUrl) ms
[ td [] [ senderView rd u ]
, td [] <| List.map wrap <| List.filterMap (messageView rd apiUrl) ms
]
messageView : Dict String UserData -> ApiUrl -> Message -> Maybe (Html Msg)
messageView ud apiUrl msg = case msg of
messageView : RoomData -> ApiUrl -> Message -> Maybe (Html Msg)
messageView rd apiUrl msg = case msg of
Sending t -> Just <| sendingMessageView t
Received re -> roomEventView ud apiUrl re
Received re -> roomEventView rd apiUrl re
sendingMessageView : SendingMessage -> Html Msg
sendingMessageView msg = case msg.body of
TextMessage t -> span [ class "sending"] [ text t ]
roomEventView : Dict String UserData -> ApiUrl -> MessageEvent -> Maybe (Html Msg)
roomEventView ud apiUrl re =
roomEventView : RoomData -> ApiUrl -> MessageEvent -> Maybe (Html Msg)
roomEventView rd apiUrl re =
let
msgtype = Decode.decodeValue (Decode.field "msgtype" Decode.string) re.content
in
case msgtype of
Ok "m.text" -> roomEventTextView re
Ok "m.notice" -> roomEventNoticeView re
Ok "m.emote" -> roomEventEmoteView ud re
Ok "m.emote" -> roomEventEmoteView rd re
Ok "m.image" -> roomEventImageView apiUrl re
Ok "m.file" -> roomEventFileView apiUrl re
Ok "m.video" -> roomEventVideoView apiUrl re
@@ -277,10 +276,10 @@ roomEventContent f re =
Just c -> Just <| f c
Nothing -> Maybe.map (f << List.singleton << text) <| Result.toMaybe body
roomEventEmoteView : Dict String UserData -> MessageEvent -> Maybe (Html Msg)
roomEventEmoteView ud re =
roomEventEmoteView : RoomData -> MessageEvent -> Maybe (Html Msg)
roomEventEmoteView rd re =
let
emoteText = "* " ++ getDisplayName ud re.sender ++ " "
emoteText = "* " ++ getLocalDisplayName rd re.sender ++ " "
in
roomEventContent (\cs -> span [] (text emoteText :: cs)) re