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)
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

View File

@ -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

View File

@ -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 })

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.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 ->

View File

@ -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

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.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

View File

@ -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.

View File

@ -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

View File

@ -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