Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation #239

Merged
merged 32 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
81329ff
Cargo.lock
mobile-bungalow Oct 30, 2024
9c5c451
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 6, 2024
b564457
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 14, 2024
f242756
8 failures, removed pointer chasing
mobile-bungalow Nov 16, 2024
cb8fdfc
lints
mobile-bungalow Nov 16, 2024
97b60eb
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 16, 2024
ef4094f
7 failing, do not newrender in conversion
mobile-bungalow Nov 18, 2024
8c653ca
clippy
mobile-bungalow Nov 18, 2024
cbac04c
Merge branch 'main' of https://github.com/liveview-native/liveview-na…
mobile-bungalow Nov 18, 2024
bac2faa
first failing navigation test
mobile-bungalow Nov 18, 2024
320308f
nav files
mobile-bungalow Nov 18, 2024
0be4f69
first test passing
mobile-bungalow Nov 18, 2024
5a95faf
tests passing
mobile-bungalow Nov 19, 2024
0671dfd
remove unnecessary state object
mobile-bungalow Nov 19, 2024
63b53d4
all traversal logic implemented
mobile-bungalow Nov 19, 2024
123caed
tests passing
mobile-bungalow Nov 20, 2024
df9df11
more thorough tests
mobile-bungalow Nov 20, 2024
039c008
typo
mobile-bungalow Nov 20, 2024
cd8bede
clippy
mobile-bungalow Nov 20, 2024
d345c6f
lints
mobile-bungalow Nov 20, 2024
64b9556
clippy passing, api's wrapped
mobile-bungalow Nov 20, 2024
6b71560
start rollback state
mobile-bungalow Nov 21, 2024
cedaec0
rollback
mobile-bungalow Nov 21, 2024
6e2f3e0
state rollback works, add the begginning of the actual network code
mobile-bungalow Nov 21, 2024
8bf4464
actually changes page
mobile-bungalow Nov 22, 2024
4260c7c
add session data
mobile-bungalow Nov 22, 2024
cb70a14
swift tests
mobile-bungalow Nov 22, 2024
994292c
simple use case kotlin and swift
mobile-bungalow Nov 22, 2024
0349d25
remove generated files
mobile-bungalow Nov 22, 2024
10dc96f
remove short circuiting nav on failure
mobile-bungalow Nov 25, 2024
571b3d2
code review, remove excessive indirection, bubble errors, try lock
mobile-bungalow Nov 26, 2024
991340b
add lock macro
mobile-bungalow Nov 26, 2024
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
5 changes: 3 additions & 2 deletions crates/core/src/live_socket/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl LiveFile {

// For non FFI functions
impl LiveChannel {
/// Retrieves the initial document received upon joining the channel.
pub fn join_document(&self) -> Result<Document, LiveSocketError> {
let new_root = match self.join_payload {
Payload::JSONPayload {
Expand Down Expand Up @@ -137,8 +138,8 @@ impl LiveChannel {
Ok(upload_id)
}

// Blocks indefinitely, processing changes to the document using the user provided callback
// In `set_event_handler`
/// Blocks indefinitely, processing changes to the document using the user provided callback
/// In `set_event_handler`
pub async fn merge_diffs(&self) -> Result<(), LiveSocketError> {
// TODO: This should probably take the event closure to send changes back to swift/kotlin
let document = self.document.clone();
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/live_socket/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod channel;
mod error;
mod navigation;
mod socket;

#[cfg(test)]
Expand Down
101 changes: 101 additions & 0 deletions crates/core/src/live_socket/navigation/ffi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use reqwest::Url;

pub type HistoryId = u64;

#[uniffi::export(callback_interface)]
pub trait NavEventHandler: Send + Sync {
/// This callback instruments events that occur when your user navigates to a
/// new view. You can add serialized metadata to these events as a byte buffer
/// through the [NavOptions] object.
fn handle_event(&self, event: NavEvent) -> HandlerResponse;
}

#[derive(uniffi::Enum, Clone, Debug, PartialEq, Default)]
pub enum HandlerResponse {
#[default]
Default,
PreventDefault,
}

#[derive(uniffi::Enum, Clone, Debug, PartialEq)]
pub enum NavEventType {
Push,
Replace,
Reload,
Traverse,
}

#[derive(uniffi::Record, Clone, Debug, PartialEq)]
pub struct NavHistoryEntry {
/// The target url.
pub url: String,
/// Unique id for this piece of nav entry state.
pub id: HistoryId,
/// state passed in by the user, to be passed in to the navigation event callback.
pub state: Option<Vec<u8>>,
}

/// An event emitted when the user navigates between views.
#[derive(uniffi::Record, Clone, Debug, PartialEq)]
pub struct NavEvent {
pub event: NavEventType,
pub same_document: bool,
/// The previous location of the page, if there was one
pub from: Option<NavHistoryEntry>,
/// Destination URL
pub to: NavHistoryEntry,
/// Additional user provided metadata handed to the event handler.
pub info: Option<Vec<u8>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this match the name state in the NavHistoryEntry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info and state are two different things, the history entry contains persistent state which will always be included whenever it shows up in a navigation event, or when NavCtx::current accquires it. info is ephemeral and is passed to the event once.

I based this off of the web navigation api's naming scheme which is honestly not great.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_specific_info might be more descriptive.

/// Persistent state kept in history for as long as the even is in the history stack.
pub state: Option<Vec<u8>>,
}

#[derive(Default, uniffi::Enum)]
pub enum NavAction {
/// Push the navigation event onto the history stack.
#[default]
Push,
/// Replace the current top of the history stack with this navigation event.
Replace,
}

#[derive(Default, uniffi::Record)]
pub struct NavOptions {
pub action: NavAction,
pub extra_event_info: Option<Vec<u8>>,
pub state: Option<Vec<u8>>,
}

impl NavEvent {
/// Create a new nav event from the details of a [NavCtx::navigate] event
pub fn new_from_navigate(
new_dest: NavHistoryEntry,
old_dest: Option<NavHistoryEntry>,
opts: NavOptions,
) -> NavEvent {
let event = match opts.action {
NavAction::Push => NavEventType::Push,
NavAction::Replace => NavEventType::Replace,
};

let new_url = Url::parse(&new_dest.url).ok();
let old_url = old_dest
.as_ref()
.and_then(|dest| Url::parse(&dest.url).ok());

let same_document = if let (Some(old_url), Some(new_url)) = (old_url, new_url) {
old_url.path() == new_url.path()
} else {
false
};

NavEvent {
event,
same_document,
from: old_dest,
to: new_dest,
info: opts.extra_event_info,
state: opts.state,
}
}
}
85 changes: 85 additions & 0 deletions crates/core/src/live_socket/navigation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
mod ffi;

use super::socket::LiveSocket;
pub use ffi::*;
use reqwest::Url;
use std::sync::Arc;

#[derive(Clone)]
struct HandlerInternal(pub Option<Arc<dyn NavEventHandler>>);

impl std::fmt::Debug for HandlerInternal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_some() {
write!(f, "Handler Active")?;
} else {
write!(f, "No Handler Present")?;
};
Ok(())
}
}

#[derive(Debug, Clone)]
pub struct NavCtx {
history: Vec<NavHistoryEntry>,
current_id: HistoryId,
navigation_event_handler: HandlerInternal,
current_dest: Option<NavHistoryEntry>,
}

impl NavHistoryEntry {
pub fn new(url: Url, id: HistoryId, state: Option<Vec<u8>>) -> Self {
Self {
url: url.to_string(),
id,
state,
}
}
}

impl NavCtx {
pub fn new() -> Self {
Self {
history: vec![],
current_id: 0,
navigation_event_handler: HandlerInternal(None),
current_dest: None,
}
}

/// Navigate to `url` with behavior and metadata specified in `opts`.
pub fn navigate(&mut self, url: Url, opts: NavOptions) {
let next_dest = self.speculative_next_dest(&url, opts.state.clone());
let event = NavEvent::new_from_navigate(next_dest.clone(), self.current_dest.clone(), opts);

match self.handle_event(event) {
HandlerResponse::Default => {}
HandlerResponse::PreventDefault => return,
};

self.current_id += 1;
self.history.push(next_dest)
}

pub fn set_event_handler(&mut self, handler: Arc<dyn NavEventHandler>) {
self.navigation_event_handler.0 = Some(handler)
}

pub fn handle_event(&mut self, event: NavEvent) -> HandlerResponse {
if let Some(handler) = self.navigation_event_handler.0.as_ref() {
handler.handle_event(event)
} else {
HandlerResponse::Default
}
}

fn speculative_next_dest(&self, url: &Url, state: Option<Vec<u8>>) -> NavHistoryEntry {
NavHistoryEntry {
id: self.current_id + 1,
url: url.to_string(),
state,
}
}
}

impl LiveSocket {}
6 changes: 6 additions & 0 deletions crates/core/src/live_socket/socket.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{collections::HashMap, sync::Arc, time::Duration};

use super::navigation::{NavCtx, NavOptions};

use log::debug;
use phoenix_channels_client::{url::Url, Number, Payload, Socket, Topic, JSON};
use reqwest::Method as ReqMethod;
Expand Down Expand Up @@ -80,6 +82,7 @@ pub struct LiveSocket {
pub dead_render: Document,
pub style_urls: Vec<String>,
pub has_live_reload: bool,
navigation_ctx: NavCtx,
cookies: Vec<String>,
timeout: Duration,
}
Expand Down Expand Up @@ -305,6 +308,8 @@ impl LiveSocket {
.last();

let has_live_reload = live_reload_iframe.is_some();
let mut navigation_ctx = NavCtx::new();
navigation_ctx.navigate(url.clone(), NavOptions::default());

debug!("iframe src: {live_reload_iframe:?}");

Expand All @@ -321,6 +326,7 @@ impl LiveSocket {
has_live_reload,
cookies,
format,
navigation_ctx,
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/core/src/live_socket/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::time::Duration;

use super::*;
mod error;
mod navigation;
mod streaming;
mod upload;

Expand Down
81 changes: 81 additions & 0 deletions crates/core/src/live_socket/tests/navigation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::sync::{Arc, Mutex};

use crate::live_socket::navigation::*;
use pretty_assertions::{assert_eq, assert_ne};
use reqwest::Url;
use serde::{Deserialize, Serialize};

// Mock event handler used to validate the internal
// navigation objects state.
pub struct NavigationInspector {
last_event: Mutex<Option<NavEvent>>,
event_ct: Mutex<usize>,
}

#[derive(Serialize, Deserialize)]
pub struct EventMetadata {
prevent_default: bool,
}

#[derive(Serialize, Deserialize)]
pub struct HistoryState {
name: String,
}

impl NavEventHandler for NavigationInspector {
fn handle_event(&self, event: NavEvent) -> HandlerResponse {
*self.last_event.lock().expect("Lock poisoned!") = Some(event);
*self.event_ct.lock().expect("Lock poisoned!") += 1;
HandlerResponse::Default
}
}

impl NavigationInspector {
pub fn new() -> Self {
Self {
last_event: None.into(),
event_ct: 0.into(),
}
}

pub fn last_event(&self) -> Option<NavEvent> {
self.last_event.lock().expect("Lock poisoned!").clone()
}

pub fn event_ct(&self) -> usize {
self.event_ct.lock().expect("Lock poisoned!").clone()
}
}

#[test]
fn basic_internal_nav() {
let handler = Arc::new(NavigationInspector::new());
let mut ctx = NavCtx::new();
ctx.set_event_handler(handler.clone());

// sanity check
assert_eq!(handler.event_ct(), 0);
assert!(handler.last_event().is_none());

// simple push nav
let url_str = "https://www.website.com/live";
let url = Url::parse(url_str).expect("URL failed to parse");
ctx.navigate(url, NavOptions::default());

assert_eq!(handler.event_ct(), 1);
assert_eq!(
NavEvent {
info: None,
state: None,
event: NavEventType::Push,
same_document: false,
from: None,
to: NavHistoryEntry {
state: None,
id: 1,
url: url_str.to_string(),
}
},
handler.last_event().expect("Missing Event")
);
}
Loading