Skip to content

Commit bf0c040

Browse files
feat: add primitive models and handler for Simplified Sliding Sync
Signed-off-by: The one with the braid <[email protected]>
1 parent e8140ed commit bf0c040

16 files changed

+500
-2
lines changed

lib/matrix.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export 'src/voip/models/webrtc_delegate.dart';
4141
export 'src/voip/models/call_participant.dart';
4242
export 'src/voip/models/key_provider.dart';
4343
export 'src/voip/models/matrixrtc_call_event.dart';
44+
export 'src/voip/models/call_membership.dart';
4445
export 'src/voip/utils/conn_tester.dart';
4546
export 'src/voip/utils/voip_constants.dart';
4647
export 'src/voip/utils/rtc_candidate_extension.dart';
@@ -80,9 +81,15 @@ export 'msc_extensions/msc_1236_widgets/msc_1236_widgets.dart';
8081
export 'msc_extensions/msc_2835_uia_login/msc_2835_uia_login.dart';
8182
export 'msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart';
8283
export 'msc_extensions/extension_timeline_export/timeline_export.dart';
84+
export 'msc_extensions/msc_4140_delayed_events/api.dart';
85+
export 'msc_extensions/msc_3381_polls/models/poll_event_content.dart';
86+
export 'msc_extensions/msc_3381_polls/poll_event_extension.dart';
87+
export 'msc_extensions/msc_3381_polls/poll_room_extension.dart';
88+
export 'msc_extensions/msc_4186_simplified_sliding_sync/msc_4186_simplified_sliding_sync.dart';
89+
8390

8491
export 'src/utils/web_worker/web_worker_stub.dart'
85-
if (dart.library.html) 'src/utils/web_worker/web_worker.dart';
92+
if (dart.library.js_interop) 'src/utils/web_worker/web_worker.dart';
8693

8794
export 'src/utils/web_worker/native_implementations_web_worker_stub.dart'
88-
if (dart.library.html) 'src/utils/web_worker/native_implementations_web_worker.dart';
95+
if (dart.library.js_interop) 'src/utils/web_worker/native_implementations_web_worker.dart';

lib/msc_extensions/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ Please try to cover the following conventions:
2121
- MSC 2835 - UIA login
2222
- MSC 3814 - Dehydrated Devices
2323
- MSC 3935 - Cute Events
24+
- MSC 4186 - Simplified Sliding Sync
2425
- `io.element.recent_emoji` - recent emoji sync in account data
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/// Different extensions have different configuration formats.
2+
abstract class ExtensionConfig {
3+
Map<String, Object?> toJson();
4+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// Used to depict the difference between null and undefined in JavaScript
2+
abstract class Maybe<T> {}
3+
4+
class Some<T> extends Maybe<T> {
5+
Some(this.data);
6+
7+
final T data;
8+
9+
@override
10+
int get hashCode => data.hashCode;
11+
12+
@override
13+
String toString() => data.toString();
14+
15+
@override
16+
bool operator ==(Object other) {
17+
return data == other;
18+
}
19+
}
20+
21+
class Undefined<T> extends Maybe<T> {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class RequiredStateRequest {
2+
/// The event type to match. If omitted then matches all types.
3+
final String? type;
4+
5+
/// The event state key to match. If omitted then matches all state keys.
6+
///
7+
/// Note: it is possible to match a specific state key, for all event types, by specifying [stateKey] but leaving [type] unset.
8+
final String? stateKey;
9+
10+
const RequiredStateRequest({required this.type, required this.stateKey});
11+
12+
Map<String, Object?> toJson() => {
13+
if (type != null) 'type': type,
14+
if (stateKey != null) 'state_key': stateKey,
15+
};
16+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import 'package:matrix/matrix.dart';
2+
3+
class RoomResult {
4+
/// An integer that can be used to sort rooms based on the last "proper" activity in the room. Greater means more recent.
5+
///
6+
/// "Proper" activity is defined as an event being received is one of the following types: m.room.create, m.room.message, m.room.encrypted, m.sticker, m.call.invite, m.poll.start, m.beacon_info.
7+
///
8+
/// For rooms that the user is not currently joined to, this instead represents when the relevant membership happened, e.g. when the user left the room.
9+
///
10+
/// The exact value of bump_stamp is opaque to the client, a server may use e.g. an auto-incrementing integer, a timestamp, etc.
11+
///
12+
/// The bump_stamp may decrease in subsequent responses, if e.g. an event was redacted/removed (or purged in cases of retention policies).
13+
final int bumpStamp;
14+
15+
/// The current membership of the user, or omitted if user not in room (for peeking).
16+
// TODO: migrate to enum
17+
final String? membership;
18+
19+
/// The name of the lists that match this room. The field is omitted if it doesn't match any list and is included only due to a subscription.
20+
final List<String>? lists;
21+
22+
// Currently or previously joined rooms
23+
// When a user is or has been in the room, the following field are also returned:
24+
25+
/// Room name or calculated room name.
26+
final String? name;
27+
28+
/// Room avatar
29+
// TODO: migrate to Uri
30+
final String? avatar;
31+
32+
/// A truncated list of users in the room that can be used to calculate the room name. Will first include joined users, then invited users, and then finally left users. The same as the m.heroes section in the /v3/sync specification
33+
final List<StrippedHero>? heroes;
34+
35+
/// Flag to specify whether the room is a direct-message room (according to account data). If absent the room is not a DM room.
36+
final bool? isDm;
37+
38+
/// Flag which is set when this is the first time the server is sending this data on this connection, or if the client should replace all room data with what is returned. Clients can use this flag to replace or update their local state. The absence of this flag means false.
39+
final bool? initial;
40+
41+
/// Flag which is set if we're returning more historic events due to the timeline limit having increased. See "Changing room configs" section.
42+
final bool? expandedTimeline;
43+
44+
/// Changes in the current state of the room.
45+
///
46+
/// To handle state being deleted, the list may include a StateStub type (c.f. schema below) that only has type and state_key fields. The presence or absence of content field can be used to differentiate between the two cases.
47+
final List<BasicEvent>? requiredState;
48+
49+
/// The latest events in the room. May not include all events if e.g. there were more events than the configured timeline_limit, c.f. the limited field.
50+
///
51+
/// If limited is true then we include bundle aggregations for the event, as per /v3/sync.
52+
///
53+
/// The last event in the list is the most recent.
54+
final List<MatrixEvent>? timelineEvents;
55+
56+
/// A token that can be passed as a start parameter to the /rooms/<room_id>/messages API to retrieve earlier messages.
57+
final String? prevBatch;
58+
59+
/// True if there are more events since the previous sync than were included in the timeline_events field, or that the client should paginate to fetch more events.
60+
///
61+
/// Note that server may return fewer than the requested number of events and still set limited to true, e.g. because there is a gap in the history the server has for the room.
62+
///
63+
/// Absence means false
64+
final bool limited;
65+
66+
/// The number of timeline events which have "just occurred" and are not historical, i.e. that have happened since the previous sync request. The last N events are 'live' and should be treated as such.
67+
///
68+
/// This is mostly useful to e.g. determine whether a given @mention event should make a noise or not. Clients cannot rely solely on the absence of initial: true to determine live events because if a room not in the sliding window bumps into the window because of an @mention it will have initial: true yet contain a single live event (with potentially other old events in the timeline).
69+
final int? numLive;
70+
71+
/// The number of users with membership of join, including the client's own user ID. (same as /v3/sync m.joined_member_count)
72+
final int? joinedCount;
73+
74+
/// The number of users with membership of invite. (same as /v3/sync m.invited_member_count)
75+
final int? invitedCount;
76+
77+
/// The total number of unread notifications for this room. (same as /v3/sync).
78+
///
79+
/// Does not included threaded notifications, which are returned in an extension.
80+
final int? notificationCount;
81+
82+
/// The number of unread notifications for this room with the highlight flag set. (same as /v3/sync)
83+
///
84+
/// Does not included threaded notifications, which are returned in an extension.
85+
final int? highlightCount;
86+
87+
// Invite/knock/rejections
88+
// For rooms the user has not been joined to the client also gets the stripped state events. This is commonly the case for invites or knocks, but can also be for when the user has rejected an invite.
89+
90+
/// Stripped state events (for rooms where the user is invited). Same as rooms.invite.$room_id.invite_state for invites in /v3/sync.
91+
final StrippedStateEvent? stripped_state;
92+
93+
const RoomResult({
94+
required this.bumpStamp,
95+
this.membership,
96+
this.lists,
97+
this.name,
98+
this.avatar,
99+
this.heroes,
100+
this.isDm,
101+
this.initial,
102+
this.expandedTimeline,
103+
this.requiredState,
104+
this.timelineEvents,
105+
this.prevBatch,
106+
this.limited = false,
107+
this.numLive,
108+
this.joinedCount,
109+
this.invitedCount,
110+
this.notificationCount,
111+
this.highlightCount,
112+
this.stripped_state,
113+
});
114+
115+
factory RoomResult.fromJson(Map<String, Object?> json) => RoomResult(
116+
bumpStamp: json['bump_stamp'] as int,
117+
membership: json['membership'] as String?,
118+
lists: (json['lists'] as List?)?.cast<String>(),
119+
name: json['name'] as String?,
120+
avatar: json['avatar'] as String?,
121+
heroes: json.containsKey('heroes')
122+
? (json['heroes'] as List)
123+
.map((v) => StrippedHero.fromJson(v))
124+
.toList()
125+
: null,
126+
isDm: json['is_dm'] as bool?,
127+
initial: json['initial'] as bool?,
128+
expandedTimeline: json['expanded_timeline'] as bool?,
129+
requiredState: json.containsKey('required_state')
130+
? (json['required_state'] as List)
131+
.map((v) => BasicEvent.fromJson(v))
132+
.toList()
133+
: null,
134+
timelineEvents: json.containsKey('timeline_events')
135+
? (json['timeline_events'] as List)
136+
.map((v) => MatrixEvent.fromJson(v))
137+
.toList()
138+
: null,
139+
prevBatch: json['prev_batch'] as String?,
140+
limited: json['limited'] as bool? ?? false,
141+
numLive: json['num_live'] as int?,
142+
joinedCount: json['joined_count'] as int?,
143+
invitedCount: json['invited_count'] as int?,
144+
notificationCount: json['notification_count'] as int?,
145+
highlightCount: json['highlight_count'] as int?,
146+
stripped_state: json.containsKey('stripped_state')
147+
? StrippedStateEvent.fromJson(
148+
(json['stripped_state'] as Map).cast<String, Object?>(),
149+
)
150+
: null,
151+
);
152+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:matrix/matrix.dart';
2+
3+
class RoomSubscription {
4+
/// The maximum number of timeline events to return per response. The server may cap this number.
5+
final int timelineLimit;
6+
7+
/// Required state for each room returned.
8+
final RequiredStateRequest requiredState;
9+
10+
const RoomSubscription({
11+
required this.timelineLimit,
12+
required this.requiredState,
13+
});
14+
15+
Map<String, Object?> toJson() => {
16+
'timeline_limit': timelineLimit,
17+
'required_state': requiredState.toJson(),
18+
};
19+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import 'package:matrix/matrix.dart';
2+
3+
class SlidingRoomFilter {
4+
/// Flag which only returns rooms present (or not) in the m.direct entry in account data.
5+
///
6+
/// If unset, both DM rooms and non-DM rooms are returned. If False, only non-DM rooms are returned. If True, only DM rooms are returned.
7+
final bool? isDm;
8+
9+
/// Filter the room based on the space they belong to according to m.space.child state events.
10+
///
11+
/// If multiple spaces are present, a room can be part of any one of the listed spaces (OR'd). The server will inspect the m.space.child state events for the JOINED space room IDs given. Servers MUST NOT navigate subspaces. It is up to the client to give a complete list of spaces to navigate. Only rooms directly mentioned as m.space.child events in these spaces will be returned. Unknown spaces or spaces the user is not joined to will be ignored.
12+
final List<String>? spaces;
13+
14+
/// Flag which only returns rooms which have an m.room.encryption state event.
15+
///
16+
/// If unset, both encrypted and unencrypted rooms are returned. If false, only unencrypted rooms are returned. If True, only encrypted rooms are returned.
17+
final bool? isEncrypted;
18+
19+
/// Flag which only returns rooms the user is currently invited to.
20+
///
21+
/// If unset, both invited and joined rooms are returned. If false, no invited rooms are returned. If true, only invited rooms are returned.
22+
final bool? isInvited;
23+
24+
/// If specified, only rooms where the m.room.create event has a type matching one of the strings in this array will be returned.
25+
///
26+
/// If this field is unset, all rooms are returned regardless of type. This can be used to get the initial set of spaces for an account. For rooms which do not have a room type, use null to include them.
27+
final List<Maybe<String>>? roomTypes;
28+
29+
/// Same as [roomTypes] but inverted.
30+
///
31+
/// This can be used to filter out spaces from the room list. If a type is in both room_types and not_room_types, then not_room_types wins and they are not included in the result.
32+
final List<Maybe<String>>? notRoomTypes;
33+
34+
/// Filter the room based on its [room tags](https://spec.matrix.org/v1.16/client-server-api/#room-tagging).
35+
///
36+
/// If multiple tags are present, a room can have any one of the listed tags (OR'd).
37+
final List<String>? tags;
38+
39+
/// Filter the room based on its [room tags](https://spec.matrix.org/v1.16/client-server-api/#room-tagging).
40+
///
41+
/// If multiple tags are present, a room can have any one of the listed tags (OR'd).
42+
final List<String>? notTags;
43+
44+
const SlidingRoomFilter({
45+
required this.isDm,
46+
required this.spaces,
47+
required this.isEncrypted,
48+
required this.isInvited,
49+
required this.roomTypes,
50+
required this.notRoomTypes,
51+
required this.tags,
52+
required this.notTags,
53+
});
54+
55+
Map<String, Object?> toJson() => {
56+
if (isDm != null) 'is_dm': isDm,
57+
if (spaces != null) 'spaces': spaces,
58+
if (isEncrypted != null) 'is_encrypted': isEncrypted,
59+
if (isInvited != null) 'is_invited': isInvited,
60+
if (roomTypes is Some) 'room_types': roomTypes,
61+
if (notRoomTypes is Some) 'not_room_types': notRoomTypes,
62+
if (tags != null) 'tags': tags,
63+
if (notTags != null) 'not_tags': notTags,
64+
};
65+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:matrix/matrix.dart';
2+
3+
/// The StateStub is used in required_state to indicate that a piece of state has been deleted.
4+
class StateStub extends BasicEvent {
5+
/// The state_key of the state entry that was deleted
6+
final String stateKey;
7+
8+
StateStub({
9+
required super.type,
10+
required super.content,
11+
required this.stateKey,
12+
});
13+
14+
StateStub.fromJson(super.json)
15+
: stateKey = json['state_key'] as String,
16+
super.fromJson();
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class StrippedHero {
2+
/// The user ID of the hero.
3+
final String user_id;
4+
5+
/// The display name of the user from the membership event, if set
6+
final String? displayName;
7+
8+
/// The avatar url from the membership event, if set
9+
// TODO: migrate to Uri
10+
final String? avatarUrl;
11+
12+
const StrippedHero({required this.user_id, this.displayName, this.avatarUrl});
13+
14+
factory StrippedHero.fromJson(Map<String, Object?> json) => StrippedHero(
15+
user_id: json['user_id'] as String,
16+
displayName: json['displayname'] as String?,
17+
avatarUrl: json['avatar_url'] as String?,
18+
);
19+
}

0 commit comments

Comments
 (0)