Compare commits

..

No commits in common. "5f8751e14223a9a26d3d92a39681d83beca3e113" and "1703c091a76e74ca4fbbd8e688003e0303a45245" have entirely different histories.

7 changed files with 69 additions and 152 deletions

View File

@ -2,8 +2,6 @@ import Browser exposing (application, UrlRequest(..))
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Browser.Dom exposing (Viewport, setViewportOf) import Browser.Dom exposing (Viewport, setViewportOf)
import Scylla.Sync exposing (..) import Scylla.Sync exposing (..)
import Scylla.Room exposing (..)
import Scylla.Messages exposing (..)
import Scylla.Login exposing (..) import Scylla.Login exposing (..)
import Scylla.Api exposing (..) import Scylla.Api exposing (..)
import Scylla.Model exposing (..) import Scylla.Model exposing (..)
@ -80,7 +78,7 @@ update msg model = case msg of
ReceiveUserData s r -> updateUserData model s r ReceiveUserData s r -> updateUserData model s r
ChangeRoomText r t -> updateChangeRoomText model r t ChangeRoomText r t -> updateChangeRoomText model r t
SendRoomText r -> updateSendRoomText model r SendRoomText r -> updateSendRoomText model r
SendRoomTextResponse t r -> ({ model | sending = Dict.remove t model.sending }, Cmd.none) SendRoomTextResponse r -> (model, Cmd.none)
ReceiveCompletedReadMarker r -> (model, Cmd.none) ReceiveCompletedReadMarker r -> (model, Cmd.none)
ReceiveCompletedTypingIndicator r -> (model, Cmd.none) ReceiveCompletedTypingIndicator r -> (model, Cmd.none)
ReceiveStoreData d -> updateStoreData model d ReceiveStoreData d -> updateStoreData model d
@ -109,12 +107,8 @@ updateMarkdown m { roomId, text, markdown } =
<| encodeLoginInfo <| encodeLoginInfo
<| LoginInfo (Maybe.withDefault "" m.token) m.apiUrl m.loginUsername (m.transactionId + 1)) <| 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 sendMessageCmd = sendMarkdownMessage m.apiUrl (Maybe.withDefault "" m.token) (m.transactionId + 1) roomId text markdown
newModel =
{ m | transactionId = m.transactionId + 1
, sending = Dict.insert (m.transactionId + 1) (roomId, TextMessage text) m.sending
}
in in
(newModel, Cmd.batch [ storeValueCmd, sendMessageCmd ]) ({ m | transactionId = m.transactionId + 1 }, Cmd.batch [ storeValueCmd, sendMessageCmd ])
updateFileUploadComplete : Model -> RoomId -> File -> (Result Http.Error String) -> (Model, Cmd Msg) updateFileUploadComplete : Model -> RoomId -> File -> (Result Http.Error String) -> (Model, Cmd Msg)
updateFileUploadComplete m rid mime ur = updateFileUploadComplete m rid mime ur =
@ -236,7 +230,7 @@ updateViewportAfterMessage : Model -> Result Browser.Dom.Error Viewport -> (Mode
updateViewportAfterMessage m vr = updateViewportAfterMessage m vr =
let let
cmd vp = if vp.scene.height - (vp.viewport.y + vp.viewport.height ) < 100 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 then Task.attempt ViewportChangeComplete <| setViewportOf "events-wrapper" vp.viewport.x vp.scene.height
else Cmd.none else Cmd.none
in in
(m, Result.withDefault Cmd.none <| Result.map cmd vr) (m, Result.withDefault Cmd.none <| Result.map cmd vr)
@ -314,7 +308,7 @@ updateSyncResponse model r notify =
setScrollCmd sr = if List.isEmpty setScrollCmd sr = if List.isEmpty
<| roomMessages sr <| roomMessages sr
then Cmd.none then Cmd.none
else Task.attempt ViewportAfterMessage (Browser.Dom.getViewportOf "messages-wrapper") else Task.attempt ViewportAfterMessage (Browser.Dom.getViewportOf "events-wrapper")
setReadReceiptCmd sr = case (room, List.head <| List.reverse <| roomMessages sr) of 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 (Just rid, Just re) -> setReadMarkers model.apiUrl token rid re.eventId <| Just re.eventId
_ -> Cmd.none _ -> Cmd.none

View File

@ -90,7 +90,7 @@ sendMessage apiUrl token transactionId room msg contents = request
} }
sendMarkdownMessage : ApiUrl -> ApiToken -> Int -> RoomId -> String -> String -> Cmd Msg sendMarkdownMessage : ApiUrl -> ApiToken -> Int -> RoomId -> String -> String -> Cmd Msg
sendMarkdownMessage apiUrl token transactionId room message md = sendMessage apiUrl token transactionId room (SendRoomTextResponse transactionId) sendMarkdownMessage apiUrl token transactionId room message md = sendMessage apiUrl token transactionId room SendRoomTextResponse
[ ("msgtype", string "m.text") [ ("msgtype", string "m.text")
, ("body", string message) , ("body", string message)
, ("formatted_body", string md) , ("formatted_body", string md)
@ -98,7 +98,7 @@ sendMarkdownMessage apiUrl token transactionId room message md = sendMessage api
] ]
sendTextMessage : ApiUrl -> ApiToken -> Int -> RoomId -> String -> Cmd Msg sendTextMessage : ApiUrl -> ApiToken -> Int -> RoomId -> String -> Cmd Msg
sendTextMessage apiUrl token transactionId room message = sendMessage apiUrl token transactionId room (SendRoomTextResponse transactionId) sendTextMessage apiUrl token transactionId room message = sendMessage apiUrl token transactionId room SendRoomTextResponse
[ ("msgtype", string "m.text") [ ("msgtype", string "m.text")
, ("body", string message) , ("body", string message)
] ]

View File

@ -1,30 +0,0 @@
module Scylla.Messages exposing (..)
import Scylla.Sync exposing (RoomEvent)
import Scylla.Login exposing (Username)
type SendingMessage = TextMessage String
type Message =
Sending SendingMessage
| Received RoomEvent
messageUsername : Username -> Message -> Username
messageUsername u msg = case msg of
Sending _ -> u
Received re -> re.sender
mergeMessages : Username -> List Message -> List (Username, List Message)
mergeMessages du xs =
let
initialState = (Nothing, [], [])
appendNamed mu ms msl = case mu of
Just u -> msl ++ [(u, ms)]
Nothing -> msl
foldFunction msg (pu, ms, msl) =
let
nu = Just <| messageUsername 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

View File

@ -4,7 +4,6 @@ import Scylla.Sync exposing (SyncResponse, HistoryResponse, JoinedRoom, senderNa
import Scylla.Login exposing (LoginResponse, Username, Password) import Scylla.Login exposing (LoginResponse, Username, Password)
import Scylla.UserData exposing (UserData) import Scylla.UserData exposing (UserData)
import Scylla.Route exposing (Route(..), RoomId) import Scylla.Route exposing (Route(..), RoomId)
import Scylla.Messages exposing (..)
import Scylla.Storage exposing (..) import Scylla.Storage exposing (..)
import Scylla.Markdown exposing (..) import Scylla.Markdown exposing (..)
import Browser.Navigation as Nav import Browser.Navigation as Nav
@ -27,8 +26,7 @@ type alias Model =
, apiUrl : ApiUrl , apiUrl : ApiUrl
, sync : SyncResponse , sync : SyncResponse
, errors : List String , errors : List String
, roomText : Dict RoomId String , roomText : Dict String String
, sending : Dict Int (RoomId, SendingMessage)
, transactionId : Int , transactionId : Int
, userData : Dict Username UserData , userData : Dict Username UserData
, connected : Bool , connected : Bool
@ -44,7 +42,7 @@ type Msg =
| ChangeRoute Route -- URL changes | ChangeRoute Route -- URL changes
| ChangeRoomText String String -- Change to a room's input text | ChangeRoomText String String -- Change to a room's input text
| SendRoomText String -- Sends a message typed into a given room's input | SendRoomText String -- Sends a message typed into a given room's input
| SendRoomTextResponse Int (Result Http.Error ()) -- A send message response finished | SendRoomTextResponse (Result Http.Error ()) -- A send message response finished
| ViewportAfterMessage (Result Browser.Dom.Error Viewport) -- A message has been received, try scroll (maybe) | ViewportAfterMessage (Result Browser.Dom.Error Viewport) -- A message has been received, try scroll (maybe)
| ViewportChangeComplete (Result Browser.Dom.Error ()) -- We're done changing the viewport. | ViewportChangeComplete (Result Browser.Dom.Error ()) -- We're done changing the viewport.
| ReceiveFirstSyncResponse (Result Http.Error SyncResponse) -- HTTP, Sync has finished | ReceiveFirstSyncResponse (Result Http.Error SyncResponse) -- HTTP, Sync has finished
@ -81,12 +79,12 @@ loginUrl = Url.Builder.absolute [ "login" ] []
newUsers : Model -> List Username -> List Username newUsers : Model -> List Username -> List Username
newUsers m lus = List.filter (\u -> not <| Dict.member u m.userData) lus newUsers m lus = List.filter (\u -> not <| Dict.member u m.userData) lus
joinedRooms : Model -> Dict RoomId JoinedRoom
joinedRooms m = Maybe.withDefault Dict.empty <| Maybe.andThen .join <| m.sync.rooms
currentRoom : Model -> Maybe JoinedRoom currentRoom : Model -> Maybe JoinedRoom
currentRoom m = currentRoom m =
Maybe.andThen (\s -> Dict.get s <| joinedRooms m) <| currentRoomId m let
roomDict = Maybe.withDefault Dict.empty <| Maybe.andThen .join <| m.sync.rooms
in
Maybe.andThen (\s -> Dict.get s roomDict) <| currentRoomId m
currentRoomId : Model -> Maybe RoomId currentRoomId : Model -> Maybe RoomId
currentRoomId m = case m.route of currentRoomId m = case m.route of

View File

@ -1,37 +0,0 @@
module Scylla.Room exposing (..)
import Scylla.Model exposing (..)
import Scylla.Sync exposing (..)
import Scylla.Messages exposing (..)
import Scylla.Route exposing (..)
import Dict
type alias RoomData =
{ joinedRoom : JoinedRoom
, sendingMessages : List (SendingMessage, Int)
, inputText : Maybe String
}
roomData : Model -> RoomId -> Maybe RoomData
roomData m rid =
case Dict.get rid (joinedRooms m) of
Just jr -> Just
{ joinedRoom = jr
, sendingMessages = List.map (\(tid, (_, sm)) -> (sm, tid)) <| List.filter (\(_, (nrid, _)) -> nrid == rid) <| Dict.toList m.sending
, inputText = Dict.get rid m.roomText
}
Nothing -> Nothing
currentRoomData : Model -> Maybe RoomData
currentRoomData m = Maybe.andThen (roomData m) <| currentRoomId m
extractMessageEvents : List RoomEvent -> List Message
extractMessageEvents es = List.map Received
<| List.filter (\e -> e.type_ == "m.room.message") es
extractMessages : RoomData -> List Message
extractMessages rd =
let
eventMessages = extractMessageEvents <| Maybe.withDefault [] <| Maybe.andThen .events rd.joinedRoom.timeline
sendingMessages = List.map (\(sm, i) -> Sending sm) rd.sendingMessages
in
eventMessages ++ sendingMessages

View File

@ -3,8 +3,6 @@ import Scylla.Model exposing (..)
import Scylla.Sync exposing (..) import Scylla.Sync exposing (..)
import Scylla.Route exposing (..) import Scylla.Route exposing (..)
import Scylla.Fnv as Fnv import Scylla.Fnv as Fnv
import Scylla.Room exposing (..)
import Scylla.Messages exposing (..)
import Scylla.Login exposing (Username) import Scylla.Login exposing (Username)
import Scylla.Http exposing (fullMediaUrl) import Scylla.Http exposing (fullMediaUrl)
import Scylla.Api exposing (ApiUrl) import Scylla.Api exposing (ApiUrl)
@ -14,7 +12,7 @@ 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, h3, table, td, tr, img, textarea, video, source, p) import Html exposing (Html, Attribute, div, input, text, button, div, span, a, h2, h3, table, td, tr, img, textarea, video, source)
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, controls, src)
import Html.Events exposing (onInput, onClick, preventDefaultOn) import Html.Events exposing (onInput, onClick, preventDefaultOn)
import Dict exposing (Dict) import Dict exposing (Dict)
@ -43,8 +41,9 @@ stringColor s =
viewFull : Model -> List (Html Msg) viewFull : Model -> List (Html Msg)
viewFull model = viewFull model =
let let
room r = Maybe.map (\rd -> (r, rd)) room r = Maybe.map (\jr -> (r, jr))
<| roomData model r <| Maybe.andThen (Dict.get r)
<| Maybe.andThen .join model.sync.rooms
core = case model.route of core = case model.route of
Login -> loginView model Login -> loginView model
Base -> baseView model Nothing Base -> baseView model Nothing
@ -60,7 +59,7 @@ errorsView = div [ class "errors-wrapper" ] << List.indexedMap errorView
errorView : Int -> String -> Html Msg errorView : Int -> String -> Html Msg
errorView i s = div [ class "error-wrapper", onClick <| DismissError i ] [ iconView "alert-triangle", text s ] errorView i s = div [ class "error-wrapper", onClick <| DismissError i ] [ iconView "alert-triangle", text s ]
baseView : Model -> Maybe (String, RoomData) -> Html Msg baseView : Model -> Maybe (String, JoinedRoom) -> Html Msg
baseView m jr = baseView m jr =
let let
roomView = Maybe.map (\(id, r) -> joinedRoomView m id r) jr roomView = Maybe.map (\(id, r) -> joinedRoomView m id r) jr
@ -137,12 +136,13 @@ loginView m = div [ class "login-wrapper" ]
, button [ onClick AttemptLogin ] [ text "Log In" ] , button [ onClick AttemptLogin ] [ text "Log In" ]
] ]
joinedRoomView : Model -> RoomId -> RoomData -> Html Msg joinedRoomView : Model -> RoomId -> JoinedRoom -> Html Msg
joinedRoomView m roomId rd = joinedRoomView m roomId jr =
let let
renderedMessages = List.map (userMessagesView m) <| mergeMessages m.loginUsername <| extractMessages rd events = Maybe.withDefault [] <| Maybe.andThen .events jr.timeline
messagesWrapper = messagesWrapperView m roomId renderedMessages renderedEvents = List.filterMap (eventView m) events
typing = List.map (displayName m) <| roomTypingUsers rd.joinedRoom eventWrapper = eventWrapperView m roomId renderedEvents
typing = List.map (displayName m) <| roomTypingUsers jr
typingText = String.join ", " typing typingText = String.join ", " typing
typingSuffix = case List.length typing of typingSuffix = case List.length typing of
0 -> "" 0 -> ""
@ -162,8 +162,8 @@ joinedRoomView m roomId rd =
] ]
in in
div [ class "room-wrapper" ] div [ class "room-wrapper" ]
[ h2 [] [ text <| Maybe.withDefault "<No Name>" <| roomName rd.joinedRoom ] [ h2 [] [ text <| Maybe.withDefault "<No Name>" <| roomName jr ]
, messagesWrapper , eventWrapper
, typingWrapper , typingWrapper
, messageInput , messageInput
] ]
@ -187,62 +187,58 @@ iconView name =
[ Svg.Attributes.class "feather-icon" [ Svg.Attributes.class "feather-icon"
] [ Svg.use [ Svg.Attributes.xlinkHref (url ++ "#" ++ name) ] [] ] ] [ Svg.use [ Svg.Attributes.xlinkHref (url ++ "#" ++ name) ] [] ]
messagesWrapperView : Model -> RoomId -> List (Html Msg) -> Html Msg eventWrapperView : Model -> RoomId -> List (Html Msg) -> Html Msg
messagesWrapperView m rid es = div [ class "messages-wrapper", id "messages-wrapper" ] eventWrapperView m rid es = div [ class "events-wrapper", id "events-wrapper" ]
[ a [ class "history-link", onClick <| History rid ] [ text "Load older messages" ] [ a [ class "history-link", onClick <| History rid ] [ text "Load older messages" ]
, table [ class "messages-table" ] es , table [ class "events-table" ] es
] ]
senderView : Model -> Username -> Html Msg eventView : Model -> RoomEvent -> Maybe (Html Msg)
senderView m s = eventView m re =
span [ style "color" <| stringColor s, class "sender-wrapper" ] [ text <| displayName m s ]
userMessagesView : Model -> (Username, List Message) -> Html Msg
userMessagesView m (u, ms) =
let let
wrap h = div [ class "message" ] [ h ] viewFunction = case re.type_ of
in "m.room.message" -> Just messageView
tr [] _ -> Nothing
[ td [] [ senderView m u ] createRow mhtml = tr []
, td [] <| List.map wrap <| List.filterMap (messageView m) ms [ td [] [ eventSenderView m re.sender ]
, td [] [ mhtml ]
] ]
in
Maybe.map createRow
<| Maybe.andThen (\f -> f m re) viewFunction
messageView : Model -> Message -> Maybe (Html Msg) eventSenderView : Model -> Username -> Html Msg
messageView m msg = case msg of eventSenderView m s =
Sending t -> Just <| sendingMessageView m t span [ style "background-color" <| stringColor s, class "sender-wrapper" ] [ text <| displayName m s ]
Received re -> roomEventView m re
sendingMessageView : Model -> SendingMessage -> Html Msg messageView : Model -> RoomEvent -> Maybe (Html Msg)
sendingMessageView m msg = case msg of messageView m re =
TextMessage t -> text t
roomEventView : Model -> RoomEvent -> Maybe (Html Msg)
roomEventView m re =
let let
msgtype = Decode.decodeValue (Decode.field "msgtype" Decode.string) re.content msgtype = Decode.decodeValue (Decode.field "msgtype" Decode.string) re.content
in in
case msgtype of case msgtype of
Ok "m.text" -> roomEventTextView m re Ok "m.text" -> messageTextView m re
Ok "m.image" -> roomEventImageView m re Ok "m.image" -> messageImageView m re
Ok "m.file" -> roomEventFileView m re Ok "m.file" -> messageFileView m re
Ok "m.video" -> roomEventVideoView m re Ok "m.video" -> messageVideoView m re
_ -> Nothing _ -> Nothing
roomEventTextView : Model -> RoomEvent -> Maybe (Html Msg) messageTextView : Model -> RoomEvent -> Maybe (Html Msg)
roomEventTextView m re = messageTextView m re =
let let
body = Decode.decodeValue (Decode.field "body" Decode.string) re.content body = Decode.decodeValue (Decode.field "body" Decode.string) re.content
customHtml = Maybe.map Html.Parser.Util.toVirtualDom customHtml = Maybe.map Html.Parser.Util.toVirtualDom
<| Maybe.andThen (Result.toMaybe << Html.Parser.run ) <| Maybe.andThen (Result.toMaybe << Html.Parser.run )
<| Result.toMaybe <| Result.toMaybe
<| Decode.decodeValue (Decode.field "formatted_body" Decode.string) re.content <| Decode.decodeValue (Decode.field "formatted_body" Decode.string) re.content
wrap mtext = span [] [ text mtext ]
in in
case customHtml of case customHtml of
Just c -> Just <| div [] c Just c -> Just <| div [ class "markdown-wrapper" ] c
Nothing -> Maybe.map (p [] << List.singleton << text) <| Result.toMaybe body Nothing -> Maybe.map wrap <| Result.toMaybe body
roomEventImageView : Model -> RoomEvent -> Maybe (Html Msg) messageImageView : Model -> RoomEvent -> Maybe (Html Msg)
roomEventImageView m re = messageImageView m re =
let let
body = Decode.decodeValue (Decode.field "url" Decode.string) re.content body = Decode.decodeValue (Decode.field "url" Decode.string) re.content
in in
@ -250,8 +246,8 @@ roomEventImageView m re =
<| Maybe.map (contentRepositoryDownloadUrl m.apiUrl) <| Maybe.map (contentRepositoryDownloadUrl m.apiUrl)
<| Result.toMaybe body <| Result.toMaybe body
roomEventFileView : Model -> RoomEvent -> Maybe (Html Msg) messageFileView : Model -> RoomEvent -> Maybe (Html Msg)
roomEventFileView m re = messageFileView m re =
let let
decoder = Decode.map2 (\l r -> (l, r)) (Decode.field "url" Decode.string) (Decode.field "body" Decode.string) decoder = Decode.map2 (\l r -> (l, r)) (Decode.field "url" Decode.string) (Decode.field "body" Decode.string)
fileData = Decode.decodeValue decoder re.content fileData = Decode.decodeValue decoder re.content
@ -260,8 +256,8 @@ roomEventFileView m re =
<| Maybe.map (\(url, name) -> (contentRepositoryDownloadUrl m.apiUrl url, name)) <| Maybe.map (\(url, name) -> (contentRepositoryDownloadUrl m.apiUrl url, name))
<| Result.toMaybe fileData <| Result.toMaybe fileData
roomEventVideoView : Model -> RoomEvent -> Maybe (Html Msg) messageVideoView : Model -> RoomEvent -> Maybe (Html Msg)
roomEventVideoView m re = messageVideoView m re =
let let
decoder = Decode.map2 (\l r -> (l, r)) decoder = Decode.map2 (\l r -> (l, r))
(Decode.field "url" Decode.string) (Decode.field "url" Decode.string)

View File

@ -4,7 +4,6 @@ $primary-color-highlight: #4298C7;
$primary-color-light: #9FDBFB; $primary-color-light: #9FDBFB;
$background-color: #1b1e21; $background-color: #1b1e21;
$background-color-light: lighten($background-color, 4%);
$background-color-dark: darken($background-color, 4%); $background-color-dark: darken($background-color, 4%);
$error-color: #f01d43; $error-color: #f01d43;
@ -17,8 +16,6 @@ $inactive-input-border-color: darken($inactive-input-color, 10%);
$transition-duration: .125s; $transition-duration: .125s;
$inset-shadow: inset 0px 0px 5px rgba(0, 0, 0, .25);
html, body { html, body {
height: 100vh; height: 100vh;
} }
@ -80,7 +77,6 @@ h2, h3 {
} }
a.file-wrapper { a.file-wrapper {
padding: 5px 0px 5px 0px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -145,7 +141,7 @@ div.base-wrapper {
height: 100%; height: 100%;
> div { > div {
padding: 10px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
} }
} }
@ -155,7 +151,6 @@ div.base-wrapper {
*/ */
div.rooms-wrapper { div.rooms-wrapper {
flex-shrink: 0; flex-shrink: 0;
background-color: $background-color-light;
} }
div.room-link-wrapper { div.room-link-wrapper {
@ -191,8 +186,6 @@ div.room-wrapper {
display: flex; display: flex;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
box-shadow: $inset-shadow;
padding: 5px;
} }
div.typing-wrapper { div.typing-wrapper {
@ -219,7 +212,7 @@ div.message-wrapper {
} }
} }
div.messages-wrapper { div.events-wrapper {
overflow-y: scroll; overflow-y: scroll;
flex-grow: 1; flex-grow: 1;
@ -232,7 +225,7 @@ div.messages-wrapper {
} }
} }
table.messages-table { table.events-table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;
@ -259,9 +252,13 @@ table.messages-table {
} }
white-space: nowrap; white-space: nowrap;
} }
tr:nth-child(2n) {
background-color: $background-color-dark;
}
} }
div.message { div.markdown-wrapper {
p { p {
margin: 0px; margin: 0px;
} }
@ -284,7 +281,7 @@ div.message {
padding: 10px; padding: 10px;
background-color: $background-color; background-color: $background-color;
border-radius: 3px; border-radius: 3px;
box-shadow: $inset-shadow; box-shadow: inset 0px 0px 5px rgba(0, 0, 0, .15);
} }
} }
@ -294,8 +291,7 @@ span.sender-wrapper {
padding-right: 5px; padding-right: 5px;
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
text-align: right; text-align: center;
font-weight: 800;
width: 100%; width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;