add diffing for a lot of events
This commit is contained in:
parent
4e140e9fc7
commit
6e2d56ec75
9 changed files with 591 additions and 168 deletions
|
|
@ -11,6 +11,7 @@ import "package:nexus/controllers/room_power_levels.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/space_edges.dart";
|
import "package:nexus/controllers/space_edges.dart";
|
||||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||||
|
import "package:nexus/helpers/extensions/join_rule_name.dart";
|
||||||
import "package:nexus/helpers/file_dialog_helper.dart";
|
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||||
import "package:nexus/helpers/hooks/saving.dart";
|
import "package:nexus/helpers/hooks/saving.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -27,6 +28,7 @@ import "package:nexus/models/content/name.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/content/topic.dart";
|
import "package:nexus/models/content/topic.dart";
|
||||||
import "package:nexus/models/join_rule.dart";
|
import "package:nexus/models/join_rule.dart";
|
||||||
|
import "package:nexus/models/power_level_permission.dart";
|
||||||
import "package:nexus/models/setting.dart";
|
import "package:nexus/models/setting.dart";
|
||||||
import "package:nexus/models/settings_category.dart";
|
import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
@ -288,22 +290,29 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A setting for the level needed to send [type], which otherwise needs [fallback].
|
Setting permissionSetting(PowerLevelPermission permission) =>
|
||||||
Setting eventSetting(
|
powerLevelSetting(
|
||||||
EventType type, {
|
title: permission.title,
|
||||||
required String title,
|
description: permission.description,
|
||||||
required String description,
|
icon: permission.icon,
|
||||||
required IconData icon,
|
value: permission.levelIn(powerLevels),
|
||||||
required int fallback,
|
update: (level) => permission.withLevel(powerLevels, level),
|
||||||
}) => powerLevelSetting(
|
);
|
||||||
title: title,
|
|
||||||
description: description,
|
/// A setting for the level needed to send [event], which otherwise needs [fallback].
|
||||||
icon: icon,
|
Setting eventSetting(PowerLevelEvent event, {required int fallback}) {
|
||||||
|
final type = event == .message ? messageType : event.types.first;
|
||||||
|
|
||||||
|
return powerLevelSetting(
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
icon: event.icon,
|
||||||
value: powerLevels.events[type.type] ?? fallback,
|
value: powerLevels.events[type.type] ?? fallback,
|
||||||
update: (level) => powerLevels.copyWith(
|
update: (level) => powerLevels.copyWith(
|
||||||
events: powerLevels.events.add(type.type, level),
|
events: powerLevels.events.add(type.type, level),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return .new({
|
return .new({
|
||||||
"Room": .new([
|
"Room": .new([
|
||||||
|
|
@ -431,14 +440,7 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
value: joinRules.joinRule,
|
value: joinRules.joinRule,
|
||||||
options: joinRuleOptions,
|
options: joinRuleOptions,
|
||||||
getName: (rule) => switch (rule) {
|
getName: (rule) => rule.displayName,
|
||||||
.public => "Public",
|
|
||||||
.invite => "Invite only",
|
|
||||||
.knock => "Knock",
|
|
||||||
.restricted => "Restricted",
|
|
||||||
.knockRestricted => "Restricted or knock",
|
|
||||||
.private => "Private",
|
|
||||||
},
|
|
||||||
onChanged: canSetJoinRules
|
onChanged: canSetJoinRules
|
||||||
? (rule) => setState(
|
? (rule) => setState(
|
||||||
.joinRules,
|
.joinRules,
|
||||||
|
|
@ -615,13 +617,7 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
title: "Permissions",
|
title: "Permissions",
|
||||||
icon: Icons.admin_panel_settings,
|
icon: Icons.admin_panel_settings,
|
||||||
settings: .new([
|
settings: .new([
|
||||||
powerLevelSetting(
|
permissionSetting(.usersDefault),
|
||||||
title: "Default Power Level",
|
|
||||||
description: "The power level of users who aren't given one.",
|
|
||||||
icon: Icons.person,
|
|
||||||
value: powerLevels.usersDefault,
|
|
||||||
update: (level) => powerLevels.copyWith(usersDefault: level),
|
|
||||||
),
|
|
||||||
header("Users", "Power levels of specific users.", Icons.group),
|
header("Users", "Power levels of specific users.", Icons.group),
|
||||||
...users.map((entry) {
|
...users.map((entry) {
|
||||||
final member = memberOf(entry.key);
|
final member = memberOf(entry.key);
|
||||||
|
|
@ -687,82 +683,21 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
"Power levels needed to send messages and other timeline events.",
|
"Power levels needed to send messages and other timeline events.",
|
||||||
Icons.chat,
|
Icons.chat,
|
||||||
),
|
),
|
||||||
eventSetting(
|
eventSetting(.message, fallback: powerLevels.eventsDefault),
|
||||||
messageType,
|
eventSetting(.sticker, fallback: powerLevels.eventsDefault),
|
||||||
title: "Send Messages",
|
eventSetting(.reaction, fallback: powerLevels.eventsDefault),
|
||||||
description: "The power level needed to send messages.",
|
permissionSetting(.eventsDefault),
|
||||||
icon: Icons.message,
|
|
||||||
fallback: powerLevels.eventsDefault,
|
|
||||||
),
|
|
||||||
eventSetting(
|
|
||||||
.sticker,
|
|
||||||
title: "Send Stickers",
|
|
||||||
description: "The power level needed to send stickers.",
|
|
||||||
icon: Icons.sticky_note_2,
|
|
||||||
fallback: powerLevels.eventsDefault,
|
|
||||||
),
|
|
||||||
eventSetting(
|
|
||||||
.reaction,
|
|
||||||
title: "Send Reactions",
|
|
||||||
description: "The power level needed to react to messages.",
|
|
||||||
icon: Icons.add_reaction,
|
|
||||||
fallback: powerLevels.eventsDefault,
|
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Default for Events",
|
|
||||||
description:
|
|
||||||
"The power level needed to send events without their own.",
|
|
||||||
icon: Icons.more_horiz,
|
|
||||||
value: powerLevels.eventsDefault,
|
|
||||||
update: (level) => powerLevels.copyWith(eventsDefault: level),
|
|
||||||
),
|
|
||||||
header(
|
header(
|
||||||
"State Events",
|
"State Events",
|
||||||
"Power levels needed to manage members and change the room's state.",
|
"Power levels needed to manage members and change the room's state.",
|
||||||
Icons.settings,
|
Icons.settings,
|
||||||
),
|
),
|
||||||
eventSetting(
|
eventSetting(.pinnedEvents, fallback: powerLevels.stateDefault),
|
||||||
.pinnedEvents,
|
permissionSetting(.invite),
|
||||||
title: "Pin Messages",
|
permissionSetting(.kick),
|
||||||
description: "The power level needed to pin messages.",
|
permissionSetting(.ban),
|
||||||
icon: Icons.push_pin,
|
permissionSetting(.redact),
|
||||||
fallback: powerLevels.stateDefault,
|
permissionSetting(.stateDefault),
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Invite Users",
|
|
||||||
description: "The power level needed to invite users.",
|
|
||||||
icon: Icons.person_add,
|
|
||||||
value: powerLevels.invite,
|
|
||||||
update: (level) => powerLevels.copyWith(invite: level),
|
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Kick Users",
|
|
||||||
description: "The power level needed to kick users.",
|
|
||||||
icon: Icons.sports_martial_arts,
|
|
||||||
value: powerLevels.kick,
|
|
||||||
update: (level) => powerLevels.copyWith(kick: level),
|
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Ban Users",
|
|
||||||
description: "The power level needed to ban and unban users.",
|
|
||||||
icon: Icons.gavel,
|
|
||||||
value: powerLevels.ban,
|
|
||||||
update: (level) => powerLevels.copyWith(ban: level),
|
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Delete Others' Messages",
|
|
||||||
description: "The power level needed to remove others' messages.",
|
|
||||||
icon: Icons.delete,
|
|
||||||
value: powerLevels.redact,
|
|
||||||
update: (level) => powerLevels.copyWith(redact: level),
|
|
||||||
),
|
|
||||||
powerLevelSetting(
|
|
||||||
title: "Default for State Events",
|
|
||||||
description: "The power level needed to send state events without their own.",
|
|
||||||
icon: Icons.tune,
|
|
||||||
value: powerLevels.stateDefault,
|
|
||||||
update: (level) => powerLevels.copyWith(stateDefault: level),
|
|
||||||
),
|
|
||||||
header(
|
header(
|
||||||
"Other Events",
|
"Other Events",
|
||||||
"Power levels for specific event types.",
|
"Power levels for specific event types.",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ extension DisplaynameColor on WidgetRef {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Like [displaynameColor], for when [userId]'s [level] is already known,
|
||||||
|
/// like a level they're being given.
|
||||||
|
Color displaynameColorAt(String userId, double level) => switch (watch(
|
||||||
|
SettingsController.provider.select((settings) => settings.value?.nameColor),
|
||||||
|
)) {
|
||||||
|
.user => userId.colorHash,
|
||||||
|
.powerLevel || null => powerLevelColor(level),
|
||||||
|
};
|
||||||
|
|
||||||
static Color powerLevelColor(double level) => HSLColor.fromAHSL(
|
static Color powerLevelColor(double level) => HSLColor.fromAHSL(
|
||||||
1,
|
1,
|
||||||
level.isInfinite ? 30 : (210 + level / 50 * 137.50776) % 360,
|
level.isInfinite ? 30 : (210 + level / 50 * 137.50776) % 360,
|
||||||
|
|
|
||||||
13
lib/helpers/extensions/join_rule_name.dart
Normal file
13
lib/helpers/extensions/join_rule_name.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import "package:nexus/models/join_rule.dart";
|
||||||
|
|
||||||
|
extension JoinRuleName on JoinRule {
|
||||||
|
/// What the rule is called in the UI, e.g. "Invite only".
|
||||||
|
String get displayName => switch (this) {
|
||||||
|
.public => "Public",
|
||||||
|
.invite => "Invite only",
|
||||||
|
.knock => "Knock",
|
||||||
|
.restricted => "Restricted",
|
||||||
|
.knockRestricted => "Restricted or knock",
|
||||||
|
.private => "Private",
|
||||||
|
};
|
||||||
|
}
|
||||||
115
lib/models/power_level_permission.dart
Normal file
115
lib/models/power_level_permission.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
|
||||||
|
enum PowerLevelPermission(
|
||||||
|
final String title,
|
||||||
|
final String description,
|
||||||
|
final IconData icon,
|
||||||
|
) {
|
||||||
|
usersDefault(
|
||||||
|
"Default Power Level",
|
||||||
|
"The power level of users who aren't given one.",
|
||||||
|
Icons.person,
|
||||||
|
),
|
||||||
|
eventsDefault(
|
||||||
|
"Default for Events",
|
||||||
|
"The power level needed to send events without their own.",
|
||||||
|
Icons.more_horiz,
|
||||||
|
),
|
||||||
|
stateDefault(
|
||||||
|
"Default for State Events",
|
||||||
|
"The power level needed to send state events without their own.",
|
||||||
|
Icons.tune,
|
||||||
|
),
|
||||||
|
invite(
|
||||||
|
"Invite Users",
|
||||||
|
"The power level needed to invite users.",
|
||||||
|
Icons.person_add,
|
||||||
|
),
|
||||||
|
kick(
|
||||||
|
"Kick Users",
|
||||||
|
"The power level needed to kick users.",
|
||||||
|
Icons.sports_martial_arts,
|
||||||
|
),
|
||||||
|
ban(
|
||||||
|
"Ban Users",
|
||||||
|
"The power level needed to ban and unban users.",
|
||||||
|
Icons.gavel,
|
||||||
|
),
|
||||||
|
redact(
|
||||||
|
"Delete Others' Messages",
|
||||||
|
"The power level needed to remove others' messages.",
|
||||||
|
Icons.delete,
|
||||||
|
),
|
||||||
|
notifyRoom(
|
||||||
|
"Notify Everyone",
|
||||||
|
"The power level needed to notify everyone in the room with @room.",
|
||||||
|
Icons.campaign,
|
||||||
|
);
|
||||||
|
|
||||||
|
int levelIn(PowerLevelsContent content) => switch (this) {
|
||||||
|
.usersDefault => content.usersDefault,
|
||||||
|
.eventsDefault => content.eventsDefault,
|
||||||
|
.stateDefault => content.stateDefault,
|
||||||
|
.invite => content.invite,
|
||||||
|
.kick => content.kick,
|
||||||
|
.ban => content.ban,
|
||||||
|
.redact => content.redact,
|
||||||
|
.notifyRoom => content.notifications?.room ?? 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
PowerLevelsContent withLevel(PowerLevelsContent content, int level) =>
|
||||||
|
switch (this) {
|
||||||
|
.usersDefault => content.copyWith(usersDefault: level),
|
||||||
|
.eventsDefault => content.copyWith(eventsDefault: level),
|
||||||
|
.stateDefault => content.copyWith(stateDefault: level),
|
||||||
|
.invite => content.copyWith(invite: level),
|
||||||
|
.kick => content.copyWith(kick: level),
|
||||||
|
.ban => content.copyWith(ban: level),
|
||||||
|
.redact => content.copyWith(redact: level),
|
||||||
|
.notifyRoom => content.copyWith(
|
||||||
|
notifications: (content.notifications ?? const Notifications())
|
||||||
|
.copyWith(room: level),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PowerLevelEvent(
|
||||||
|
final String title,
|
||||||
|
final String description,
|
||||||
|
final IconData icon,
|
||||||
|
) {
|
||||||
|
message(
|
||||||
|
"Send Messages",
|
||||||
|
"The power level needed to send messages.",
|
||||||
|
Icons.message,
|
||||||
|
),
|
||||||
|
sticker(
|
||||||
|
"Send Stickers",
|
||||||
|
"The power level needed to send stickers.",
|
||||||
|
Icons.sticky_note_2,
|
||||||
|
),
|
||||||
|
reaction(
|
||||||
|
"Send Reactions",
|
||||||
|
"The power level needed to react to messages.",
|
||||||
|
Icons.add_reaction,
|
||||||
|
),
|
||||||
|
pinnedEvents(
|
||||||
|
"Pin Messages",
|
||||||
|
"The power level needed to pin messages.",
|
||||||
|
Icons.push_pin,
|
||||||
|
);
|
||||||
|
|
||||||
|
IList<EventType> get types => .new(switch (this) {
|
||||||
|
.message => [.message, .encrypted],
|
||||||
|
.sticker => [.sticker],
|
||||||
|
.reaction => [.reaction],
|
||||||
|
.pinnedEvents => [.pinnedEvents],
|
||||||
|
});
|
||||||
|
|
||||||
|
static PowerLevelEvent? of(String type) => values
|
||||||
|
.where((event) => event.types.any((eventType) => eventType.type == type))
|
||||||
|
.firstOrNull;
|
||||||
|
}
|
||||||
72
lib/widgets/renderers/change_line.dart
Normal file
72
lib/widgets/renderers/change_line.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
|
/// A line of what a state event changed, like a room's old name, struck
|
||||||
|
/// through, then its new one.
|
||||||
|
class const ChangeLine({
|
||||||
|
/// What changed, e.g. `Denied`, in bold before the items.
|
||||||
|
final String? label,
|
||||||
|
final IList<String> removed = const .empty(),
|
||||||
|
final IList<String> added = const .empty(),
|
||||||
|
required final Color removedColor,
|
||||||
|
required final Color addedColor,
|
||||||
|
|
||||||
|
/// Whether to quote the items, like server names, but not whole sentences.
|
||||||
|
final bool quoted = true,
|
||||||
|
|
||||||
|
/// Shown after the items, like a change that isn't one of them.
|
||||||
|
final List<Widget> trailing = const [],
|
||||||
|
|
||||||
|
/// How many removed and how many added items to show, as some events, like
|
||||||
|
/// a room's first server ACL, can change hundreds.
|
||||||
|
final int maxShown = 5,
|
||||||
|
|
||||||
|
/// The most lines each item can take, as some, like topics, can be long.
|
||||||
|
final int maxLines = 2,
|
||||||
|
super.key,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final style = theme.textTheme.labelMedium;
|
||||||
|
|
||||||
|
Iterable<Widget> items(
|
||||||
|
IList<String> items, {
|
||||||
|
required Color color,
|
||||||
|
required bool struck,
|
||||||
|
}) => [
|
||||||
|
...items
|
||||||
|
.take(maxShown)
|
||||||
|
.map(
|
||||||
|
(item) => Text(
|
||||||
|
quoted ? "\"$item\"" : item,
|
||||||
|
maxLines: maxLines,
|
||||||
|
overflow: .ellipsis,
|
||||||
|
style: style?.copyWith(
|
||||||
|
color: color,
|
||||||
|
decoration: struck ? .lineThrough : null,
|
||||||
|
decorationColor: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (items.length > maxShown)
|
||||||
|
Text(
|
||||||
|
"+${items.length - maxShown} more",
|
||||||
|
style: style?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 2,
|
||||||
|
crossAxisAlignment: .center,
|
||||||
|
children: [
|
||||||
|
if (label case final label?)
|
||||||
|
Text(label, style: style?.copyWith(fontWeight: .bold)),
|
||||||
|
...items(removed, color: removedColor, struck: true),
|
||||||
|
...items(added, color: addedColor, struck: false),
|
||||||
|
...trailing,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,17 @@ import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/helpers/extensions/is_shown_as_message.dart";
|
import "package:nexus/helpers/extensions/is_shown_as_message.dart";
|
||||||
import "package:nexus/helpers/extensions/show_adaptive_menu.dart";
|
import "package:nexus/helpers/extensions/show_adaptive_menu.dart";
|
||||||
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
import "package:nexus/models/content/avatar.dart";
|
import "package:nexus/models/content/avatar.dart";
|
||||||
import "package:nexus/models/content/canonical_alias.dart";
|
import "package:nexus/models/content/canonical_alias.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/create.dart";
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/content/encryption.dart";
|
||||||
import "package:nexus/models/content/history_visibility.dart";
|
import "package:nexus/models/content/history_visibility.dart";
|
||||||
import "package:nexus/models/content/join_rules.dart";
|
import "package:nexus/models/content/join_rules.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
|
import "package:nexus/models/content/name.dart";
|
||||||
import "package:nexus/models/content/pinned_events.dart";
|
import "package:nexus/models/content/pinned_events.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/content/server_acl.dart";
|
import "package:nexus/models/content/server_acl.dart";
|
||||||
|
|
@ -28,7 +31,10 @@ import "package:nexus/widgets/renderers/message.dart";
|
||||||
import "package:nexus/widgets/reaction_row.dart";
|
import "package:nexus/widgets/reaction_row.dart";
|
||||||
import "package:nexus/widgets/read_receipts_row.dart";
|
import "package:nexus/widgets/read_receipts_row.dart";
|
||||||
import "package:nexus/widgets/renderers/membership.dart";
|
import "package:nexus/widgets/renderers/membership.dart";
|
||||||
|
import "package:nexus/widgets/renderers/change_line.dart";
|
||||||
import "package:nexus/widgets/renderers/generic_event.dart";
|
import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
|
import "package:nexus/widgets/renderers/join_rules_diff.dart";
|
||||||
|
import "package:nexus/widgets/renderers/power_levels_diff.dart";
|
||||||
import "package:nexus/widgets/renderers/pinned_events_diff.dart";
|
import "package:nexus/widgets/renderers/pinned_events_diff.dart";
|
||||||
import "package:nexus/widgets/renderers/server_acl_diff.dart";
|
import "package:nexus/widgets/renderers/server_acl_diff.dart";
|
||||||
import "package:nexus/widgets/swipe_to_reply.dart";
|
import "package:nexus/widgets/swipe_to_reply.dart";
|
||||||
|
|
@ -105,6 +111,10 @@ class const EventRenderer(
|
||||||
event.sender == previousEvent?.sender &&
|
event.sender == previousEvent?.sender &&
|
||||||
event.pmp?.id == previousEvent?.pmp?.id;
|
event.pmp?.id == previousEvent?.pmp?.id;
|
||||||
|
|
||||||
|
// For what a state event changed, from and to
|
||||||
|
final removedColor = colorScheme.error;
|
||||||
|
final addedColor = theme.successScheme.primary;
|
||||||
|
|
||||||
GenericEventRenderer generic(
|
GenericEventRenderer generic(
|
||||||
IconData icon,
|
IconData icon,
|
||||||
String text, {
|
String text, {
|
||||||
|
|
@ -167,18 +177,57 @@ class const EventRenderer(
|
||||||
"changed the room avatar",
|
"changed the room avatar",
|
||||||
),
|
),
|
||||||
CreateContent() => generic(Icons.add, "created the room"),
|
CreateContent() => generic(Icons.add, "created the room"),
|
||||||
PowerLevelsContent() => generic(
|
final PowerLevelsContent content => generic(
|
||||||
Icons.power,
|
Icons.power,
|
||||||
"changed the room's power levels",
|
"changed the room's power levels",
|
||||||
|
details: [
|
||||||
|
PowerLevelsDiff(
|
||||||
|
roomId,
|
||||||
|
content,
|
||||||
|
previous: switch (event.previousContent) {
|
||||||
|
final PowerLevelsContent previous => previous,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
JoinRulesContent() => generic(
|
],
|
||||||
|
),
|
||||||
|
final JoinRulesContent content => generic(
|
||||||
Icons.rule,
|
Icons.rule,
|
||||||
"changed the room's join rules",
|
"changed the room's join rules",
|
||||||
|
details: [
|
||||||
|
JoinRulesDiff(
|
||||||
|
content,
|
||||||
|
previous: switch (event.previousContent) {
|
||||||
|
final JoinRulesContent previous => previous,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
TopicContent() => generic(
|
],
|
||||||
|
),
|
||||||
|
TopicContent(:final topic) => () {
|
||||||
|
final previous = switch (event.previousContent) {
|
||||||
|
TopicContent(:final topic) when topic.isNotEmpty => topic,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return generic(
|
||||||
Icons.description,
|
Icons.description,
|
||||||
"updated the room topic",
|
topic.isEmpty
|
||||||
|
? "removed the room topic"
|
||||||
|
: previous == null
|
||||||
|
? "set the room topic"
|
||||||
|
: "changed the room topic",
|
||||||
|
details: [
|
||||||
|
ChangeLine(
|
||||||
|
removed: [?previous].lock,
|
||||||
|
added: [if (topic.isNotEmpty) topic].lock,
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
quoted: false,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}(),
|
||||||
HistoryVisibilityContent(:final historyVisibility) => generic(
|
HistoryVisibilityContent(:final historyVisibility) => generic(
|
||||||
Icons.history,
|
Icons.history,
|
||||||
"changed the room's history visibility to ${switch (historyVisibility) {
|
"changed the room's history visibility to ${switch (historyVisibility) {
|
||||||
|
|
@ -230,32 +279,71 @@ class const EventRenderer(
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
CanonicalAliasContent(:final alias, :final altAliases) => generic(
|
CanonicalAliasContent(:final alias, :final altAliases) => () {
|
||||||
Icons.numbers,
|
final previous = switch (event.previousContent) {
|
||||||
switch ([
|
final CanonicalAliasContent previous => previous,
|
||||||
if (event.previousContent case CanonicalAliasContent(
|
_ => null,
|
||||||
alias: final prevAlias,
|
};
|
||||||
altAliases: final prevAltAliases,
|
final previousAltAliases =
|
||||||
)) ...[
|
previous?.altAliases ?? const ISet<String>.empty();
|
||||||
if (prevAlias != alias)
|
|
||||||
if (alias == null)
|
|
||||||
"removed the room's canonical alias"
|
|
||||||
else
|
|
||||||
"changed the room's canonical alias to $alias",
|
|
||||||
|
|
||||||
if (prevAltAliases
|
return generic(
|
||||||
.remove(alias ?? "")
|
Icons.numbers,
|
||||||
.remove(prevAlias ?? "") !=
|
previous == null
|
||||||
altAliases.remove(alias ?? "").remove(prevAlias ?? ""))
|
? "set the room's aliases"
|
||||||
"changed the room's aliases",
|
: "changed the room's aliases",
|
||||||
] else ...[
|
details: [
|
||||||
if (alias != null) "set the room's canonical alias",
|
if (previous?.alias != alias)
|
||||||
if (altAliases.isNotEmpty) "set the room's aliases",
|
ChangeLine(
|
||||||
|
label: "Canonical alias",
|
||||||
|
removed: [?previous?.alias].lock,
|
||||||
|
added: [?alias].lock,
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
),
|
||||||
|
if (previousAltAliases != altAliases)
|
||||||
|
ChangeLine(
|
||||||
|
label: "Aliases",
|
||||||
|
removed: previousAltAliases
|
||||||
|
.difference(altAliases)
|
||||||
|
.toIList(),
|
||||||
|
added: altAliases
|
||||||
|
.difference(previousAltAliases)
|
||||||
|
.toIList(),
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
]) {
|
);
|
||||||
[] => "did something related to room aliases",
|
}(),
|
||||||
List prev => prev.join(" and "),
|
NameContent(:final name) => () {
|
||||||
},
|
final previous = switch (event.previousContent) {
|
||||||
|
NameContent(:final name) when name.isNotEmpty => name,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return generic(
|
||||||
|
Icons.title,
|
||||||
|
name.isEmpty
|
||||||
|
? "removed the room name"
|
||||||
|
: previous == null
|
||||||
|
? "set the room name"
|
||||||
|
: "changed the room name",
|
||||||
|
details: [
|
||||||
|
ChangeLine(
|
||||||
|
removed: [?previous].lock,
|
||||||
|
added: [if (name.isNotEmpty) name].lock,
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
EncryptionContent() => generic(
|
||||||
|
Icons.lock,
|
||||||
|
event.previousContent is EncryptionContent
|
||||||
|
? "changed the room's encryption settings"
|
||||||
|
: "enabled encryption",
|
||||||
),
|
),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
72
lib/widgets/renderers/join_rules_diff.dart
Normal file
72
lib/widgets/renderers/join_rules_diff.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/helpers/extensions/join_rule_name.dart";
|
||||||
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
|
import "package:nexus/models/content/join_rules.dart";
|
||||||
|
import "package:nexus/widgets/renderers/change_line.dart";
|
||||||
|
|
||||||
|
/// The join rule a join rules event changed to, and for restricted rooms, the
|
||||||
|
/// rooms whose members it let join, or stopped letting join.
|
||||||
|
class const JoinRulesDiff(
|
||||||
|
final JoinRulesContent content, {
|
||||||
|
final JoinRulesContent? previous,
|
||||||
|
super.key,
|
||||||
|
}) extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final removedColor = theme.colorScheme.error;
|
||||||
|
final addedColor = theme.successScheme.primary;
|
||||||
|
|
||||||
|
ISet<String> roomIdsOf(JoinRulesContent? content) =>
|
||||||
|
(content?.allow ?? const ISet<AllowCondition>.empty())
|
||||||
|
.map((condition) => condition.roomId)
|
||||||
|
.nonNulls
|
||||||
|
.toISet();
|
||||||
|
final before = roomIdsOf(previous);
|
||||||
|
final after = roomIdsOf(content);
|
||||||
|
final removed = before.difference(after);
|
||||||
|
final added = after.difference(before);
|
||||||
|
|
||||||
|
// Rooms the user isn't in are shown by ID
|
||||||
|
final names = ref.watch(
|
||||||
|
RoomsController.provider.select(
|
||||||
|
(rooms) => IMap.fromEntries(
|
||||||
|
removed
|
||||||
|
.addAll(added)
|
||||||
|
.map(
|
||||||
|
(roomId) =>
|
||||||
|
MapEntry(roomId, rooms[roomId]?.metadata?.name ?? roomId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
IList<String> named(ISet<String> roomIds) =>
|
||||||
|
roomIds.map((roomId) => names[roomId] ?? roomId).toIList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: .start,
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
if (previous?.joinRule != content.joinRule)
|
||||||
|
ChangeLine(
|
||||||
|
removed: [?previous?.joinRule.displayName].lock,
|
||||||
|
added: [content.joinRule.displayName].lock,
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
quoted: false,
|
||||||
|
),
|
||||||
|
if (removed.isNotEmpty || added.isNotEmpty)
|
||||||
|
ChangeLine(
|
||||||
|
label: "Members of",
|
||||||
|
removed: named(removed),
|
||||||
|
added: named(added),
|
||||||
|
removedColor: removedColor,
|
||||||
|
addedColor: addedColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
lib/widgets/renderers/power_levels_diff.dart
Normal file
141
lib/widgets/renderers/power_levels_diff.dart
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/user.dart";
|
||||||
|
import "package:nexus/helpers/extensions/displayname_color.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
import "package:nexus/models/power_level_permission.dart";
|
||||||
|
|
||||||
|
/// Whose power levels, and which permissions' levels, a power levels event
|
||||||
|
/// changed, in green when raised, and red when lowered.
|
||||||
|
class const PowerLevelsDiff(
|
||||||
|
final String roomId,
|
||||||
|
final PowerLevelsContent content, {
|
||||||
|
final PowerLevelsContent? previous,
|
||||||
|
super.key,
|
||||||
|
}) extends ConsumerWidget {
|
||||||
|
static const maxShown = 5;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final style = theme.textTheme.labelMedium;
|
||||||
|
final before = previous ?? PowerLevelsContent();
|
||||||
|
|
||||||
|
Text levels(int? from, int? to, {int? fromLevel, int? toLevel}) => Text(
|
||||||
|
"${from ?? "Default"} → ${to ?? "Default"}",
|
||||||
|
style: style?.copyWith(
|
||||||
|
color: switch ((fromLevel ?? from, toLevel ?? to)) {
|
||||||
|
(final from?, final to?) when to > from =>
|
||||||
|
theme.successScheme.primary,
|
||||||
|
(final from?, final to?) when to < from => theme.colorScheme.error,
|
||||||
|
_ => theme.colorScheme.onSurfaceVariant,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Iterable<Widget> capped(Iterable<Widget> lines) {
|
||||||
|
final list = lines.toList();
|
||||||
|
return [
|
||||||
|
...list.take(maxShown),
|
||||||
|
if (list.length > maxShown)
|
||||||
|
Text(
|
||||||
|
"+${list.length - maxShown} more",
|
||||||
|
style: style?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
final users = before.users.keys
|
||||||
|
.toISet()
|
||||||
|
.addAll(content.users.keys)
|
||||||
|
.map(
|
||||||
|
(userId) => (
|
||||||
|
userId,
|
||||||
|
from: before.users[userId],
|
||||||
|
to: content.users[userId],
|
||||||
|
fromLevel: before.users[userId] ?? before.usersDefault,
|
||||||
|
toLevel: content.users[userId] ?? content.usersDefault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where((change) => change.from != change.to)
|
||||||
|
// The most powerful first
|
||||||
|
.toIList()
|
||||||
|
.sort((a, b) => b.toLevel.compareTo(a.toLevel));
|
||||||
|
|
||||||
|
final permissions = <(String, int?, int?)>[
|
||||||
|
...PowerLevelPermission.values.map(
|
||||||
|
(permission) => (
|
||||||
|
permission.title,
|
||||||
|
permission.levelIn(before),
|
||||||
|
permission.levelIn(content),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...before.events.keys
|
||||||
|
.toISet()
|
||||||
|
.addAll(content.events.keys)
|
||||||
|
.map(
|
||||||
|
(type) => (
|
||||||
|
PowerLevelEvent.of(type)?.title ?? type,
|
||||||
|
before.events[type],
|
||||||
|
content.events[type],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].where((permission) => permission.$2 != permission.$3);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: .start,
|
||||||
|
spacing: 2,
|
||||||
|
children: [
|
||||||
|
...capped(
|
||||||
|
users.map(
|
||||||
|
(change) => Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: .center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
ref
|
||||||
|
.watch(
|
||||||
|
UserController.provider(
|
||||||
|
.new(roomId: roomId, userId: change.$1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.value
|
||||||
|
?.displayName ??
|
||||||
|
change.$1.localpart,
|
||||||
|
style: style?.copyWith(
|
||||||
|
fontWeight: .bold,
|
||||||
|
color: ref.displaynameColorAt(
|
||||||
|
change.$1,
|
||||||
|
change.toLevel.toDouble(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
levels(
|
||||||
|
change.from,
|
||||||
|
change.to,
|
||||||
|
fromLevel: change.fromLevel,
|
||||||
|
toLevel: change.toLevel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...capped(
|
||||||
|
permissions.map(
|
||||||
|
(permission) => Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: .center,
|
||||||
|
children: [
|
||||||
|
Text(permission.$1, style: style?.copyWith(fontWeight: .bold)),
|
||||||
|
levels(permission.$2, permission.$3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/helpers/extensions/success_scheme.dart";
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
import "package:nexus/models/content/server_acl.dart";
|
import "package:nexus/models/content/server_acl.dart";
|
||||||
|
import "package:nexus/widgets/renderers/change_line.dart";
|
||||||
|
|
||||||
/// The servers newly denied (in red) and allowed (in green) by a server ACL
|
/// The servers newly denied (in red) and allowed (in green) by a server ACL
|
||||||
/// event, and those no longer denied or allowed, struck through.
|
/// event, and those no longer denied or allowed, struck through.
|
||||||
|
|
@ -10,13 +11,9 @@ class const ServerAclDiff(
|
||||||
final ServerACLContent? previous,
|
final ServerACLContent? previous,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
static const maxShown = 5;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final style = theme.textTheme.labelMedium;
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
|
|
||||||
final ipLiteralsAllowed = switch (previous) {
|
final ipLiteralsAllowed = switch (previous) {
|
||||||
final previous?
|
final previous?
|
||||||
|
|
@ -32,43 +29,24 @@ class const ServerAclDiff(
|
||||||
IList<String> after, {
|
IList<String> after, {
|
||||||
required bool ipLiterals,
|
required bool ipLiterals,
|
||||||
}) {
|
}) {
|
||||||
|
// Sets, as an ACL can list hundreds of servers
|
||||||
final beforeSet = before.toISet();
|
final beforeSet = before.toISet();
|
||||||
final afterSet = after.toISet();
|
final afterSet = after.toISet();
|
||||||
final servers = [
|
final removed = before.where((server) => !afterSet.contains(server));
|
||||||
...after
|
final added = after.where((server) => !beforeSet.contains(server));
|
||||||
.where((server) => !beforeSet.contains(server))
|
if (removed.isEmpty && added.isEmpty && !ipLiterals) return null;
|
||||||
.map((server) => (server, removed: false)),
|
|
||||||
...before
|
|
||||||
.where((server) => !afterSet.contains(server))
|
|
||||||
.map((server) => (server, removed: true)),
|
|
||||||
];
|
|
||||||
if (servers.isEmpty && !ipLiterals) return null;
|
|
||||||
|
|
||||||
return Wrap(
|
return ChangeLine(
|
||||||
spacing: 8,
|
label: label,
|
||||||
runSpacing: 2,
|
removed: .new(removed),
|
||||||
children: [
|
added: .new(added),
|
||||||
Text(label, style: style?.copyWith(fontWeight: .bold)),
|
removedColor: color,
|
||||||
...servers
|
addedColor: color,
|
||||||
.take(maxShown)
|
trailing: [
|
||||||
.map(
|
|
||||||
(server) => Text(
|
|
||||||
"\"${server.$1}\"",
|
|
||||||
style: style?.copyWith(
|
|
||||||
color: color,
|
|
||||||
decoration: server.removed ? .lineThrough : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (servers.length > maxShown)
|
|
||||||
Text(
|
|
||||||
"+${servers.length - maxShown} more",
|
|
||||||
style: style?.copyWith(color: colorScheme.onSurfaceVariant),
|
|
||||||
),
|
|
||||||
if (ipLiterals)
|
if (ipLiterals)
|
||||||
Text(
|
Text(
|
||||||
"servers using IP addresses",
|
"servers using IP addresses",
|
||||||
style: style?.copyWith(color: color),
|
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -80,7 +58,7 @@ class const ServerAclDiff(
|
||||||
children: [
|
children: [
|
||||||
?listDiff(
|
?listDiff(
|
||||||
"Denied",
|
"Denied",
|
||||||
colorScheme.error,
|
theme.colorScheme.error,
|
||||||
previous?.deny ?? const .empty(),
|
previous?.deny ?? const .empty(),
|
||||||
content.deny,
|
content.deny,
|
||||||
ipLiterals: ipLiteralsAllowed == false,
|
ipLiterals: ipLiteralsAllowed == false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue