Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 229 additions & 129 deletions app/application.ml

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions app/journal_header.ml
Original file line number Diff line number Diff line change
Expand Up @@ -541,15 +541,16 @@ let reactive_view
~body
;;

let graph_pages ~enabled ~page_id ~resident_page_ids ~target_page_id ~target_id ~on_prepare ~on_commit ~on_cancel ~current ~preview =
let graph_pages ~payload_signal ~on_event ~children =
V.of_lui (Journal_lui_native.mount ~key:"journal-graph-pages"
~payload:(Signal.sample payload_signal) ~payload_signal ~children
~on_event:(fun event->on_event event.Journal_lui_native.payload)
Journal_lui_native.chrome_identifier)
;;

let timeline_host ~owner content =
let payload=Yojson.Safe.to_string (`Assoc
["mode",`String "graph-pages";"enabled",`Bool enabled;"pageID",`String page_id;
"targetID",(match target_id with None->`Null | Some id->`String id);
"residentPageIDs",`List(List.map(fun id->`String id) resident_page_ids);
"targetPageID",(match target_page_id with None->`Null | Some id->`String id)]) in
V.of_lui (Journal_lui_native.mount ~key:"journal-graph-pages" ~payload
~on_event:(fun event->match event.Journal_lui_native.payload with
| "prepare"->on_prepare () | "commit"->on_commit () | "cancel"->on_cancel () | _->())
~children:[Lui_elements.stack ~grow:1. [Ui.mount current];
Lui_elements.stack ~grow:1. [Ui.mount preview]] Journal_lui_native.chrome_identifier)
["mode",`String "timeline-host";"ownerID",`String owner]) in
V.of_lui (Journal_lui_native.mount ~key:("timeline-host:" ^ owner)
~payload ~children:[Ui.mount content] Journal_lui_native.chrome_identifier)
;;
9 changes: 5 additions & 4 deletions app/journal_header.mli
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ val graph_picker
-> Journal_view.View.Body.t
-> Journal_view.View.Body.t

val graph_pages : enabled:bool -> page_id:string -> resident_page_ids:string list
-> target_page_id:string option -> target_id:string option
-> on_prepare:(unit -> unit) -> on_commit:(unit -> unit) -> on_cancel:(unit -> unit)
-> current:Journal_view.View.t -> preview:Journal_view.View.t -> Journal_view.View.t
(** A bounded keyed collection of persistent graph presentation hosts. *)
val graph_pages : payload_signal:string Signal.signal -> on_event:(string -> unit)
-> children:Lui_elements.t list -> Journal_view.View.t

val timeline_host : owner:string -> Journal_view.View.t -> Journal_view.View.t
6 changes: 5 additions & 1 deletion app/journal_lui_native.ml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,14 @@ let decode_event = function
| _ -> None
;;

let mount ?key ~payload ~children ?on_event identifier context parent =
let mount ?key ~payload ?payload_signal ~children ?on_event identifier context parent =
let node = Lui_ui.extension context identifier in
Option.iter (Lui_ui.key context node) key;
Lui_ui.extension_property context node "payload" (StringValue payload);
Option.iter (fun source ->
let values=Signal.map (fun value->StringValue value) source in
Signal.on_dispose context.Lui_ui.ui_scope (fun ()->Signal.dispose_signal values);
Lui_ui.extension_property_signal context node "payload" values) payload_signal;
Option.iter
(fun handler ->
Lui_ui.on_event context node (fun raw ->
Expand Down
1 change: 1 addition & 0 deletions app/journal_lui_native.mli
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type event =
val mount
: ?key:string
-> payload:string
-> ?payload_signal:string Signal.signal
-> children:Lui_elements.t list
-> ?on_event:(event -> unit)
-> string
Expand Down
51 changes: 45 additions & 6 deletions app/journal_media_view.ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Store = struct
{ epoch : int
; mutable active : bool
; callback : unit -> unit
; mutable pending : string option
}

type t =
Expand All @@ -27,6 +28,9 @@ module Store = struct
; items : (string * string, (int, listener) Hashtbl.t) Hashtbl.t
; observe : string -> unit
; mutable epoch : int
; mutable active : bool
; mutable interactive : bool
; interactions : (int, unit -> unit) Hashtbl.t
; mutable serial : int
}

Expand All @@ -36,6 +40,9 @@ module Store = struct
; items = Hashtbl.create 64
; observe
; epoch = 0
; active = true
; interactive = true
; interactions = Hashtbl.create 8
; serial = 0
}
;;
Expand All @@ -58,7 +65,7 @@ module Store = struct
in
t.serial <- t.serial + 1;
let id = t.serial in
let listener = { epoch = t.epoch; active = true; callback } in
let listener = { epoch = t.epoch; active = true; callback; pending=None } in
Hashtbl.add bucket id listener;
fun () ->
if listener.active
Expand All @@ -77,14 +84,43 @@ module Store = struct
| Some bucket ->
let listeners = Hashtbl.fold (fun _ listener acc -> listener :: acc) bucket [] in
List.iter
(fun listener ->
(fun (listener:listener) ->
if listener.active && listener.epoch = t.epoch
then (
t.observe name;
listener.callback ()))
if t.active then (t.observe name;listener.callback ())
else listener.pending <- Some name))
listeners
;;

let set_active t active =
if t.active <> active then (
t.active <- active;
if active then (
let collect table = Hashtbl.fold (fun _ bucket rest->
Hashtbl.fold (fun _ listener rest->listener::rest) bucket rest) table [] in
let pending=collect t.structure @ collect t.items in
List.iter (fun listener->match listener.pending with
| Some name when listener.active && listener.epoch=t.epoch && t.active ->
listener.pending<-None;t.observe name;listener.callback ()
| _ when not listener.active || listener.epoch<>t.epoch ->listener.pending<-None
| _->()) pending))
;;

let interactive t = t.interactive

let subscribe_interaction t callback =
t.serial<-t.serial+1;let id=t.serial in
Hashtbl.add t.interactions id callback;
fun ()->Hashtbl.remove t.interactions id
;;

let set_interactive t interactive =
if t.interactive<>interactive then (
t.interactive<-interactive;
let listeners=Hashtbl.to_seq t.interactions |> List.of_seq in
List.iter(fun(id,callback)->if Hashtbl.mem t.interactions id then callback ()) listeners)
;;

let shape = function
| None -> [], false, None
| Some (view : Runtime.view) ->
Expand Down Expand Up @@ -249,7 +285,7 @@ let view_content
in
let selected_slot = root ^ ":" ^ item.token in
let selected = owner, item.token, path, selected_slot in
if unchanged selected
if Store.interactive store && unchanged selected
then (
let group =
if is_image_type item.file_type
Expand Down Expand Up @@ -301,7 +337,10 @@ let view_content
unsubscribe_preview
:= fun () -> List.iter (fun unsubscribe -> unsubscribe ()) subscriptions)
in
Signal.on_dispose context.ui_scope close_preview;
let stop_interaction=Store.subscribe_interaction store (fun ()->
if not(Store.interactive store) && (!selected_preview<>[] || Signal.sample(Signal.value preview)<>None)
then close_preview ()) in
Signal.on_dispose context.ui_scope (fun ()->stop_interaction ();close_preview ());
let reactive_item (item : Journal_media_runtime.item) render =
fun context parent ->
let owner = asset_root item.token in
Expand Down
4 changes: 4 additions & 0 deletions app/journal_media_view.mli
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ module Store : sig
val find : t -> string -> Journal_media_runtime.view option
val update : t -> root:string -> Journal_media_runtime.view option -> unit

(** Data keeps advancing while hidden; each dirty subscription catches up once. *)
val set_active : t -> bool -> unit
val set_interactive : t -> bool -> unit

(* Reset presentation and fence subscribers from the previous graph/session. *)
val reset : t -> unit
end
Expand Down
30 changes: 23 additions & 7 deletions app/journal_timeline.ml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module Store = struct
{ epoch : int
; mutable active : bool
; callback : unit -> unit
; mutable pending : bool
}

type shape =
Expand All @@ -49,6 +50,7 @@ module Store = struct
; mutable shape : (int * shape list) option
; mutable revision : int
; mutable epoch : int
; mutable active : bool
; mutable serial : int
}

Expand All @@ -62,6 +64,7 @@ module Store = struct
; shape = None
; revision = 0
; epoch = 0
; active = true
; serial = 0
}
;;
Expand All @@ -73,7 +76,7 @@ module Store = struct
let subscribe_scroll t callback =
t.serial <- t.serial + 1;
let id = t.serial in
let listener = { epoch = t.epoch; active = true; callback } in
let listener = { epoch = t.epoch; active = true; callback; pending=false } in
Hashtbl.add t.scroll_listeners id listener;
fun () ->
listener.active <- false;
Expand All @@ -84,8 +87,9 @@ module Store = struct
if t.scroll_target <> target then (
t.scroll_target <- target;
let listeners = Hashtbl.fold (fun _ value rest -> value :: rest) t.scroll_listeners [] in
List.iter (fun listener ->
if listener.active && listener.epoch = t.epoch then listener.callback ()) listeners)
List.iter (fun (listener:listener) ->
if listener.active && listener.epoch = t.epoch then
if t.active then listener.callback () else listener.pending<-true) listeners)
;;

let subscribe t block_id callback =
Expand All @@ -99,7 +103,7 @@ module Store = struct
in
t.serial <- t.serial + 1;
let id = t.serial in
let listener = { epoch = t.epoch; active = true; callback } in
let listener = { epoch = t.epoch; active = true; callback; pending=false } in
Hashtbl.add bucket id listener;
fun () ->
if listener.active
Expand All @@ -114,15 +118,27 @@ module Store = struct
(fun bucket ->
let listeners = Hashtbl.fold (fun _ value rest -> value :: rest) bucket [] in
List.iter
(fun listener ->
(fun (listener:listener) ->
if listener.active && listener.epoch = t.epoch
then (
t.observe "timeline-item-notify";
listener.callback ()))
if t.active then (t.observe "timeline-item-notify";listener.callback ())
else listener.pending<-true))
listeners)
(Hashtbl.find_opt t.listeners block_id)
;;

let set_active t active =
if t.active<>active then (
t.active<-active;
if active then (
let rows=Hashtbl.fold(fun _ bucket rest->
Hashtbl.fold(fun _ listener rest->listener::rest) bucket rest) t.listeners [] in
let pending=Hashtbl.fold(fun _ listener rest->listener::rest) t.scroll_listeners rows in
List.iter(fun (listener:listener)->
if listener.pending && listener.active && listener.epoch=t.epoch && t.active then (
listener.pending<-false;listener.callback ())) pending))
;;

(* Only a changed timeline presentation reaches this traversal. Media,
navigation, draft and visible-demand writes do not scan retained rows. *)
let synchronize t state =
Expand Down
1 change: 1 addition & 0 deletions app/journal_timeline.mli
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Store : sig

val create : ?observe:(string -> unit) -> unit -> t
val synchronize : t -> Journal_timeline_state.t -> int
val set_active : t -> bool -> unit

(* Return the native collection's structural presentation revision. Only
actual timeline changes reconcile retained slots; item publications go
Expand Down
45 changes: 45 additions & 0 deletions apple-tests/native-list/JournalListSnapshotTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation
import SwiftUI
import Combine
#if os(iOS)
import UIKit
#endif
@testable import LUIAppleBackend

// Native payload parsing/lifecycle ownership is not reachable through an OCaml
Expand Down Expand Up @@ -35,6 +38,48 @@ import Combine
}
static func settle() async { for _ in 0..<10 { await Task.yield() } }
static func main() async throws {
#if os(iOS)
let handoff = JournalGraphHandoff()
let cancelledGesture = handoff.startGesture()
handoff.cancel()
check(!handoff.acceptsGesture(cancelledGesture), "scene cancellation rejects an animation completion")
let laterGesture = handoff.startGesture()
check(!handoff.acceptsGesture(cancelledGesture) && handoff.acceptsGesture(laterGesture), "new gesture fences old completion")
handoff.cancel()
let changedFocusGesture = handoff.startGesture()
handoff.focusChanged("B")
check(!handoff.acceptsGesture(changedFocusGesture), "external focus change before cover rejects old animation completion")
// Retained pages no longer have snapshot/layout deadlines. Preserve the
// existing owner/nonce/cancel checks against the new focus-only handoff.
let firstHandoff = handoff.begin(source: "A", target: "B")
check(!handoff.finish(firstHandoff, page: "A", current: "B"), "source cannot finish target focus")
check(!handoff.finish(firstHandoff, page: "B", current: "A"), "target cannot finish while Save still queues focus")
check(handoff.finish(firstHandoff, page: "B", current: "B"), "matching focus completes the retained page transition")
let second = handoff.begin(source: "B", target: "A")
let currentHandoff = handoff.begin(source: "A", target: "B")
check(!handoff.finish(firstHandoff, page: "B", current: "B"), "repeated page ID rejects old completion nonce")
check(!handoff.finish(second, page: "A", current: "A"), "old focus cannot complete a later transition")
check(handoff.request?.id == currentHandoff, "stale callbacks leave current target intact")
handoff.focusChanged("A")
check(handoff.request?.id == currentHandoff, "source remains selected while Save queues focus")
try await Task.sleep(for: .milliseconds(2100))
check(handoff.request?.id == currentHandoff, "queued focus is not expired by a presentation timeout")
check(handoff.finish(currentHandoff, page: "B", current: "B"), "queued focus completes after Save publication")
_ = handoff.begin(source: "A", target: "B")
handoff.focusChanged("C")
check(handoff.request == nil, "external selection fences the pending transition")
_ = handoff.begin(source: "A", target: "B")
handoff.retain(["A", "B", "C"])
check(handoff.request != nil, "unchanged resident set retains pending focus")
handoff.retain(["A", "C"])
check(handoff.request == nil, "closing pending target cancels its transition")
_ = handoff.begin(source: "A", target: "B")
handoff.cancel()
check(handoff.request == nil, "scene or size cancellation clears pending transition")
_ = handoff.begin(source: "A", target: "B")
handoff.retain([])
check(handoff.request == nil, "account exit clears all pending page owners")
#endif
let offsets = JournalGraphScrollOffsets()
offsets.retain(["account1:A:g1", "account1:B:g2"])
offsets.write(page: "account1:A:g1", list: "timeline", y: 515)
Expand Down
Loading
Loading