@@ -6,12 +6,13 @@ use futures_util::FutureExt;
66use matrix_sdk:: {
77 authentication:: oauth:: { error:: OAuthTokenRevocationError , OAuthError } ,
88 config:: { RequestConfig , StoreConfig , SyncSettings , SyncToken } ,
9- store:: RoomLoadSettings ,
9+ sleep:: sleep,
10+ store:: { RoomLoadSettings , ThreadSubscriptionStatus } ,
1011 sync:: { RoomUpdate , State } ,
1112 test_utils:: {
1213 client:: mock_matrix_session, mocks:: MatrixMockServer , no_retry_test_client_with_server,
1314 } ,
14- Client , Error , MemoryStore , StateChanges , StateStore ,
15+ Client , Error , MemoryStore , SlidingSyncList , StateChanges , StateStore , ThreadingSupport ,
1516} ;
1617use matrix_sdk_base:: { sync:: RoomUpdates , RoomState } ;
1718use matrix_sdk_common:: executor:: spawn;
@@ -36,6 +37,7 @@ use ruma::{
3637 get_public_rooms,
3738 get_public_rooms_filtered:: { self , v3:: Request as PublicRoomsFilterRequest } ,
3839 } ,
40+ sync:: sync_events:: v5,
3941 threads:: get_thread_subscriptions_changes:: unstable:: {
4042 ThreadSubscription , ThreadUnsubscription ,
4143 } ,
@@ -56,7 +58,7 @@ use ruma::{
5658 room:: JoinRule ,
5759 room_id,
5860 serde:: Raw ,
59- uint, user_id, OwnedUserId ,
61+ uint, user_id, EventId , OwnedUserId , RoomId ,
6062} ;
6163use serde_json:: { json, Value as JsonValue } ;
6264use stream_assert:: { assert_next_matches, assert_pending} ;
@@ -1587,3 +1589,211 @@ async fn test_fetch_thread_subscriptions() {
15871589 let u = & response. unsubscribed [ & room3] [ & thread3] ;
15881590 assert_eq ! ( u. bump_stamp, uint!( 13 ) ) ;
15891591}
1592+
1593+ /// Create a sliding sync thread_subscription response with no `prev_batch`
1594+ /// token.
1595+ fn thread_subscription_response (
1596+ room1 : & RoomId ,
1597+ thread1 : & EventId ,
1598+ room2 : & RoomId ,
1599+ thread2 : & EventId ,
1600+ ) -> v5:: response:: ThreadSubscriptions {
1601+ assign ! ( v5:: response:: ThreadSubscriptions :: default ( ) , {
1602+ subscribed: {
1603+ let mut map = BTreeMap :: new( ) ;
1604+ map. insert( room1. to_owned( ) , {
1605+ let mut threads = BTreeMap :: new( ) ;
1606+ threads. insert( thread1. to_owned( ) , ThreadSubscription :: new( true , uint!( 42 ) ) ) ;
1607+ threads
1608+ } ) ;
1609+ map
1610+ } ,
1611+ unsubscribed: {
1612+ let mut map = BTreeMap :: new( ) ;
1613+ map. insert( room2. to_owned( ) , {
1614+ let mut threads = BTreeMap :: new( ) ;
1615+ threads. insert( thread2. to_owned( ) , ThreadUnsubscription :: new( uint!( 7 ) ) ) ;
1616+ threads
1617+ } ) ;
1618+ map
1619+ } ,
1620+ prev_batch: None ,
1621+ } )
1622+ }
1623+
1624+ #[ async_test]
1625+ async fn test_sync_thread_subscriptions ( ) {
1626+ let server = MatrixMockServer :: new ( ) . await ;
1627+ let client = server. client_builder ( ) . build ( ) . await ;
1628+
1629+ let room1 = owned_room_id ! ( "!room1:example.com" ) ;
1630+ let room2 = owned_room_id ! ( "!room2:example.com" ) ;
1631+
1632+ let thread1 = owned_event_id ! ( "$thread1:example.com" ) ;
1633+ let thread2 = owned_event_id ! ( "$thread2:example.com" ) ;
1634+
1635+ // At first, there are no thread subscriptions at all.
1636+ let stored1 = client
1637+ . state_store ( )
1638+ . load_thread_subscription ( & room1, & thread1)
1639+ . await
1640+ . expect ( "loading room1/thread1 works fine" ) ;
1641+ assert_matches ! ( stored1, None ) ;
1642+
1643+ let stored2 = client
1644+ . state_store ( )
1645+ . load_thread_subscription ( & room2, & thread2)
1646+ . await
1647+ . expect ( "loading room2/thread2 works fine" ) ;
1648+ assert_matches ! ( stored2, None ) ;
1649+
1650+ // When I sliding-sync thread subscriptions,
1651+ server
1652+ . mock_sliding_sync ( )
1653+ . ok_and_run (
1654+ & client,
1655+ |config_builder| {
1656+ config_builder. with_thread_subscriptions_extension (
1657+ assign ! ( v5:: request:: ThreadSubscriptions :: default ( ) , {
1658+ enabled: Some ( true ) ,
1659+ limit: Some ( uint!( 10 ) ) ,
1660+ } ) ,
1661+ )
1662+ } ,
1663+ assign ! ( v5:: Response :: new( "pos" . to_owned( ) ) , {
1664+ extensions: assign!( v5:: response:: Extensions :: default ( ) , {
1665+ thread_subscriptions: thread_subscription_response(
1666+ & room1, & thread1, & room2, & thread2,
1667+ ) ,
1668+ } ) ,
1669+ } ) ,
1670+ )
1671+ . await ;
1672+
1673+ // Then they're stored in the local database.
1674+ let stored1 = client
1675+ . state_store ( )
1676+ . load_thread_subscription ( & room1, & thread1)
1677+ . await
1678+ . expect ( "loading room1/thread1 works fine" )
1679+ . expect ( "found room1/thread1 subscription" ) ;
1680+
1681+ assert_eq ! ( stored1. status, ThreadSubscriptionStatus :: Subscribed { automatic: true } ) ;
1682+ assert_eq ! ( stored1. bump_stamp, Some ( 42 ) ) ;
1683+
1684+ let stored2 = client
1685+ . state_store ( )
1686+ . load_thread_subscription ( & room2, & thread2)
1687+ . await
1688+ . expect ( "loading room2/thread2 works fine" )
1689+ . expect ( "found room2/thread2 unsubscription" ) ;
1690+
1691+ assert_eq ! ( stored2. status, ThreadSubscriptionStatus :: Unsubscribed ) ;
1692+ assert_eq ! ( stored2. bump_stamp, Some ( 7 ) ) ;
1693+ }
1694+
1695+ #[ async_test]
1696+ async fn test_sync_thread_subscriptions_with_catchup ( ) {
1697+ let server = MatrixMockServer :: new ( ) . await ;
1698+ let client = server
1699+ . client_builder ( )
1700+ . on_builder ( |builder| {
1701+ builder. with_threading_support ( ThreadingSupport :: Enabled { with_subscriptions : true } )
1702+ } )
1703+ . build ( )
1704+ . await ;
1705+
1706+ let room_id1 = owned_room_id ! ( "!room1:example.com" ) ;
1707+ let room_id2 = owned_room_id ! ( "!room2:example.com" ) ;
1708+
1709+ let thread1 = owned_event_id ! ( "$thread1:example.com" ) ;
1710+ let thread2 = owned_event_id ! ( "$thread2:example.com" ) ;
1711+ let thread3 = owned_event_id ! ( "$thread3:example.com" ) ;
1712+
1713+ // The provided catchup token will be used to fetch more thread
1714+ // subscriptions via the msc4308 companion endpoint.
1715+ server
1716+ . mock_get_thread_subscriptions ( )
1717+ . match_from ( "catchup_token" )
1718+ . add_subscription (
1719+ room_id1. clone ( ) ,
1720+ thread3. clone ( ) ,
1721+ ThreadSubscription :: new ( false , uint ! ( 1337 ) ) ,
1722+ )
1723+ . with_delay ( Duration :: from_millis ( 300 ) ) // Simulate some network delay.
1724+ // No more subscriptions after the first catchup request.
1725+ . ok ( None )
1726+ . mock_once ( )
1727+ . mount ( )
1728+ . await ;
1729+
1730+ // When I sliding-sync thread subscriptions, and the response includes this
1731+ // catch-up token,
1732+ let mut thread_subscriptions =
1733+ thread_subscription_response ( & room_id1, & thread1, & room_id2, & thread2) ;
1734+ thread_subscriptions. prev_batch = Some ( "catchup_token" . to_owned ( ) ) ;
1735+
1736+ server
1737+ . mock_sliding_sync ( )
1738+ . ok_and_run (
1739+ & client,
1740+ |config_builder| {
1741+ config_builder
1742+ . with_thread_subscriptions_extension (
1743+ assign ! ( v5:: request:: ThreadSubscriptions :: default ( ) , {
1744+ enabled: Some ( true ) ,
1745+ limit: Some ( uint!( 10 ) ) ,
1746+ } ) ,
1747+ )
1748+ . add_list ( SlidingSyncList :: builder ( "rooms" ) )
1749+ } ,
1750+ assign ! ( v5:: Response :: new( "pos" . to_owned( ) ) , {
1751+ rooms: {
1752+ let mut rooms = BTreeMap :: new( ) ;
1753+ rooms. insert( room_id1. clone( ) , v5:: response:: Room :: default ( ) ) ;
1754+ rooms. insert( room_id2. clone( ) , v5:: response:: Room :: default ( ) ) ;
1755+ rooms
1756+ } ,
1757+ extensions: assign!( v5:: response:: Extensions :: default ( ) , {
1758+ thread_subscriptions,
1759+ } ) ,
1760+ } ) ,
1761+ )
1762+ . await ;
1763+
1764+ // If I try to get the subscription status for thread 1, it's still hitting
1765+ // network, because it doesn't know yet about the result of the catch-up
1766+ // request. (Ideally, the choice of whether some information is outdated or
1767+ // not would be per room/thread pair, but for simplicity it's global right
1768+ // now.)
1769+ server
1770+ . mock_room_get_thread_subscription ( )
1771+ . match_room_id ( room_id1. clone ( ) )
1772+ . match_thread_id ( thread1. clone ( ) )
1773+ . ok ( true )
1774+ . mock_once ( )
1775+ . mount ( )
1776+ . await ;
1777+
1778+ let room1 = client. get_room ( & room_id1) . unwrap ( ) ;
1779+ let sub1 = room1. load_or_fetch_thread_subscription ( & thread1) . await . unwrap ( ) ;
1780+ assert_eq ! ( sub1, Some ( matrix_sdk:: room:: ThreadSubscription { automatic: true } ) ) ;
1781+
1782+ // All the thread subscriptions are eventually known in the database.
1783+ sleep ( Duration :: from_millis ( 400 ) ) . await ;
1784+
1785+ let stored3 = client
1786+ . state_store ( )
1787+ . load_thread_subscription ( & room_id1, & thread3)
1788+ . await
1789+ . expect ( "loading room1/thread3 works fine" )
1790+ . expect ( "found room1/thread3 subscription" ) ;
1791+ assert_eq ! ( stored3. status, ThreadSubscriptionStatus :: Subscribed { automatic: false } ) ;
1792+ assert_eq ! ( stored3. bump_stamp, Some ( 1337 ) ) ;
1793+
1794+ // So the client will use the database only to load_or_fetch thread
1795+ // subscriptions. (Which is confirmed by the absence of mocking the
1796+ // room_get_thread_subscription endpoint for thread3.)
1797+ let sub3 = room1. load_or_fetch_thread_subscription ( & thread3) . await . unwrap ( ) ;
1798+ assert_eq ! ( sub3, Some ( matrix_sdk:: room:: ThreadSubscription { automatic: false } ) ) ;
1799+ }
0 commit comments