diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart
index 39860ab..3a1c51d 100644
--- a/lib/famedlysdk.dart
+++ b/lib/famedlysdk.dart
@@ -28,6 +28,7 @@ export 'package:famedlysdk/src/utils/uri_extension.dart';
export 'package:famedlysdk/src/utils/matrix_localizations.dart';
export 'package:famedlysdk/src/utils/receipt.dart';
export 'package:famedlysdk/src/utils/states_map.dart';
+export 'package:famedlysdk/src/utils/sync_update_extension.dart';
export 'package:famedlysdk/src/utils/to_device_event.dart';
export 'package:famedlysdk/src/client.dart';
export 'package:famedlysdk/src/event.dart';
diff --git a/lib/src/utils/sync_update_extension.dart b/lib/src/utils/sync_update_extension.dart
new file mode 100644
index 0000000..c4b9ecb
--- /dev/null
+++ b/lib/src/utils/sync_update_extension.dart
@@ -0,0 +1,44 @@
+/*
+ * Famedly Matrix SDK
+ * Copyright (C) 2020 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import 'package:famedlysdk/matrix_api.dart';
+
+/// This extension adds easy-to-use filters for the sync update, meant to be used on the `client.onSync` stream, e.g.
+/// `client.onSync.stream.where((s) => s.hasRoomUpdate)`. Multiple filters can easily be
+/// combind with boolean logic: `client.onSync.stream.where((s) => s.hasRoomUpdate || s.hasPresenceUpdate)`
+extension SyncUpdateFilters on SyncUpdate {
+ /// Returns true if this sync updat has a room update
+ /// That means there is account data, if there is a room in one of the `join`, `leave` or `invite` blocks of the sync or if there is a to_device event.
+ bool get hasRoomUpdate {
+ // if we have an account data change we need to re-render, as `m.direct` might have changed
+ if (accountData?.isNotEmpty ?? false) {
+ return true;
+ }
+ // check for a to_device event
+ if (toDevice?.isNotEmpty ?? false) {
+ return true;
+ }
+ // return if there are rooms to update
+ return (rooms?.join?.isNotEmpty ?? false) ||
+ (rooms?.invite?.isNotEmpty ?? false) ||
+ (rooms?.leave?.isNotEmpty ?? false);
+ }
+
+ /// Returns if this sync update has presence updates
+ bool get hasPresenceUpdate => presence != null && presence.isNotEmpty;
+}
diff --git a/test/sync_filter_test.dart b/test/sync_filter_test.dart
new file mode 100644
index 0000000..71ac01b
--- /dev/null
+++ b/test/sync_filter_test.dart
@@ -0,0 +1,166 @@
+/*
+ * Ansible inventory script used at Famedly GmbH for managing many hosts
+ * Copyright (C) 2020 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import 'package:famedlysdk/famedlysdk.dart';
+import 'package:test/test.dart';
+
+const UPDATES = {
+ 'empty': {
+ 'next_batch': 'blah',
+ 'account_data': {
+ 'events': [],
+ },
+ 'presences': {
+ 'events': [],
+ },
+ 'rooms': {
+ 'join': {},
+ 'leave': {},
+ 'invite': {},
+ },
+ 'to_device': {
+ 'events': [],
+ },
+ },
+ 'presence': {
+ 'next_batch': 'blah',
+ 'presence': {
+ 'events': [
+ {
+ 'content': {
+ 'avatar_url': 'mxc://localhost:wefuiwegh8742w',
+ 'last_active_ago': 2478593,
+ 'presence': 'online',
+ 'currently_active': false,
+ 'status_msg': 'Making cupcakes'
+ },
+ 'type': 'm.presence',
+ 'sender': '@example:localhost',
+ },
+ ],
+ },
+ },
+ 'account_data': {
+ 'next_batch': 'blah',
+ 'account_data': {
+ 'events': [
+ {
+ 'type': 'blah',
+ 'content': {
+ 'beep': 'boop',
+ },
+ },
+ ],
+ },
+ },
+ 'invite': {
+ 'next_batch': 'blah',
+ 'rooms': {
+ 'invite': {
+ '!room': {
+ 'invite_state': {
+ 'events': [],
+ },
+ },
+ },
+ },
+ },
+ 'leave': {
+ 'next_batch': 'blah',
+ 'rooms': {
+ 'leave': {
+ '!room': {},
+ },
+ },
+ },
+ 'join': {
+ 'next_batch': 'blah',
+ 'rooms': {
+ 'join': {
+ '!room': {
+ 'timeline': {
+ 'events': [],
+ },
+ 'state': {
+ 'events': [],
+ },
+ 'account_data': {
+ 'events': [],
+ },
+ 'ephemeral': {
+ 'events': [],
+ },
+ 'unread_notifications': {},
+ 'summary': {},
+ },
+ },
+ },
+ },
+ 'to_device': {
+ 'next_batch': 'blah',
+ 'to_device': {
+ 'events': [
+ {
+ 'type': 'beep',
+ 'content': {
+ 'blah': 'blubb',
+ },
+ },
+ ],
+ },
+ },
+};
+
+void testUpdates(bool Function(SyncUpdate s) test, Map expected) {
+ for (final update in UPDATES.entries) {
+ var sync = SyncUpdate.fromJson(update.value);
+ expect(test(sync), expected[update.key]);
+ }
+}
+
+void main() {
+ group('Sync Filters', () {
+ test('room update', () {
+ var testFn = (SyncUpdate s) => s.hasRoomUpdate;
+ final expected = {
+ 'empty': false,
+ 'presence': false,
+ 'account_data': true,
+ 'invite': true,
+ 'leave': true,
+ 'join': true,
+ 'to_device': true,
+ };
+ testUpdates(testFn, expected);
+ });
+
+ test('presence update', () {
+ var testFn = (SyncUpdate s) => s.hasPresenceUpdate;
+ final expected = {
+ 'empty': false,
+ 'presence': true,
+ 'account_data': false,
+ 'invite': false,
+ 'leave': false,
+ 'join': false,
+ 'to_device': false,
+ };
+ testUpdates(testFn, expected);
+ });
+ });
+}