mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-24 14:32:37 +01:00
design: Hide unimportant state events instead of folding
This commit is contained in:
parent
b4bf23cd34
commit
c365469dc5
@ -2949,5 +2949,6 @@
|
||||
"placeholders": {
|
||||
"number": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hideUnimportantStateEvents": "Hide unimportant state events"
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ abstract class AppConfig {
|
||||
static bool renderHtml = true;
|
||||
static bool hideRedactedEvents = false;
|
||||
static bool hideUnknownEvents = true;
|
||||
static bool hideUnimportantStateEvents = true;
|
||||
static bool showDirectChatsInSpaces = true;
|
||||
static bool separateChatTypes = false;
|
||||
static bool autoplayImages = true;
|
||||
|
@ -3,6 +3,8 @@ abstract class SettingKeys {
|
||||
static const String renderHtml = 'chat.fluffy.renderHtml';
|
||||
static const String hideRedactedEvents = 'chat.fluffy.hideRedactedEvents';
|
||||
static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents';
|
||||
static const String hideUnimportantStateEvents =
|
||||
'chat.fluffy.hideUnimportantStateEvents';
|
||||
static const String showDirectChatsInSpaces =
|
||||
'chat.fluffy.showDirectChatsInSpaces';
|
||||
static const String separateChatTypes = 'chat.fluffy.separateChatTypes';
|
||||
|
@ -28,7 +28,6 @@ import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/account_bundles.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||
import 'send_file_dialog.dart';
|
||||
import 'send_location_dialog.dart';
|
||||
@ -111,8 +110,6 @@ class ChatController extends State<Chat> {
|
||||
|
||||
List<Event> selectedEvents = [];
|
||||
|
||||
late List<Event> filteredEvents;
|
||||
|
||||
final Set<String> unfolded = {};
|
||||
|
||||
Event? replyEvent;
|
||||
@ -184,22 +181,7 @@ class ChatController extends State<Chat> {
|
||||
|
||||
void updateView() {
|
||||
if (!mounted) return;
|
||||
setState(
|
||||
() {
|
||||
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void unfold(String eventId) {
|
||||
var i = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
setState(() {
|
||||
while (i < filteredEvents.length - 1 && filteredEvents[i].isState) {
|
||||
unfolded.add(filteredEvents[i].eventId);
|
||||
i++;
|
||||
}
|
||||
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<bool> getTimeline() async {
|
||||
@ -225,7 +207,6 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
});
|
||||
}
|
||||
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
|
||||
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
||||
return true;
|
||||
}
|
||||
@ -656,7 +637,7 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
|
||||
void scrollToEventId(String eventId) async {
|
||||
var eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
var eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||
if (eventIndex == -1) {
|
||||
// event id not found...maybe we can fetch it?
|
||||
// the try...finally is here to start and close the loading dialog reliably
|
||||
@ -693,7 +674,7 @@ class ChatController extends State<Chat> {
|
||||
rethrow;
|
||||
}
|
||||
eventIndex =
|
||||
filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
||||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class ChatEventList extends StatelessWidget {
|
||||
@ -26,9 +27,10 @@ class ChatEventList extends StatelessWidget {
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
// ListView's findChildIndexCallback
|
||||
final thisEventsKeyMap = <String, int>{};
|
||||
for (var i = 0; i < controller.filteredEvents.length; i++) {
|
||||
thisEventsKeyMap[controller.filteredEvents[i].eventId] = i;
|
||||
for (var i = 0; i < controller.timeline!.events.length; i++) {
|
||||
thisEventsKeyMap[controller.timeline!.events[i].eventId] = i;
|
||||
}
|
||||
|
||||
return ListView.custom(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
@ -55,7 +57,7 @@ class ChatEventList extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Request history button or progress indicator:
|
||||
if (i == controller.filteredEvents.length + 1) {
|
||||
if (i == controller.timeline!.events.length + 1) {
|
||||
if (controller.timeline!.isRequestingHistory) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
@ -76,37 +78,40 @@ class ChatEventList extends StatelessWidget {
|
||||
}
|
||||
|
||||
// The message at this index:
|
||||
final event = controller.timeline!.events[i - 1];
|
||||
|
||||
return AutoScrollTag(
|
||||
key: ValueKey(controller.filteredEvents[i - 1].eventId),
|
||||
key: ValueKey(event.eventId),
|
||||
index: i - 1,
|
||||
controller: controller.scrollController,
|
||||
child: Message(controller.filteredEvents[i - 1],
|
||||
onSwipe: (direction) => controller.replyAction(
|
||||
replyTo: controller.filteredEvents[i - 1]),
|
||||
onInfoTab: controller.showEventInfo,
|
||||
onAvatarTab: (Event event) => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: event.senderFromMemoryOrFallback,
|
||||
outerContext: context,
|
||||
onMention: () => controller.sendController.text +=
|
||||
'${event.senderFromMemoryOrFallback.mention} ',
|
||||
),
|
||||
),
|
||||
unfold: controller.unfold,
|
||||
onSelect: controller.onSelectMessage,
|
||||
scrollToEventId: (String eventId) =>
|
||||
controller.scrollToEventId(eventId),
|
||||
longPressSelect: controller.selectedEvents.isEmpty,
|
||||
selected: controller.selectedEvents.any((e) =>
|
||||
e.eventId == controller.filteredEvents[i - 1].eventId),
|
||||
timeline: controller.timeline!,
|
||||
nextEvent: i < controller.filteredEvents.length
|
||||
? controller.filteredEvents[i]
|
||||
: null),
|
||||
child: event.isVisibleInGui
|
||||
? Message(event,
|
||||
onSwipe: (direction) =>
|
||||
controller.replyAction(replyTo: event),
|
||||
onInfoTab: controller.showEventInfo,
|
||||
onAvatarTab: (Event event) => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: event.senderFromMemoryOrFallback,
|
||||
outerContext: context,
|
||||
onMention: () => controller.sendController.text +=
|
||||
'${event.senderFromMemoryOrFallback.mention} ',
|
||||
),
|
||||
),
|
||||
onSelect: controller.onSelectMessage,
|
||||
scrollToEventId: (String eventId) =>
|
||||
controller.scrollToEventId(eventId),
|
||||
longPressSelect: controller.selectedEvents.isEmpty,
|
||||
selected: controller.selectedEvents
|
||||
.any((e) => e.eventId == event.eventId),
|
||||
timeline: controller.timeline!,
|
||||
nextEvent: i < controller.timeline!.events.length
|
||||
? controller.timeline!.events[i]
|
||||
: null)
|
||||
: Container(),
|
||||
);
|
||||
},
|
||||
childCount: controller.filteredEvents.length + 2,
|
||||
childCount: controller.timeline!.events.length + 2,
|
||||
findChildIndexCallback: (key) =>
|
||||
controller.findChildIndexCallback(key, thisEventsKeyMap),
|
||||
),
|
||||
|
@ -22,7 +22,6 @@ class Message extends StatelessWidget {
|
||||
final void Function(Event)? onAvatarTab;
|
||||
final void Function(Event)? onInfoTab;
|
||||
final void Function(String)? scrollToEventId;
|
||||
final void Function(String) unfold;
|
||||
final void Function(SwipeDirection) onSwipe;
|
||||
final bool longPressSelect;
|
||||
final bool selected;
|
||||
@ -36,7 +35,6 @@ class Message extends StatelessWidget {
|
||||
this.onAvatarTab,
|
||||
this.scrollToEventId,
|
||||
required this.onSwipe,
|
||||
required this.unfold,
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
Key? key})
|
||||
@ -57,7 +55,7 @@ class Message extends StatelessWidget {
|
||||
if (event.type.startsWith('m.call.')) {
|
||||
return Container();
|
||||
}
|
||||
return StateMessage(event, unfold: unfold);
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
||||
if (event.type == EventTypes.Message &&
|
||||
|
@ -8,9 +8,7 @@ import '../../../config/app_config.dart';
|
||||
|
||||
class StateMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
final void Function(String) unfold;
|
||||
const StateMessage(this.event, {required this.unfold, Key? key})
|
||||
: super(key: key);
|
||||
const StateMessage(this.event, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -25,48 +23,43 @@ class StateMessage extends StatelessWidget {
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: counter != 0 ? () => unfold(event.eventId) : null,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.grey.shade900,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FutureBuilder<String>(
|
||||
future: event
|
||||
.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!)),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).textTheme.bodyText2!.color,
|
||||
decoration: event.redacted
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (counter != 0)
|
||||
Text(
|
||||
L10n.of(context)!.moreEvents(counter),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.grey.shade900,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FutureBuilder<String>(
|
||||
future:
|
||||
event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!)),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).textTheme.bodyText2!.color,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (counter != 0)
|
||||
Text(
|
||||
L10n.of(context)!.moreEvents(counter),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -12,11 +12,7 @@ class SeenByRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final seenByUsers = controller.room!.getSeenByUsers(
|
||||
controller.timeline!,
|
||||
controller.filteredEvents,
|
||||
controller.unfolded,
|
||||
);
|
||||
final seenByUsers = controller.room!.getSeenByUsers(controller.timeline!);
|
||||
const maxAvatars = 7;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@ -28,8 +24,8 @@ class SeenByRow extends StatelessWidget {
|
||||
duration: seenByUsers.isEmpty
|
||||
? const Duration(milliseconds: 0)
|
||||
: const Duration(milliseconds: 300),
|
||||
alignment: controller.filteredEvents.isNotEmpty &&
|
||||
controller.filteredEvents.first.senderId ==
|
||||
alignment: controller.timeline!.events.isNotEmpty &&
|
||||
controller.timeline!.events.first.senderId ==
|
||||
Matrix.of(context).client.userID
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
|
@ -26,8 +26,8 @@ class TypingIndicators extends StatelessWidget {
|
||||
height: typingUsers.isEmpty ? 0 : Avatar.defaultSize + bottomPadding,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.bounceInOut,
|
||||
alignment: controller.filteredEvents.isNotEmpty &&
|
||||
controller.filteredEvents.first.senderId ==
|
||||
alignment: controller.timeline!.events.isNotEmpty &&
|
||||
controller.timeline!.events.first.senderId ==
|
||||
Matrix.of(context).client.userID
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
|
@ -45,6 +45,12 @@ class SettingsChatView extends StatelessWidget {
|
||||
storeKey: SettingKeys.hideUnknownEvents,
|
||||
defaultValue: AppConfig.hideUnknownEvents,
|
||||
),
|
||||
SettingsSwitchListTile.adaptive(
|
||||
title: L10n.of(context)!.hideUnimportantStateEvents,
|
||||
onChanged: (b) => AppConfig.hideUnimportantStateEvents = b,
|
||||
storeKey: SettingKeys.hideUnimportantStateEvents,
|
||||
defaultValue: AppConfig.hideUnimportantStateEvents,
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
SettingsSwitchListTile.adaptive(
|
||||
title: L10n.of(context)!.autoplayImages,
|
||||
|
@ -110,8 +110,6 @@ class StoryPageController extends State<StoryPage> {
|
||||
if (timeline == null || currentEvent == null) return [];
|
||||
return Matrix.of(context).client.getRoomById(roomId)?.getSeenByUsers(
|
||||
timeline,
|
||||
events,
|
||||
{},
|
||||
eventId: currentEvent.eventId,
|
||||
) ??
|
||||
[];
|
||||
|
@ -2,50 +2,39 @@ import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../config/app_config.dart';
|
||||
|
||||
extension FilteredTimelineExtension on Timeline {
|
||||
List<Event> getFilteredEvents({Set<String> unfolded = const {}}) {
|
||||
final filteredEvents = events
|
||||
.where((e) =>
|
||||
// always filter out edit and reaction relationships
|
||||
!{RelationshipTypes.edit, RelationshipTypes.reaction}
|
||||
.contains(e.relationshipType) &&
|
||||
// always filter out m.key.* events
|
||||
!e.type.startsWith('m.key.verification.') &&
|
||||
// event types to hide: redaction and reaction events
|
||||
// if a reaction has been redacted we also want it to be hidden in the timeline
|
||||
!{EventTypes.Reaction, EventTypes.Redaction}.contains(e.type) &&
|
||||
// if we enabled to hide all redacted events, don't show those
|
||||
(!AppConfig.hideRedactedEvents || !e.redacted) &&
|
||||
// if we enabled to hide all unknown events, don't show those
|
||||
(!AppConfig.hideUnknownEvents || e.isEventTypeKnown) &&
|
||||
// remove state events that we don't want to render
|
||||
(e.isState || !AppConfig.hideAllStateEvents))
|
||||
.toList();
|
||||
|
||||
// Fold state events
|
||||
var counter = 0;
|
||||
for (var i = filteredEvents.length - 1; i >= 0; i--) {
|
||||
if (!filteredEvents[i].isState) continue;
|
||||
if (i > 0 &&
|
||||
filteredEvents[i - 1].isState &&
|
||||
!unfolded.contains(filteredEvents[i - 1].eventId)) {
|
||||
counter++;
|
||||
filteredEvents[i].unsigned ??= {};
|
||||
filteredEvents[i].unsigned!['im.fluffychat.collapsed_state_event'] =
|
||||
true;
|
||||
} else {
|
||||
filteredEvents[i].unsigned!['im.fluffychat.collapsed_state_event'] =
|
||||
false;
|
||||
filteredEvents[i]
|
||||
.unsigned!['im.fluffychat.collapsed_state_event_count'] = counter;
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
return filteredEvents;
|
||||
}
|
||||
}
|
||||
|
||||
extension IsStateExtension on Event {
|
||||
bool get isVisibleInGui =>
|
||||
// always filter out edit and reaction relationships
|
||||
!{RelationshipTypes.edit, RelationshipTypes.reaction}
|
||||
.contains(relationshipType) &&
|
||||
// always filter out m.key.* events
|
||||
!type.startsWith('m.key.verification.') &&
|
||||
// event types to hide: redaction and reaction events
|
||||
// if a reaction has been redacted we also want it to be hidden in the timeline
|
||||
!{EventTypes.Reaction, EventTypes.Redaction}.contains(type) &&
|
||||
// if we enabled to hide all redacted events, don't show those
|
||||
(!AppConfig.hideRedactedEvents || !redacted) &&
|
||||
// if we enabled to hide all unknown events, don't show those
|
||||
(!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
// remove state events that we don't want to render
|
||||
(isState || !AppConfig.hideAllStateEvents) &&
|
||||
// hide unimportant state events
|
||||
(!AppConfig.hideUnimportantStateEvents ||
|
||||
!isState ||
|
||||
importantStateEvents.contains(type)) &&
|
||||
// hide member events in public rooms
|
||||
(!AppConfig.hideUnimportantStateEvents ||
|
||||
type != EventTypes.RoomMember ||
|
||||
room.joinRules != JoinRules.public);
|
||||
|
||||
static const Set<String> importantStateEvents = {
|
||||
EventTypes.Encryption,
|
||||
EventTypes.RoomCreate,
|
||||
EventTypes.RoomMember,
|
||||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
};
|
||||
|
||||
bool get isState => !{
|
||||
EventTypes.Message,
|
||||
EventTypes.Sticker,
|
||||
|
@ -5,7 +5,6 @@ import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../config/app_config.dart';
|
||||
import 'date_time_extension.dart';
|
||||
import 'matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
||||
|
||||
extension RoomStatusExtension on Room {
|
||||
CachedPresence? get directChatPresence =>
|
||||
@ -65,14 +64,9 @@ extension RoomStatusExtension on Room {
|
||||
return typingText;
|
||||
}
|
||||
|
||||
List<User> getSeenByUsers(
|
||||
Timeline timeline, List<Event> filteredEvents, Set<String> unfolded,
|
||||
{String? eventId}) {
|
||||
List<User> getSeenByUsers(Timeline timeline, {String? eventId}) {
|
||||
if (timeline.events.isEmpty) return [];
|
||||
|
||||
final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
|
||||
if (filteredEvents.isEmpty) return [];
|
||||
eventId ??= filteredEvents.first.eventId;
|
||||
eventId ??= timeline.events.first.eventId;
|
||||
|
||||
final lastReceipts = <User>{};
|
||||
// now we iterate the timeline events until we hit the first rendered event
|
||||
@ -83,7 +77,7 @@ extension RoomStatusExtension on Room {
|
||||
}
|
||||
}
|
||||
lastReceipts.removeWhere((user) =>
|
||||
user.id == client.userID || user.id == filteredEvents.first.senderId);
|
||||
user.id == client.userID || user.id == timeline.events.first.senderId);
|
||||
return lastReceipts.toList();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user