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

New timeline API #940

Merged
merged 2 commits into from
Oct 12, 2022
Merged

New timeline API #940

merged 2 commits into from
Oct 12, 2022

Conversation

jplatte
Copy link
Collaborator

@jplatte jplatte commented Aug 10, 2022

Status:

  • Basic message receiving
  • Basic back-pagination
  • Live edits
  • Live reactions
  • Server-side reaction aggregations
  • Server-side edit aggregations
  • JSON source of timeline items
  • Reaction redactions
  • Testing
  • Local echo
  • Encryption info

Not planned for this PR, but planned for later:

  • Catch-all "raw" timeline item
  • Decrypting existing previously-undecryptable events in the timeline when the key is received
  • Storage

@jplatte
Copy link
Collaborator Author

jplatte commented Aug 10, 2022

Things I would like to discuss at the current stage:

  • What do we want to do with the existing experimental-timeline flag?
    • We have an example plus the FFI crate activating it, for IDE experience to not be horrible when working on the new one, those need to allow both features to be active at the same time or make those dependencies optional (or possibly just the FFI one)
  • How do we deal with thing like replies
    • I'm pretty sure we want users to be able to render the reply, even if they can't render the replied-to message yet
    • One approach: Don't make the replied-to message part of the timeline item, but offer a separate async function to obtain the model bits required to render it
    • Another approach: Make the replied-to message part of the timeline item, but have three states: [no reply, reply loading, reply] and automatically obtain the required model bits when encountering a reply, then update the timeline item again
    • The same question applies to media, although the answer could be different (the first approach seems more reasonable there because you might want to do the media loading at different times depending on network conditions)

@poljar
Copy link
Contributor

poljar commented Aug 10, 2022

Things I would like to discuss at the current stage:

* What do we want to do with the existing `experimental-timeline` flag?
  
  * We have an example plus the FFI crate activating it, for IDE experience to not be horrible when working on the new one, those need to allow both features to be active at the same time or make those dependencies optional (or possibly just the FFI one)

I think we should just remove the feature from the codebase. The example should be updated to use the new feature. For the FFI I would expect the FFI crate to be updated and hopefully tested in Element X as part of this PR. This does mean some coordination with the Element X team and live testing but should overall assure us that the PR works and is what Element X wants.

  * One approach: Don't make the replied-to message part of the timeline item, but offer a separate async function to obtain the model bits required to render it
  * Another approach: Make the replied-to message part of the timeline item, but have three states: [no reply, reply loading, reply] and automatically obtain the required model bits when encountering a reply, then update the timeline item again
  * The same question applies to media, although the answer could be different (the first approach seems more reasonable there because you might want to do the media loading at different times depending on network conditions)

In both cases you can't be too sure if the client actually wants to actually render the individual item, i.e. is the message in view or not. Seems like any decision on whether we should fetch additional data from the server or not should be delegated to the client itself.

In both cases the client can and should display a placeholder until it has all the data to render things fully.

@jplatte
Copy link
Collaborator Author

jplatte commented Aug 10, 2022

Re. experimental-timeline, I was kind of hoping for this PR to not get huge, i.e. merge it before we add any FFI things. But there's definitely merit to getting it close to feature parity before merging, and replacing the old API immediately.

In both cases you can't be too sure if the client actually wants to actually render the individual item, i.e. is the message in view or not. Seems like any decision on whether we should fetch additional data from the server or not should be delegated to the client itself.

This is a good point. I guess we can still have the three-states thing though, have the client call timeline.load_reply(event_id) which doesn't return anything, but causes another update to that timeline item to be sent once the data is available.

@jplatte jplatte force-pushed the jplatte/new-timeline branch 4 times, most recently from 8d6c890 to 7af4457 Compare August 15, 2022 17:24
@zecakeh

This comment was marked as resolved.

@jplatte

This comment was marked as resolved.

@zecakeh
Copy link
Collaborator

zecakeh commented Aug 16, 2022

I'm guessing all timeline event types will be supported later? What is the plan with events that are not successfully deserialized?

@jplatte
Copy link
Collaborator Author

jplatte commented Aug 16, 2022

Yeah, the plan is to support all specced event types. Events that aren't successfully deserialized haven't been discussed so far, but I'm pretty sure we'll want to cover that somehow. Element Web has a dev mode thing where reactions (and I think edits too) show up as greyed-out extra entries in the timeline, I think we'll want to support something like that, maybe in the form of a "raw" timeline item that is added whenever an event doesn't otherwise produce a new timeline item.

@zecakeh
Copy link
Collaborator

zecakeh commented Aug 16, 2022

We plan to do the same thing in Fractal in debug builds, that's why I was asking. This seems like a good solution.

One basic thing currently missing for TimelineItem is the access to origin_server_ts.

@jplatte jplatte force-pushed the jplatte/new-timeline branch 6 times, most recently from 8780377 to fb44613 Compare August 23, 2022 08:45
@jplatte jplatte mentioned this pull request Aug 23, 2022
@jplatte jplatte marked this pull request as ready for review August 23, 2022 14:33
@jplatte
Copy link
Collaborator Author

jplatte commented Aug 23, 2022

I want to see whether bindings are generating correctly, but this isn't actually ready for review.

@jplatte jplatte force-pushed the jplatte/new-timeline branch 3 times, most recently from 2bb9970 to 518785d Compare August 23, 2022 16:53
@codecov
Copy link

codecov bot commented Aug 23, 2022

Codecov Report

Base: 72.77% // Head: 73.25% // Increases project coverage by +0.48% 🎉

Coverage data is based on head (ff8f732) compared to base (67d968d).
Patch coverage: 76.78% of modified lines in pull request are covered.

❗ Current head ff8f732 differs from pull request most recent head 533484f. Consider uploading reports for the commit 533484f to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #940      +/-   ##
==========================================
+ Coverage   72.77%   73.25%   +0.48%     
==========================================
  Files         106      109       +3     
  Lines       11867    12145     +278     
==========================================
+ Hits         8636     8897     +261     
- Misses       3231     3248      +17     
Impacted Files Coverage Δ
crates/matrix-sdk/src/room/mod.rs 35.71% <ø> (ø)
...ates/matrix-sdk/src/room/timeline/event_handler.rs 71.00% <71.00%> (ø)
crates/matrix-sdk/src/room/timeline/event_item.rs 84.61% <84.61%> (ø)
crates/matrix-sdk/src/room/timeline/mod.rs 85.96% <85.96%> (ø)
crates/matrix-sdk/src/room/common.rs 76.12% <100.00%> (+8.72%) ⬆️
crates/matrix-sdk/src/error.rs 25.92% <0.00%> (-16.94%) ⬇️
crates/matrix-sdk/src/client/login_builder.rs 22.42% <0.00%> (-4.68%) ⬇️
...ix-sdk-crypto/src/types/events/room_key_request.rs 72.50% <0.00%> (-1.31%) ⬇️
crates/matrix-sdk-crypto/src/requests.rs 77.46% <0.00%> (-0.92%) ⬇️
crates/matrix-sdk-crypto/src/machine.rs 66.66% <0.00%> (-0.35%) ⬇️
... and 15 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@zecakeh
Copy link
Collaborator

zecakeh commented Aug 25, 2022

This is currently used in this MR on Fractal: fractal!1136.

I'll try to update it frequently with new changes from this PR.

@jplatte jplatte force-pushed the jplatte/new-timeline branch 9 times, most recently from 8cf8f34 to 2d6db64 Compare September 20, 2022 13:40
@jplatte jplatte force-pushed the jplatte/new-timeline branch 4 times, most recently from f7f0cb4 to b76484d Compare September 30, 2022 13:12
@jplatte
Copy link
Collaborator Author

jplatte commented Sep 30, 2022

This is now ready for review! I have removed the FFI from this branch, and will post it as a separate PR.

Copy link
Contributor

@poljar poljar left a comment

Choose a reason for hiding this comment

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

This is a bit longish so bear with me, I didn't yet look at the implementation in detail but these things did catch my attention while I tried to use the new API.

Docs

Getting started with this was non-trivial, the docs are very generic and just
point you towards the docs for the StreamExt API, these have a lot of methods and there's no real way to know what you want.

Example isn't that nice

The new timeline feature is now used for multiple things:

  1. Consuming events that are part of the timeline
  2. Scrolling through the timeline

The old one was only used for point no 2, and the timeline example reflects
that, we should split that example into two, one for each timeline feature.

What the first example could look like:

diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs
index ea35976a0b..345fc1d6a8 100644
--- a/examples/timeline/src/main.rs
+++ b/examples/timeline/src/main.rs
@@ -1,124 +1,81 @@
-use std::{env, process::exit, sync::Mutex, time::Duration};
-
+use anyhow::Result;
+use clap::Parser;
 use futures::StreamExt;
 use futures_signals::signal_vec::SignalVecExt;
-use matrix_sdk::{
-    self,
-    config::SyncSettings,
-    room::Room,
-    ruma::{
-        api::client::filter::{FilterDefinition, LazyLoadOptions},
-        events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent},
-        uint,
-    },
-    store::make_store_config,
-    Client, LoopCtrl,
-};
-use tokio::sync::oneshot;
+use matrix_sdk::{self, config::SyncSettings, ruma::OwnedRoomId, Client};
 use url::Url;
 
-async fn login(homeserver_url: String, username: &str, password: &str) -> Client {
-    let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
-    let path = "./";
-    let store_config = make_store_config(path, Some("some password")).unwrap();
-    let client = Client::builder()
-        .homeserver_url(homeserver_url)
-        .store_config(store_config)
-        .build()
-        .await
-        .unwrap();
+#[derive(Parser, Debug)]
+struct Cli {
+    /// The homeserver to connect to.
+    #[clap(value_parser)]
+    homeserver: Url,
 
-    client
-        .login_username(username, password)
-        .initial_device_display_name("rust-sdk")
-        .send()
-        .await
-        .unwrap();
-    client
-}
+    /// The user name that should be used for the login.
+    #[clap(value_parser)]
+    user_name: String,
+
+    /// The password that should be used for the login.
+    #[clap(value_parser)]
+    password: String,
+
+    /// Set the proxy that should be used for the connection.
+    #[clap(short, long)]
+    proxy: Option<Url>,
 
-fn _event_content(event: AnySyncTimelineEvent) -> Option<String> {
-    if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
-        SyncMessageLikeEvent::Original(event),
-    )) = event
-    {
-        Some(event.content.msgtype.body().to_owned())
-    } else {
-        None
-    }
+    /// Enable verbose logging output.
+    #[clap(short, long, action)]
+    verbose: bool,
+
+    /// The room id that we should listen for the,
+    #[clap(value_parser)]
+    room_id: OwnedRoomId,
 }
 
-async fn print_timeline(room: Room) {
-    let timeline = room.timeline();
-    let mut timeline_stream = timeline.signal().to_stream();
-    tokio::spawn(async move {
-        while let Some(_diff) = timeline_stream.next().await {
-            // Is a straight-forward CLI example of dynamic timeline items
-            // possible?? let event = event.unwrap();
-            //if let Some(content) =
-            // event_content(event.event.deserialize().unwrap()) {
-            //    println!("{content}");
-            //}
-        }
-    });
+async fn login(cli: Cli) -> Result<Client> {
+    let builder = Client::builder().homeserver_url(cli.homeserver);
 
-    loop {
-        match timeline.paginate_backwards(uint!(10)).await {
-            Ok(outcome) if !outcome.more_messages => break,
-            Ok(_) => {}
-            Err(e) => {
-                eprintln!("error paginating: {e}");
-            }
-        }
-    }
+    let builder = if let Some(proxy) = cli.proxy { builder.proxy(proxy) } else { builder };
+
+    let client = builder.build().await?;
+
+    client
+        .login_username(&cli.user_name, &cli.password)
+        .initial_device_display_name("rust-sdk")
+        .send()
+        .await?;
+
+    Ok(client)
 }
 
 #[tokio::main]
-async fn main() -> anyhow::Result<()> {
+async fn main() -> Result<()> {
     tracing_subscriber::fmt::init();
 
-    let (homeserver_url, username, password, room_id) =
-        match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) {
-            (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
-            _ => {
-                eprintln!(
-                    "Usage: {} <homeserver_url> <username> <password> <room_id>",
-                    env::args().next().unwrap()
-                );
-                exit(1)
-            }
-        };
-
-    let client = login(homeserver_url, &username, &password).await;
-
-    let mut filter = FilterDefinition::default();
-    filter.room.include_leave = true;
-    filter.room.state.lazy_load_options =
-        LazyLoadOptions::Enabled { include_redundant_members: false };
-
-    let sync_settings = SyncSettings::new().timeout(Duration::from_secs(30)).filter(filter.into());
-    let (sender, receiver) = oneshot::channel::<()>();
-    let sender = Mutex::new(Some(sender));
-    let client_clone = client.clone();
-    tokio::spawn(async move {
-        client_clone
-            .sync_with_callback(sync_settings, |_| async {
-                if let Some(sender) = sender.lock().unwrap().take() {
-                    sender.send(()).unwrap();
-                }
-                LoopCtrl::Continue
-            })
-            .await
-            .unwrap();
-    });
+    let cli = Cli::parse();
+    let room_id = cli.room_id.clone();
+    let client = login(cli).await?;
+
+    let sync_settings = SyncSettings::default();
 
     // Wait for the first sync response
     println!("Wait for the first sync");
-    receiver.await.unwrap();
 
-    let room = client.get_room(room_id.as_str().try_into().unwrap()).unwrap();
+    client.sync_once(sync_settings.clone()).await?;
+
+    // Get the timeline stream and listen to it.
+    let room = client.get_room(&room_id).unwrap();
+    let timeline = room.timeline();
+    let mut timeline_stream = timeline.signal().to_stream();
+
+    tokio::spawn(async move {
+        while let Some(diff) = timeline_stream.next().await {
+            println!("Received a timeline diff {diff:#?}");
+        }
+    });
 
-    print_timeline(room).await;
+    // Sync forever
+    client.sync(sync_settings).await?;
 
     Ok(())
 }

Incidentally, putting something along those lines into the docs would have
helped quite a bit.

Local echo support hard to use

Sending a message means:

  1. VecDiff::Push(TimelineKey::TransactionId) - The sending of the event
  2. VecDiff::UpdateAt(_, TimelineKey::TransactionId) - The receival of the /send response. First remote echo.
  3. VecDiff::UpdateAt(_, TimelineKey::EventId) - The remote echo through the /sync call.

Clients might want to do one of the following:

  1. Render only at no. 1 or no. 3 - No local echo support
  2. Render at no. 1 and 2 - Simple local echo support, i.e. grey message or a single checkmark.
  3. Render at no. 1, 2, and 3 - What element does, two checkmarks.

The first two can be connected without additional state, while the last one
can't easily.

You need to pull out the Raw event and look for the unsigned field and if it contains the TransactionId

No. 2, the first VecDiff::UpdateAt, should contain the EventId, since this is what the response gives us. This way we can already mark the event as completed.

No 3, the second VecDif::UpdateAt should have a simple accessor for the TransactionId that's part of the unsigned field.

Looking at this further, this seems to be a bit low level and generic, or perhaps just not explicit enough.

If I pretend to now know the Matrix spec, I would be wholly confused by the TransactionId and EventId timeline keys. But if I saw an instant messenger I could guess what a local echo is.

So as a client developer that wants to add local echo I would like my
VecDiff tell me explicitly this TimelineItem is one of the three scenarios mentioned above. As it is now, I have to guestimate what the different TimelineKeys mean and how to render them.

I think that this would also let us to document things much better. Edits seem to have a similar problem, though an is_edit() method exists which let's you figure this out a bit easier.

Things to fix

The PR seems to work well for what it tries to achieve. It is much easier to
consume events using this API, but I still think we might tweak the API to make it even simpler. There are some things I would like to see fixed before we merge this:

  1. Support for m.room.encrypted events. E2EE is one of our best selling points yet we always treat it as an afterthought. After Sliding Sync timeline events and extensions #1054 is merged, Element X will need this anyways.

  2. We don't have access to the redacted_because or rather RedactedUnsigned field when we receive a diff for a TimelineItemContent::RedactedMessage.

  3. VecDiffs should at least let you have the event id at the first remote echo stage, step 2. Later on I would like to see a more explicit local echo API.

@zecakeh
Copy link
Collaborator

zecakeh commented Oct 8, 2022

Like I said on matrix, currently we don't get the aggregated reactions. However if we forward the Relations from the event to the NewEventTimelineItem, it works, since the rest of the code is in place.

@zecakeh
Copy link
Collaborator

zecakeh commented Oct 11, 2022

I can confirm that I get all the reactions now.

@jplatte jplatte requested a review from poljar October 11, 2022 15:00
Copy link
Contributor

@poljar poljar left a comment

Choose a reason for hiding this comment

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

Please add the license headers, the rest of my comments can wait for another PR, like the comments from my initial review.

crates/matrix-sdk/src/room/timeline/mod.rs Show resolved Hide resolved
TimelineKey,
};

pub(super) fn handle_live_event(
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be part of TimelineInner?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TimelineInner is a fairly recent addition compared to the rest of this module but yeah it sounds reasonable.

_ => {}
}

if !self.event_added {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't this be handled like the did_update flag? Where we return the value instead of mutating things in some magic place somewhere else?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, there is only a single place where it is set, so I think it's still obvious enough what happens and this way the method signatures are shorter. We can try out the other way though.

}
}

fn find_event(
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, don't we expect this to be problematic? Do we ever remove items from the timeline? I don't think we do. How many items do we want to support? Don't we want to let the caller configure how many items we keep?

We'll likely need an index that maps from the key to the index in any case. Let's at least add a fixme.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm a little confused, do you mean this is problematic if we keeps those indices around? The intention of this function is just to find the right index to update, it is only meant to be valid for a very short duration.

We could have a separate index so we don't have to do a linear scan, but that would be more complex and I don't think we need it right now (maybe never if we put a reasonable upper bound on the amount of entries that can be in the timeline).

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I meant the growth of the MutableVec is unbounded thus the rfind() and in turn the find_event() function will take an increasing amount of time in the worst case.

}
}

/// Add more events to the start of the timeline.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if calling this operation scrolling would add more clarity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🤷🏼

crates/matrix-sdk/src/room/timeline/mod.rs Show resolved Hide resolved

self.maybe_update_timeline_item(event_id, "edit", |item| {
if self.meta.sender != item.sender() {
info!(
Copy link
Contributor

Choose a reason for hiding this comment

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

All the log lines seem to be missing the room id, this will be quite annoying to debug if we don't know the room the log line applies to.

Copy link
Collaborator Author

@jplatte jplatte Oct 12, 2022

Choose a reason for hiding this comment

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

I've added it for local echo and back-pagination, for live events I think we should just add a useful span for all event handlers, not just the timeline ones.

Copy link
Contributor

@gnunicorn gnunicorn left a comment

Choose a reason for hiding this comment

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

Aside from docs, and the big amount of FIXMEs and ToDos I'd prefer to see in our issue tracker rather than in some code comments, this looks good to me as the first iteration! Good job, well done!

crates/matrix-sdk/src/room/common.rs Outdated Show resolved Hide resolved
inner: TimelineInner,
room: room::Common,
start_token: Mutex<Option<String>>,
_end_token: Mutex<Option<String>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for? Is that for future usage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, future forwards pagination support.


pub(super) fn to_redacted(&self) -> Self {
build!(Self {
// FIXME: Change when we support state events
Copy link
Contributor

Choose a reason for hiding this comment

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

Tracking issue for the FIXME's? There's quite a few in this file...


/// The reactions.
///
/// Key: The reaction, usually an emoji.\
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Key: The reaction, usually an emoji.\
/// Key: The reaction, usually an emoji.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is actually load-bearing, it inserts a line break before the Value line without putting them on entirely separate paragraphs (the same way as double space at the end of the line, but that is stripped by many editors).

}

let mut lock = self.timeline.items.lock_mut();
if let Some((idx, item)) = find_event(&lock, event_id) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if that could become a bottle neck if we have scrolled back a room for a bit ... but I'll put that off as a future problem.

@jplatte jplatte enabled auto-merge (rebase) October 12, 2022 13:48
@jplatte jplatte merged commit 086b612 into main Oct 12, 2022
@jplatte jplatte deleted the jplatte/new-timeline branch October 12, 2022 14:12
@jplatte jplatte mentioned this pull request Oct 12, 2022
29 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants