parent
e14ab3e708
commit
e38307657d
5 changed files with 445 additions and 63 deletions
|
|
@ -8,6 +8,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|||
import "package:ffi/ffi.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:nexus/controllers/settings.dart";
|
||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||
import "package:nexus/models/capabilities.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
|
|
@ -344,9 +345,14 @@ class ClientController extends AsyncNotifier<int> {
|
|||
|
||||
if (event == null) return;
|
||||
|
||||
// Private until settings have loaded, in case the user turned sending off
|
||||
final sendPublic =
|
||||
ref.read(SettingsController.provider).value?.readReceipts ==
|
||||
.showAndSend;
|
||||
|
||||
await _sendCommand("mark_read", {
|
||||
"room_id": room.metadata!.id,
|
||||
"receipt_type": "m.read",
|
||||
"receipt_type": sendPublic ? "m.read" : "m.read.private",
|
||||
"event_id": event.eventId,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
90
lib/controllers/read_receipts.dart
Normal file
90
lib/controllers/read_receipts.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/room_chat.dart";
|
||||
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/models/read_receipt.dart";
|
||||
|
||||
/// Other users' read receipts in a chat, by the event ID of the message to
|
||||
/// show them under: the nearest message at or before what they've read.
|
||||
class ReadReceiptsController(final ChatParams chat)
|
||||
extends Notifier<IMap<String, IList<ReadReceipt>>> {
|
||||
@override
|
||||
IMap<String, IList<ReadReceipt>> build() {
|
||||
final shouldShow = ref.watch(
|
||||
SettingsController.provider.select(
|
||||
(settings) => settings.value?.readReceipts != .neither,
|
||||
),
|
||||
);
|
||||
if (!shouldShow) return const .empty();
|
||||
|
||||
final userId = ref.watch(
|
||||
ClientStateController.provider.select((state) => state?.userId),
|
||||
);
|
||||
// The chat's own timeline, so receipts follow what's actually shown
|
||||
final timeline = ref.watch(
|
||||
RoomChatController.provider(chat)
|
||||
.select((roomChat) => roomChat.value?.timeline),
|
||||
);
|
||||
final room = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => switch (rooms[chat.roomId]) {
|
||||
final room? => (events: room.events, receipts: room.receipts),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (userId == null || timeline == null || room == null) {
|
||||
return const .empty();
|
||||
}
|
||||
|
||||
final events = timeline.map((rowId) => room.events[rowId]).nonNulls;
|
||||
final indexes = IMap.fromEntries(
|
||||
events.indexed.map((entry) => MapEntry(entry.$2.eventId, entry.$1)),
|
||||
);
|
||||
final messages = events.indexed
|
||||
.where((entry) => entry.$2.isShownAsMessage)
|
||||
.toIList();
|
||||
final messageIndexes = messages.map((entry) => entry.$1).toList();
|
||||
|
||||
return room.receipts.values.flattened
|
||||
.where(
|
||||
(receipt) =>
|
||||
receipt.userId != userId &&
|
||||
// Receipts in threads are for the thread, not the timeline
|
||||
receipt.thread == "main",
|
||||
)
|
||||
.map(
|
||||
(receipt) => switch (indexes[receipt.eventId]) {
|
||||
final index? => (
|
||||
receipt: receipt,
|
||||
message: lowerBound(messageIndexes, index + 1) - 1,
|
||||
),
|
||||
null => null,
|
||||
},
|
||||
)
|
||||
.nonNulls
|
||||
.where((entry) => entry.message >= 0)
|
||||
.groupListsBy((entry) => messages[entry.message].$2.eventId)
|
||||
.map(
|
||||
(eventId, entries) => MapEntry(
|
||||
eventId,
|
||||
entries
|
||||
.map((entry) => entry.receipt)
|
||||
.sorted((a, b) => b.timestamp.compareTo(a.timestamp))
|
||||
.toIList(),
|
||||
),
|
||||
)
|
||||
.toIMap();
|
||||
}
|
||||
|
||||
static final provider = NotifierProvider.family
|
||||
.autoDispose<
|
||||
ReadReceiptsController,
|
||||
IMap<String, IList<ReadReceipt>>,
|
||||
ChatParams
|
||||
>(ReadReceiptsController.new);
|
||||
}
|
||||
|
|
@ -156,6 +156,7 @@ class RoomChatController(final ChatParams info)
|
|||
valueMapper: (event) => event,
|
||||
),
|
||||
hasMore: response.hasMore,
|
||||
receipts: response.receipts,
|
||||
timeline: IMap.fromIterable(
|
||||
response.events,
|
||||
keyMapper: (event) => event.timelineRowId,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import "package:collection/collection.dart";
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/read_receipt.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
|
||||
class RoomsController extends Notifier<IMap<String, Room>> {
|
||||
|
|
@ -33,6 +34,26 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
|||
.new(),
|
||||
);
|
||||
|
||||
static IMap<String, IList<ReadReceipt>> mergeReceipts(
|
||||
IMap<String, IList<ReadReceipt>> existing,
|
||||
IMap<String, IList<ReadReceipt>> incoming,
|
||||
) => incoming.isEmpty
|
||||
? existing
|
||||
: existing.values.flattened
|
||||
.followedBy(incoming.values.flattened)
|
||||
.groupListsBy((receipt) => (receipt.userId, receipt.thread))
|
||||
.values
|
||||
.map(
|
||||
(receipts) => receipts.reduce(
|
||||
(newest, receipt) => newest.timestamp.isAfter(receipt.timestamp)
|
||||
? newest
|
||||
: receipt,
|
||||
),
|
||||
)
|
||||
.groupListsBy((receipt) => receipt.eventId)
|
||||
.map((eventId, receipts) => MapEntry(eventId, receipts.toIList()))
|
||||
.toIMap();
|
||||
|
||||
/// Adds events to their rooms, without changing the rooms' timelines.
|
||||
void storeEvents(Iterable<Event> events) => update(
|
||||
.new(
|
||||
|
|
@ -96,15 +117,11 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
|||
timeline: incoming.reset
|
||||
? incoming.timeline
|
||||
: existing.timeline.addAll(incoming.timeline),
|
||||
receipts: incoming.receipts.entries.fold(
|
||||
existing.receipts,
|
||||
(receiptAcc, event) => receiptAcc.add(
|
||||
event.key,
|
||||
(receiptAcc[event.key] ?? .new()).addAll(event.value),
|
||||
),
|
||||
),
|
||||
receipts: mergeReceipts(existing.receipts, incoming.receipts),
|
||||
) ??
|
||||
incoming,
|
||||
incoming.copyWith(
|
||||
receipts: mergeReceipts(const .empty(), incoming.receipts),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import "package:nexus/controllers/unified_push_distributors.dart";
|
|||
import "package:nexus/helpers/extensions/distributor_name.dart";
|
||||
import "package:nexus/models/account_data.dart";
|
||||
import "package:nexus/models/room_order.dart";
|
||||
import "package:nexus/models/settings.dart";
|
||||
import "package:nexus/models/settings_category.dart";
|
||||
import "package:nexus/models/typing_indicators.dart";
|
||||
import "package:nexus/models/show_and_send.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/widgets/settings/dialog_list_tile.dart";
|
||||
import "package:nexus/widgets/reset_encryption_sheet.dart";
|
||||
|
|
@ -32,6 +33,30 @@ class SettingsSectionsController
|
|||
Future<IMap<String, IList<SettingsCategory>>> build() async {
|
||||
final settings = await ref.watch(SettingsController.provider.future);
|
||||
|
||||
/// A tile for a [ShowAndSend] setting, saving the settings from [update].
|
||||
Widget showAndSendTile(
|
||||
String title,
|
||||
String description,
|
||||
IconData icon, {
|
||||
required ShowAndSend value,
|
||||
required Settings Function(ShowAndSend value) update,
|
||||
}) => DialogListTile<ShowAndSend>(
|
||||
icon: Icon(icon),
|
||||
title: title,
|
||||
subtitle: Text(description),
|
||||
value: value,
|
||||
options: ShowAndSend.values,
|
||||
getName: (option) => switch (option) {
|
||||
.showAndSend => "Show and send",
|
||||
.show => "Only show",
|
||||
.neither => "Neither",
|
||||
},
|
||||
onChanged: (value) => ref
|
||||
.read(SettingsController.provider.notifier)
|
||||
.set(update(value))
|
||||
.onError(showError),
|
||||
);
|
||||
|
||||
return .new({
|
||||
"General": .new([
|
||||
.new(
|
||||
|
|
@ -192,23 +217,25 @@ class SettingsSectionsController
|
|||
title: "Typing Indicators",
|
||||
description: "Show when others are typing, and let others see when you're typing.",
|
||||
icon: Icons.keyboard,
|
||||
builder: (title, description, icon) =>
|
||||
DialogListTile<TypingIndicators>(
|
||||
icon: Icon(icon),
|
||||
title: title,
|
||||
subtitle: Text(description),
|
||||
value: settings.typingIndicators,
|
||||
options: TypingIndicators.values,
|
||||
getName: (option) => switch (option) {
|
||||
.showAndSend => "Show and send",
|
||||
.show => "Only show",
|
||||
.neither => "Neither",
|
||||
},
|
||||
onChanged: (value) => ref
|
||||
.read(SettingsController.provider.notifier)
|
||||
.set(settings.copyWith(typingIndicators: value))
|
||||
.onError(showError),
|
||||
),
|
||||
builder: (title, description, icon) => showAndSendTile(
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
value: settings.typingIndicators,
|
||||
update: (value) => settings.copyWith(typingIndicators: value),
|
||||
),
|
||||
),
|
||||
.new(
|
||||
title: "Read Receipts",
|
||||
description: "Show how far others have read, and let others see what you've read. When not sent, read receipts are still sent privately, so your other devices know what you've read.",
|
||||
icon: Icons.done_all,
|
||||
builder: (title, description, icon) => showAndSendTile(
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
value: settings.readReceipts,
|
||||
update: (value) => settings.copyWith(readReceipts: value),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue