add typing indicators

Fixes #92
This commit is contained in:
Henry Hiles 2026-10-21 15:07:02 -04:00
commit 0fcbc275cd
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 326 additions and 1 deletions

View file

@ -320,6 +320,11 @@ class ClientController extends AsyncNotifier<int> {
Future<void> logout() => _sendCommand("logout");
Future<void> setTyping(String roomId, Duration timeout) => _sendCommand(
"set_typing",
{"room_id": roomId, "timeout": timeout.inMilliseconds},
);
Future<void> markRead(Room room) async {
if (room.timeline.isEmpty || room.metadata == null) return;
final eventRowId = room.timeline[room.timeline.keys.reduce(max)];

View file

@ -15,6 +15,7 @@ import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/space_edges.dart";
import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/controllers/typing.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart";
import "package:ffi/ffi.dart";
@ -63,6 +64,10 @@ class GomuksListenerController extends AsyncNotifier<void> {
event.roomId: .new(events: .new({event.rowId: event})),
}),
);
case "typing":
ref
.read(TypingController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
case "events_decrypted":
final decrypted = EventsDecrypted.fromJson(decodedMuksEvent);
final metadata = ref
@ -96,6 +101,7 @@ class GomuksListenerController extends AsyncNotifier<void> {
ref.invalidate(roomProvider);
ref.invalidate(accountDataProvider);
ref.invalidate(invitesProvider);
ref.invalidate(TypingController.provider);
}
ref
@ -153,6 +159,7 @@ class GomuksListenerController extends AsyncNotifier<void> {
if (paused) {
GomuksPauseSync(handle);
} else {
ref.invalidate(TypingController.provider);
GomuksResumeSync(handle);
}
}, fireImmediately: true);

View file

@ -0,0 +1,34 @@
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/settings.dart";
import "package:nexus/controllers/typing.dart";
/// The other users typing in a room, or none if typing indicators are hidden
/// in settings.
class RoomTypingController(final String roomId)
extends Notifier<IList<String>> {
@override
IList<String> build() {
final shouldShow = ref.watch(
SettingsController.provider.select(
(settings) => settings.value?.typingIndicators != .neither,
),
);
if (!shouldShow) return const .empty();
final userId = ref.watch(
ClientStateController.provider.select((state) => state?.userId),
);
return ref
.watch(TypingController.provider.select((typing) => typing[roomId]))
?.remove(userId ?? "") ??
const .empty();
}
static final provider = NotifierProvider.family
.autoDispose<RoomTypingController, IList<String>, String>(
RoomTypingController.new,
);
}

View file

@ -20,6 +20,7 @@ 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_category.dart";
import "package:nexus/models/typing_indicators.dart";
import "package:nexus/main.dart";
import "package:nexus/widgets/settings/dialog_list_tile.dart";
import "package:nexus/widgets/reset_encryption_sheet.dart";
@ -183,6 +184,34 @@ class SettingsSectionsController
),
]),
),
.new(
title: "Privacy",
icon: Icons.privacy_tip,
settings: .new([
.new(
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),
),
),
]),
),
]),
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
"Account": .new([

View file

@ -0,0 +1,15 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/typing.dart";
class TypingController extends Notifier<IMap<String, IList<String>>> {
@override
IMap<String, IList<String>> build() => .new();
void set(Typing typing) => state = state.add(typing.roomId, typing.userIds);
static final provider =
NotifierProvider<TypingController, IMap<String, IList<String>>>(
TypingController.new,
);
}