From 75971fec66d99a069585813c9be54cc9add83437 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Wed, 5 Apr 2023 13:57:25 +0200 Subject: [PATCH] Add temporary events The SDK now supports temporarily showing events before getting them from sync. One example is to let users show the messages they sent themselves before the sync confirms that their events are on the timeline. --- src/Internal/Api/SendMessageEvent/Main.elm | 2 +- src/Internal/Api/SendStateKey/Main.elm | 2 +- src/Internal/Api/Task.elm | 2 + src/Internal/Api/VaultUpdate.elm | 19 ++++++++- src/Internal/Room.elm | 1 + src/Internal/Tools/Context.elm | 24 +++++++++++ src/Internal/Values/Room.elm | 38 ++++++++++++++++- src/Internal/Values/Vault.elm | 18 +++++++++ src/Internal/Vault.elm | 47 ++++++++++++++++++---- 9 files changed, 140 insertions(+), 13 deletions(-) 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