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> 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> 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:" "
  1. just take screenshots on a few devices, and redo them " "whenever the UI changes
" "or" '
  1. overengineer a test that makes them ' "automatically, in a fake room
" "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: #174 Automatic ' "scripts to make screenshots for different pages and viewports " "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:
" '41f5225: 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 _event( String sender, String type, Map content, { String? key, int minutesAgo = 60, Map 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 _message( String sender, int minutesAgo, String body, { String? html, Map 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 _receipt(String userId, Map 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? lastEvent, Map 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> _stateIndex( List> 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"; }