diff --git a/src/Internal/Api/SendMessageEvent/Main.elm b/src/Internal/Api/SendMessageEvent/Main.elm index efaf4ad..9c6b68e 100644 --- a/src/Internal/Api/SendMessageEvent/Main.elm +++ b/src/Internal/Api/SendMessageEvent/Main.elm @@ -7,7 +7,7 @@ import Internal.Tools.VersionControl as VC import Task exposing (Task) -sendMessageEvent : Context (VBAT a) -> SendMessageEventInput -> Task X.Error SendMessageEventOutput +sendMessageEvent : Context (VBAT { a | timestamp : () }) -> SendMessageEventInput -> Task X.Error SendMessageEventOutput sendMessageEvent context input = VC.withBottomLayer { current = Api.sendMessageEventV1 diff --git a/src/Internal/Api/SendStateKey/Main.elm b/src/Internal/Api/SendStateKey/Main.elm index 6871b1d..a71ba6a 100644 --- a/src/Internal/Api/SendStateKey/Main.elm +++ b/src/Internal/Api/SendStateKey/Main.elm @@ -7,7 +7,7 @@ import Internal.Tools.VersionControl as VC import Task exposing (Task) -sendStateKey : Context (VBA a) -> SendStateKeyInput -> Task X.Error SendStateKeyOutput +sendStateKey : Context (VBA { a | timestamp : () }) -> SendStateKeyInput -> Task X.Error SendStateKeyOutput sendStateKey context input = VC.withBottomLayer { current = Api.sendStateKeyV1 diff --git a/src/Internal/Api/Task.elm b/src/Internal/Api/Task.elm index 71eaad3..e13db66 100644 --- a/src/Internal/Api/Task.elm +++ b/src/Internal/Api/Task.elm @@ -123,6 +123,7 @@ sendMessageEvent { content, eventType, extraTransactionNoise, roomId } cred = |> List.foldl Hash.independent (Hash.fromString "send message") |> Hash.toString ) + |> Chain.andThen C.getTimestamp |> Chain.andThen (C.sendMessageEvent { content = content, eventType = eventType, roomId = roomId }) |> Chain.andThen (Chain.maybe <| C.getEvent { roomId = roomId }) @@ -132,6 +133,7 @@ sendMessageEvent { content, eventType, extraTransactionNoise, roomId } cred = sendStateEvent : SendStateKeyInput -> Credentials -> FutureTask sendStateEvent data cred = C.makeVBA cred + |> Chain.andThen C.getTimestamp |> Chain.andThen (C.sendStateEvent data) |> Chain.andThen (Chain.maybe <| C.getEvent { roomId = data.roomId }) diff --git a/src/Internal/Api/VaultUpdate.elm b/src/Internal/Api/VaultUpdate.elm index 3af9502..f12420a 100644 --- a/src/Internal/Api/VaultUpdate.elm +++ b/src/Internal/Api/VaultUpdate.elm @@ -21,6 +21,7 @@ import Internal.Api.WhoAmI.Main as WhoAmI import Internal.Tools.Context as Context exposing (VB, VBA, VBAT) import Internal.Tools.Exceptions as X import Internal.Tools.LoginValues exposing (AccessToken(..)) +import Internal.Tools.Timestamp exposing (Timestamp) import Task exposing (Task) import Time @@ -30,6 +31,7 @@ type VaultUpdate -- Updates as a result of API calls | AccountDataSet SetAccountData.SetAccountInput SetAccountData.SetAccountOutput | BanUser Ban.BanInput Ban.BanOutput + | CurrentTimestamp Timestamp | GetEvent GetEvent.EventInput GetEvent.EventOutput | GetMessages GetMessages.GetMessagesInput GetMessages.GetMessagesOutput | InviteSent Invite.InviteInput Invite.InviteOutput @@ -178,6 +180,19 @@ getMessages input = input +getTimestamp : TaskChain VaultUpdate a { a | timestamp : () } +getTimestamp = + toChain + (\output -> + Chain.TaskChainPiece + { contextChange = Context.setTimestamp output + , messages = [ CurrentTimestamp output ] + } + ) + (always <| always Time.now) + () + + {-| Get the supported spec versions from the homeserver. -} getVersions : TaskChain VaultUpdate { a | baseUrl : () } (VB a) @@ -325,7 +340,7 @@ redact input = {-| Send a message event to a room. -} -sendMessageEvent : SendMessageEvent.SendMessageEventInput -> TaskChain VaultUpdate (VBAT a) (VBA { a | sentEvent : () }) +sendMessageEvent : SendMessageEvent.SendMessageEventInput -> TaskChain VaultUpdate (VBAT { a | timestamp : () }) (VBA { a | sentEvent : (), timestamp : () }) sendMessageEvent input = toChain (\output -> @@ -341,7 +356,7 @@ sendMessageEvent input = {-| Send a state key event to a room. -} -sendStateEvent : SendStateKey.SendStateKeyInput -> TaskChain VaultUpdate (VBA a) (VBA { a | sentEvent : () }) +sendStateEvent : SendStateKey.SendStateKeyInput -> TaskChain VaultUpdate (VBA { a | timestamp : () }) (VBA { a | sentEvent : (), timestamp : () }) sendStateEvent input = toChain (\output -> diff --git a/src/Internal/Room.elm b/src/Internal/Room.elm index 10be7e4..6d13c07 100644 --- a/src/Internal/Room.elm +++ b/src/Internal/Room.elm @@ -58,6 +58,7 @@ initFromJoinedRoom data jroom = |> List.map (Event.initFromClientEventWithoutRoomId data.roomId) |> Hashdict.fromList IEvent.eventId , roomId = data.roomId + , tempEvents = [] , timeline = jroom.timeline |> Maybe.map diff --git a/src/Internal/Tools/Context.elm b/src/Internal/Tools/Context.elm index d880f20..c130a84 100644 --- a/src/Internal/Tools/Context.elm +++ b/src/Internal/Tools/Context.elm @@ -16,6 +16,7 @@ Additionaly, there are remove functions which are intended to tell the compiler import Internal.Config.Leaking as L import Internal.Tools.LoginValues exposing (AccessToken(..)) +import Internal.Tools.Timestamp exposing (Timestamp) type Context a @@ -24,6 +25,7 @@ type Context a , baseUrl : String , loginParts : Maybe LoginParts , sentEvent : String + , timestamp : Timestamp , transactionId : String , userId : String , versions : List String @@ -59,6 +61,7 @@ init = , baseUrl = L.baseUrl , loginParts = Nothing , sentEvent = L.eventId + , timestamp = L.originServerTs , transactionId = L.transactionId , userId = L.sender , versions = L.versions @@ -93,6 +96,13 @@ getSentEvent (Context { sentEvent }) = sentEvent +{-| Get the event that has been sent to the API recently. +-} +getTimestamp : Context { a | timestamp : () } -> Timestamp +getTimestamp (Context { timestamp }) = + timestamp + + {-| Get the transaction id from the Context. -} getTransactionId : Context { a | transactionId : () } -> String @@ -135,6 +145,13 @@ setSentEvent sentEvent (Context data) = Context { data | sentEvent = sentEvent } +{-| Insert a sent event id into the context. +-} +setTimestamp : Timestamp -> Context a -> Context { a | timestamp : () } +setTimestamp timestamp (Context data) = + Context { data | timestamp = timestamp } + + {-| Insert a transaction id into the context. -} setTransactionId : String -> Context a -> Context { a | transactionId : () } @@ -177,6 +194,13 @@ removeSentEvent (Context data) = Context data +{-| Remove the sent event's id from the Context +-} +removeTimestamp : Context { a | timestamp : () } -> Context a +removeTimestamp (Context data) = + Context data + + {-| Remove the transaction id from the Context -} removeTransactionId : Context { a | transactionId : () } -> Context a diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index fb8f760..260bb50 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -3,7 +3,8 @@ module Internal.Values.Room exposing (..) import Dict exposing (Dict) import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) import Internal.Tools.SpecEnums exposing (SessionDescriptionType(..)) -import Internal.Values.Event exposing (BlindEvent, IEvent) +import Internal.Tools.Timestamp exposing (Timestamp) +import Internal.Values.Event as IEvent exposing (BlindEvent, IEvent) import Internal.Values.StateManager as StateManager exposing (StateManager) import Internal.Values.Timeline as Timeline exposing (Timeline) import Json.Encode as E @@ -15,6 +16,7 @@ type IRoom , ephemeral : List BlindEvent , events : Hashdict IEvent , roomId : String + , tempEvents : List IEvent , timeline : Timeline } @@ -33,6 +35,35 @@ addEvent event (IRoom ({ events } as room)) = IRoom { room | events = Hashdict.insert event events } +{-| Sometimes, we know that an event exists before the API has told us. +For example, when we send an event to a room but we haven't synced up yet. + +In such a case, it is better to "temporarily" store the event until the next sync - +this prevents temporary jittering for a user where events can sometimes disappear and reappear +back and forth for a few seconds. + +-} +addTemporaryEvent : { content : E.Value, eventId : String, eventType : String, originServerTs : Timestamp, sender : String, stateKey : Maybe String } -> IRoom -> IRoom +addTemporaryEvent data (IRoom ({ tempEvents } as room)) = + IRoom + { room + | tempEvents = + List.append tempEvents + ({ content = data.content + , eventId = data.eventId + , originServerTs = data.originServerTs + , roomId = room.roomId + , sender = data.sender + , stateKey = data.stateKey + , eventType = data.eventType + , unsigned = Nothing + } + |> IEvent.init + |> List.singleton + ) + } + + {-| Add new events as the most recent events. -} addEvents : @@ -49,6 +80,7 @@ addEvents ({ events } as data) (IRoom room) = { room | events = List.foldl Hashdict.insert room.events events , timeline = Timeline.addNewEvents data room.timeline + , tempEvents = [] } @@ -101,7 +133,9 @@ latestGap (IRoom room) = -} mostRecentEvents : IRoom -> List IEvent mostRecentEvents (IRoom room) = - Timeline.mostRecentEvents room.timeline + List.append + (Timeline.mostRecentEvents room.timeline) + room.tempEvents {-| Get the room's id. diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index 20985c4..af7b7c8 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -5,7 +5,9 @@ It handles all communication with the homeserver. -} import Dict exposing (Dict) +import Internal.Config.Leaking as L import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) +import Internal.Tools.Timestamp exposing (Timestamp) import Internal.Values.Room as Room exposing (IRoom) import Internal.Values.RoomInvite as Invite exposing (IRoomInvite) import Json.Encode as E @@ -15,6 +17,7 @@ type IVault = IVault { accountData : Dict String E.Value , invites : List IRoomInvite + , latestUpdate : Timestamp , rooms : Hashdict IRoom , since : Maybe String } @@ -76,6 +79,7 @@ init = IVault { accountData = Dict.empty , invites = [] + , latestUpdate = L.originServerTs , rooms = Hashdict.empty Room.roomId , since = Nothing } @@ -109,6 +113,20 @@ insertRoom room (IVault data) = { data | rooms = Hashdict.insert room data.rooms } +{-| Insert a timestamp of when a timestamp was last delivered. +-} +insertTimestamp : Timestamp -> IVault -> IVault +insertTimestamp time (IVault data) = + IVault { data | latestUpdate = time } + + +{-| Last time the vault was updated. Often used as an approximation. +-} +lastUpdate : IVault -> Timestamp +lastUpdate (IVault { latestUpdate }) = + latestUpdate + + {-| Remove an invite. This is usually done when the invite has been accepted or rejected. -} removeInvite : String -> IVault -> IVault diff --git a/src/Internal/Vault.elm b/src/Internal/Vault.elm index e0a5987..fa5041d 100644 --- a/src/Internal/Vault.elm +++ b/src/Internal/Vault.elm @@ -128,6 +128,9 @@ updateWith vaultUpdate ((Vault ({ cred, context } as data)) as vault) = BanUser input () -> vault + CurrentTimestamp t -> + Vault { cred = Internal.insertTimestamp t cred, context = context } + GetEvent input output -> case getRoomById input.roomId vault of Just room -> @@ -215,23 +218,53 @@ updateWith vaultUpdate ((Vault ({ cred, context } as data)) as vault) = |> Vault -- TODO - LeftRoom input _ -> + LeftRoom input () -> cred |> Internal.removeInvite input.roomId |> (\x -> { cred = x, context = context }) |> Vault - -- TODO - MessageEventSent _ _ -> - vault + MessageEventSent { content, eventType, roomId } { eventId } -> + Maybe.map2 + (\room sender -> + room + |> Room.withoutCredentials + |> IRoom.addTemporaryEvent + { content = content + , eventType = eventType + , eventId = eventId + , originServerTs = Internal.lastUpdate cred + , sender = sender + , stateKey = Nothing + } + ) + (getRoomById roomId vault) + (getUsername vault) + |> Maybe.map (Room.withCredentials context >> insertRoom >> (|>) vault) + |> Maybe.withDefault vault -- TODO RedactedEvent _ _ -> vault - -- TODO - StateEventSent _ _ -> - vault + StateEventSent { content, eventType, roomId, stateKey } { eventId } -> + Maybe.map2 + (\room sender -> + room + |> Room.withoutCredentials + |> IRoom.addTemporaryEvent + { content = content + , eventType = eventType + , eventId = eventId + , originServerTs = Internal.lastUpdate cred + , sender = sender + , stateKey = Just stateKey + } + ) + (getRoomById roomId vault) + (getUsername vault) + |> Maybe.map (Room.withCredentials context >> insertRoom >> (|>) vault) + |> Maybe.withDefault vault SyncUpdate input output -> let