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/space_edges.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/hooks/saving.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/topic.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/settings_category.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 eventSetting(
|
||||
EventType type, {
|
||||
required String title,
|
||||
required String description,
|
||||
required IconData icon,
|
||||
required int fallback,
|
||||
}) => powerLevelSetting(
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
value: powerLevels.events[type.type] ?? fallback,
|
||||
update: (level) => powerLevels.copyWith(
|
||||
events: powerLevels.events.add(type.type, level),
|
||||
),
|
||||
);
|
||||
Setting permissionSetting(PowerLevelPermission permission) =>
|
||||
powerLevelSetting(
|
||||
title: permission.title,
|
||||
description: permission.description,
|
||||
icon: permission.icon,
|
||||
value: permission.levelIn(powerLevels),
|
||||
update: (level) => permission.withLevel(powerLevels, level),
|
||||
);
|
||||
|
||||
/// A setting for the level needed to send [event], which otherwise needs [fallback].
|
||||
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,
|
||||
update: (level) => powerLevels.copyWith(
|
||||
events: powerLevels.events.add(type.type, level),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return .new({
|
||||
"Room": .new([
|
||||
|
|
@ -431,14 +440,7 @@ class RoomSettingsSectionsController(final String roomId)
|
|||
subtitle: Text(description),
|
||||
value: joinRules.joinRule,
|
||||
options: joinRuleOptions,
|
||||
getName: (rule) => switch (rule) {
|
||||
.public => "Public",
|
||||
.invite => "Invite only",
|
||||
.knock => "Knock",
|
||||
.restricted => "Restricted",
|
||||
.knockRestricted => "Restricted or knock",
|
||||
.private => "Private",
|
||||
},
|
||||
getName: (rule) => rule.displayName,
|
||||
onChanged: canSetJoinRules
|
||||
? (rule) => setState(
|
||||
.joinRules,
|
||||
|
|
@ -615,13 +617,7 @@ class RoomSettingsSectionsController(final String roomId)
|
|||
title: "Permissions",
|
||||
icon: Icons.admin_panel_settings,
|
||||
settings: .new([
|
||||
powerLevelSetting(
|
||||
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),
|
||||
),
|
||||
permissionSetting(.usersDefault),
|
||||
header("Users", "Power levels of specific users.", Icons.group),
|
||||
...users.map((entry) {
|
||||
final member = memberOf(entry.key);
|
||||
|
|
@ -687,82 +683,21 @@ class RoomSettingsSectionsController(final String roomId)
|
|||
"Power levels needed to send messages and other timeline events.",
|
||||
Icons.chat,
|
||||
),
|
||||
eventSetting(
|
||||
messageType,
|
||||
title: "Send Messages",
|
||||
description: "The power level needed to send messages.",
|
||||
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),
|
||||
),
|
||||
eventSetting(.message, fallback: powerLevels.eventsDefault),
|
||||
eventSetting(.sticker, fallback: powerLevels.eventsDefault),
|
||||
eventSetting(.reaction, fallback: powerLevels.eventsDefault),
|
||||
permissionSetting(.eventsDefault),
|
||||
header(
|
||||
"State Events",
|
||||
"Power levels needed to manage members and change the room's state.",
|
||||
Icons.settings,
|
||||
),
|
||||
eventSetting(
|
||||
.pinnedEvents,
|
||||
title: "Pin Messages",
|
||||
description: "The power level needed to pin messages.",
|
||||
icon: Icons.push_pin,
|
||||
fallback: powerLevels.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),
|
||||
),
|
||||
eventSetting(.pinnedEvents, fallback: powerLevels.stateDefault),
|
||||
permissionSetting(.invite),
|
||||
permissionSetting(.kick),
|
||||
permissionSetting(.ban),
|
||||
permissionSetting(.redact),
|
||||
permissionSetting(.stateDefault),
|
||||
header(
|
||||
"Other Events",
|
||||
"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(
|
||||
1,
|
||||
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/helpers/extensions/is_shown_as_message.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/models/content/avatar.dart";
|
||||
import "package:nexus/models/content/canonical_alias.dart";
|
||||
import "package:nexus/models/content/content.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/join_rules.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/power_levels.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/read_receipts_row.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/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/server_acl_diff.dart";
|
||||
import "package:nexus/widgets/swipe_to_reply.dart";
|
||||
|
|
@ -105,6 +111,10 @@ class const EventRenderer(
|
|||
event.sender == previousEvent?.sender &&
|
||||
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(
|
||||
IconData icon,
|
||||
String text, {
|
||||
|
|
@ -167,18 +177,57 @@ class const EventRenderer(
|
|||
"changed the room avatar",
|
||||
),
|
||||
CreateContent() => generic(Icons.add, "created the room"),
|
||||
PowerLevelsContent() => generic(
|
||||
final PowerLevelsContent content => generic(
|
||||
Icons.power,
|
||||
"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,
|
||||
"changed the room's join rules",
|
||||
details: [
|
||||
JoinRulesDiff(
|
||||
content,
|
||||
previous: switch (event.previousContent) {
|
||||
final JoinRulesContent previous => previous,
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
TopicContent() => generic(
|
||||
Icons.description,
|
||||
"updated the room topic",
|
||||
),
|
||||
TopicContent(:final topic) => () {
|
||||
final previous = switch (event.previousContent) {
|
||||
TopicContent(:final topic) when topic.isNotEmpty => topic,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return generic(
|
||||
Icons.description,
|
||||
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(
|
||||
Icons.history,
|
||||
"changed the room's history visibility to ${switch (historyVisibility) {
|
||||
|
|
@ -230,32 +279,71 @@ class const EventRenderer(
|
|||
),
|
||||
],
|
||||
),
|
||||
CanonicalAliasContent(:final alias, :final altAliases) => generic(
|
||||
Icons.numbers,
|
||||
switch ([
|
||||
if (event.previousContent case CanonicalAliasContent(
|
||||
alias: final prevAlias,
|
||||
altAliases: final prevAltAliases,
|
||||
)) ...[
|
||||
if (prevAlias != alias)
|
||||
if (alias == null)
|
||||
"removed the room's canonical alias"
|
||||
else
|
||||
"changed the room's canonical alias to $alias",
|
||||
CanonicalAliasContent(:final alias, :final altAliases) => () {
|
||||
final previous = switch (event.previousContent) {
|
||||
final CanonicalAliasContent previous => previous,
|
||||
_ => null,
|
||||
};
|
||||
final previousAltAliases =
|
||||
previous?.altAliases ?? const ISet<String>.empty();
|
||||
|
||||
if (prevAltAliases
|
||||
.remove(alias ?? "")
|
||||
.remove(prevAlias ?? "") !=
|
||||
altAliases.remove(alias ?? "").remove(prevAlias ?? ""))
|
||||
"changed the room's aliases",
|
||||
] else ...[
|
||||
if (alias != null) "set the room's canonical alias",
|
||||
if (altAliases.isNotEmpty) "set the room's aliases",
|
||||
return generic(
|
||||
Icons.numbers,
|
||||
previous == null
|
||||
? "set the room's aliases"
|
||||
: "changed the room's aliases",
|
||||
details: [
|
||||
if (previous?.alias != alias)
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
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:nexus/helpers/extensions/success_scheme.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
|
||||
/// event, and those no longer denied or allowed, struck through.
|
||||
|
|
@ -10,13 +11,9 @@ class const ServerAclDiff(
|
|||
final ServerACLContent? previous,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
static const maxShown = 5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final style = theme.textTheme.labelMedium;
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
final ipLiteralsAllowed = switch (previous) {
|
||||
final previous?
|
||||
|
|
@ -32,43 +29,24 @@ class const ServerAclDiff(
|
|||
IList<String> after, {
|
||||
required bool ipLiterals,
|
||||
}) {
|
||||
// Sets, as an ACL can list hundreds of servers
|
||||
final beforeSet = before.toISet();
|
||||
final afterSet = after.toISet();
|
||||
final servers = [
|
||||
...after
|
||||
.where((server) => !beforeSet.contains(server))
|
||||
.map((server) => (server, removed: false)),
|
||||
...before
|
||||
.where((server) => !afterSet.contains(server))
|
||||
.map((server) => (server, removed: true)),
|
||||
];
|
||||
if (servers.isEmpty && !ipLiterals) return null;
|
||||
final removed = before.where((server) => !afterSet.contains(server));
|
||||
final added = after.where((server) => !beforeSet.contains(server));
|
||||
if (removed.isEmpty && added.isEmpty && !ipLiterals) return null;
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
Text(label, style: style?.copyWith(fontWeight: .bold)),
|
||||
...servers
|
||||
.take(maxShown)
|
||||
.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),
|
||||
),
|
||||
return ChangeLine(
|
||||
label: label,
|
||||
removed: .new(removed),
|
||||
added: .new(added),
|
||||
removedColor: color,
|
||||
addedColor: color,
|
||||
trailing: [
|
||||
if (ipLiterals)
|
||||
Text(
|
||||
"servers using IP addresses",
|
||||
style: style?.copyWith(color: color),
|
||||
style: theme.textTheme.labelMedium?.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -80,7 +58,7 @@ class const ServerAclDiff(
|
|||
children: [
|
||||
?listDiff(
|
||||
"Denied",
|
||||
colorScheme.error,
|
||||
theme.colorScheme.error,
|
||||
previous?.deny ?? const .empty(),
|
||||
content.deny,
|
||||
ipLiterals: ipLiteralsAllowed == false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue