[fractal/fractal-next] timeline: Use streaming api to load timeline



commit b530e11a680003e390188e6ea5cf83bb29551ec8
Author: Julian Sparber <julian sparber net>
Date:   Mon Feb 7 18:28:44 2022 +0100

    timeline: Use streaming api to load timeline
    
    This load the timeline only once the user opends the room.
    This also updates some deps, including the sdk.

 Cargo.lock                              | 169 ++++++++++++---------
 Cargo.toml                              |   3 +-
 src/login.rs                            |   2 +-
 src/session/content/room_history/mod.rs |  82 +++++++---
 src/session/mod.rs                      |  11 +-
 src/session/room/event.rs               |   4 +-
 src/session/room/mod.rs                 |  40 ++---
 src/session/room/timeline.rs            | 257 ++++++++++++++++++++------------
 src/utils.rs                            |   7 +-
 9 files changed, 352 insertions(+), 223 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index aefdfeb68..c62cb132d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -313,14 +313,14 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backoff"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9fe17f59a06fe8b87a6fc8bf53bb70b3aba76d7685f432487a68cd5552853625"
+checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
 dependencies = [
  "futures-core",
  "getrandom 0.2.4",
  "instant",
- "pin-project",
+ "pin-project-lite",
  "rand 0.8.4",
  "tokio",
 ]
@@ -387,6 +387,15 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "block-modes"
 version = "0.7.0"
@@ -710,20 +719,19 @@ dependencies = [
 ]
 
 [[package]]
-name = "crypto-mac"
-version = "0.10.1"
+name = "crypto-common"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
+checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06"
 dependencies = [
  "generic-array",
- "subtle",
 ]
 
 [[package]]
 name = "crypto-mac"
-version = "0.11.1"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
 dependencies = [
  "generic-array",
  "subtle",
@@ -745,7 +753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
 dependencies = [
  "byteorder",
- "digest",
+ "digest 0.9.0",
  "rand_core 0.5.1",
  "subtle",
  "zeroize",
@@ -800,6 +808,17 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837"
+dependencies = [
+ "block-buffer 0.10.2",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "easy-parallel"
 version = "3.2.0"
@@ -825,7 +844,7 @@ dependencies = [
  "ed25519",
  "rand 0.7.3",
  "serde",
- "sha2",
+ "sha2 0.9.9",
  "zeroize",
 ]
 
@@ -981,6 +1000,7 @@ name = "fractal"
 version = "0.0.1"
 dependencies = [
  "ashpd",
+ "async-stream",
  "futures",
  "gettext-rs",
  "gst-plugin-gtk4",
@@ -1812,7 +1832,7 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
 dependencies = [
- "digest",
+ "digest 0.9.0",
  "hmac 0.10.1",
 ]
 
@@ -1822,18 +1842,17 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
 dependencies = [
- "crypto-mac 0.10.1",
- "digest",
+ "crypto-mac",
+ "digest 0.9.0",
 ]
 
 [[package]]
 name = "hmac"
-version = "0.11.0"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2"
 dependencies = [
- "crypto-mac 0.11.1",
- "digest",
+ "digest 0.10.2",
 ]
 
 [[package]]
@@ -2207,6 +2226,15 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "lru"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "274353858935c992b13c0ca408752e2121da852d07dec7ce5f108c77dfa14d1f"
+dependencies = [
+ "hashbrown",
+]
+
 [[package]]
 name = "mac"
 version = "0.1.1"
@@ -2263,7 +2291,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
 [[package]]
 name = "matrix-qrcode"
 version = "0.2.0"
-source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=messages-api#cb8a156b3ac13138398354d75e0b766143fdc660";
+source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=timeline_stream#887986ebce4c72dbf1d2e8198e453162e94ea11f";
 dependencies = [
  "base64",
  "byteorder",
@@ -2271,13 +2299,14 @@ dependencies = [
  "qrcode",
  "rqrr",
  "ruma-identifiers",
+ "ruma-serde",
  "thiserror",
 ]
 
 [[package]]
 name = "matrix-sdk"
 version = "0.4.1"
-source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=messages-api#cb8a156b3ac13138398354d75e0b766143fdc660";
+source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=timeline_stream#887986ebce4c72dbf1d2e8198e453162e94ea11f";
 dependencies = [
  "anymap2",
  "async-stream",
@@ -2306,14 +2335,15 @@ dependencies = [
 [[package]]
 name = "matrix-sdk-base"
 version = "0.4.1"
-source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=messages-api#cb8a156b3ac13138398354d75e0b766143fdc660";
+source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=timeline_stream#887986ebce4c72dbf1d2e8198e453162e94ea11f";
 dependencies = [
  "chacha20poly1305",
  "dashmap",
+ "futures-channel",
  "futures-core",
  "futures-util",
- "hmac 0.11.0",
- "lru",
+ "hmac 0.12.0",
+ "lru 0.7.2",
  "matrix-sdk-common",
  "matrix-sdk-crypto",
  "pbkdf2",
@@ -2321,7 +2351,7 @@ dependencies = [
  "ruma",
  "serde",
  "serde_json",
- "sha2",
+ "sha2 0.10.1",
  "sled",
  "thiserror",
  "tokio",
@@ -2332,7 +2362,7 @@ dependencies = [
 [[package]]
 name = "matrix-sdk-common"
 version = "0.4.1"
-source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=messages-api#cb8a156b3ac13138398354d75e0b766143fdc660";
+source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=timeline_stream#887986ebce4c72dbf1d2e8198e453162e94ea11f";
 dependencies = [
  "async-lock",
  "async-trait",
@@ -2342,13 +2372,15 @@ dependencies = [
  "serde",
  "tokio",
  "uuid",
+ "wasm-bindgen",
  "wasm-bindgen-futures",
+ "web-sys",
 ]
 
 [[package]]
 name = "matrix-sdk-crypto"
 version = "0.4.1"
-source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=messages-api#cb8a156b3ac13138398354d75e0b766143fdc660";
+source = 
"git+https://github.com/jsparber/matrix-rust-sdk.git?branch=timeline_stream#887986ebce4c72dbf1d2e8198e453162e94ea11f";
 dependencies = [
  "aes 0.7.5",
  "aes-gcm",
@@ -2359,7 +2391,7 @@ dependencies = [
  "dashmap",
  "futures-util",
  "getrandom 0.2.4",
- "hmac 0.11.0",
+ "hmac 0.12.0",
  "matrix-qrcode",
  "matrix-sdk-common",
  "olm-rs",
@@ -2368,7 +2400,7 @@ dependencies = [
  "ruma",
  "serde",
  "serde_json",
- "sha2",
+ "sha2 0.10.1",
  "sled",
  "thiserror",
  "tracing",
@@ -2862,11 +2894,11 @@ checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5"
 
 [[package]]
 name = "pbkdf2"
-version = "0.9.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739"
+checksum = "a4628cc3cf953b82edcd3c1388c5715401420ce5524fedbab426bd5aba017434"
 dependencies = [
- "crypto-mac 0.11.1",
+ "digest 0.10.2",
 ]
 
 [[package]]
@@ -2928,26 +2960,6 @@ dependencies = [
  "siphasher",
 ]
 
-[[package]]
-name = "pin-project"
-version = "1.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "1.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
-dependencies = [
- "proc-macro2 1.0.36",
- "quote 1.0.14",
- "syn 1.0.85",
-]
-
 [[package]]
 name = "pin-project-lite"
 version = "0.2.8"
@@ -3374,13 +3386,13 @@ checksum = "6fa79947f53b20adb909a323d828d0fd744fa9d854792df07913b083bcd4d63b"
 dependencies = [
  "g2p",
  "image",
- "lru",
+ "lru 0.6.6",
 ]
 
 [[package]]
 name = "ruma"
 version = "0.4.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "assign",
  "js_int",
@@ -3398,7 +3410,7 @@ dependencies = [
 [[package]]
 name = "ruma-api"
 version = "0.18.5"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "bytes",
  "http",
@@ -3414,7 +3426,7 @@ dependencies = [
 [[package]]
 name = "ruma-api-macros"
 version = "0.18.5"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "proc-macro-crate 1.1.0",
  "proc-macro2 1.0.36",
@@ -3425,7 +3437,7 @@ dependencies = [
 [[package]]
 name = "ruma-client-api"
 version = "0.12.3"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "assign",
  "bytes",
@@ -3445,7 +3457,7 @@ dependencies = [
 [[package]]
 name = "ruma-common"
 version = "0.6.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "indexmap",
  "js_int",
@@ -3460,7 +3472,7 @@ dependencies = [
 [[package]]
 name = "ruma-events"
 version = "0.24.6"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "indoc",
  "js_int",
@@ -3472,12 +3484,13 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
+ "wildmatch",
 ]
 
 [[package]]
 name = "ruma-events-macros"
 version = "0.24.6"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "proc-macro-crate 1.1.0",
  "proc-macro2 1.0.36",
@@ -3488,7 +3501,7 @@ dependencies = [
 [[package]]
 name = "ruma-federation-api"
 version = "0.3.1"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "js_int",
  "ruma-api",
@@ -3503,20 +3516,22 @@ dependencies = [
 [[package]]
 name = "ruma-identifiers"
 version = "0.20.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "percent-encoding",
+ "rand 0.8.4",
  "ruma-identifiers-macros",
  "ruma-identifiers-validation",
  "ruma-serde",
  "ruma-serde-macros",
  "serde",
+ "uuid",
 ]
 
 [[package]]
 name = "ruma-identifiers-macros"
 version = "0.20.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "quote 1.0.14",
  "ruma-identifiers-validation",
@@ -3526,7 +3541,7 @@ dependencies = [
 [[package]]
 name = "ruma-identifiers-validation"
 version = "0.5.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "thiserror",
 ]
@@ -3534,8 +3549,9 @@ dependencies = [
 [[package]]
 name = "ruma-serde"
 version = "0.5.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
+ "base64",
  "bytes",
  "form_urlencoded",
  "itoa 0.4.8",
@@ -3548,7 +3564,7 @@ dependencies = [
 [[package]]
 name = "ruma-serde-macros"
 version = "0.5.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "proc-macro-crate 1.1.0",
  "proc-macro2 1.0.36",
@@ -3559,7 +3575,7 @@ dependencies = [
 [[package]]
 name = "ruma-signatures"
 version = "0.9.0"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "base64",
  "ed25519-dalek",
@@ -3568,7 +3584,7 @@ dependencies = [
  "ruma-identifiers",
  "ruma-serde",
  "serde_json",
- "sha2",
+ "sha2 0.9.9",
  "thiserror",
  "tracing",
 ]
@@ -3576,7 +3592,7 @@ dependencies = [
 [[package]]
 name = "ruma-state-res"
 version = "0.4.1"
-source = 
"git+https://github.com/ruma/ruma/?rev=fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d#fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d";
+source = 
"git+https://github.com/ruma/ruma/?rev=37095f88553b311e7a70adaaabe39976fb8ff71c#37095f88553b311e7a70adaaabe39976fb8ff71c";
 dependencies = [
  "itertools",
  "js_int",
@@ -3652,7 +3668,7 @@ dependencies = [
  "num",
  "rand 0.8.4",
  "serde",
- "sha2",
+ "sha2 0.9.9",
  "zbus 1.9.1",
  "zbus_macros 1.9.1",
  "zvariant 2.10.0",
@@ -3781,13 +3797,24 @@ version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.9.0",
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest",
+ "digest 0.9.0",
  "opaque-debug",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.10.2",
+]
+
 [[package]]
 name = "sharded-slab"
 version = "0.1.4"
diff --git a/Cargo.toml b/Cargo.toml
index c6e78d183..70b57a85a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ incremental = false
 codegen-units = 16
 
 [dependencies]
+async-stream = "0.3"
 log = "0.4"
 mime = "0.3.16"
 tracing-subscriber = "0.3"
@@ -59,7 +60,7 @@ version = "0.1.0"
 
 [dependencies.matrix-sdk]
 git = "https://github.com/jsparber/matrix-rust-sdk.git";
-branch = "messages-api"
+branch = "timeline_stream"
 features = [
     "socks",
     "encryption",
diff --git a/src/login.rs b/src/login.rs
index c477ea4ca..2ee7f2703 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -331,7 +331,7 @@ impl Login {
         self.freeze();
 
         let handle: JoinHandle<MatrixResult<_>> = spawn_tokio!(async move {
-            let client = Client::new(homeserver_clone)?;
+            let client = Client::new(homeserver_clone).await?;
             Ok(client
                 .send(
                     get_supported_versions::Request::new(),
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index aaefc2fc1..834dfc869 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -29,6 +29,7 @@ use crate::{
         room::{Item, Room, RoomType, Timeline, TimelineState},
         user::UserExt,
     },
+    spawn,
 };
 
 mod imp {
@@ -76,6 +77,7 @@ mod imp {
         pub error: TemplateChild<adw::StatusPage>,
         #[template_child]
         pub stack: TemplateChild<gtk::Stack>,
+        pub is_loading: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -245,13 +247,13 @@ mod imp {
                 } else {
                     obj.set_sticky(adj.value() + adj.page_size() == adj.upper());
                 }
-                obj.load_more_messages(adj);
+                obj.start_loading();
             }));
-            adj.connect_upper_notify(clone!(@weak obj => move |adj| {
+            adj.connect_upper_notify(clone!(@weak obj => move |_| {
                 if obj.sticky() {
                     obj.scroll_down();
                 }
-                obj.load_more_messages(adj);
+                obj.start_loading();
             }));
 
             let key_events = gtk::EventControllerKey::new();
@@ -318,19 +320,15 @@ impl RoomHistory {
             return;
         }
 
-        if let Some(category_handler) = priv_.category_handler.take() {
-            if let Some(room) = self.room() {
+        if let Some(room) = self.room() {
+            if let Some(category_handler) = priv_.category_handler.take() {
                 room.disconnect(category_handler);
             }
-        }
 
-        if let Some(empty_timeline_handler) = priv_.empty_timeline_handler.take() {
-            if let Some(room) = self.room() {
+            if let Some(empty_timeline_handler) = priv_.empty_timeline_handler.take() {
                 room.timeline().disconnect(empty_timeline_handler);
             }
-        }
 
-        if let Some(room) = self.room() {
             if let Some(state_timeline_handler) = priv_.state_timeline_handler.take() {
                 room.timeline().disconnect(state_timeline_handler);
             }
@@ -373,10 +371,10 @@ impl RoomHistory {
             .map(|room| gtk::NoSelection::new(Some(room.timeline())));
 
         priv_.listview.set_model(model.as_ref());
+        priv_.is_loading.set(false);
         priv_.room.replace(room);
-        let adj = priv_.listview.vadjustment().unwrap();
-        self.load_more_messages(&adj);
         self.update_view();
+        self.start_loading();
         self.update_room_state();
         self.notify("room");
         self.notify("empty");
@@ -537,17 +535,57 @@ impl RoomHistory {
         }
     }
 
-    fn load_more_messages(&self, adj: &gtk::Adjustment) {
+    fn need_messages(&self) -> bool {
+        let adj = self.imp().listview.vadjustment().unwrap();
         // Load more messages when the user gets close to the end of the known room
         // history Use the page size twice to detect if the user gets close to
-        // the end
-        if let Some(room) = self.room() {
-            if adj.value() < adj.page_size() * 2.0
-                || adj.upper() <= adj.page_size() / 2.0
-                || room.timeline().is_empty()
-            {
-                room.timeline().load_previous_events();
+        // the endload_timeline
+        adj.value() < adj.page_size() * 2.0 || adj.upper() <= adj.page_size() / 2.0
+    }
+
+    fn start_loading(&self) {
+        let priv_ = self.imp();
+        if !priv_.is_loading.get() {
+            let room = if let Some(room) = self.room() {
+                room
+            } else {
+                return;
+            };
+
+            if !self.need_messages() && !room.timeline().is_empty() {
+                return;
             }
+
+            priv_.is_loading.set(true);
+
+            let obj_weak = self.downgrade();
+            spawn!(async move {
+                loop {
+                    // We don't want to hold a strong ref to `obj` on `await`
+                    let need = if let Some(obj) = obj_weak.upgrade() {
+                        if obj.room().as_ref() == Some(&room) {
+                            obj.need_messages() || room.timeline().is_empty()
+                        } else {
+                            return;
+                        }
+                    } else {
+                        return;
+                    };
+
+                    if need {
+                        if !room.timeline().load().await {
+                            break;
+                        }
+                    } else {
+                        break;
+                    }
+                }
+
+                // Remove the task
+                if let Some(obj) = obj_weak.upgrade() {
+                    obj.imp().is_loading.set(false);
+                }
+            });
         }
     }
 
@@ -585,9 +623,7 @@ impl RoomHistory {
     }
 
     fn try_again(&self) {
-        if let Some(room) = self.room() {
-            room.timeline().load_previous_events();
-        }
+        self.start_loading();
     }
 }
 
diff --git a/src/session/mod.rs b/src/session/mod.rs
index 96e5fa702..f44a29505 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -40,7 +40,6 @@ use matrix_sdk::{
         assign,
         identifiers::RoomId,
     },
-    uuid::Uuid,
     Client, Error as MatrixError, HttpError,
 };
 use rand::{distributions::Alphanumeric, thread_rng, Rng};
@@ -293,11 +292,7 @@ impl Session {
     ) {
         self.imp().logout_on_dispose.set(true);
         let mut path = glib::user_data_dir();
-        path.push(
-            &Uuid::new_v4()
-                .to_hyphenated()
-                .encode_lower(&mut Uuid::encode_buffer()),
-        );
+        path.push(glib::uuid_string_random().as_str());
 
         let handle = spawn_tokio!(async move {
             let passphrase: String = {
@@ -322,7 +317,7 @@ impl Session {
                 config
             };
 
-            let client = Client::new_with_config(homeserver.clone(), config).unwrap();
+            let client = Client::new_with_config(homeserver.clone(), config).await?;
             let response = client
                 .login(&username, &password, None, Some("Fractal Next"))
                 .await;
@@ -371,7 +366,7 @@ impl Session {
                 .passphrase(session.secret.passphrase.clone())
                 .store_path(session.path.clone());
 
-            let client = Client::new_with_config(session.homeserver.clone(), config).unwrap();
+            let client = Client::new_with_config(session.homeserver.clone(), config).await?;
             client
                 .restore_login(matrix_sdk::Session {
                     user_id: session.user_id.clone(),
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 0e1234755..1c1e6c3df 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -16,7 +16,7 @@ use matrix_sdk::{
             AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent,
             Unsigned,
         },
-        identifiers::{EventId, UserId},
+        identifiers::{EventId, TransactionId, UserId},
         MilliSecondsSinceUnixEpoch,
     },
     Error as MatrixError,
@@ -255,7 +255,7 @@ impl Event {
         }
     }
 
-    pub fn matrix_transaction_id(&self) -> Option<String> {
+    pub fn matrix_transaction_id(&self) -> Option<Box<TransactionId>> {
         self.imp()
             .pure_event
             .borrow()
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index ff201db9f..809a26c9c 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -35,11 +35,10 @@ use matrix_sdk::{
             AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, EventType, SyncMessageEvent,
             Unsigned,
         },
-        identifiers::{EventId, RoomId, UserId},
+        identifiers::{EventId, RoomId, TransactionId, UserId},
         serde::Raw,
         MilliSecondsSinceUnixEpoch,
     },
-    uuid::Uuid,
 };
 use serde_json::value::RawValue;
 
@@ -75,7 +74,7 @@ mod imp {
 
     use super::*;
 
-    #[derive(Debug, Default)]
+    #[derive(Default)]
     pub struct Room {
         pub room_id: OnceCell<Box<RoomId>>,
         pub matrix_room: RefCell<Option<MatrixRoom>>,
@@ -318,6 +317,14 @@ mod imp {
             obj.bind_property("display-name", obj.avatar(), "display-name")
                 .flags(glib::BindingFlags::SYNC_CREATE)
                 .build();
+
+            // Load the room history when idle
+            spawn!(
+                glib::source::PRIORITY_LOW,
+                clone!(@weak obj => async move {
+                    obj.timeline().load().await;
+                })
+            );
         }
     }
 }
@@ -491,30 +498,28 @@ impl Room {
                 MatrixRoom::Joined(room) => match category {
                     RoomType::Invited => Ok(()),
                     RoomType::Favorite => {
-                        room.set_tag(TagName::Favorite.as_ref(), TagInfo::new())
-                            .await?;
+                        room.set_tag(TagName::Favorite, TagInfo::new()).await?;
                         if previous_category == RoomType::LowPriority {
-                            room.remove_tag(TagName::LowPriority.as_ref()).await?;
+                            room.remove_tag(TagName::LowPriority).await?;
                         }
                         Ok(())
                     }
                     RoomType::Normal => {
                         match previous_category {
                             RoomType::Favorite => {
-                                room.remove_tag(TagName::Favorite.as_ref()).await?;
+                                room.remove_tag(TagName::Favorite).await?;
                             }
                             RoomType::LowPriority => {
-                                room.remove_tag(TagName::LowPriority.as_ref()).await?;
+                                room.remove_tag(TagName::LowPriority).await?;
                             }
                             _ => {}
                         }
                         Ok(())
                     }
                     RoomType::LowPriority => {
-                        room.set_tag(TagName::LowPriority.as_ref(), TagInfo::new())
-                            .await?;
+                        room.set_tag(TagName::LowPriority, TagInfo::new()).await?;
                         if previous_category == RoomType::Favorite {
-                            room.remove_tag(TagName::Favorite.as_ref()).await?;
+                            room.remove_tag(TagName::Favorite).await?;
                         }
                         Ok(())
                     }
@@ -793,7 +798,7 @@ impl Room {
         self.notify("inviter");
     }
 
-    /// Add new events to the timeline
+    /// Update the room state based on the new sync response
     pub fn append_events(&self, batch: Vec<Event>) {
         let priv_ = self.imp();
 
@@ -832,7 +837,6 @@ impl Room {
             }
         }
 
-        priv_.timeline.get().unwrap().append(batch);
         priv_.latest_change.replace(latest_change);
         self.notify("latest-change");
         self.emit_by_name::<()>("order-changed", &[]);
@@ -907,7 +911,7 @@ impl Room {
     }
 
     /// Send the given `event` in this room, with the temporary ID `txn_id`.
-    fn send_room_message_event(&self, event: AnySyncMessageEvent, txn_id: Uuid) {
+    fn send_room_message_event(&self, event: AnySyncMessageEvent, txn_id: Box<TransactionId>) {
         if let MatrixRoom::Joined(matrix_room) = self.matrix_room() {
             let content = event.content();
             let json = serde_json::to_string(&AnySyncRoomEvent::Message(event)).unwrap();
@@ -918,9 +922,10 @@ impl Room {
                 .timeline
                 .get()
                 .unwrap()
-                .append_pending(txn_id, event);
+                .append_pending(&txn_id, event);
 
-            let handle = spawn_tokio!(async move { matrix_room.send(content, Some(txn_id)).await });
+            let handle =
+                spawn_tokio!(async move { matrix_room.send(content, Some(&txn_id)).await });
 
             spawn!(
                 glib::PRIORITY_DEFAULT_IDLE,
@@ -989,7 +994,7 @@ impl Room {
                 .timeline
                 .get()
                 .unwrap()
-                .append_pending(txn_id, event);
+                .append_pending(&txn_id, event);
 
             let handle = spawn_tokio!(async move {
                 matrix_room
@@ -1103,7 +1108,6 @@ impl Room {
 
     pub fn handle_left_response(&self, response_room: LeftRoom) {
         self.set_matrix_room(self.session().client().get_room(self.room_id()).unwrap());
-
         self.append_events(
             response_room
                 .timeline
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline.rs
index 899e59e99..282f532ae 100644
--- a/src/session/room/timeline.rs
+++ b/src/session/room/timeline.rs
@@ -1,16 +1,19 @@
-use std::collections::HashMap;
+use std::{
+    collections::{HashMap, HashSet, VecDeque},
+    pin::Pin,
+    sync::Arc,
+};
 
-use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use futures::{lock::Mutex, pin_mut, Stream, StreamExt};
+use gtk::{gio, glib, prelude::*, subclass::prelude::*};
 use log::{error, warn};
 use matrix_sdk::{
+    deserialized_responses::SyncRoomEvent,
+    room::Room as MatrixRoom,
     ruma::{
-        api::client::r0::message::get_message_events::Direction,
-        events::{
-            room::message::MessageType, AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent,
-        },
-        identifiers::EventId,
+        events::{room::message::MessageType, AnySyncMessageEvent, AnySyncRoomEvent},
+        identifiers::{EventId, TransactionId},
     },
-    uuid::Uuid,
     Error as MatrixError,
 };
 
@@ -20,7 +23,7 @@ use crate::{
         user::UserExt,
         verification::{IdentityVerification, VERIFICATION_CREATION_TIMEOUT},
     },
-    spawn, spawn_tokio,
+    spawn_tokio,
 };
 
 #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
@@ -40,11 +43,12 @@ impl Default for TimelineState {
     }
 }
 
+const MAX_BATCH_SIZE: usize = 20;
+type BackwardStream =
+    Pin<Box<dyn Stream<Item = Vec<matrix_sdk::Result<SyncRoomEvent>>> + 'static + Send>>;
+
 mod imp {
-    use std::{
-        cell::{Cell, RefCell},
-        collections::{HashSet, VecDeque},
-    };
+    use std::cell::{Cell, RefCell};
 
     use glib::object::WeakRef;
     use once_cell::{sync::Lazy, unsync::OnceCell};
@@ -62,13 +66,13 @@ mod imp {
         pub event_map: RefCell<HashMap<Box<EventId>, Event>>,
         /// Maps the temporary `EventId` of the pending Event to the real
         /// `EventId`
-        pub pending_events: RefCell<HashMap<String, Box<EventId>>>,
+        pub pending_events: RefCell<HashMap<Box<TransactionId>, Box<EventId>>>,
         /// A Hashset of `EventId`s that where just redacted.
         pub redacted_events: RefCell<HashSet<Box<EventId>>>,
-        pub oldest_event: RefCell<Option<Box<EventId>>>,
         pub state: Cell<TimelineState>,
         /// The most recent verification request event
         pub verification: RefCell<Option<IdentityVerification>>,
+        pub backward_stream: Arc<Mutex<Option<BackwardStream>>>,
     }
 
     #[glib::object_subclass]
@@ -510,8 +514,92 @@ impl Timeline {
         }
     }
 
+    /// Load the timeline
+    /// This function should also be called to load more events
+    /// Returns `true` when messages where successfully added
+    pub async fn load(&self) -> bool {
+        let priv_ = self.imp();
+
+        if matches!(
+            self.state(),
+            TimelineState::Loading | TimelineState::Complete
+        ) {
+            return false;
+        }
+
+        self.set_state(TimelineState::Loading);
+        self.add_loading_spinner();
+
+        let matrix_room = self.room().matrix_room();
+        let room_weak = self.downgrade().into();
+        let backward_stream = priv_.backward_stream.clone();
+
+        let handle: tokio::task::JoinHandle<matrix_sdk::Result<_>> = spawn_tokio!(async move {
+            let mut backward_stream_guard = backward_stream.lock().await;
+            if backward_stream_guard.is_none() {
+                backward_stream_guard
+                    .replace(create_streams_handler(room_weak, matrix_room).await?);
+            }
+
+            Ok(backward_stream_guard.as_mut().unwrap().next().await)
+        });
+
+        match handle.await.unwrap() {
+            Ok(Some(events)) => {
+                let events: Vec<Event> = events
+                    .into_iter()
+                    .filter_map(|event| match event {
+                        Ok(event) => Some(event),
+                        Err(error) => {
+                            error!("Failed to load messages: {}", error);
+                            None
+                        }
+                    })
+                    .map(|event| Event::new(event, &self.room()))
+                    .collect();
+
+                self.remove_loading_spinner();
+                if events.is_empty() {
+                    self.set_state(TimelineState::Error);
+                    return false;
+                }
+                self.set_state(TimelineState::Ready);
+                self.prepend(events);
+                true
+            }
+            Ok(None) => {
+                self.remove_loading_spinner();
+                self.set_state(TimelineState::Complete);
+                false
+            }
+            Err(error) => {
+                error!("Failed to load timeline: {}", error);
+                self.set_state(TimelineState::Error);
+                self.remove_loading_spinner();
+                false
+            }
+        }
+    }
+
+    async fn clear(&self) {
+        let priv_ = self.imp();
+        // Remove backward stream so that we create new streams
+        let mut backward_stream = priv_.backward_stream.lock().await;
+        backward_stream.take();
+
+        let length = priv_.list.borrow().len();
+        priv_.relates_to_events.replace(HashMap::new());
+        priv_.list.replace(VecDeque::new());
+        priv_.event_map.replace(HashMap::new());
+        priv_.pending_events.replace(HashMap::new());
+        priv_.redacted_events.replace(HashSet::new());
+
+        self.notify("empty");
+        self.upcast_ref::<gio::ListModel>()
+            .items_changed(0, length as u32, 0);
+    }
+
     /// Append the new events
-    // TODO: This should be lazy, for inspiration see: 
https://blogs.gnome.org/ebassi/documentation/lazy-loading/
     pub fn append(&self, batch: Vec<Event>) {
         let priv_ = self.imp();
 
@@ -527,12 +615,6 @@ impl Timeline {
                 // multiple times
                 list.reserve(batch.len());
 
-                if list.is_empty() {
-                    priv_
-                        .oldest_event
-                        .replace(batch.first().as_ref().map(|event| event.matrix_event_id()));
-                }
-
                 list.len()
             };
 
@@ -578,7 +660,7 @@ impl Timeline {
     }
 
     /// Append an event that wasn't yet fully sent and received via a sync
-    pub fn append_pending(&self, txn_id: Uuid, event: Event) {
+    pub fn append_pending(&self, txn_id: &TransactionId, event: Event) {
         let priv_ = self.imp();
 
         priv_
@@ -589,7 +671,7 @@ impl Timeline {
         priv_
             .pending_events
             .borrow_mut()
-            .insert(txn_id.to_string(), event.matrix_event_id());
+            .insert(txn_id.to_owned(), event.matrix_event_id());
 
         let index = {
             let mut list = priv_.list.borrow_mut();
@@ -634,7 +716,7 @@ impl Timeline {
             let handle =
                 spawn_tokio!(async move { matrix_room.event(event_id_clone.as_ref()).await });
             match handle.await.unwrap() {
-                Ok(room_event) => Ok(Event::new(room_event.event.into(), &room)),
+                Ok(room_event) => Ok(Event::new(room_event.into(), &room)),
                 Err(error) => {
                     // TODO: Retry on connection error?
                     warn!("Could not fetch event {}: {}", event_id, error);
@@ -645,15 +727,10 @@ impl Timeline {
     }
 
     /// Prepends a batch of events
-    // TODO: This should be lazy, see: https://blogs.gnome.org/ebassi/documentation/lazy-loading/
     pub fn prepend(&self, batch: Vec<Event>) {
         let priv_ = self.imp();
         let mut added = batch.len();
 
-        priv_
-            .oldest_event
-            .replace(batch.last().as_ref().map(|event| event.matrix_event_id()));
-
         {
             let mut hidden_events: Vec<Event> = vec![];
             // Extend the size of the list so that rust doesn't need to reallocate memory
@@ -712,9 +789,6 @@ impl Timeline {
             || (priv_.list.borrow().len() == 1 && self.state() == TimelineState::Loading)
     }
 
-    fn oldest_event(&self) -> Option<Box<EventId>> {
-        self.imp().oldest_event.borrow().clone()
-    }
     fn add_loading_spinner(&self) {
         self.imp()
             .list
@@ -728,68 +802,6 @@ impl Timeline {
         self.upcast_ref::<gio::ListModel>().items_changed(0, 1, 0);
     }
 
-    pub fn load_previous_events(&self) {
-        if matches!(
-            self.state(),
-            TimelineState::Loading | TimelineState::Complete
-        ) {
-            return;
-        }
-
-        self.set_state(TimelineState::Loading);
-        self.add_loading_spinner();
-
-        let matrix_room = self.room().matrix_room();
-        let last_event = self.oldest_event();
-        let contains_last_event = last_event.is_some();
-
-        let handle = spawn_tokio!(async move {
-            matrix_room
-                .messages(last_event.as_deref(), None, 20, Direction::Backward)
-                .await
-        });
-
-        spawn!(
-            glib::PRIORITY_LOW,
-            clone!(@weak self as obj => async move {
-                obj.remove_loading_spinner();
-
-                // FIXME: If the request fails it's automatically restarted because the added events (none), 
didn't fill the screen.
-                // We should block the loading for some time before retrying
-                match handle.await.unwrap() {
-                       Ok(Some(events)) => {
-                            let events: Vec<Event> = if contains_last_event {
-                                            events
-                                           .into_iter()
-                                           .skip(1)
-                                           .map(|event| Event::new(event, &obj.room())).collect()
-                            } else {
-                                            events
-                                           .into_iter()
-                                           .map(|event| Event::new(event, &obj.room())).collect()
-                            };
-
-                            if events.iter().any(|event| matches!(event.matrix_event(), 
Some(AnySyncRoomEvent::State(AnySyncStateEvent::RoomCreate(_))))) {
-                                obj.set_state(TimelineState::Complete);
-                            } else {
-                                obj.set_state(TimelineState::Ready);
-                            }
-
-                            obj.prepend(events);
-                       },
-                       Ok(None) => {
-                           error!("The start event wasn't found in the timeline for room {}.", 
obj.room().room_id());
-                           obj.set_state(TimelineState::Error);
-                       },
-                       Err(error) => {
-                           error!("Couldn't load previous events for room {}: {}", error, 
obj.room().room_id());
-                           obj.set_state(TimelineState::Error);
-                       }
-               }
-            })
-        );
-    }
-
     fn set_verification(&self, verification: IdentityVerification) {
         self.imp().verification.replace(Some(verification));
         self.notify("verification");
@@ -899,3 +911,58 @@ impl Timeline {
         }
     }
 }
+
+async fn create_streams_handler(
+    timeline: glib::SendWeakRef<Timeline>,
+    matrix_room: MatrixRoom,
+) -> matrix_sdk::Result<BackwardStream> {
+    let (forward_stream, backward_stream) = matrix_room.timeline().await?;
+
+    tokio::spawn(async move {
+        handle_forward_stream(timeline, forward_stream).await;
+    });
+
+    Ok(Box::pin(backward_stream.ready_chunks(MAX_BATCH_SIZE)))
+}
+
+async fn handle_forward_stream(
+    timeline: glib::SendWeakRef<Timeline>,
+    stream: impl Stream<Item = SyncRoomEvent>,
+) {
+    let stream = stream.ready_chunks(MAX_BATCH_SIZE);
+    pin_mut!(stream);
+
+    while let Some(events) = stream.next().await {
+        let timeline = timeline.clone();
+        let (sender, receiver) = futures::channel::oneshot::channel();
+        let ctx = glib::MainContext::default();
+        ctx.spawn(async move {
+            let result = if let Some(timeline) = timeline.upgrade() {
+                timeline.append(
+                    events
+                        .into_iter()
+                        .map(|event| Event::new(event, &timeline.room()))
+                        .collect(),
+                );
+
+                true
+            } else {
+                false
+            };
+            sender.send(result).unwrap();
+        });
+
+        if !receiver.await.unwrap() {
+            break;
+        }
+    }
+
+    let ctx = glib::MainContext::default();
+    ctx.spawn(async move {
+        crate::spawn!(async move {
+            if let Some(timeline) = timeline.upgrade() {
+                timeline.clear().await;
+            };
+        });
+    });
+}
diff --git a/src/utils.rs b/src/utils.rs
index 1ff7e3860..450ea9aab 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -69,8 +69,7 @@ use gtk::{
 };
 use matrix_sdk::{
     media::MediaType,
-    ruma::{EventId, UInt},
-    uuid::Uuid,
+    ruma::{EventId, TransactionId, UInt},
 };
 use mime::Mime;
 
@@ -209,8 +208,8 @@ pub fn filename_for_mime(mime_type: Option<&str>, fallback: Option<mime::Name>)
 ///
 /// Returns a `(transaction_id, event_id)` tuple. The `event_id` is derived from
 /// the `transaction_id`.
-pub fn pending_event_ids() -> (Uuid, Box<EventId>) {
-    let txn_id = Uuid::new_v4();
+pub fn pending_event_ids() -> (Box<TransactionId>, Box<EventId>) {
+    let txn_id = TransactionId::new();
     let event_id = EventId::parse(format!("${}:fractal.gnome.org", txn_id)).unwrap();
     (txn_id, event_id)
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]