nexus/integration_test/screenshots/screenshot_data.dart

365 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/<file>` 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<String> topLevelSpaces = .new([
nexusSpace,
...otherSpaces.map(_roomId),
]);
static final IMap<String, IList<SpaceEdge>> 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<String, Room> 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<Map<String, dynamic>> nexusState = [
_event(me, "m.room.power_levels", {
"users": Map.fromEntries(
users.map((user) => .new(user.id, user.powerLevel)),
),
"users_default": 0,
"events_default": 0,
"state_default": 50,
}, key: ""),
...users.map(
(user) => _event(user.id, "m.room.member", {
"membership": "join",
"displayname": user.name,
"avatar_url": ?user.avatar,
}, key: user.id),
),
];
late final List<Map<String, dynamic>> timeline = [
_message(
alice,
35,
"the screenshots in the readme are pretty out of date btw",
),
_message(
me,
32,
"Should I:\n"
"1. just take screenshots on a few devices, and redo them whenever the "
"UI changes\n"
"or\n"
"2. overengineer a test that makes them automatically, in a fake room\n"
"I think the answer is 2 :p",
html:
"Should I:"
"<ol><li>just take screenshots on a few devices, and redo them "
"whenever the UI changes</li></ol>"
"or"
'<ol start="2"><li>overengineer a test that makes them '
"automatically, in a fake room</li></ol>"
"I think the answer is 2 :p",
reactions: {"2️⃣": 3},
),
_message(pumpkin, 30, "2, obviously"),
// Linked only in the HTML, so the body has no link to preview
_message(
bot,
29,
"[Nexus/nexus] Issue opened: #174 Automatic scripts to make screenshots "
"for different pages and viewports by Henry-Hiles",
html:
'[Nexus/nexus] Issue opened: <a href="$issueUrl">#174 Automatic '
"scripts to make screenshots for different pages and viewports</a> "
"by Henry-Hiles",
),
_message(
bot,
7,
"[Nexus/nexus] Henry-Hiles pushed 1 commit to main: $commitUrl: "
"update screenshots - Henry-Hiles",
html:
"[Nexus/nexus] Henry-Hiles pushed 1 commit to main:<br>"
'<a href="$commitUrl">41f5225</a>: update screenshots - Henry-Hiles',
reactions: {"🚀": 3},
),
_message(
pumpkin,
5,
"they're generated now?? let's gooo",
reactions: {"🎉": 2, "❤️": 1},
),
];
/// The link preview shown under NexusBot's message
static const urlPreview = {
"og:title": "update screenshots · 41f5225420",
"og:description":
"nexus - A simple and user-friendly Matrix client made with Flutter "
"and a Gomuks backend.",
};
int _rowId = 0;
/// An event in the Nexus room
Map<String, dynamic> _event(
String sender,
String type,
Map<String, dynamic> content, {
String? key,
int minutesAgo = 60,
Map<String, int> reactions = const {},
}) {
final rowId = ++_rowId;
return {
"rowid": rowId,
"timeline_rowid": rowId,
"room_id": nexusRoom,
"event_id": "\$screenshots-$rowId",
"sender": sender,
"type": type,
"state_key": ?key,
"timestamp": now
.subtract(.new(minutes: minutesAgo))
.millisecondsSinceEpoch,
"content": content,
"reactions": reactions,
};
}
Map<String, dynamic> _message(
String sender,
int minutesAgo,
String body, {
String? html,
Map<String, int> reactions = const {},
}) => _event(
sender,
"m.room.message",
{
"msgtype": sender == bot ? "m.notice" : "m.text",
"body": body,
if (html != null) ...{
"format": "org.matrix.custom.html",
"formatted_body": html,
},
},
minutesAgo: minutesAgo,
reactions: reactions,
);
Map<String, dynamic> _receipt(String userId, Map<String, dynamic> event) => {
"user_id": userId,
"event_id": event["event_id"],
"room_id": nexusRoom,
"timestamp": event["timestamp"],
};
/// A room, with [extra] JSON besides its metadata
Room _room(
String id,
String name, {
String avatar = nexusAvatar,
String? topic,
int unread = 0,
Map<String, dynamic>? lastEvent,
Map<String, dynamic> extra = const {},
}) => .fromJson(
// Round-tripped, so the JSON has the types gomuks' decoded JSON would
jsonDecode(
jsonEncode({
"meta": {
"room_id": id,
"name": name,
"avatar": avatar,
"topic": ?topic,
"has_member_list": true,
"preview_event_rowid": lastEvent?["rowid"] ?? 0,
"sorting_timestamp":
lastEvent?["timestamp"] ??
now.subtract(const .new(hours: 1)).millisecondsSinceEpoch,
"unread_highlights": 0,
"unread_notifications": 0,
"unread_messages": unread,
},
...extra,
}),
),
);
static Map<String, Map<String, int>> _stateIndex(
List<Map<String, dynamic>> state,
) => state
.groupListsBy((event) => event["type"] as String)
.map(
(type, events) => .new(
type,
Map.fromEntries(
events.map(
(event) =>
.new(event["state_key"] as String, event["rowid"] as int),
),
),
),
);
static String _avatarOfRoom(String name) =>
"mxc://screenshots/${_slug(name)}.webp";
static String _slug(String name) =>
name.toLowerCase().replaceAll(RegExp("[^a-z]+"), "-");
static String _roomId(String name) => "!${_slug(name)}:federated.nexus";
}