Refactor Timeline architecture
parent
81b0b1c166
commit
79aff7af3b
|
@ -3,7 +3,7 @@ module Internal.Tools.Hashdict exposing
|
||||||
, empty, singleton, insert, remove, removeKey
|
, empty, singleton, insert, remove, removeKey
|
||||||
, isEmpty, member, memberKey, get, size, isEqual
|
, isEmpty, member, memberKey, get, size, isEqual
|
||||||
, keys, values, toList, fromList
|
, keys, values, toList, fromList
|
||||||
, rehash, union
|
, rehash, union, map
|
||||||
, encode, decoder, softDecoder
|
, encode, decoder, softDecoder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ This allows you to store values based on an externally defined identifier.
|
||||||
|
|
||||||
## Transform
|
## Transform
|
||||||
|
|
||||||
@docs rehash, union
|
@docs rehash, union, map
|
||||||
|
|
||||||
|
|
||||||
## JSON coders
|
## JSON coders
|
||||||
|
@ -173,6 +173,34 @@ keys (Hashdict h) =
|
||||||
Dict.keys h.values
|
Dict.keys h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Map a value on a given key. If the outcome of the function changes the hash,
|
||||||
|
the operation does nothing.
|
||||||
|
-}
|
||||||
|
map : String -> (a -> a) -> Hashdict a -> Hashdict a
|
||||||
|
map key f (Hashdict h) =
|
||||||
|
Hashdict
|
||||||
|
{ h
|
||||||
|
| values =
|
||||||
|
Dict.update
|
||||||
|
key
|
||||||
|
(Maybe.map
|
||||||
|
(\value ->
|
||||||
|
let
|
||||||
|
newValue : a
|
||||||
|
newValue =
|
||||||
|
f value
|
||||||
|
in
|
||||||
|
if h.hash newValue == h.hash value then
|
||||||
|
newValue
|
||||||
|
|
||||||
|
else
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
h.values
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Determine if a value's hash is in a hashdict.
|
{-| Determine if a value's hash is in a hashdict.
|
||||||
-}
|
-}
|
||||||
member : a -> Hashdict a -> Bool
|
member : a -> Hashdict a -> Bool
|
||||||
|
|
|
@ -3,7 +3,7 @@ module Internal.Tools.Mashdict exposing
|
||||||
, empty, singleton, insert, remove, removeKey
|
, empty, singleton, insert, remove, removeKey
|
||||||
, isEmpty, member, memberKey, get, size, isEqual
|
, isEmpty, member, memberKey, get, size, isEqual
|
||||||
, keys, values, toList, fromList
|
, keys, values, toList, fromList
|
||||||
, rehash, union
|
, rehash, union, map
|
||||||
, encode, decoder, softDecoder
|
, encode, decoder, softDecoder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ In general, you are advised to learn more about the
|
||||||
|
|
||||||
## Transform
|
## Transform
|
||||||
|
|
||||||
@docs rehash, union
|
@docs rehash, union, map
|
||||||
|
|
||||||
|
|
||||||
## JSON coders
|
## JSON coders
|
||||||
|
@ -191,6 +191,34 @@ keys (Mashdict h) =
|
||||||
Dict.keys h.values
|
Dict.keys h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Map a value on a given key. If the outcome of the function changes the hash,
|
||||||
|
the operation does nothing.
|
||||||
|
-}
|
||||||
|
map : String -> (a -> a) -> Mashdict a -> Mashdict a
|
||||||
|
map key f (Mashdict h) =
|
||||||
|
Mashdict
|
||||||
|
{ h
|
||||||
|
| values =
|
||||||
|
Dict.update
|
||||||
|
key
|
||||||
|
(Maybe.map
|
||||||
|
(\value ->
|
||||||
|
case h.hash (f value) of
|
||||||
|
Just newHash ->
|
||||||
|
if newHash == key then
|
||||||
|
f value
|
||||||
|
|
||||||
|
else
|
||||||
|
value
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
h.values
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Determine if a value's hash is in a mashdict.
|
{-| Determine if a value's hash is in a mashdict.
|
||||||
-}
|
-}
|
||||||
member : a -> Mashdict a -> Bool
|
member : a -> Mashdict a -> Bool
|
||||||
|
|
|
@ -43,20 +43,23 @@ and maintain this room state.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
import Internal.Filter.Timeline as Filter exposing (Filter)
|
|
||||||
import Internal.Tools.Mashdict as Mashdict exposing (Mashdict)
|
|
||||||
import Internal.Tools.Iddict as Iddict exposing (Iddict)
|
|
||||||
import FastDict as Dict exposing (Dict)
|
import FastDict as Dict exposing (Dict)
|
||||||
|
import Internal.Filter.Timeline as Filter exposing (Filter)
|
||||||
|
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict)
|
||||||
|
import Internal.Tools.Iddict as Iddict exposing (Iddict)
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
|
|
||||||
|
|
||||||
{-| A batch is a batch of events that is placed onto the Timeline. Functions
|
{-| A batch is a batch of events that is placed onto the Timeline. Functions
|
||||||
that require an insertion, generally require this data type.
|
that require an insertion, generally require this data type.
|
||||||
-}
|
-}
|
||||||
type Batch
|
type alias Batch =
|
||||||
= StartOfTimeline
|
{ events : List String
|
||||||
| BatchToken String
|
, filter : Filter
|
||||||
| BatchSlice Batch (List String) Filter String
|
, start : Maybe TokenValue
|
||||||
|
, end : TokenValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Internal batch that's being saved by the Timeline to track a list of events.
|
{-| Internal batch that's being saved by the Timeline to track a list of events.
|
||||||
-}
|
-}
|
||||||
|
@ -67,33 +70,53 @@ type alias IBatch =
|
||||||
, end : ITokenPTR
|
, end : ITokenPTR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Pointer to an IBatch in the Timeline.
|
{-| Pointer to an IBatch in the Timeline.
|
||||||
-}
|
-}
|
||||||
type IBatchPTR = IBatchPTR Int
|
type IBatchPTR
|
||||||
|
= IBatchPTR IBatchPTRValue
|
||||||
|
|
||||||
|
|
||||||
|
{-| Location indicator of an IBatch in the Timeline.
|
||||||
|
-}
|
||||||
|
type alias IBatchPTRValue =
|
||||||
|
Int
|
||||||
|
|
||||||
|
|
||||||
{-| Internal token value that's being stored by the Timeline.
|
{-| Internal token value that's being stored by the Timeline.
|
||||||
|
|
||||||
If name is `Nothing`, it indicates the start of the timeline.
|
If name is `Nothing`, it indicates the start of the timeline.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
type alias IToken =
|
type alias IToken =
|
||||||
{ name : Maybe String
|
{ name : TokenValue
|
||||||
, starts : Set Int -- This itoken starts the following batches
|
, starts : Set IBatchPTRValue -- This itoken starts the following batches
|
||||||
, ends : Set Int -- This itoken ends the following batches
|
, ends : Set IBatchPTRValue -- This itoken ends the following batches
|
||||||
, inFrontOf : Set Int -- This itoken is in front of the following tokens
|
, inFrontOf : Set ITokenPTRValue -- This itoken is in front of the following tokens
|
||||||
, behind : Set Int -- This itoken is behind the following tokens
|
, behind : Set ITokenPTRValue -- This itoken is behind the following tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Pointer to an IToken in the Timeline.
|
{-| Pointer to an IToken in the Timeline.
|
||||||
-}
|
-}
|
||||||
type ITokenPTR = ITokenPTR String
|
type ITokenPTR
|
||||||
|
= ITokenPTR ITokenPTRValue
|
||||||
|
| StartOfTimeline
|
||||||
|
|
||||||
|
|
||||||
|
{-| Location indicator of an IToken in the Timeline.
|
||||||
|
-}
|
||||||
|
type alias ITokenPTRValue =
|
||||||
|
String
|
||||||
|
|
||||||
|
|
||||||
{-| The Timeline type represents the timeline state in a Matrix room.
|
{-| The Timeline type represents the timeline state in a Matrix room.
|
||||||
|
|
||||||
Following the description of the Matrix spec, a timeline contains the following
|
Following the description of the Matrix spec, a timeline contains the following
|
||||||
items:
|
items:
|
||||||
|
|
||||||
- Events that indicate timeline events
|
- Events that indicate timeline events
|
||||||
- Batch values that can be used to paginate through the timeline
|
- Batch values that can be used to paginate through the timeline
|
||||||
|
|
||||||
The topological shape of the timeline makes older API responses somewhat
|
The topological shape of the timeline makes older API responses somewhat
|
||||||
unreliable - as a result,
|
unreliable - as a result,
|
||||||
|
@ -102,11 +125,19 @@ unreliable - as a result,
|
||||||
type Timeline
|
type Timeline
|
||||||
= Timeline
|
= Timeline
|
||||||
{ batches : Iddict IBatch
|
{ batches : Iddict IBatch
|
||||||
|
, events : Dict String ( IBatchPTR, List IBatchPTR )
|
||||||
|
, filledBatches : Int
|
||||||
, mostRecentSync : ITokenPTR
|
, mostRecentSync : ITokenPTR
|
||||||
, tokens : Mashdict IToken
|
, tokens : Hashdict IToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Opaque token value sent by the Matrix API
|
||||||
|
-}
|
||||||
|
type alias TokenValue =
|
||||||
|
String
|
||||||
|
|
||||||
|
|
||||||
{-| When syncing a Matrix room to its most recent state, add the most recent
|
{-| When syncing a Matrix room to its most recent state, add the most recent
|
||||||
batch to the front of the Timeline.
|
batch to the front of the Timeline.
|
||||||
-}
|
-}
|
||||||
|
@ -114,36 +145,87 @@ addSync : Batch -> Timeline -> Timeline
|
||||||
addSync _ timeline =
|
addSync _ timeline =
|
||||||
timeline
|
timeline
|
||||||
|
|
||||||
|
|
||||||
|
{-| Append a token at the end of a batch.
|
||||||
|
-}
|
||||||
|
connectIBatchToIToken : IBatchPTR -> ITokenPTR -> Timeline -> Timeline
|
||||||
|
connectIBatchToIToken (IBatchPTR bptr) pointer (Timeline tl) =
|
||||||
|
case pointer of
|
||||||
|
StartOfTimeline ->
|
||||||
|
Timeline tl
|
||||||
|
|
||||||
|
ITokenPTR tptr ->
|
||||||
|
Timeline
|
||||||
|
{ tl
|
||||||
|
| batches =
|
||||||
|
Iddict.map bptr
|
||||||
|
(\batch -> { batch | end = pointer })
|
||||||
|
tl.batches
|
||||||
|
, tokens =
|
||||||
|
Hashdict.map tptr
|
||||||
|
(\token -> { token | ends = Set.insert bptr token.ends })
|
||||||
|
tl.tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Append a token at the start of a batch.
|
||||||
|
-}
|
||||||
|
connectITokenToIBatch : ITokenPTR -> IBatchPTR -> Timeline -> Timeline
|
||||||
|
connectITokenToIBatch pointer (IBatchPTR bptr) (Timeline tl) =
|
||||||
|
case pointer of
|
||||||
|
StartOfTimeline ->
|
||||||
|
Timeline tl
|
||||||
|
|
||||||
|
ITokenPTR tptr ->
|
||||||
|
Timeline
|
||||||
|
{ tl
|
||||||
|
| tokens =
|
||||||
|
Hashdict.map tptr
|
||||||
|
(\token -> { token | starts = Set.insert bptr token.starts })
|
||||||
|
tl.tokens
|
||||||
|
, batches =
|
||||||
|
Iddict.map bptr
|
||||||
|
(\batch -> { batch | start = pointer })
|
||||||
|
tl.batches
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Connect two tokens to each other, revealing their relative location.
|
{-| Connect two tokens to each other, revealing their relative location.
|
||||||
-}
|
-}
|
||||||
connectITokentoIToken : ITokenPTR -> ITokenPTR -> Timeline -> Timeline
|
connectITokenToIToken : ITokenPTR -> ITokenPTR -> Timeline -> Timeline
|
||||||
connectITokentoIToken (ITokenPTR early) (ITokenPTR late) (Timeline tl) =
|
connectITokenToIToken pointer1 pointer2 (Timeline tl) =
|
||||||
Timeline
|
case ( pointer1, pointer2 ) of
|
||||||
{ tl
|
( ITokenPTR early, ITokenPTR late ) ->
|
||||||
| tokens =
|
Timeline
|
||||||
tl.tokens
|
{ tl
|
||||||
|> Iddict.map early
|
| tokens =
|
||||||
(\data ->
|
tl.tokens
|
||||||
{ data | behind = Set.insert late data.behind }
|
|> Hashdict.map early
|
||||||
)
|
(\data ->
|
||||||
|> Iddict.map late
|
{ data | behind = Set.insert late data.behind }
|
||||||
(\data ->
|
)
|
||||||
{ data | inFrontOf = Set.insert early data.inFrontOf }
|
|> Hashdict.map late
|
||||||
)
|
(\data ->
|
||||||
}
|
{ data | inFrontOf = Set.insert early data.inFrontOf }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
( _, _ ) ->
|
||||||
|
Timeline tl
|
||||||
|
|
||||||
|
|
||||||
{-| Create a new empty timeline.
|
{-| Create a new empty timeline.
|
||||||
-}
|
-}
|
||||||
empty : Timeline
|
empty : Timeline
|
||||||
empty =
|
empty =
|
||||||
case Iddict.singleton Nothing of
|
Timeline
|
||||||
( key, iddict ) ->
|
{ batches = Iddict.empty
|
||||||
Timeline
|
, events = Dict.empty
|
||||||
{ batches = Iddict.empty
|
, filledBatches = 0
|
||||||
, mostRecentSync = ITokenPTR key
|
, mostRecentSync = StartOfTimeline
|
||||||
, tokens = iddict
|
, tokens = Hashdict.empty .name
|
||||||
, tokenToPtr = Dict.empty
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{-| Get an IBatch from the Timeline.
|
{-| Get an IBatch from the Timeline.
|
||||||
-}
|
-}
|
||||||
|
@ -151,110 +233,92 @@ getIBatch : IBatchPTR -> Timeline -> Maybe IBatch
|
||||||
getIBatch (IBatchPTR ptr) (Timeline { batches }) =
|
getIBatch (IBatchPTR ptr) (Timeline { batches }) =
|
||||||
Iddict.get ptr batches
|
Iddict.get ptr batches
|
||||||
|
|
||||||
|
|
||||||
getITokenFromPTR : ITokenPTR -> Timeline -> Maybe IToken
|
getITokenFromPTR : ITokenPTR -> Timeline -> Maybe IToken
|
||||||
getITokenFromPTR (ITokenPTR ptr) ( Timeline { tokens }) =
|
getITokenFromPTR pointer (Timeline { tokens }) =
|
||||||
Iddict.get ptr tokens
|
case pointer of
|
||||||
|
ITokenPTR ptr ->
|
||||||
|
Hashdict.get ptr tokens
|
||||||
|
|
||||||
{-| Turn a single token into a batch.
|
StartOfTimeline ->
|
||||||
-}
|
Nothing
|
||||||
fromToken : String -> Batch
|
|
||||||
fromToken token =
|
|
||||||
BatchToken token
|
|
||||||
|
|
||||||
|
|
||||||
{-| Turn a slice of events into a batch.
|
|
||||||
|
|
||||||
NOTE: `start` must generally be a value. If it is `Nothing`, then it is
|
|
||||||
connected until the start of the timeline.
|
|
||||||
|
|
||||||
-}
|
|
||||||
fromSlice : { start : Maybe String, events : List String, filter : Filter, end : String } -> Batch
|
|
||||||
fromSlice data =
|
|
||||||
BatchSlice
|
|
||||||
( case data.start of
|
|
||||||
Just s ->
|
|
||||||
BatchToken s
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
StartOfTimeline
|
|
||||||
)
|
|
||||||
data.events data.filter data.end
|
|
||||||
|
|
||||||
|
|
||||||
{-| Insert a batch anywhere else in the timeline.
|
{-| Insert a batch anywhere else in the timeline.
|
||||||
-}
|
-}
|
||||||
insert : Batch -> Timeline -> Timeline
|
insert : Batch -> Timeline -> Timeline
|
||||||
insert batch (Timeline tl) =
|
insert batch (Timeline tl) =
|
||||||
(Timeline tl)
|
Timeline tl
|
||||||
|
|
||||||
|
|
||||||
|
{-| Insert a batch into the timeline.
|
||||||
|
-}
|
||||||
|
insertBatch : Batch -> Timeline -> Timeline
|
||||||
|
insertBatch batch timeline =
|
||||||
|
case batch.start of
|
||||||
|
Just start ->
|
||||||
|
timeline
|
||||||
|
|> invokeIToken start
|
||||||
|
|> Tuple.mapSecond (invokeIToken batch.end)
|
||||||
|
|> (\( startPTR, ( endPTR, newTimeline ) ) ->
|
||||||
|
insertIBatch
|
||||||
|
{ events = batch.events
|
||||||
|
, filter = batch.filter
|
||||||
|
, start = startPTR
|
||||||
|
, end = endPTR
|
||||||
|
}
|
||||||
|
newTimeline
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
timeline
|
||||||
|
|> invokeIToken batch.end
|
||||||
|
|> (\( endPTR, newTimeline ) ->
|
||||||
|
insertIBatch
|
||||||
|
{ events = batch.events
|
||||||
|
, filter = batch.filter
|
||||||
|
, start = StartOfTimeline
|
||||||
|
, end = endPTR
|
||||||
|
}
|
||||||
|
newTimeline
|
||||||
|
)
|
||||||
|
|
||||||
-- {-| Insert a batch anywhere else in the timeline, and gain a ptr to its
|
|
||||||
-- location.
|
|
||||||
-- -}
|
|
||||||
-- insertBatch : Batch -> Timeline -> { start : ITokenPTR, end : ITokenPTR, tl : Timeline }
|
|
||||||
-- insertBatch batch (Timeline tl) =
|
|
||||||
-- case batch of
|
|
||||||
-- StartOfTimeline ->
|
|
||||||
-- case Iddict.insert Nothing tl.tokens of
|
|
||||||
-- ( key, iddict ) ->
|
|
||||||
-- { start = ITokenPTR key
|
|
||||||
-- , end = ITokenPTR key
|
|
||||||
-- , tl = Timeline { tl | tokens = iddict }
|
|
||||||
-- }
|
|
||||||
|
|
||||||
-- BatchToken token ->
|
|
||||||
-- -- TODO: Do not insert if it already exists
|
|
||||||
-- case Iddict.insert (Just token) tl.tokens of
|
|
||||||
-- ( key, iddict ) ->
|
|
||||||
-- { start = ITokenPTR key
|
|
||||||
-- , end = ITokenPTR key
|
|
||||||
-- , tl = Timeline
|
|
||||||
-- { tl
|
|
||||||
-- | tokens = iddict
|
|
||||||
-- , tokenToPtr =
|
|
||||||
-- Dict.insert token (ITokenPTR key) tl.tokenToPtr
|
|
||||||
-- }
|
|
||||||
-- }
|
|
||||||
|
|
||||||
-- BatchSlice prevBatch events filter end ->
|
|
||||||
-- -- Insert previous batch
|
|
||||||
-- case insertBatch prevBatch (Timeline tl) of
|
|
||||||
-- result ->
|
|
||||||
-- case result.tl of
|
|
||||||
-- (Timeline tl2) ->
|
|
||||||
-- { start = result.start
|
|
||||||
-- , end =
|
|
||||||
-- }
|
|
||||||
|
|
||||||
{-| Insert an internal batch into the timeline, and determine its result.
|
{-| Insert an internal batch into the timeline, and determine its result.
|
||||||
-}
|
-}
|
||||||
insertIBatch : IBatch -> Timeline -> ( IBatchPTR, Timeline )
|
insertIBatch : IBatch -> Timeline -> Timeline
|
||||||
insertIBatch ibatch (Timeline tl) =
|
insertIBatch ibatch (Timeline tl) =
|
||||||
case Iddict.insert ibatch tl.batches of
|
case Iddict.insert ibatch tl.batches of
|
||||||
( key, iddict ) ->
|
( batchPTR, newBatches ) ->
|
||||||
( IBatchPTR key, Timeline { tl | batches = iddict } )
|
{ tl | batches = newBatches }
|
||||||
|
|> Timeline
|
||||||
|
|> connectITokenToIBatch ibatch.start (IBatchPTR batchPTR)
|
||||||
|
|> connectIBatchToIToken (IBatchPTR batchPTR) ibatch.end
|
||||||
|
|
||||||
insertIToken : IToken -> Timeline -> ( ITokenPTR, Timeline )
|
|
||||||
insertIToken itoken (Timeline tl) =
|
{-| Invoke an itoken to guarantee that it exists.
|
||||||
case Maybe.andThen (\n -> Dict.get n tl.tokenToPtr) itoken.name of
|
-}
|
||||||
-- Already exists: merge
|
invokeIToken : TokenValue -> Timeline -> ( ITokenPTR, Timeline )
|
||||||
Just ((ITokenPTR ptr) as pointer) ->
|
invokeIToken value (Timeline tl) =
|
||||||
( pointer
|
( ITokenPTR value
|
||||||
, Timeline
|
, Timeline
|
||||||
{ tl
|
{ tl
|
||||||
| tokens =
|
| tokens =
|
||||||
Iddict.map ptr
|
case Hashdict.get value tl.tokens of
|
||||||
(\data ->
|
Just _ ->
|
||||||
{ name = data.name
|
tl.tokens
|
||||||
,
|
|
||||||
|
Nothing ->
|
||||||
|
Hashdict.insert
|
||||||
|
{ name = value
|
||||||
|
, starts = Set.empty
|
||||||
|
, ends = Set.empty
|
||||||
|
, inFrontOf = Set.empty
|
||||||
|
, behind = Set.empty
|
||||||
}
|
}
|
||||||
)
|
tl.tokens
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
-- Doesn't exist yet: insert!
|
|
||||||
Nothing ->
|
|
||||||
(ITokenPTR 0, Timeline tl)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{-| Under a given filter, find the most recent events.
|
{-| Under a given filter, find the most recent events.
|
||||||
|
@ -264,6 +328,22 @@ mostRecentEvents _ _ =
|
||||||
[]
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Recount the Timeline's amount of filled batches. Since the Timeline
|
||||||
|
automatically tracks the count on itself, this is generally exclusively used in
|
||||||
|
specific scenarios like decoding JSON values.
|
||||||
|
-}
|
||||||
|
recountFilledBatches : Timeline -> Timeline
|
||||||
|
recountFilledBatches (Timeline tl) =
|
||||||
|
Timeline
|
||||||
|
{ tl
|
||||||
|
| filledBatches =
|
||||||
|
tl.batches
|
||||||
|
|> Iddict.values
|
||||||
|
|> List.filter (\v -> v.events /= [])
|
||||||
|
|> List.length
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Create a timeline with a single batch inserted. This batch is considered the
|
{-| Create a timeline with a single batch inserted. This batch is considered the
|
||||||
most recent batch, as if created by a sync.
|
most recent batch, as if created by a sync.
|
||||||
-}
|
-}
|
||||||
|
|
Loading…
Reference in New Issue