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.
pull/1/head
Bram van den Heuvel 2023-04-05 13:57:25 +02:00
parent 70cbe5b682
commit 75971fec66
9 changed files with 140 additions and 13 deletions

View File

@ -7,7 +7,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task) 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 = sendMessageEvent context input =
VC.withBottomLayer VC.withBottomLayer
{ current = Api.sendMessageEventV1 { current = Api.sendMessageEventV1

View File

@ -7,7 +7,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task) 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 = sendStateKey context input =
VC.withBottomLayer VC.withBottomLayer
{ current = Api.sendStateKeyV1 { current = Api.sendStateKeyV1

View File

@ -123,6 +123,7 @@ sendMessageEvent { content, eventType, extraTransactionNoise, roomId } cred =
|> List.foldl Hash.independent (Hash.fromString "send message") |> List.foldl Hash.independent (Hash.fromString "send message")
|> Hash.toString |> Hash.toString
) )
|> Chain.andThen C.getTimestamp
|> Chain.andThen (C.sendMessageEvent { content = content, eventType = eventType, roomId = roomId }) |> Chain.andThen (C.sendMessageEvent { content = content, eventType = eventType, roomId = roomId })
|> Chain.andThen |> Chain.andThen
(Chain.maybe <| C.getEvent { roomId = roomId }) (Chain.maybe <| C.getEvent { roomId = roomId })
@ -132,6 +133,7 @@ sendMessageEvent { content, eventType, extraTransactionNoise, roomId } cred =
sendStateEvent : SendStateKeyInput -> Credentials -> FutureTask sendStateEvent : SendStateKeyInput -> Credentials -> FutureTask
sendStateEvent data cred = sendStateEvent data cred =
C.makeVBA cred C.makeVBA cred
|> Chain.andThen C.getTimestamp
|> Chain.andThen (C.sendStateEvent data) |> Chain.andThen (C.sendStateEvent data)
|> Chain.andThen |> Chain.andThen
(Chain.maybe <| C.getEvent { roomId = data.roomId }) (Chain.maybe <| C.getEvent { roomId = data.roomId })

View File

@ -21,6 +21,7 @@ import Internal.Api.WhoAmI.Main as WhoAmI
import Internal.Tools.Context as Context exposing (VB, VBA, VBAT) import Internal.Tools.Context as Context exposing (VB, VBA, VBAT)
import Internal.Tools.Exceptions as X import Internal.Tools.Exceptions as X
import Internal.Tools.LoginValues exposing (AccessToken(..)) import Internal.Tools.LoginValues exposing (AccessToken(..))
import Internal.Tools.Timestamp exposing (Timestamp)
import Task exposing (Task) import Task exposing (Task)
import Time import Time
@ -30,6 +31,7 @@ type VaultUpdate
-- Updates as a result of API calls -- Updates as a result of API calls
| AccountDataSet SetAccountData.SetAccountInput SetAccountData.SetAccountOutput | AccountDataSet SetAccountData.SetAccountInput SetAccountData.SetAccountOutput
| BanUser Ban.BanInput Ban.BanOutput | BanUser Ban.BanInput Ban.BanOutput
| CurrentTimestamp Timestamp
| GetEvent GetEvent.EventInput GetEvent.EventOutput | GetEvent GetEvent.EventInput GetEvent.EventOutput
| GetMessages GetMessages.GetMessagesInput GetMessages.GetMessagesOutput | GetMessages GetMessages.GetMessagesInput GetMessages.GetMessagesOutput
| InviteSent Invite.InviteInput Invite.InviteOutput | InviteSent Invite.InviteInput Invite.InviteOutput
@ -178,6 +180,19 @@ getMessages input =
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. {-| Get the supported spec versions from the homeserver.
-} -}
getVersions : TaskChain VaultUpdate { a | baseUrl : () } (VB a) getVersions : TaskChain VaultUpdate { a | baseUrl : () } (VB a)
@ -325,7 +340,7 @@ redact input =
{-| Send a message event to a room. {-| 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 = sendMessageEvent input =
toChain toChain
(\output -> (\output ->
@ -341,7 +356,7 @@ sendMessageEvent input =
{-| Send a state key event to a room. {-| 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 = sendStateEvent input =
toChain toChain
(\output -> (\output ->

View File

@ -58,6 +58,7 @@ initFromJoinedRoom data jroom =
|> List.map (Event.initFromClientEventWithoutRoomId data.roomId) |> List.map (Event.initFromClientEventWithoutRoomId data.roomId)
|> Hashdict.fromList IEvent.eventId |> Hashdict.fromList IEvent.eventId
, roomId = data.roomId , roomId = data.roomId
, tempEvents = []
, timeline = , timeline =
jroom.timeline jroom.timeline
|> Maybe.map |> Maybe.map

View File

@ -16,6 +16,7 @@ Additionaly, there are remove functions which are intended to tell the compiler
import Internal.Config.Leaking as L import Internal.Config.Leaking as L
import Internal.Tools.LoginValues exposing (AccessToken(..)) import Internal.Tools.LoginValues exposing (AccessToken(..))
import Internal.Tools.Timestamp exposing (Timestamp)
type Context a type Context a
@ -24,6 +25,7 @@ type Context a
, baseUrl : String , baseUrl : String
, loginParts : Maybe LoginParts , loginParts : Maybe LoginParts
, sentEvent : String , sentEvent : String
, timestamp : Timestamp
, transactionId : String , transactionId : String
, userId : String , userId : String
, versions : List String , versions : List String
@ -59,6 +61,7 @@ init =
, baseUrl = L.baseUrl , baseUrl = L.baseUrl
, loginParts = Nothing , loginParts = Nothing
, sentEvent = L.eventId , sentEvent = L.eventId
, timestamp = L.originServerTs
, transactionId = L.transactionId , transactionId = L.transactionId
, userId = L.sender , userId = L.sender
, versions = L.versions , versions = L.versions
@ -93,6 +96,13 @@ getSentEvent (Context { sentEvent }) =
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. {-| Get the transaction id from the Context.
-} -}
getTransactionId : Context { a | transactionId : () } -> String getTransactionId : Context { a | transactionId : () } -> String
@ -135,6 +145,13 @@ setSentEvent sentEvent (Context data) =
Context { data | sentEvent = sentEvent } 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. {-| Insert a transaction id into the context.
-} -}
setTransactionId : String -> Context a -> Context { a | transactionId : () } setTransactionId : String -> Context a -> Context { a | transactionId : () }
@ -177,6 +194,13 @@ removeSentEvent (Context data) =
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 {-| Remove the transaction id from the Context
-} -}
removeTransactionId : Context { a | transactionId : () } -> Context a removeTransactionId : Context { a | transactionId : () } -> Context a

View File

@ -3,7 +3,8 @@ module Internal.Values.Room exposing (..)
import Dict exposing (Dict) import Dict exposing (Dict)
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) import Internal.Tools.Hashdict as Hashdict exposing (Hashdict)
import Internal.Tools.SpecEnums exposing (SessionDescriptionType(..)) 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.StateManager as StateManager exposing (StateManager)
import Internal.Values.Timeline as Timeline exposing (Timeline) import Internal.Values.Timeline as Timeline exposing (Timeline)
import Json.Encode as E import Json.Encode as E
@ -15,6 +16,7 @@ type IRoom
, ephemeral : List BlindEvent , ephemeral : List BlindEvent
, events : Hashdict IEvent , events : Hashdict IEvent
, roomId : String , roomId : String
, tempEvents : List IEvent
, timeline : Timeline , timeline : Timeline
} }
@ -33,6 +35,35 @@ addEvent event (IRoom ({ events } as room)) =
IRoom { room | events = Hashdict.insert event events } 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. {-| Add new events as the most recent events.
-} -}
addEvents : addEvents :
@ -49,6 +80,7 @@ addEvents ({ events } as data) (IRoom room) =
{ room { room
| events = List.foldl Hashdict.insert room.events events | events = List.foldl Hashdict.insert room.events events
, timeline = Timeline.addNewEvents data room.timeline , timeline = Timeline.addNewEvents data room.timeline
, tempEvents = []
} }
@ -101,7 +133,9 @@ latestGap (IRoom room) =
-} -}
mostRecentEvents : IRoom -> List IEvent mostRecentEvents : IRoom -> List IEvent
mostRecentEvents (IRoom room) = mostRecentEvents (IRoom room) =
Timeline.mostRecentEvents room.timeline List.append
(Timeline.mostRecentEvents room.timeline)
room.tempEvents
{-| Get the room's id. {-| Get the room's id.

View File

@ -5,7 +5,9 @@ It handles all communication with the homeserver.
-} -}
import Dict exposing (Dict) import Dict exposing (Dict)
import Internal.Config.Leaking as L
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) 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.Room as Room exposing (IRoom)
import Internal.Values.RoomInvite as Invite exposing (IRoomInvite) import Internal.Values.RoomInvite as Invite exposing (IRoomInvite)
import Json.Encode as E import Json.Encode as E
@ -15,6 +17,7 @@ type IVault
= IVault = IVault
{ accountData : Dict String E.Value { accountData : Dict String E.Value
, invites : List IRoomInvite , invites : List IRoomInvite
, latestUpdate : Timestamp
, rooms : Hashdict IRoom , rooms : Hashdict IRoom
, since : Maybe String , since : Maybe String
} }
@ -76,6 +79,7 @@ init =
IVault IVault
{ accountData = Dict.empty { accountData = Dict.empty
, invites = [] , invites = []
, latestUpdate = L.originServerTs
, rooms = Hashdict.empty Room.roomId , rooms = Hashdict.empty Room.roomId
, since = Nothing , since = Nothing
} }
@ -109,6 +113,20 @@ insertRoom room (IVault data) =
{ data | rooms = Hashdict.insert room data.rooms } { 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. {-| Remove an invite. This is usually done when the invite has been accepted or rejected.
-} -}
removeInvite : String -> IVault -> IVault removeInvite : String -> IVault -> IVault

View File

@ -128,6 +128,9 @@ updateWith vaultUpdate ((Vault ({ cred, context } as data)) as vault) =
BanUser input () -> BanUser input () ->
vault vault
CurrentTimestamp t ->
Vault { cred = Internal.insertTimestamp t cred, context = context }
GetEvent input output -> GetEvent input output ->
case getRoomById input.roomId vault of case getRoomById input.roomId vault of
Just room -> Just room ->
@ -215,23 +218,53 @@ updateWith vaultUpdate ((Vault ({ cred, context } as data)) as vault) =
|> Vault |> Vault
-- TODO -- TODO
LeftRoom input _ -> LeftRoom input () ->
cred cred
|> Internal.removeInvite input.roomId |> Internal.removeInvite input.roomId
|> (\x -> { cred = x, context = context }) |> (\x -> { cred = x, context = context })
|> Vault |> Vault
-- TODO MessageEventSent { content, eventType, roomId } { eventId } ->
MessageEventSent _ _ -> Maybe.map2
vault (\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 -- TODO
RedactedEvent _ _ -> RedactedEvent _ _ ->
vault vault
-- TODO StateEventSent { content, eventType, roomId, stateKey } { eventId } ->
StateEventSent _ _ -> Maybe.map2
vault (\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 -> SyncUpdate input output ->
let let