parent
47645ea327
commit
7f7bdbf525
42 changed files with 1477 additions and 577 deletions
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
|||
Subproject commit 9e6254b703a841d1e9ee64dbaf90d5a1b798696e
|
||||
Subproject commit 46ab9e9afa54c12f585cbbf559c395b9c2dcb545
|
||||
|
|
@ -4,9 +4,10 @@ import "package:nexus/controllers/client.dart";
|
|||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/composer_params.dart";
|
||||
import "package:path/path.dart";
|
||||
|
||||
class AttachmentController(final String roomId)
|
||||
class AttachmentController(final ComposerParams composer)
|
||||
extends Notifier<(String, MessageContent?)?> {
|
||||
@override
|
||||
Null build() => null;
|
||||
|
|
@ -18,7 +19,10 @@ class AttachmentController(final String roomId)
|
|||
final isEncrypted = ref.read(
|
||||
RoomsController.provider.select(
|
||||
(value) =>
|
||||
value[roomId]?.state[EventType.encryption.type]?.isNotEmpty == true,
|
||||
value[composer.roomId]
|
||||
?.state[EventType.encryption.type]
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -32,7 +36,9 @@ class AttachmentController(final String roomId)
|
|||
void set((String, MessageContent?)? attachment) => state = attachment;
|
||||
|
||||
static final provider = NotifierProvider.family
|
||||
.autoDispose<AttachmentController, (String, MessageContent?)?, String>(
|
||||
AttachmentController.new,
|
||||
);
|
||||
.autoDispose<
|
||||
AttachmentController,
|
||||
(String, MessageContent?)?,
|
||||
ComposerParams
|
||||
>(AttachmentController.new);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,17 @@ import "package:nexus/helpers/extensions/get_localpart.dart";
|
|||
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||
import "package:nexus/models/bot_argument_suggestion.dart";
|
||||
import "package:nexus/models/bot_command_draft.dart";
|
||||
import "package:nexus/models/composer_params.dart";
|
||||
import "package:nexus/models/content/bot_command_description.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
|
||||
class BotArgumentSuggestionsController(final String roomId)
|
||||
class BotArgumentSuggestionsController(final ComposerParams composer)
|
||||
extends Notifier<IList<BotArgumentSuggestion>> {
|
||||
@override
|
||||
IList<BotArgumentSuggestion> build() {
|
||||
final draft = ref.watch(BotCommandDraftController.provider(roomId));
|
||||
final draft = ref.watch(BotCommandDraftController.provider(composer));
|
||||
final (parameter, text) = switch (draft) {
|
||||
BotCommandDraft(focusedParameter: final parameter?, :final texts) => (
|
||||
parameter,
|
||||
|
|
@ -38,7 +39,7 @@ class BotArgumentSuggestionsController(final String roomId)
|
|||
? ref
|
||||
.watch(
|
||||
MembersByStatusController.provider(
|
||||
.new(roomId: roomId, status: .join),
|
||||
.new(roomId: composer.roomId, status: .join),
|
||||
),
|
||||
)
|
||||
.value ??
|
||||
|
|
@ -102,7 +103,7 @@ class BotArgumentSuggestionsController(final String roomId)
|
|||
}
|
||||
|
||||
void pick(BotArgumentSuggestion suggestion) => ref
|
||||
.read(BotCommandDraftController.provider(roomId).notifier)
|
||||
.read(BotCommandDraftController.provider(composer).notifier)
|
||||
.pick(
|
||||
suggestion,
|
||||
via: switch (suggestion.room) {
|
||||
|
|
@ -112,7 +113,7 @@ class BotArgumentSuggestionsController(final String roomId)
|
|||
);
|
||||
|
||||
bool pickTop() {
|
||||
final draft = ref.read(BotCommandDraftController.provider(roomId));
|
||||
final draft = ref.read(BotCommandDraftController.provider(composer));
|
||||
if (state.firstOrNull case final suggestion?
|
||||
when draft != null && !draft.arguments.containsKey(draft.focused)) {
|
||||
pick(suggestion);
|
||||
|
|
@ -125,6 +126,6 @@ class BotArgumentSuggestionsController(final String roomId)
|
|||
.family<
|
||||
BotArgumentSuggestionsController,
|
||||
IList<BotArgumentSuggestion>,
|
||||
String
|
||||
ComposerParams
|
||||
>(BotArgumentSuggestionsController.new);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
|||
import "package:nexus/models/bot_argument_suggestion.dart";
|
||||
import "package:nexus/models/bot_command.dart";
|
||||
import "package:nexus/models/bot_command_draft.dart";
|
||||
import "package:nexus/models/composer_params.dart";
|
||||
import "package:nexus/models/content/bot_command_description.dart";
|
||||
|
||||
class BotCommandDraftController(final String roomId)
|
||||
class BotCommandDraftController(final ComposerParams composer)
|
||||
extends Notifier<BotCommandDraft?> {
|
||||
@override
|
||||
BotCommandDraft? build() => null;
|
||||
|
|
@ -14,7 +15,7 @@ class BotCommandDraftController(final String roomId)
|
|||
void start(BotCommand command, {String text = ""}) {
|
||||
state = BotCommandDraft(
|
||||
command: command,
|
||||
roomId: roomId,
|
||||
composer: composer,
|
||||
texts: .fromEntries(
|
||||
command.description.parameters
|
||||
.where((parameter) => !parameter.optional)
|
||||
|
|
@ -97,7 +98,7 @@ class BotCommandDraftController(final String roomId)
|
|||
void clear() => state = null;
|
||||
|
||||
static final provider = NotifierProvider.autoDispose
|
||||
.family<BotCommandDraftController, BotCommandDraft?, String>(
|
||||
.family<BotCommandDraftController, BotCommandDraft?, ComposerParams>(
|
||||
BotCommandDraftController.new,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import "package:nexus/controllers/active_account.dart";
|
|||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/settings.dart";
|
||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||
import "package:nexus/helpers/extensions/thread_events.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/content/bot_command_description.dart";
|
||||
import "package:nexus/models/account.dart";
|
||||
|
|
@ -37,6 +38,7 @@ import "package:nexus/models/requests/get_room_state.dart";
|
|||
import "package:nexus/models/requests/get_specific_room_state.dart";
|
||||
import "package:nexus/models/requests/get_space_hierarchy.dart";
|
||||
import "package:nexus/models/requests/get_room_directory.dart";
|
||||
import "package:nexus/models/requests/get_threads.dart";
|
||||
import "package:nexus/models/requests/join_room.dart";
|
||||
import "package:nexus/models/profile_response.dart";
|
||||
import "package:nexus/models/push_notification.dart";
|
||||
|
|
@ -367,6 +369,9 @@ class ClientController(final String? accountId) extends AsyncNotifier<int> {
|
|||
Future<Paginate> paginate(PaginateRequest request) async =>
|
||||
.fromJson(await _sendCommand("paginate", request.toJson()));
|
||||
|
||||
Future<PaginateManual> getThreads(GetThreadsRequest request) async =>
|
||||
.fromJson(await _sendCommand("get_threads", request.toJson()));
|
||||
|
||||
Future<PaginateManual> paginateManual(PaginateManualRequest request) async =>
|
||||
.fromJson(await _sendCommand("paginate_manual", request.toJson()));
|
||||
|
||||
|
|
@ -438,10 +443,16 @@ class ClientController(final String? accountId) extends AsyncNotifier<int> {
|
|||
{"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)];
|
||||
final event = eventRowId == null ? null : room.events[eventRowId];
|
||||
Future<void> markRead(Room room, {String? threadRoot}) async {
|
||||
if (room.metadata == null) return;
|
||||
final event = switch (threadRoot) {
|
||||
final threadRoot? => room.latestThreadEvent(threadRoot),
|
||||
null when room.timeline.isEmpty => null,
|
||||
null => switch (room.timeline[room.timeline.keys.reduce(max)]) {
|
||||
final rowId? => room.events[rowId],
|
||||
null => null,
|
||||
},
|
||||
};
|
||||
|
||||
if (event == null) return;
|
||||
|
||||
|
|
@ -454,6 +465,7 @@ class ClientController(final String? accountId) extends AsyncNotifier<int> {
|
|||
"room_id": room.metadata!.id,
|
||||
"receipt_type": sendPublic ? "m.read" : "m.read.private",
|
||||
"event_id": event.eventId,
|
||||
"thread_id": ?threadRoot,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/requests/get_event.dart";
|
||||
|
||||
/// The row ID of an event, fetching it if it isn't stored yet.
|
||||
class EventController(final GetEventRequest request)
|
||||
extends AsyncNotifier<Event?> {
|
||||
extends AsyncNotifier<int?> {
|
||||
@override
|
||||
Future<Event?> build() async {
|
||||
final event = ref.watch(
|
||||
RoomsController.provider.select((rooms) {
|
||||
final room = rooms[request.roomId];
|
||||
|
||||
return switch (room?.eventIds[request.eventId]) {
|
||||
final rowId? => room?.events[rowId],
|
||||
null => null,
|
||||
};
|
||||
}),
|
||||
Future<int?> build() async {
|
||||
final rowId = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => rooms[request.roomId]?.eventIds[request.eventId],
|
||||
),
|
||||
);
|
||||
|
||||
if (event != null) return event;
|
||||
if (rowId != null) return rowId;
|
||||
|
||||
final link = ref.keepAlive();
|
||||
final fetchedEvent = await ref
|
||||
|
|
@ -44,11 +39,9 @@ class EventController(final GetEventRequest request)
|
|||
);
|
||||
}
|
||||
|
||||
return fetchedEvent;
|
||||
return fetchedEvent?.rowId;
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.family
|
||||
.autoDispose<EventController, Event?, GetEventRequest>(
|
||||
EventController.new,
|
||||
);
|
||||
.autoDispose<EventController, int?, GetEventRequest>(EventController.new);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class PinnedEventsController(final String roomId)
|
|||
pinIds.map(
|
||||
(eventId) => ref.watch(
|
||||
EventController.provider(.new(eventId: eventId, roomId: roomId))
|
||||
.selectAsync((data) => data?.rowId),
|
||||
.future,
|
||||
),
|
||||
),
|
||||
)).nonNulls.toIList();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ class ReadReceiptsController(final ChatParams chat)
|
|||
final room = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => switch (rooms[chat.roomId]) {
|
||||
final room? => (events: room.events, receipts: room.receipts),
|
||||
final room? => (
|
||||
events: room.events,
|
||||
eventIds: room.eventIds,
|
||||
receipts: room.receipts,
|
||||
timeline: room.timeline,
|
||||
),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
|
|
@ -50,15 +55,48 @@ class ReadReceiptsController(final ChatParams chat)
|
|||
.toIList();
|
||||
final messageIndexes = messages.map((entry) => entry.$1).toList();
|
||||
|
||||
final positions = IMap.fromEntries(
|
||||
room.timeline.entries
|
||||
.map(
|
||||
(entry) => switch (entry.value) {
|
||||
final rowId? => MapEntry(rowId, entry.key),
|
||||
null => null,
|
||||
},
|
||||
)
|
||||
.nonNulls,
|
||||
);
|
||||
final placed = events.indexed
|
||||
.map(
|
||||
(entry) => switch (positions[entry.$2.rowId]) {
|
||||
final position? => (index: entry.$1, position: position),
|
||||
null => null,
|
||||
},
|
||||
)
|
||||
.nonNulls
|
||||
.toList();
|
||||
final placedPositions = placed.map((entry) => entry.position).toList();
|
||||
|
||||
int? indexOf(String eventId) {
|
||||
if (indexes[eventId] case final index?) return index;
|
||||
|
||||
final position = switch (room.eventIds[eventId]) {
|
||||
final rowId? => positions[rowId],
|
||||
null => null,
|
||||
};
|
||||
if (position == null) return null;
|
||||
|
||||
final before = lowerBound(placedPositions, position + 1) - 1;
|
||||
return before < 0 ? null : placed[before].index;
|
||||
}
|
||||
|
||||
return room.receipts.values.flattened
|
||||
.where(
|
||||
(receipt) =>
|
||||
receipt.userId != userId &&
|
||||
// Receipts in threads are for the thread, not the timeline
|
||||
receipt.thread == "main",
|
||||
receipt.thread == (chat.threadRoot ?? "main"),
|
||||
)
|
||||
.map(
|
||||
(receipt) => switch (indexes[receipt.eventId]) {
|
||||
(receipt) => switch (indexOf(receipt.eventId)) {
|
||||
final index? => (
|
||||
receipt: receipt,
|
||||
message: lowerBound(messageIndexes, index + 1) - 1,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,14 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
|||
import "package:fluttertagger/fluttertagger.dart";
|
||||
import "package:nexus/controllers/attachment.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/event.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/controllers/thread_replies.dart";
|
||||
import "package:nexus/helpers/extensions/is_media.dart";
|
||||
import "package:nexus/helpers/extensions/send_failed.dart";
|
||||
import "package:nexus/helpers/extensions/thread_events.dart";
|
||||
import "package:nexus/models/bot_command_draft.dart";
|
||||
import "package:nexus/models/composer_params.dart";
|
||||
import "package:nexus/models/content/bot_command.dart";
|
||||
import "package:nexus/models/content/content.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
|
|
@ -21,6 +25,7 @@ import "package:nexus/models/content/reaction.dart";
|
|||
import "package:nexus/models/content/sticker.dart";
|
||||
import "package:nexus/models/direction.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/paginate_manual.dart";
|
||||
import "package:nexus/models/requests/redact_event.dart";
|
||||
import "package:nexus/models/relation_type.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
|
|
@ -28,7 +33,11 @@ import "package:nexus/models/requests/send_message.dart";
|
|||
import "package:nexus/models/room.dart";
|
||||
import "package:nexus/models/room_chat.dart";
|
||||
|
||||
typedef ChatParams = ({String roomId, int? contextualEvent});
|
||||
typedef ChatParams = ({
|
||||
String roomId,
|
||||
int? contextualEvent,
|
||||
String? threadRoot,
|
||||
});
|
||||
|
||||
class RoomChatController(final ChatParams info)
|
||||
extends AsyncNotifier<RoomChat?> {
|
||||
|
|
@ -47,52 +56,173 @@ class RoomChatController(final ChatParams info)
|
|||
ref.read(RoomsController.provider.notifier).addState(info.roomId, state);
|
||||
}
|
||||
|
||||
final timeline = room.timeline
|
||||
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||
.map((e) => e.value)
|
||||
.toIList()
|
||||
.addAll(room.clientSticky)
|
||||
.nonNulls
|
||||
.toIList();
|
||||
if (info.contextualEvent == null ||
|
||||
timeline.contains(info.contextualEvent)) {
|
||||
ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) =>
|
||||
(rooms[info.roomId]?.timeline, rooms[info.roomId]?.clientSticky),
|
||||
),
|
||||
if (info.threadRoot case final threadRoot?
|
||||
when info.contextualEvent == null) {
|
||||
final nextBatch = await ref.watch(
|
||||
ThreadRepliesController.provider((
|
||||
roomId: info.roomId,
|
||||
threadRoot: threadRoot,
|
||||
)).future,
|
||||
);
|
||||
|
||||
return .new(
|
||||
timeline: timeline,
|
||||
hasMoreBackward: room.hasMore,
|
||||
timeline: ref.watch(RoomsController.provider.select(threadTimeline)),
|
||||
hasMoreBackward: nextBatch != null,
|
||||
hasMoreForward: false,
|
||||
);
|
||||
} else {
|
||||
final context = await client.getEventContext(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
eventId: room.events[info.contextualEvent!]!.eventId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// `before` is in reverse chronological order
|
||||
final events = context.before.reversed
|
||||
if (info.threadRoot == null) {
|
||||
final timeline = room.timeline
|
||||
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||
.map((e) => e.value)
|
||||
.toIList()
|
||||
.add(context.event)
|
||||
.addAll(context.after);
|
||||
.addAll(room.clientSticky)
|
||||
.nonNulls
|
||||
// Threads are shown from their root instead
|
||||
.where((rowId) => room.events[rowId]?.threadRoot == null)
|
||||
.toIList();
|
||||
|
||||
if (info.contextualEvent == null ||
|
||||
timeline.contains(info.contextualEvent)) {
|
||||
ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => (
|
||||
rooms[info.roomId]?.timeline,
|
||||
rooms[info.roomId]?.clientSticky,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return .new(
|
||||
timeline: timeline,
|
||||
hasMoreBackward: room.hasMore,
|
||||
hasMoreForward: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final context = await client.getEventContext(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
eventId: room.events[info.contextualEvent!]!.eventId,
|
||||
),
|
||||
);
|
||||
|
||||
final contextEvents = context.before.reversed
|
||||
.toIList()
|
||||
.add(context.event)
|
||||
.addAll(context.after);
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(contextEvents.addAll(context.relatedEvents));
|
||||
|
||||
if (info.threadRoot case final threadRoot?) {
|
||||
final (older, newer) = await (
|
||||
client.paginateManual(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
threadRoot: threadRoot,
|
||||
since: context.start,
|
||||
direction: .backward,
|
||||
),
|
||||
),
|
||||
client.paginateManual(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
threadRoot: threadRoot,
|
||||
since: context.end,
|
||||
direction: .forward,
|
||||
),
|
||||
),
|
||||
).wait;
|
||||
final rootRowId = await threadRootBefore(older);
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(events.addAll(context.relatedEvents));
|
||||
.storeEvents(
|
||||
older.events
|
||||
.addAll(older.relatedEvents)
|
||||
.addAll(newer.events)
|
||||
.addAll(newer.relatedEvents),
|
||||
);
|
||||
|
||||
return .new(
|
||||
timeline: .new(events.map((element) => element.rowId)),
|
||||
hasMoreBackward: true,
|
||||
hasMoreForward: true,
|
||||
historicalData: .new(start: context.start, end: context.end),
|
||||
timeline: .new([?rootRowId])
|
||||
.addAll(
|
||||
// Paginating backwards gives the newest first
|
||||
older.events.reversed
|
||||
.addAll(
|
||||
contextEvents.where(
|
||||
(event) => event.threadRoot == threadRoot,
|
||||
),
|
||||
)
|
||||
.addAll(newer.events)
|
||||
.map((event) => event.rowId),
|
||||
)
|
||||
.removeDuplicates(),
|
||||
hasMoreBackward: older.nextBatch != null,
|
||||
hasMoreForward: newer.nextBatch != null,
|
||||
historicalData: .new(
|
||||
start: older.nextBatch ?? "",
|
||||
end: newer.nextBatch ?? "",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return .new(
|
||||
timeline: .new(
|
||||
contextEvents
|
||||
.where((event) => event.threadRoot == null)
|
||||
.map((event) => event.rowId),
|
||||
),
|
||||
hasMoreBackward: true,
|
||||
hasMoreForward: true,
|
||||
historicalData: .new(start: context.start, end: context.end),
|
||||
);
|
||||
}
|
||||
|
||||
/// The thread's root, which comes before all of its events, if paginating
|
||||
/// backwards to [older] reached the start of the thread.
|
||||
Future<int?> threadRootBefore(PaginateManual older) async =>
|
||||
switch (info.threadRoot) {
|
||||
final threadRoot? when older.nextBatch == null => await ref.read(
|
||||
EventController.provider(
|
||||
.new(roomId: info.roomId, eventId: threadRoot),
|
||||
).future,
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// The thread's root, then its events, if this is a thread's chat.
|
||||
IList<int> threadTimeline(IMap<String, Room> rooms) =>
|
||||
switch ((rooms[info.roomId], info.threadRoot)) {
|
||||
(final room?, final threadRoot?) => .new([
|
||||
?room.eventIds[threadRoot],
|
||||
...room.threadEvents(threadRoot).map((event) => event.rowId),
|
||||
]),
|
||||
_ => const .empty(),
|
||||
};
|
||||
|
||||
Relation? replyRelation(Event? replied) =>
|
||||
switch (info.threadRoot ?? replied?.threadRoot) {
|
||||
null =>
|
||||
replied == null
|
||||
? null
|
||||
: .new(eventId: replied.eventId, relationType: .reply),
|
||||
final threadRoot => .new(
|
||||
eventId:
|
||||
replied?.eventId ??
|
||||
ref
|
||||
.read(RoomsController.provider)[info.roomId]
|
||||
?.latestThreadEvent(threadRoot)
|
||||
?.eventId ??
|
||||
threadRoot,
|
||||
relationType: .reply,
|
||||
threadRoot: threadRoot,
|
||||
isFallingBack: replied == null,
|
||||
),
|
||||
};
|
||||
|
||||
Future<void> deleteMessage(Event event, {String? reason}) => ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.redactEvent(
|
||||
|
|
@ -133,97 +263,128 @@ class RoomChatController(final ChatParams info)
|
|||
return;
|
||||
}
|
||||
|
||||
if ((info.threadRoot, chat) case (final threadRoot?, final chat?)
|
||||
when chat.historicalData == null) {
|
||||
return ref
|
||||
.read(
|
||||
ThreadRepliesController.provider((
|
||||
roomId: info.roomId,
|
||||
threadRoot: threadRoot,
|
||||
)).notifier,
|
||||
)
|
||||
.loadOlder();
|
||||
}
|
||||
|
||||
state = .loading();
|
||||
|
||||
final client = ref.read(ClientController.provider.notifier);
|
||||
|
||||
if (chat?.historicalData == null) {
|
||||
final timelineKeys = ref
|
||||
.read(RoomsController.provider.select((value) => value[info.roomId]))
|
||||
?.timeline
|
||||
.keys;
|
||||
final response = await client.paginate(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||
? timelineKeys?.reduce(min)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
try {
|
||||
if (chat?.historicalData == null) {
|
||||
final timelineKeys = ref
|
||||
.read(
|
||||
RoomsController.provider.select((value) => value[info.roomId]),
|
||||
)
|
||||
?.timeline
|
||||
.keys;
|
||||
final response = await client.paginate(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||
? timelineKeys?.reduce(min)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.events.isEmpty) {
|
||||
state = .data(chat);
|
||||
}
|
||||
if (response.events.isEmpty) {
|
||||
state = .data(chat);
|
||||
}
|
||||
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.update(
|
||||
IMap({
|
||||
info.roomId: Room(
|
||||
events: IMap.fromIterable(
|
||||
response.events.addAll(response.relatedEvents),
|
||||
keyMapper: (event) => event.rowId,
|
||||
valueMapper: (event) => event,
|
||||
),
|
||||
hasMore: response.hasMore,
|
||||
receipts: response.receipts,
|
||||
timeline: IMap.fromIterable(
|
||||
response.events,
|
||||
keyMapper: (event) => event.timelineRowId,
|
||||
valueMapper: (event) => event.rowId,
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.update(
|
||||
.new({
|
||||
info.roomId: .new(
|
||||
// ignore: unused_result - I am using it you donkey
|
||||
events: .fromIterable(
|
||||
response.events.addAll(response.relatedEvents),
|
||||
keyMapper: (event) => event.rowId,
|
||||
valueMapper: (event) => event,
|
||||
),
|
||||
hasMore: response.hasMore,
|
||||
receipts: response.receipts,
|
||||
// ignore: unused_result - I am using it you donkey
|
||||
timeline: .fromIterable(
|
||||
response.events,
|
||||
keyMapper: (event) => event.timelineRowId,
|
||||
valueMapper: (event) => event.rowId,
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
final paginationResponse = await client.paginateManual(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
threadRoot: info.threadRoot,
|
||||
direction: direction,
|
||||
since: direction == .forward
|
||||
? chat!.historicalData!.end
|
||||
: chat!.historicalData!.start,
|
||||
),
|
||||
);
|
||||
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(
|
||||
paginationResponse.events.addAll(
|
||||
paginationResponse.relatedEvents,
|
||||
),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
final paginationResponse = await client.paginateManual(
|
||||
.new(
|
||||
roomId: info.roomId,
|
||||
direction: direction,
|
||||
since: direction == .forward
|
||||
? chat!.historicalData!.end
|
||||
: chat!.historicalData!.start,
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(
|
||||
paginationResponse.events.addAll(paginationResponse.relatedEvents),
|
||||
);
|
||||
|
||||
final eventRowIds =
|
||||
(direction == .forward
|
||||
final rootRowId = direction == .backward
|
||||
? await threadRootBefore(paginationResponse)
|
||||
: null;
|
||||
final eventRowIds = [
|
||||
?rootRowId,
|
||||
...(direction == .forward
|
||||
? paginationResponse.events
|
||||
: paginationResponse.events.reversed)
|
||||
.map((element) => element.rowId)
|
||||
.toIList();
|
||||
.where((event) => event.threadRoot == info.threadRoot)
|
||||
.map((element) => element.rowId),
|
||||
].lock;
|
||||
|
||||
state = .data(
|
||||
.new(
|
||||
timeline: direction == .forward
|
||||
? chat.timeline.addAll(eventRowIds)
|
||||
: eventRowIds.addAll(chat.timeline),
|
||||
hasMoreForward:
|
||||
direction == .forward && paginationResponse.nextBatch == null
|
||||
? false
|
||||
: chat.hasMoreForward,
|
||||
hasMoreBackward:
|
||||
direction == .backward && paginationResponse.nextBatch == null
|
||||
? false
|
||||
: chat.hasMoreBackward,
|
||||
historicalData: chat.historicalData?.copyWith(
|
||||
start:
|
||||
(direction == .backward
|
||||
? paginationResponse.nextBatch
|
||||
: null) ??
|
||||
chat.historicalData!.start,
|
||||
end:
|
||||
(direction == .forward ? paginationResponse.nextBatch : null) ??
|
||||
chat.historicalData!.end,
|
||||
state = .data(
|
||||
.new(
|
||||
timeline: direction == .forward
|
||||
? chat.timeline.addAll(eventRowIds)
|
||||
: eventRowIds.addAll(chat.timeline),
|
||||
hasMoreForward:
|
||||
direction == .forward && paginationResponse.nextBatch == null
|
||||
? false
|
||||
: chat.hasMoreForward,
|
||||
hasMoreBackward:
|
||||
direction == .backward && paginationResponse.nextBatch == null
|
||||
? false
|
||||
: chat.hasMoreBackward,
|
||||
historicalData: chat.historicalData?.copyWith(
|
||||
start:
|
||||
(direction == .backward
|
||||
? paginationResponse.nextBatch
|
||||
: null) ??
|
||||
chat.historicalData!.start,
|
||||
end:
|
||||
(direction == .forward
|
||||
? paginationResponse.nextBatch
|
||||
: null) ??
|
||||
chat.historicalData!.end,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
state = .data(chat);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +400,11 @@ class RoomChatController(final ChatParams info)
|
|||
? null
|
||||
: events?[relatedEventRowId];
|
||||
|
||||
final attachmentProvider = AttachmentController.provider(info.roomId);
|
||||
final ComposerParams composer = (
|
||||
roomId: info.roomId,
|
||||
threadRoot: info.threadRoot,
|
||||
);
|
||||
final attachmentProvider = AttachmentController.provider(composer);
|
||||
final attachment = ref.read(attachmentProvider)?.$2;
|
||||
ref.invalidate(attachmentProvider);
|
||||
|
||||
|
|
@ -275,9 +440,9 @@ class RoomChatController(final ChatParams info)
|
|||
room: taggedMessage.contains("@room"),
|
||||
),
|
||||
text: taggedMessage,
|
||||
relation: relatedEvent == null
|
||||
? null
|
||||
: .new(eventId: relatedEvent.eventId, relationType: relationType),
|
||||
relation: relationType == .edit && relatedEvent != null
|
||||
? .new(eventId: relatedEvent.eventId, relationType: .edit)
|
||||
: replyRelation(relatedEvent),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -384,7 +549,7 @@ class RoomChatController(final ChatParams info)
|
|||
if (shouldMention && repliedEvent != null) repliedEvent.sender,
|
||||
].toIList(),
|
||||
),
|
||||
replyTo: repliedEvent?.eventId,
|
||||
reply: replyRelation(repliedEvent),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -451,9 +616,7 @@ class RoomChatController(final ChatParams info)
|
|||
command: draft.input,
|
||||
),
|
||||
mentions: .new(userIds: [draft.command.sender].lock),
|
||||
relation: repliedEvent == null
|
||||
? null
|
||||
: .new(eventId: repliedEvent.eventId, relationType: .reply),
|
||||
relation: replyRelation(repliedEvent),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
37
lib/controllers/thread_list.dart
Normal file
37
lib/controllers/thread_list.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
|
||||
class ThreadListController(final String roomId) extends AsyncNotifier<String?> {
|
||||
@override
|
||||
Future<String?> build() => fetch(null);
|
||||
|
||||
Future<String?> fetch(String? since) async {
|
||||
final threads = await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.getThreads(.new(roomId: roomId, since: since));
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(threads.events.addAll(threads.relatedEvents));
|
||||
|
||||
return threads.nextBatch;
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
final since = await future;
|
||||
if (state.isLoading || since == null) return;
|
||||
|
||||
state = .loading();
|
||||
try {
|
||||
state = .data(await fetch(since));
|
||||
} catch (_) {
|
||||
state = .data(since);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.family
|
||||
.autoDispose<ThreadListController, String?, String>(
|
||||
ThreadListController.new,
|
||||
);
|
||||
}
|
||||
49
lib/controllers/thread_replies.dart
Normal file
49
lib/controllers/thread_replies.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
|
||||
typedef ThreadParams = ({String roomId, String threadRoot});
|
||||
|
||||
/// Loads a thread's replies from the server, newest first, storing them. Its
|
||||
/// state is the token for older replies, or null if there are none.
|
||||
class ThreadRepliesController(final ThreadParams thread)
|
||||
extends AsyncNotifier<String?> {
|
||||
@override
|
||||
Future<String?> build() => fetch(null);
|
||||
|
||||
Future<String?> fetch(String? since) async {
|
||||
final response = await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.paginateManual(
|
||||
.new(
|
||||
roomId: thread.roomId,
|
||||
threadRoot: thread.threadRoot,
|
||||
since: since,
|
||||
direction: .backward,
|
||||
),
|
||||
);
|
||||
ref
|
||||
.read(RoomsController.provider.notifier)
|
||||
.storeEvents(response.events.addAll(response.relatedEvents));
|
||||
|
||||
return response.nextBatch;
|
||||
}
|
||||
|
||||
Future<void> loadOlder() async {
|
||||
final since = await future;
|
||||
if (state.isLoading || since == null) return;
|
||||
|
||||
state = .loading();
|
||||
try {
|
||||
state = .data(await fetch(since));
|
||||
} catch (_) {
|
||||
state = .data(since);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider.family
|
||||
.autoDispose<ThreadRepliesController, String?, ThreadParams>(
|
||||
ThreadRepliesController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ extension BuildEventOptions on Event {
|
|||
required String roomId,
|
||||
required String userId,
|
||||
required void Function(int, RelationType) onRelation,
|
||||
void Function(String threadRoot)? onStartThread,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final danger = theme.colorScheme.error;
|
||||
|
|
@ -34,11 +35,15 @@ extension BuildEventOptions on Event {
|
|||
final notifier = RoomChatController.provider((
|
||||
roomId: roomId,
|
||||
contextualEvent: null,
|
||||
threadRoot: null,
|
||||
)).notifier;
|
||||
|
||||
final isPinned = ref
|
||||
.read(PinnedIdsController.provider(roomId))
|
||||
.contains(eventId);
|
||||
final canSend = ref.read(
|
||||
PowerLevelController.provider(.new(eventType: .message, roomId: roomId)),
|
||||
);
|
||||
|
||||
void showReasonSheet({
|
||||
required String title,
|
||||
|
|
@ -170,11 +175,7 @@ extension BuildEventOptions on Event {
|
|||
),
|
||||
),
|
||||
|
||||
if (ref.read(
|
||||
PowerLevelController.provider(
|
||||
.new(eventType: .message, roomId: roomId),
|
||||
),
|
||||
))
|
||||
if (canSend)
|
||||
PopupMenuItem(
|
||||
onTap: () => onRelation(rowId, .reply),
|
||||
child: const ListTile(
|
||||
|
|
@ -183,6 +184,18 @@ extension BuildEventOptions on Event {
|
|||
),
|
||||
),
|
||||
|
||||
if (onStartThread != null &&
|
||||
threadCount == 0 &&
|
||||
canSend &&
|
||||
relatesTo == null)
|
||||
PopupMenuItem(
|
||||
onTap: () => onStartThread(eventId),
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.forum),
|
||||
title: Text("Start Thread"),
|
||||
),
|
||||
),
|
||||
|
||||
if (sender == userId &&
|
||||
switch (content) {
|
||||
LocationMessageContent() ||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ extension OpenLinkInfo on WidgetRef {
|
|||
: await read(
|
||||
EventController.provider(
|
||||
.new(roomId: summary.roomId, eventId: linkInfo.eventId!),
|
||||
).selectAsync((data) => data?.rowId),
|
||||
).future,
|
||||
).onError((_, _) => null);
|
||||
|
||||
if (await focusRoom(summary.roomId, eventRowId) || !context.mounted) return;
|
||||
|
|
|
|||
50
lib/helpers/extensions/thread_events.dart
Normal file
50
lib/helpers/extensions/thread_events.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:nexus/helpers/extensions/event_id_is_fake.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
|
||||
typedef ThreadInfo = ({int count, Event? latest});
|
||||
typedef Thread = ({Event root, ThreadInfo info});
|
||||
|
||||
extension ThreadEvents on Room {
|
||||
/// The threads in the loaded events, most recently active first.
|
||||
IList<Thread> get threads {
|
||||
DateTime lastActive(Thread thread) =>
|
||||
thread.info.latest?.timestamp ?? thread.root.timestamp;
|
||||
|
||||
return events.values
|
||||
.where((event) => event.threadCount > 0)
|
||||
.map((root) => (root: root, info: thread(root)))
|
||||
.sorted((a, b) => lastActive(b).compareTo(lastActive(a)))
|
||||
.toIList();
|
||||
}
|
||||
|
||||
/// The events in the thread from [threadRoot], oldest first, without the
|
||||
/// root itself.
|
||||
IList<Event> threadEvents(String threadRoot) =>
|
||||
(relations[threadRoot] ?? const .empty())
|
||||
.map((rowId) => events[rowId])
|
||||
.nonNulls
|
||||
.where(
|
||||
(event) =>
|
||||
event.threadRoot == threadRoot &&
|
||||
(!event.eventId.isFake || clientSticky.contains(event.rowId)),
|
||||
)
|
||||
.sorted((a, b) => a.timestamp.compareTo(b.timestamp))
|
||||
.toIList();
|
||||
|
||||
/// How many replies the thread from [root] has, and its latest event, from
|
||||
/// gomuks' aggregate on it
|
||||
ThreadInfo thread(Event root) =>
|
||||
(count: root.threadCount, latest: events[root.threadLatestRowId]);
|
||||
|
||||
/// The latest event in the thread from [threadRoot], if it's loaded.
|
||||
Event? latestThreadEvent(String threadRoot) => switch (eventIds[threadRoot]) {
|
||||
final rowId? => switch (events[rowId]) {
|
||||
final root? => thread(root).latest,
|
||||
null => null,
|
||||
},
|
||||
null => null,
|
||||
};
|
||||
}
|
||||
40
lib/helpers/hooks/chat_composer.dart
Normal file
40
lib/helpers/hooks/chat_composer.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import "package:flutter/services.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/models/relation_type.dart";
|
||||
|
||||
final class ChatComposer({
|
||||
required final ValueNotifier<int?> relatedEvent,
|
||||
required final ValueNotifier<RelationType> relationType,
|
||||
required final FocusNode node,
|
||||
}) {
|
||||
factory use() {
|
||||
final relatedEvent = useState<int?>(null);
|
||||
final relationType = useState(RelationType.reply);
|
||||
final node = useFocusNode(
|
||||
onKeyEvent: (_, event) {
|
||||
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
||||
relatedEvent.value = null;
|
||||
return .handled;
|
||||
}
|
||||
|
||||
return .ignored;
|
||||
},
|
||||
);
|
||||
|
||||
return .new(
|
||||
relatedEvent: relatedEvent,
|
||||
relationType: relationType,
|
||||
node: node,
|
||||
);
|
||||
}
|
||||
|
||||
int? get repliedEventRowId =>
|
||||
relationType.value == .reply ? relatedEvent.value : null;
|
||||
|
||||
void relate(int rowId, RelationType type) {
|
||||
relatedEvent.value = rowId;
|
||||
relationType.value = type;
|
||||
node.requestFocus();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ final class ChatScroll({
|
|||
required final GlobalKey centerKey,
|
||||
required final ScrollController scrollController,
|
||||
required final bool atBottom,
|
||||
required final int? highlightedEvent,
|
||||
|
||||
required final JumpToEvent jumpToEvent,
|
||||
required final Future<void> Function() jumpToBottom,
|
||||
required final GlobalKey Function(int eventRowId) keyFor,
|
||||
|
|
@ -28,6 +30,8 @@ final class ChatScroll({
|
|||
final anchorId = useState<int?>(null);
|
||||
final atBottom = useState(true);
|
||||
final pendingJump = useState<int?>(null);
|
||||
final highlightedEvent = useState<int?>(null);
|
||||
final hookContext = useContext();
|
||||
final pendingJumpDone = useRef<Completer<void>?>(null);
|
||||
void finishPendingJump() {
|
||||
pendingJumpDone.value?.complete();
|
||||
|
|
@ -95,15 +99,14 @@ final class ChatScroll({
|
|||
|
||||
final anchorIndex = items.indexOf(anchor);
|
||||
|
||||
if (anchorIndex == -1) return (history: const .empty(), live: items);
|
||||
|
||||
final historyLength = anchorIndex == -1 ? items.length : anchorIndex + 1;
|
||||
return (
|
||||
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
||||
live: items.skip(anchorIndex).toIList(),
|
||||
history: items.take(historyLength).toIList().reversed.toIList(),
|
||||
live: items.skip(historyLength).toIList(),
|
||||
);
|
||||
}, [controllerData, anchorId.value]);
|
||||
|
||||
final lastRowId = split.live.lastOrNull;
|
||||
final lastRowId = controllerData.value?.timeline.lastOrNull;
|
||||
final previousLastRowId = useRef<int?>(null);
|
||||
|
||||
useEffect(() {
|
||||
|
|
@ -118,7 +121,8 @@ final class ChatScroll({
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
scrollController.jumpTo(scrollController.position.minScrollExtent);
|
||||
anchorId.value = lastRowId;
|
||||
scrollController.position.correctPixels(0);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
@ -173,37 +177,49 @@ final class ChatScroll({
|
|||
],
|
||||
);
|
||||
|
||||
Future<void> scrollToEvent(int rowId) async {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
final context = keyFor(rowId).currentContext;
|
||||
if (context != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
duration: Durations.extralong1,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
finishPendingJump();
|
||||
final done = pendingJumpDone.value = Completer();
|
||||
pendingJump.value = rowId;
|
||||
if (controllerData.value?.timeline.contains(rowId) ?? false) {
|
||||
anchorId.value = rowId;
|
||||
} else {
|
||||
anchorId.value = null;
|
||||
contextualEvent.value = rowId;
|
||||
}
|
||||
await done.future;
|
||||
}
|
||||
|
||||
return .new(
|
||||
historyRows: split.history,
|
||||
liveRows: split.live,
|
||||
centerKey: centerKey,
|
||||
scrollController: scrollController,
|
||||
atBottom: atBottom.value,
|
||||
highlightedEvent: highlightedEvent.value,
|
||||
keyFor: keyFor,
|
||||
jumpToEvent: (int rowId) async {
|
||||
if (!scrollController.hasClients) return;
|
||||
highlightedEvent.value = rowId;
|
||||
|
||||
final context = keyFor(rowId).currentContext;
|
||||
if (context != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
duration: Durations.extralong1,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
finishPendingJump();
|
||||
final done = pendingJumpDone.value = Completer();
|
||||
pendingJump.value = rowId;
|
||||
if (controllerData.value?.timeline.contains(rowId) ?? false) {
|
||||
anchorId.value = rowId;
|
||||
} else {
|
||||
anchorId.value = null;
|
||||
contextualEvent.value = rowId;
|
||||
}
|
||||
await done.future;
|
||||
await scrollToEvent(rowId);
|
||||
await Future.delayed(Durations.extralong1, () {
|
||||
if (hookContext.mounted && highlightedEvent.value == rowId) {
|
||||
highlightedEvent.value = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
jumpToBottom: () async {
|
||||
finishPendingJump();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import "package:collection/collection.dart";
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
import "package:nexus/models/bot_command.dart";
|
||||
import "package:nexus/models/composer_params.dart";
|
||||
import "package:nexus/models/content/bot_command.dart";
|
||||
import "package:nexus/models/content/bot_command_description.dart";
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ part "bot_command_draft.freezed.dart";
|
|||
@Freezed(toJson: false, fromJson: false)
|
||||
class const BotCommandDraft({
|
||||
required final BotCommand command,
|
||||
required final String roomId,
|
||||
required final ComposerParams composer,
|
||||
final IMap<String, String> texts = const .empty(),
|
||||
|
||||
// The servers to join rooms picked from suggestions through, by room ID
|
||||
|
|
@ -33,8 +34,9 @@ class const BotCommandDraft({
|
|||
BotParameter? get focusedParameter => command.description.parameters
|
||||
.firstWhereOrNull((parameter) => parameter.key == focused);
|
||||
|
||||
({Object? value, String? error}) parse(BotParameter parameter) =>
|
||||
parameter.schema.parseText(texts[parameter.key] ?? "", roomId: roomId);
|
||||
({Object? value, String? error}) parse(BotParameter parameter) => parameter
|
||||
.schema
|
||||
.parseText(texts[parameter.key] ?? "", roomId: composer.roomId);
|
||||
|
||||
IMap<String, Object> get arguments => .fromEntries(
|
||||
shown
|
||||
|
|
|
|||
1
lib/models/composer_params.dart
Normal file
1
lib/models/composer_params.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
typedef ComposerParams = ({String roomId, String? threadRoot});
|
||||
|
|
@ -29,6 +29,8 @@ class const Event({
|
|||
final String? sendError,
|
||||
final IMap<String, int> reactions = const .empty(),
|
||||
@JsonKey(name: "last_edit_rowid") final int lastEditRowId = 0,
|
||||
final int threadCount = 0,
|
||||
@JsonKey(name: "thread_latest_rowid") final int threadLatestRowId = 0,
|
||||
final UnreadType? unreadType,
|
||||
final Profile? pmp,
|
||||
required final Content content,
|
||||
|
|
@ -36,6 +38,8 @@ class const Event({
|
|||
}) with _$Event {
|
||||
Map<String, Object?> toJson() => _$EventToJson(this);
|
||||
|
||||
String? get threadRoot => relationType == "m.thread" ? relatesTo : null;
|
||||
|
||||
static String typeJsonFromJson(Map<dynamic, dynamic> json, _) =>
|
||||
json["decrypted_type"] ?? json["type"];
|
||||
|
||||
|
|
@ -47,7 +51,15 @@ class const Event({
|
|||
|
||||
static String? replyToFromJson(Map<dynamic, dynamic> json) {
|
||||
try {
|
||||
return json["m.relates_to"]?["m.in_reply_to"]?["event_id"];
|
||||
final relatesTo = json["m.relates_to"];
|
||||
|
||||
// Thread fallbacks are only for clients without thread support
|
||||
if (relatesTo?["rel_type"] == "m.thread" &&
|
||||
relatesTo?["is_falling_back"] == true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relatesTo?["m.in_reply_to"]?["event_id"];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
17
lib/models/requests/get_threads.dart
Normal file
17
lib/models/requests/get_threads.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
|
||||
part "get_threads.freezed.dart";
|
||||
part "get_threads.g.dart";
|
||||
|
||||
@Freezed(toJson: false, fromJson: false)
|
||||
@JsonSerializable()
|
||||
class const GetThreadsRequest({
|
||||
required final String roomId,
|
||||
final String? since,
|
||||
final int limit = 20,
|
||||
}) with _$GetThreadsRequest {
|
||||
Map<String, Object?> toJson() => _$GetThreadsRequestToJson(this);
|
||||
|
||||
factory GetThreadsRequest.fromJson(Map<String, Object?> json) =>
|
||||
_$GetThreadsRequestFromJson(json);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue