diff --git a/README.md b/README.md
index 35530ce..f94a6d3 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
| Dark Mode | Light Mode |
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: |
-|  |  |
+|  |  |
## Try it out
diff --git a/assets/screenshotDark.avif b/assets/screenshotDark.avif
deleted file mode 100644
index c1a8e9f..0000000
Binary files a/assets/screenshotDark.avif and /dev/null differ
diff --git a/assets/screenshotLight.avif b/assets/screenshotLight.avif
deleted file mode 100644
index c26c0b7..0000000
Binary files a/assets/screenshotLight.avif and /dev/null differ
diff --git a/assets/screenshots/desktop-dark.png b/assets/screenshots/desktop-dark.png
new file mode 100644
index 0000000..e9753bd
Binary files /dev/null and b/assets/screenshots/desktop-dark.png differ
diff --git a/assets/screenshots/desktop-light.png b/assets/screenshots/desktop-light.png
new file mode 100644
index 0000000..2d5c876
Binary files /dev/null and b/assets/screenshots/desktop-light.png differ
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 0000000..e310d29
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,10 @@
+
Nexus is a simple and user-friendly client for Matrix, the open and decentralized chat network. It's made with Flutter, with gomuks handling the Matrix side.
+
+Features include:
+
+- Spaces, threads, replies and reactions
+- End-to-end encryption, with verification and recovery keys
+- Polls, location sharing, and custom emoji and sticker packs
+- Multiple accounts
+- Notifications through UnifiedPush, or Firebase Cloud Messaging
+
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 0000000..6b1b81c
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
new file mode 100644
index 0000000..c6688a0
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
new file mode 100644
index 0000000..1d4ff07
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
new file mode 100644
index 0000000..f969bf1
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
new file mode 100644
index 0000000..82915b6
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 0000000..570387a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+A simple and user-friendly Matrix client
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 0000000..755a6d7
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Nexus
diff --git a/integration_test/helpers/pump_until.dart b/integration_test/helpers/pump_until.dart
index 5ae8358..95c4944 100644
--- a/integration_test/helpers/pump_until.dart
+++ b/integration_test/helpers/pump_until.dart
@@ -10,9 +10,23 @@ extension PumpUntil on WidgetTester {
Future pumpUntil(Finder finder) async {
const timeout = Duration(seconds: 30);
- String textsIn(Finder finder) => widgetList(
- find.descendant(of: finder, matching: find.byType(Text)),
- ).map((text) => text.data).nonNulls.join("\n");
+ String textsIn(Finder finder) =>
+ widgetList(
+ find.descendant(
+ of: finder,
+ matching: find.byWidgetPredicate(
+ (widget) => widget is Text || widget is SelectableText,
+ ),
+ ),
+ )
+ .map(
+ (widget) => switch (widget) {
+ Text(:final data) || SelectableText(:final data) => data,
+ _ => null,
+ },
+ )
+ .nonNulls
+ .join("\n");
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsed < timeout) {
diff --git a/integration_test/screenshots/avatars/SOURCES.md b/integration_test/screenshots/avatars/SOURCES.md
new file mode 100644
index 0000000..e6094db
--- /dev/null
+++ b/integration_test/screenshots/avatars/SOURCES.md
@@ -0,0 +1,22 @@
+# Screenshot avatars
+
+Avatars for the people, rooms and spaces in the screenshots, cropped from photos on Wikimedia Commons and Public Domain Pictures that are in the public domain or CC0, so they need no attribution.
+
+| File | License | Source |
+| ---------------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
+| pumpkin.webp | Public domain | https://commons.wikimedia.org/wiki/File:Cucurbita_2011_G1.jpg |
+| orange.webp | Public domain | https://commons.wikimedia.org/wiki/File:Orange-fruit-2.jpg |
+| log-1.webp | CC0 | https://commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg (tinted green) |
+| strudel.webp | CC0 | https://commons.wikimedia.org/wiki/File:Strudel-de-maca-2.png |
+| opal.webp | Public domain | https://commons.wikimedia.org/wiki/File:Opal_6.jpg |
+| cinnamon.webp | CC0 | https://commons.wikimedia.org/wiki/File:Cassia_bark_(2).jpg |
+| green-tea.webp | CC0 | https://www.publicdomainpictures.net/en/view-image.php?image=165622&picture=hot-green-tea-with-heart-art |
+| development.webp | CC0 | https://commons.wikimedia.org/wiki/File:Code_on_computer_monitor_(Unsplash).jpg |
+| fediverse.webp | Public domain | https://commons.wikimedia.org/wiki/File:Blackeyegalaxy.jpg |
+| flutter-devs.webp | CC0 | https://commons.wikimedia.org/wiki/File:Anna%27s_Hummingbird_Feeding.jpg |
+| linux-mobile.webp | CC0 | https://commons.wikimedia.org/wiki/File:Gentoo_penguin_with_a_feather_in_its_beak.jpg |
+| matrix-community.webp | Public domain | https://commons.wikimedia.org/wiki/File:Earth_at_Night,_Asia_(8270740728).jpg |
+| off-topic.webp | CC0 | https://commons.wikimedia.org/wiki/File:Coffee_cup_surrounded_by_coffee_beans.jpg |
+| open-source-games.webp | Public domain | https://commons.wikimedia.org/wiki/File:LodgeNet-Nintendo-N64-Controller.jpg |
+| rust.webp | CC0 | https://commons.wikimedia.org/wiki/File:Carcinus_maenas_146434844.jpg |
+| self-hosting.webp | CC0 | https://commons.wikimedia.org/wiki/File:Legrand_S.A._server_rack.jpg |
diff --git a/integration_test/screenshots/avatars/cinnamon.webp b/integration_test/screenshots/avatars/cinnamon.webp
new file mode 100644
index 0000000..8520f24
Binary files /dev/null and b/integration_test/screenshots/avatars/cinnamon.webp differ
diff --git a/integration_test/screenshots/avatars/development.webp b/integration_test/screenshots/avatars/development.webp
new file mode 100644
index 0000000..21a47b1
Binary files /dev/null and b/integration_test/screenshots/avatars/development.webp differ
diff --git a/integration_test/screenshots/avatars/fediverse.webp b/integration_test/screenshots/avatars/fediverse.webp
new file mode 100644
index 0000000..8952d99
Binary files /dev/null and b/integration_test/screenshots/avatars/fediverse.webp differ
diff --git a/integration_test/screenshots/avatars/flutter-devs.webp b/integration_test/screenshots/avatars/flutter-devs.webp
new file mode 100644
index 0000000..79634a2
Binary files /dev/null and b/integration_test/screenshots/avatars/flutter-devs.webp differ
diff --git a/integration_test/screenshots/avatars/green-tea.webp b/integration_test/screenshots/avatars/green-tea.webp
new file mode 100644
index 0000000..d58801c
Binary files /dev/null and b/integration_test/screenshots/avatars/green-tea.webp differ
diff --git a/integration_test/screenshots/avatars/linux-mobile.webp b/integration_test/screenshots/avatars/linux-mobile.webp
new file mode 100644
index 0000000..8a525bd
Binary files /dev/null and b/integration_test/screenshots/avatars/linux-mobile.webp differ
diff --git a/integration_test/screenshots/avatars/log-1.webp b/integration_test/screenshots/avatars/log-1.webp
new file mode 100644
index 0000000..e954d0c
Binary files /dev/null and b/integration_test/screenshots/avatars/log-1.webp differ
diff --git a/integration_test/screenshots/avatars/matrix-community.webp b/integration_test/screenshots/avatars/matrix-community.webp
new file mode 100644
index 0000000..2e58828
Binary files /dev/null and b/integration_test/screenshots/avatars/matrix-community.webp differ
diff --git a/integration_test/screenshots/avatars/off-topic.webp b/integration_test/screenshots/avatars/off-topic.webp
new file mode 100644
index 0000000..ea9ec17
Binary files /dev/null and b/integration_test/screenshots/avatars/off-topic.webp differ
diff --git a/integration_test/screenshots/avatars/opal.webp b/integration_test/screenshots/avatars/opal.webp
new file mode 100644
index 0000000..807223f
Binary files /dev/null and b/integration_test/screenshots/avatars/opal.webp differ
diff --git a/integration_test/screenshots/avatars/open-source-games.webp b/integration_test/screenshots/avatars/open-source-games.webp
new file mode 100644
index 0000000..3b92ac7
Binary files /dev/null and b/integration_test/screenshots/avatars/open-source-games.webp differ
diff --git a/integration_test/screenshots/avatars/orange.webp b/integration_test/screenshots/avatars/orange.webp
new file mode 100644
index 0000000..1f9c199
Binary files /dev/null and b/integration_test/screenshots/avatars/orange.webp differ
diff --git a/integration_test/screenshots/avatars/pumpkin.webp b/integration_test/screenshots/avatars/pumpkin.webp
new file mode 100644
index 0000000..d300ec9
Binary files /dev/null and b/integration_test/screenshots/avatars/pumpkin.webp differ
diff --git a/integration_test/screenshots/avatars/quadradical.webp b/integration_test/screenshots/avatars/quadradical.webp
new file mode 100644
index 0000000..79e26c1
Binary files /dev/null and b/integration_test/screenshots/avatars/quadradical.webp differ
diff --git a/integration_test/screenshots/avatars/rust.webp b/integration_test/screenshots/avatars/rust.webp
new file mode 100644
index 0000000..5089187
Binary files /dev/null and b/integration_test/screenshots/avatars/rust.webp differ
diff --git a/integration_test/screenshots/avatars/self-hosting.webp b/integration_test/screenshots/avatars/self-hosting.webp
new file mode 100644
index 0000000..30535e9
Binary files /dev/null and b/integration_test/screenshots/avatars/self-hosting.webp differ
diff --git a/integration_test/screenshots/avatars/strudel.webp b/integration_test/screenshots/avatars/strudel.webp
new file mode 100644
index 0000000..e69e842
Binary files /dev/null and b/integration_test/screenshots/avatars/strudel.webp differ
diff --git a/integration_test/screenshots/screenshot_data.dart b/integration_test/screenshots/screenshot_data.dart
new file mode 100644
index 0000000..fbabe4b
--- /dev/null
+++ b/integration_test/screenshots/screenshot_data.dart
@@ -0,0 +1,365 @@
+import "dart:convert";
+
+import "package:collection/collection.dart";
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
+import "package:nexus/models/client_state.dart";
+import "package:nexus/models/room.dart";
+import "package:nexus/models/space_edge.dart";
+
+/// What the screenshots show: the Nexus space, with its room mid-conversation.
+/// Rooms are written as gomuks reports them.
+class ScreenshotData(final DateTime now) {
+ static const me = "@quadradical:federated.nexus";
+ static const bot = "@nexusbot:federated.nexus";
+ static const alice = "@alice:example.org";
+ static const pumpkin = "@pumpkin:example.org";
+ static const strudel = "@strudel:example.org";
+ static const opal = "@opal:example.org";
+ static const greenTea = "@greentea:example.org";
+
+ static const nexusSpace = "!nexus-space:federated.nexus";
+ static const nexusRoom = "!nexus-client:federated.nexus";
+
+ /// Nexus' own avatar, the app icon. Every other `mxc://screenshots/` is
+ /// a file in avatars/, see avatars/SOURCES.md.
+ static const nexusAvatar = "mxc://screenshots/nexus";
+ static const myAvatar = "mxc://screenshots/quadradical.webp";
+ static const issueUrl = "https://git.federated.nexus/Nexus/nexus/issues/174";
+ static const commitUrl =
+ "https://git.federated.nexus/Nexus/nexus/commit/41f5225420";
+
+ static const topic =
+ "A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.";
+
+ /// The users shown, with their power level in the Nexus room. Those without
+ /// an avatar get Nexus' fallback. Everyone but QuadRadical and NexusBot is
+ /// made up, on example.org so as not to be anyone real.
+ static const users = IListConst([
+ (id: me, name: "QuadRadical", powerLevel: 100, avatar: myAvatar),
+ (id: alice, name: "Alice", powerLevel: 100, avatar: null),
+ (id: bot, name: "NexusBot", powerLevel: 50, avatar: nexusAvatar),
+ (
+ id: pumpkin,
+ name: "Pumpkin",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/pumpkin.webp",
+ ),
+ (
+ id: strudel,
+ name: "Strudel",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/strudel.webp",
+ ),
+ (
+ id: opal,
+ name: "Opal",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/opal.webp",
+ ),
+ (
+ id: "@orange:example.org",
+ name: "Orange",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/orange.webp",
+ ),
+ (
+ id: "@log1:example.org",
+ name: "log(1)",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/log-1.webp",
+ ),
+ (
+ id: greenTea,
+ name: "Green Tea",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/green-tea.webp",
+ ),
+ (
+ id: "@cinnamon:example.org",
+ name: "Cinnamon",
+ powerLevel: 0,
+ avatar: "mxc://screenshots/cinnamon.webp",
+ ),
+ ]);
+
+ /// The Nexus space's other rooms
+ static const otherRooms = IListConst([
+ (name: "Off-topic", unread: 3),
+ (name: "Development", unread: 0),
+ ]);
+
+ /// Spaces shown in the rail besides Nexus, so it doesn't look empty
+ static const otherSpaces = IListConst([
+ "Matrix Community",
+ "Flutter Devs",
+ "Linux Mobile",
+ "Rust",
+ "Open Source Games",
+ "Fediverse",
+ "Self-Hosting",
+ ]);
+
+ static const clientState = ClientState(
+ isInitialized: true,
+ isLoggedIn: true,
+ isVerified: true,
+ userId: me,
+ deviceId: "SCREENSHOTS",
+ homeserverUrl: "https://federated.nexus",
+ );
+
+ static final IList topLevelSpaces = .new([
+ nexusSpace,
+ ...otherSpaces.map(_roomId),
+ ]);
+
+ static final IMap> spaceEdges = .new({
+ nexusSpace: .new(
+ [
+ nexusRoom,
+ ...otherRooms.map((room) => _roomId(room.name)),
+ ].map((id) => .new(childId: id)),
+ ),
+ });
+
+ /// The rooms, with the Nexus room's state already fetched so it isn't
+ /// requested
+ late final IMap rooms = {
+ nexusSpace: _room(nexusSpace, "Nexus Client", topic: topic),
+ nexusRoom: _room(
+ nexusRoom,
+ "Nexus Client",
+ topic: topic,
+ lastEvent: timeline.last,
+ extra: {
+ "timeline": timeline
+ .map(
+ (event) => {
+ "timeline_rowid": event["rowid"],
+ "event_rowid": event["rowid"],
+ },
+ )
+ .toList(),
+ "events": [...nexusState, ...timeline],
+ "state": _stateIndex(nexusState),
+ "receipts": {
+ timeline.last["event_id"]: [
+ _receipt(strudel, timeline.last),
+ _receipt(greenTea, timeline.last),
+ ],
+ timeline[timeline.length - 2]["event_id"]: [
+ _receipt(opal, timeline[timeline.length - 2]),
+ ],
+ },
+ "has_more": false,
+ },
+ ).copyWith(hasFetchedState: true, hasFetchedMembers: true),
+ for (final room in otherRooms)
+ _roomId(room.name): _room(
+ _roomId(room.name),
+ room.name,
+ avatar: _avatarOfRoom(room.name),
+ unread: room.unread,
+ ),
+ for (final space in otherSpaces)
+ _roomId(space): _room(
+ _roomId(space),
+ space,
+ avatar: _avatarOfRoom(space),
+ ),
+ }.lock;
+
+ /// The Nexus room's state: who's in it, and their power levels
+ late final List