mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-26 16:12:37 +01:00
Merge branch 'krille/readreceipts' into 'main'
fix: Read receipts and filtered events See merge request famedly/fluffychat!317
This commit is contained in:
commit
a7549647d1
41
lib/utils/filtered_timeline_extension.dart
Normal file
41
lib/utils/filtered_timeline_extension.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
|
||||||
|
import '../app_config.dart';
|
||||||
|
|
||||||
|
extension FilteredTimelineExtension on Timeline {
|
||||||
|
List<Event> getFilteredEvents({bool collapseRoomCreate = true}) {
|
||||||
|
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
|
||||||
|
(!{EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted}
|
||||||
|
.contains(e.type) ||
|
||||||
|
!AppConfig.hideAllStateEvents))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Hide state events from the room creater right after the room created event
|
||||||
|
if (collapseRoomCreate &&
|
||||||
|
filteredEvents[filteredEvents.length - 1].type ==
|
||||||
|
EventTypes.RoomCreate) {
|
||||||
|
while (filteredEvents.length >= 3 &&
|
||||||
|
filteredEvents[filteredEvents.length - 2].senderId ==
|
||||||
|
filteredEvents[filteredEvents.length - 1].senderId &&
|
||||||
|
![EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted]
|
||||||
|
.contains(filteredEvents[filteredEvents.length - 2].type)) {
|
||||||
|
filteredEvents.removeAt(filteredEvents.length - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredEvents;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'filtered_timeline_extension.dart';
|
||||||
import '../app_config.dart';
|
import '../app_config.dart';
|
||||||
import 'date_time_extension.dart';
|
import 'date_time_extension.dart';
|
||||||
|
|
||||||
@ -61,4 +61,36 @@ extension RoomStatusExtension on Room {
|
|||||||
}
|
}
|
||||||
return typingText;
|
return typingText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getLocalizedSeenByText(
|
||||||
|
BuildContext context, Timeline timeline, List<Event> filteredEvents) {
|
||||||
|
var seenByText = '';
|
||||||
|
if (timeline.events.isNotEmpty) {
|
||||||
|
final filteredEvents =
|
||||||
|
timeline.getFilteredEvents(collapseRoomCreate: false);
|
||||||
|
final lastReceipts = <User>{};
|
||||||
|
// now we iterate the timeline events until we hit the first rendered event
|
||||||
|
for (final event in timeline.events) {
|
||||||
|
lastReceipts.addAll(event.receipts.map((r) => r.user));
|
||||||
|
if (event.eventId == filteredEvents.first.eventId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastReceipts.removeWhere((user) =>
|
||||||
|
user.id == client.userID || user.id == filteredEvents.first.senderId);
|
||||||
|
if (lastReceipts.length == 1) {
|
||||||
|
seenByText =
|
||||||
|
L10n.of(context).seenByUser(lastReceipts.first.calcDisplayname());
|
||||||
|
} else if (lastReceipts.length == 2) {
|
||||||
|
seenByText = seenByText = L10n.of(context).seenByUserAndUser(
|
||||||
|
lastReceipts.first.calcDisplayname(),
|
||||||
|
lastReceipts.last.calcDisplayname());
|
||||||
|
} else if (lastReceipts.length > 2) {
|
||||||
|
seenByText = L10n.of(context).seenByUserAndCountOthers(
|
||||||
|
lastReceipts.first.calcDisplayname(),
|
||||||
|
(lastReceipts.length - 1).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seenByText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
import '../components/dialogs/send_file_dialog.dart';
|
import '../components/dialogs/send_file_dialog.dart';
|
||||||
import '../components/input_bar.dart';
|
import '../components/input_bar.dart';
|
||||||
import '../app_config.dart';
|
import '../utils/filtered_timeline_extension.dart';
|
||||||
import '../utils/matrix_file_extension.dart';
|
import '../utils/matrix_file_extension.dart';
|
||||||
import 'chat_details.dart';
|
import 'chat_details.dart';
|
||||||
import 'chat_list.dart';
|
import 'chat_list.dart';
|
||||||
@ -88,6 +88,8 @@ class _ChatState extends State<_Chat> {
|
|||||||
|
|
||||||
List<Event> selectedEvents = [];
|
List<Event> selectedEvents = [];
|
||||||
|
|
||||||
|
List<Event> filteredEvents;
|
||||||
|
|
||||||
bool _collapseRoomCreate = true;
|
bool _collapseRoomCreate = true;
|
||||||
|
|
||||||
Event replyEvent;
|
Event replyEvent;
|
||||||
@ -151,39 +153,14 @@ class _ChatState extends State<_Chat> {
|
|||||||
|
|
||||||
void updateView() {
|
void updateView() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
var seenByText = '';
|
() {
|
||||||
if (timeline.events.isNotEmpty) {
|
filteredEvents =
|
||||||
final filteredEvents = getFilteredEvents();
|
timeline.getFilteredEvents(collapseRoomCreate: _collapseRoomCreate);
|
||||||
final lastReceipts = <User>{};
|
|
||||||
// now we iterate the timeline events until we hit the first rendered event
|
|
||||||
for (final event in timeline.events) {
|
|
||||||
lastReceipts.addAll(event.receipts.map((r) => r.user));
|
|
||||||
if (event.eventId == filteredEvents.first.eventId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastReceipts.removeWhere((user) =>
|
|
||||||
user.id == room.client.userID ||
|
|
||||||
user.id == filteredEvents.first.senderId);
|
|
||||||
if (lastReceipts.length == 1) {
|
|
||||||
seenByText =
|
seenByText =
|
||||||
L10n.of(context).seenByUser(lastReceipts.first.calcDisplayname());
|
room.getLocalizedSeenByText(context, timeline, filteredEvents);
|
||||||
} else if (lastReceipts.length == 2) {
|
},
|
||||||
seenByText = seenByText = L10n.of(context).seenByUserAndUser(
|
);
|
||||||
lastReceipts.first.calcDisplayname(),
|
|
||||||
lastReceipts.last.calcDisplayname());
|
|
||||||
} else if (lastReceipts.length > 2) {
|
|
||||||
seenByText = L10n.of(context).seenByUserAndCountOthers(
|
|
||||||
lastReceipts.first.calcDisplayname(),
|
|
||||||
(lastReceipts.length - 1).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (timeline != null) {
|
|
||||||
setState(() {
|
|
||||||
this.seenByText = seenByText;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> getTimeline(BuildContext context) async {
|
Future<bool> getTimeline(BuildContext context) async {
|
||||||
@ -385,8 +362,7 @@ class _ChatState extends State<_Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToEventId(String eventId, {BuildContext context}) async {
|
void _scrollToEventId(String eventId, {BuildContext context}) async {
|
||||||
var eventIndex =
|
var eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||||
getFilteredEvents().indexWhere((e) => e.eventId == eventId);
|
|
||||||
if (eventIndex == -1) {
|
if (eventIndex == -1) {
|
||||||
// event id not found...maybe we can fetch it?
|
// event id not found...maybe we can fetch it?
|
||||||
// the try...finally is here to start and close the loading dialog reliably
|
// the try...finally is here to start and close the loading dialog reliably
|
||||||
@ -420,8 +396,7 @@ class _ChatState extends State<_Chat> {
|
|||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
eventIndex =
|
eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId);
|
||||||
getFilteredEvents().indexWhere((e) => e.eventId == eventId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
@ -438,42 +413,6 @@ class _ChatState extends State<_Chat> {
|
|||||||
_updateScrollController();
|
_updateScrollController();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Event> getFilteredEvents() {
|
|
||||||
final filteredEvents = timeline.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
|
|
||||||
(!{EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted}
|
|
||||||
.contains(e.type) ||
|
|
||||||
!AppConfig.hideAllStateEvents))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Hide state events from the room creater right after the room created event
|
|
||||||
if (_collapseRoomCreate &&
|
|
||||||
filteredEvents[filteredEvents.length - 1].type ==
|
|
||||||
EventTypes.RoomCreate) {
|
|
||||||
while (filteredEvents.length >= 3 &&
|
|
||||||
filteredEvents[filteredEvents.length - 2].senderId ==
|
|
||||||
filteredEvents[filteredEvents.length - 1].senderId &&
|
|
||||||
![EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted]
|
|
||||||
.contains(filteredEvents[filteredEvents.length - 2].type)) {
|
|
||||||
filteredEvents.removeAt(filteredEvents.length - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _pickEmojiAction(
|
void _pickEmojiAction(
|
||||||
BuildContext context, Iterable<Event> allReactionEvents) async {
|
BuildContext context, Iterable<Event> allReactionEvents) async {
|
||||||
final emoji = await showModalBottomSheet(
|
final emoji = await showModalBottomSheet(
|
||||||
@ -677,11 +616,13 @@ class _ChatState extends State<_Chat> {
|
|||||||
timeline != null &&
|
timeline != null &&
|
||||||
timeline.events.isNotEmpty &&
|
timeline.events.isNotEmpty &&
|
||||||
Matrix.of(context).webHasFocus) {
|
Matrix.of(context).webHasFocus) {
|
||||||
room.sendReadMarker(timeline.events.first.eventId);
|
room.sendReadMarker(
|
||||||
|
timeline.events.first.eventId,
|
||||||
|
readReceiptLocationEventId:
|
||||||
|
timeline.events.first.eventId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final filteredEvents = getFilteredEvents();
|
|
||||||
|
|
||||||
// create a map of eventId --> index to greatly improve performance of
|
// create a map of eventId --> index to greatly improve performance of
|
||||||
// ListView's findChildIndexCallback
|
// ListView's findChildIndexCallback
|
||||||
final thisEventsKeyMap = <String, int>{};
|
final thisEventsKeyMap = <String, int>{};
|
||||||
|
Loading…
Reference in New Issue
Block a user