add /room/:room/:thread route
edit chat.dart to support threads edit SendFileDialog and SendPollDialog to support threads
This commit is contained in:
parent
74c05be615
commit
251ca9893d
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:extera_next/pages/thread/thread.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -148,6 +149,21 @@ abstract class AppRoutes {
|
|||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':threadroot',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ThreadPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
threadRootEventId:
|
||||
state.pathParameters['threadroot']!,
|
||||
eventId: state.uri.queryParameters['event'],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -352,6 +368,18 @@ abstract class AppRoutes {
|
|||
},
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':threadroot',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ThreadPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
threadRootEventId: state.pathParameters['threadroot']!,
|
||||
eventId: state.uri.queryParameters['event'],
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'search',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
|
|
|
|||
|
|
@ -85,12 +85,14 @@ class ChatPage extends StatelessWidget {
|
|||
|
||||
class ChatPageWithRoom extends StatefulWidget {
|
||||
final Room room;
|
||||
final Thread? thread;
|
||||
final List<ShareItem>? shareItems;
|
||||
final String? eventId;
|
||||
|
||||
const ChatPageWithRoom({
|
||||
super.key,
|
||||
required this.room,
|
||||
this.thread,
|
||||
this.shareItems,
|
||||
this.eventId,
|
||||
});
|
||||
|
|
@ -102,14 +104,18 @@ class ChatPageWithRoom extends StatefulWidget {
|
|||
class ChatController extends State<ChatPageWithRoom>
|
||||
with WidgetsBindingObserver {
|
||||
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||
Thread? get thread =>
|
||||
sendingClient.getRoomById(roomId)?.threads[threadRootEventId] ??
|
||||
widget.room.threads[threadRootEventId];
|
||||
|
||||
late Client sendingClient;
|
||||
|
||||
RoomTimeline? timeline;
|
||||
Timeline? timeline;
|
||||
|
||||
late final String readMarkerEventId;
|
||||
|
||||
String get roomId => widget.room.id;
|
||||
String? get threadRootEventId => widget.thread?.rootEvent.eventId;
|
||||
|
||||
final AutoScrollController scrollController = AutoScrollController();
|
||||
|
||||
|
|
@ -134,6 +140,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: details.files,
|
||||
room: room,
|
||||
thread: thread,
|
||||
replyEvent: replyEvent,
|
||||
outerContext: context,
|
||||
),
|
||||
|
|
@ -274,6 +281,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: files,
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
replyEvent: replyEvent,
|
||||
),
|
||||
|
|
@ -324,6 +332,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void _tryLoadTimeline() async {
|
||||
final initialEventId = widget.eventId;
|
||||
loadTimelineFuture = _getTimeline();
|
||||
Logs().v("Trying to load timeline...");
|
||||
try {
|
||||
await loadTimelineFuture;
|
||||
if (initialEventId != null) scrollToEventId(initialEventId);
|
||||
|
|
@ -387,15 +396,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
animateInEventIndex = i;
|
||||
}
|
||||
|
||||
Future<void> _getTimeline({
|
||||
String? eventContextId,
|
||||
}) async {
|
||||
await Matrix.of(context).client.roomsLoading;
|
||||
await Matrix.of(context).client.accountDataLoading;
|
||||
if (eventContextId != null &&
|
||||
(!eventContextId.isValidMatrixId || eventContextId.sigil != '\$')) {
|
||||
eventContextId = null;
|
||||
}
|
||||
Future<void> _loadRoomTimeline({String? eventContextId}) async {
|
||||
try {
|
||||
timeline?.cancelSubscriptions();
|
||||
timeline = await room.getTimeline(
|
||||
|
|
@ -415,6 +416,56 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
_showScrollUpMaterialBanner(eventContextId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadThreadTimeline({String? eventContextId}) async {
|
||||
if (thread == null) {
|
||||
throw Exception(
|
||||
"_loadThreadTimeline should not be called, thread == null",
|
||||
);
|
||||
}
|
||||
try {
|
||||
timeline?.cancelSubscriptions();
|
||||
timeline = await thread!.getTimeline(
|
||||
onUpdate: updateView,
|
||||
eventContextId: eventContextId,
|
||||
onInsert: onInsert,
|
||||
);
|
||||
if (timeline is ThreadTimeline) {
|
||||
(timeline as ThreadTimeline).getThreadEvents();
|
||||
}
|
||||
Logs().v("Thread timeline loaded");
|
||||
} catch (e, s) {
|
||||
Logs().w(
|
||||
'Unable to load timeline on event ID $eventContextId (in thread)',
|
||||
e,
|
||||
s);
|
||||
if (!mounted) return;
|
||||
timeline = await thread!.getTimeline(
|
||||
onUpdate: updateView,
|
||||
onInsert: onInsert,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (e is TimeoutException || e is IOException) {
|
||||
_showScrollUpMaterialBanner(eventContextId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getTimeline({
|
||||
String? eventContextId,
|
||||
}) async {
|
||||
await Matrix.of(context).client.roomsLoading;
|
||||
await Matrix.of(context).client.accountDataLoading;
|
||||
if (eventContextId != null &&
|
||||
(!eventContextId.isValidMatrixId || eventContextId.sigil != '\$')) {
|
||||
eventContextId = null;
|
||||
}
|
||||
if (thread == null) {
|
||||
await _loadRoomTimeline(eventContextId: eventContextId);
|
||||
} else {
|
||||
await _loadThreadTimeline(eventContextId: eventContextId);
|
||||
}
|
||||
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
||||
if (room.markedUnread) room.markUnread(false);
|
||||
|
||||
|
|
@ -471,9 +522,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
.then((_) {
|
||||
_setReadMarkerFuture = null;
|
||||
});
|
||||
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
|
||||
Matrix.of(context).backgroundPush?.cancelNotification(roomId);
|
||||
|
||||
if (timeline is RoomTimeline) {
|
||||
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
|
||||
Matrix.of(context).backgroundPush?.cancelNotification(roomId);
|
||||
}
|
||||
}
|
||||
// TODO same for Threads
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -540,12 +595,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
// ignore: unawaited_futures
|
||||
room.sendTextEvent(
|
||||
sendController.text,
|
||||
inReplyTo: replyEvent,
|
||||
editEventId: editEvent?.eventId,
|
||||
parseCommands: parseCommands,
|
||||
);
|
||||
room.sendTextEvent(sendController.text,
|
||||
inReplyTo: replyEvent,
|
||||
editEventId: editEvent?.eventId,
|
||||
parseCommands: parseCommands,
|
||||
threadRootEventId: thread?.rootEvent.eventId,
|
||||
threadLastEventId:
|
||||
thread?.lastEvent?.eventId ?? thread?.rootEvent.eventId);
|
||||
sendController.value = TextEditingValue(
|
||||
text: pendingText,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
|
|
@ -562,8 +618,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void sendPollAction() async {
|
||||
await showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (c) => SendPollDialog(room: room, outerContext: context));
|
||||
context: context,
|
||||
builder: (c) => SendPollDialog(
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
),
|
||||
);
|
||||
replyEvent = null;
|
||||
}
|
||||
|
||||
|
|
@ -582,6 +643,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: files,
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
replyEvent: replyEvent,
|
||||
),
|
||||
|
|
@ -596,6 +658,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: [XFile.fromData(image)],
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
),
|
||||
);
|
||||
|
|
@ -612,6 +675,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: [file],
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
),
|
||||
);
|
||||
|
|
@ -631,6 +695,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
builder: (c) => SendFileDialog(
|
||||
files: [file],
|
||||
room: room,
|
||||
thread: thread,
|
||||
outerContext: context,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class ChatEventList extends StatelessWidget {
|
|||
|
||||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
final events = timeline.events.filterByVisibleInGui().filterByThreaded(false);
|
||||
final events = timeline.events.filterByVisibleInGui().filterByThreaded(controller.thread != null);
|
||||
final animateInEventIndex = controller.animateInEventIndex;
|
||||
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import 'dart:ui' as ui;
|
|||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:extera_next/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:extera_next/utils/poll_events.dart';
|
||||
import 'package:extera_next/widgets/mxc_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:swipe_to_action/swipe_to_action.dart';
|
||||
|
||||
|
|
@ -690,11 +692,58 @@ class Message extends StatelessWidget {
|
|||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
thread == null
|
||||
? 'No thread'
|
||||
: 'Has thread, last event: ${thread!.lastEvent != null ? thread!.lastEvent!.eventId : 'None'}',
|
||||
),
|
||||
thread != null
|
||||
? Align(
|
||||
alignment: ownMessage
|
||||
? Alignment.bottomRight
|
||||
: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
color: Colors.grey[200],
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
thread!.lastEvent != null
|
||||
? FutureBuilder<User?>(
|
||||
future: thread!.lastEvent!
|
||||
.fetchSenderUser(),
|
||||
builder:
|
||||
(context, snapshot) {
|
||||
final user = snapshot
|
||||
.data ??
|
||||
event
|
||||
.senderFromMemoryOrFallback;
|
||||
|
||||
return Avatar(
|
||||
mxContent:
|
||||
user.avatarUrl,
|
||||
name: user
|
||||
.calcDisplayname(),
|
||||
size: 24,
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(width: 6),
|
||||
thread!.lastEvent != null
|
||||
? Text(
|
||||
thread!.lastEvent!.text)
|
||||
: const Text('Thread'),
|
||||
],
|
||||
),
|
||||
onTap: () => context.go(
|
||||
'/rooms/${event.roomId}/${event.eventId}',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@ import 'package:html_unescape/html_unescape.dart';
|
|||
|
||||
class SendFileDialog extends StatefulWidget {
|
||||
final Room room;
|
||||
final Thread? thread;
|
||||
final List<XFile> files;
|
||||
final BuildContext outerContext;
|
||||
final Event? replyEvent;
|
||||
|
||||
const SendFileDialog({
|
||||
required this.room,
|
||||
required this.thread,
|
||||
required this.files,
|
||||
required this.outerContext,
|
||||
this.replyEvent,
|
||||
|
|
@ -151,6 +153,8 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||
thumbnail: thumbnail,
|
||||
shrinkImageMaxDimension: compress ? 1600 : null,
|
||||
extraContent: extraContent,
|
||||
threadLastEventId: widget.thread?.lastEvent?.eventId ?? widget.thread?.rootEvent.eventId,
|
||||
threadRootEventId: widget.thread?.rootEvent.eventId
|
||||
);
|
||||
} on MatrixException catch (e) {
|
||||
final retryAfterMs = e.retryAfterMs;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:uuid/uuid.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -8,9 +7,11 @@ class SendPollDialog extends StatefulWidget {
|
|||
final Room room;
|
||||
final BuildContext outerContext;
|
||||
final Event? replyEvent;
|
||||
final Thread? thread;
|
||||
|
||||
const SendPollDialog({
|
||||
required this.room,
|
||||
required this.thread,
|
||||
required this.outerContext,
|
||||
this.replyEvent,
|
||||
super.key,
|
||||
|
|
@ -74,11 +75,13 @@ class SendPollDialogState extends State<SendPollDialog> {
|
|||
'm.text': question,
|
||||
},
|
||||
'answers': answers
|
||||
.map((answer) => {
|
||||
'id': const Uuid().v4(),
|
||||
'org.matrix.msc1767.text': answer,
|
||||
'm.text': answer,
|
||||
})
|
||||
.map(
|
||||
(answer) => {
|
||||
'id': const Uuid().v4(),
|
||||
'org.matrix.msc1767.text': answer,
|
||||
'm.text': answer,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'max_selections': _maxSelections,
|
||||
'kind': _kind,
|
||||
|
|
@ -86,7 +89,13 @@ class SendPollDialogState extends State<SendPollDialog> {
|
|||
};
|
||||
|
||||
try {
|
||||
await widget.room.sendEvent(pollContent, type: 'org.matrix.msc3381.poll.start');
|
||||
await widget.room.sendEvent(
|
||||
pollContent,
|
||||
type: 'org.matrix.msc3381.poll.start',
|
||||
threadLastEventId: widget.thread?.lastEvent?.eventId ??
|
||||
widget.thread?.rootEvent.eventId,
|
||||
threadRootEventId: widget.thread?.rootEvent.eventId,
|
||||
);
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
|
|
@ -154,7 +163,7 @@ class SendPollDialogState extends State<SendPollDialog> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
value: _maxSelections,
|
||||
initialValue: _maxSelections,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).maxSelections,
|
||||
border: const OutlineInputBorder(),
|
||||
|
|
@ -170,7 +179,7 @@ class SendPollDialogState extends State<SendPollDialog> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _kind,
|
||||
initialValue: _kind,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).pollType,
|
||||
border: const OutlineInputBorder(),
|
||||
|
|
@ -202,4 +211,4 @@ class SendPollDialogState extends State<SendPollDialog> {
|
|||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:extera_next/generated/l10n/l10n.dart';
|
||||
import 'package:extera_next/pages/chat/chat.dart';
|
||||
import 'package:extera_next/widgets/matrix.dart';
|
||||
import 'package:extera_next/widgets/share_scaffold_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
final String roomId;
|
||||
final List<ShareItem>? shareItems;
|
||||
final String? threadRootEventId;
|
||||
final String? eventId;
|
||||
|
||||
const ThreadPage({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
required this.threadRootEventId,
|
||||
this.eventId,
|
||||
this.shareItems,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final thread = room.threads[threadRootEventId];
|
||||
|
||||
return ChatPageWithRoom(
|
||||
key: Key('chat_page_${roomId}_${threadRootEventId}_$eventId'),
|
||||
room: room,
|
||||
thread: thread,
|
||||
shareItems: shareItems,
|
||||
eventId: eventId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2171,7 +2171,7 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ dependencies:
|
|||
wakelock_plus: ^1.2.2
|
||||
webrtc_interface: ^1.0.13
|
||||
dio: ^5.9.0
|
||||
uuid: ^4.5.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue