feat(meta): Paginate message requests
This commit is contained in:
parent
28591a6787
commit
de2e2f3987
@ -265,6 +265,16 @@ files:
|
||||
stickerPack:
|
||||
type: StickerPack
|
||||
deserialise: true
|
||||
# Returned by [GetPagedMessagesCommand]
|
||||
- name: PagedMessagesResultEvent
|
||||
extends: BackgroundEvent
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
messages:
|
||||
type: List<Message>
|
||||
deserialise: true
|
||||
hasOlderMessages: bool
|
||||
generate_builder: true
|
||||
builder_name: "Event"
|
||||
builder_baseclass: "BackgroundEvent"
|
||||
@ -545,6 +555,13 @@ files:
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
- name: GetPagedMessagesCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
conversationJid: String
|
||||
oldestMessageTimestamp: int?
|
||||
generate_builder: true
|
||||
# get${builder_Name}FromJson
|
||||
builder_name: "Command"
|
||||
|
@ -261,7 +261,9 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
case conversationRoute: return PageTransition<dynamic>(
|
||||
type: PageTransitionType.rightToLeft,
|
||||
settings: settings,
|
||||
child: const ConversationPage(),
|
||||
child: ConversationPage(
|
||||
conversationJid: settings.arguments as String,
|
||||
),
|
||||
);
|
||||
case sharedMediaRoute: return SharedMediaPage.route;
|
||||
case blocklistRoute: return BlocklistPage.route;
|
||||
|
@ -41,6 +41,7 @@ import 'package:moxxyv2/service/not_specified.dart';
|
||||
import 'package:moxxyv2/service/omemo/omemo.dart';
|
||||
import 'package:moxxyv2/service/omemo/types.dart';
|
||||
import 'package:moxxyv2/service/roster.dart';
|
||||
import 'package:moxxyv2/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||
import 'package:moxxyv2/shared/models/media.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
@ -287,6 +288,39 @@ class DatabaseService {
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<List<Message>> getPaginatedMessagesForJid(String jid, int? oldestTimestamp) async {
|
||||
final query = oldestTimestamp != null ?
|
||||
'conversationJid = ? AND timestamp < ?' :
|
||||
'conversationJid = ?';
|
||||
final args = oldestTimestamp != null ?
|
||||
[jid, oldestTimestamp!] :
|
||||
[jid];
|
||||
final rawMessages = await _db.query(
|
||||
'Messages',
|
||||
where: query,
|
||||
whereArgs: args,
|
||||
orderBy: 'timestamp DESC',
|
||||
limit: paginatedMessageFetchAmount,
|
||||
);
|
||||
|
||||
final messages = List<Message>.empty(growable: true);
|
||||
for (final m in rawMessages) {
|
||||
Message? quotes;
|
||||
if (m['quote_id'] != null) {
|
||||
final rawQuote = (await _db.query(
|
||||
'Messages',
|
||||
where: 'conversationJid = ? AND id = ?',
|
||||
whereArgs: [jid, m['quote_id']! as int],
|
||||
)).first;
|
||||
quotes = Message.fromDatabaseJson(rawQuote, null);
|
||||
}
|
||||
|
||||
messages.add(Message.fromDatabaseJson(m, quotes));
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// Updates the conversation with JID [jid] inside the database.
|
||||
Future<Conversation> updateConversation(String jid, {
|
||||
int? lastChangeTimestamp,
|
||||
|
@ -82,6 +82,7 @@ void setupBackgroundEventHandler() {
|
||||
EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack),
|
||||
EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall),
|
||||
EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist),
|
||||
EventTypeMatcher<GetPagedMessagesCommand>(performGetPagedMessages),
|
||||
]);
|
||||
|
||||
GetIt.I.registerSingleton<EventHandler>(handler);
|
||||
@ -1012,3 +1013,21 @@ Future<void> performGetBlocklist(GetBlocklistCommand command, { dynamic extra })
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> performGetPagedMessages(GetPagedMessagesCommand command, { dynamic extra }) async {
|
||||
final id = extra as String;
|
||||
|
||||
final result = await GetIt.I.get<MessageService>().getPaginatedMessagesForJid(
|
||||
command.conversationJid,
|
||||
command.oldestMessageTimestamp,
|
||||
);
|
||||
|
||||
sendEvent(
|
||||
PagedMessagesResultEvent(
|
||||
messages: result,
|
||||
// TODO
|
||||
hasOlderMessages: true,
|
||||
),
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
@ -14,9 +14,11 @@ import 'package:moxxyv2/shared/models/media.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
|
||||
class MessageService {
|
||||
MessageService() : _messageCache = HashMap(), _log = Logger('MessageService');
|
||||
final HashMap<String, List<Message>> _messageCache;
|
||||
final Logger _log;
|
||||
// TODO(PapaTutuWawa): Maybe remove the cache
|
||||
final HashMap<String, List<Message>> _messageCache = HashMap();
|
||||
|
||||
/// Logger
|
||||
final Logger _log = Logger('MessageService');
|
||||
|
||||
/// Returns the messages for [jid], either from cache or from the database.
|
||||
Future<List<Message>> getMessagesForJid(String jid) async {
|
||||
@ -33,6 +35,13 @@ class MessageService {
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<List<Message>> getPaginatedMessagesForJid(String jid, int? oldestTimestamp) async {
|
||||
return GetIt.I.get<DatabaseService>().getPaginatedMessagesForJid(
|
||||
jid,
|
||||
oldestTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
/// Wrapper around [DatabaseService]'s addMessageFromData that updates the cache.
|
||||
Future<Message> addMessageFromData(
|
||||
String body,
|
||||
|
@ -1 +1,4 @@
|
||||
const int timestampNever = -1;
|
||||
|
||||
/// The amount of messages that are fetched by a paginated message request
|
||||
const int paginatedMessageFetchAmount = 30;
|
||||
|
@ -44,7 +44,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
on<JidBlockedEvent>(_onJidBlocked);
|
||||
on<JidAddedEvent>(_onJidAdded);
|
||||
on<CurrentConversationResetEvent>(_onCurrentConversationReset);
|
||||
on<MessageAddedEvent>(_onMessageAdded);
|
||||
on<MessageUpdatedEvent>(_onMessageUpdated);
|
||||
on<ConversationUpdatedEvent>(_onConversationUpdated);
|
||||
on<AppStateChanged>(_onAppStateChanged);
|
||||
@ -160,25 +159,23 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
|
||||
final navEvent = event.removeUntilConversations ? (
|
||||
PushedNamedAndRemoveUntilEvent(
|
||||
const NavigationDestination(conversationRoute),
|
||||
NavigationDestination(
|
||||
conversationRoute,
|
||||
arguments: event.jid,
|
||||
),
|
||||
ModalRoute.withName(conversationsRoute),
|
||||
)
|
||||
) : (
|
||||
PushedNamedEvent(
|
||||
const NavigationDestination(conversationRoute),
|
||||
NavigationDestination(
|
||||
conversationRoute,
|
||||
arguments: event.jid,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
GetIt.I.get<NavigationBloc>().add(navEvent);
|
||||
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetMessagesForJidCommand(
|
||||
jid: event.jid,
|
||||
),
|
||||
) as events.MessagesResultEvent;
|
||||
emit(state.copyWith(messages: result.messages));
|
||||
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
SetOpenConversationCommand(jid: event.jid),
|
||||
awaitable: false,
|
||||
@ -302,7 +299,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
conversation: null,
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
messages: [],
|
||||
),
|
||||
);
|
||||
|
||||
@ -312,16 +308,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onMessageAdded(MessageAddedEvent event, Emitter<ConversationState> emit) async {
|
||||
if (!_isMessageForConversation(event.message)) return;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: List.from(<Message>[ ...state.messages, event.message ]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onMessageUpdated(MessageUpdatedEvent event, Emitter<ConversationState> emit) async {
|
||||
if (!_isMessageForConversation(event.message)) return;
|
||||
|
||||
@ -338,19 +324,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We don't have to re-sort the list here as timestamps never change
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: List.from(
|
||||
state.messages.map<dynamic>((Message m) {
|
||||
if (m.id == event.message.id) return event.message;
|
||||
|
||||
return m;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onConversationUpdated(ConversationUpdatedEvent event, Emitter<ConversationState> emit) async {
|
||||
@ -570,86 +543,88 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
}
|
||||
|
||||
Future<void> _onReactionAdded(ReactionAddedEvent event, Emitter<ConversationState> emit) async {
|
||||
// TODO: Fix
|
||||
// Check if such a reaction already exists
|
||||
final message = state.messages[event.index];
|
||||
final msgs = List<Message>.from(state.messages);
|
||||
final reactionIndex = message.reactions.indexWhere(
|
||||
(Reaction r) => r.emoji == event.emoji,
|
||||
);
|
||||
if (reactionIndex != -1) {
|
||||
// Ignore the request when the reaction would be invalid
|
||||
final reaction = message.reactions[reactionIndex];
|
||||
if (reaction.reactedBySelf) return;
|
||||
// final message = state.messages[event.index];
|
||||
// final msgs = List<Message>.from(state.messages);
|
||||
// final reactionIndex = message.reactions.indexWhere(
|
||||
// (Reaction r) => r.emoji == event.emoji,
|
||||
// );
|
||||
// if (reactionIndex != -1) {
|
||||
// // Ignore the request when the reaction would be invalid
|
||||
// final reaction = message.reactions[reactionIndex];
|
||||
// if (reaction.reactedBySelf) return;
|
||||
|
||||
final reactions = List<Reaction>.from(message.reactions);
|
||||
reactions[reactionIndex] = reaction.copyWith(
|
||||
reactedBySelf: true,
|
||||
);
|
||||
msgs[event.index] = message.copyWith(
|
||||
reactions: reactions,
|
||||
);
|
||||
} else {
|
||||
// The reaction is new
|
||||
msgs[event.index] = message.copyWith(
|
||||
reactions: [
|
||||
...message.reactions,
|
||||
Reaction(
|
||||
[],
|
||||
event.emoji,
|
||||
true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// final reactions = List<Reaction>.from(message.reactions);
|
||||
// reactions[reactionIndex] = reaction.copyWith(
|
||||
// reactedBySelf: true,
|
||||
// );
|
||||
// msgs[event.index] = message.copyWith(
|
||||
// reactions: reactions,
|
||||
// );
|
||||
// } else {
|
||||
// // The reaction is new
|
||||
// msgs[event.index] = message.copyWith(
|
||||
// reactions: [
|
||||
// ...message.reactions,
|
||||
// Reaction(
|
||||
// [],
|
||||
// event.emoji,
|
||||
// true,
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: msgs,
|
||||
),
|
||||
);
|
||||
// emit(
|
||||
// state.copyWith(
|
||||
// messages: msgs,
|
||||
// ),
|
||||
// );
|
||||
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
AddReactionToMessageCommand(
|
||||
messageId: message.id,
|
||||
emoji: event.emoji,
|
||||
conversationJid: message.conversationJid,
|
||||
),
|
||||
awaitable: false,
|
||||
);
|
||||
// await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
// AddReactionToMessageCommand(
|
||||
// messageId: message.id,
|
||||
// emoji: event.emoji,
|
||||
// conversationJid: message.conversationJid,
|
||||
// ),
|
||||
// awaitable: false,
|
||||
// );
|
||||
}
|
||||
|
||||
Future<void> _onReactionRemoved(ReactionRemovedEvent event, Emitter<ConversationState> emit) async {
|
||||
final message = state.messages[event.index];
|
||||
final msgs = List<Message>.from(state.messages);
|
||||
final reactionIndex = message.reactions.indexWhere(
|
||||
(Reaction r) => r.emoji == event.emoji,
|
||||
);
|
||||
// TODO
|
||||
// final message = state.messages[event.index];
|
||||
// final msgs = List<Message>.from(state.messages);
|
||||
// final reactionIndex = message.reactions.indexWhere(
|
||||
// (Reaction r) => r.emoji == event.emoji,
|
||||
// );
|
||||
|
||||
// We assume that reactionIndex >= 0
|
||||
assert(reactionIndex >= 0, 'The reaction must be found');
|
||||
final reactions = List<Reaction>.from(message.reactions);
|
||||
if (message.reactions[reactionIndex].senders.isEmpty) {
|
||||
reactions.removeAt(reactionIndex);
|
||||
} else {
|
||||
reactions[reactionIndex] = reactions[reactionIndex].copyWith(
|
||||
reactedBySelf: false,
|
||||
);
|
||||
}
|
||||
msgs[event.index] = message.copyWith(reactions: reactions);
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: msgs,
|
||||
),
|
||||
);
|
||||
// // We assume that reactionIndex >= 0
|
||||
// assert(reactionIndex >= 0, 'The reaction must be found');
|
||||
// final reactions = List<Reaction>.from(message.reactions);
|
||||
// if (message.reactions[reactionIndex].senders.isEmpty) {
|
||||
// reactions.removeAt(reactionIndex);
|
||||
// } else {
|
||||
// reactions[reactionIndex] = reactions[reactionIndex].copyWith(
|
||||
// reactedBySelf: false,
|
||||
// );
|
||||
// }
|
||||
// msgs[event.index] = message.copyWith(reactions: reactions);
|
||||
// emit(
|
||||
// state.copyWith(
|
||||
// messages: msgs,
|
||||
// ),
|
||||
// );
|
||||
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
RemoveReactionFromMessageCommand(
|
||||
messageId: message.id,
|
||||
emoji: event.emoji,
|
||||
conversationJid: message.conversationJid,
|
||||
),
|
||||
awaitable: false,
|
||||
);
|
||||
// await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
// RemoveReactionFromMessageCommand(
|
||||
// messageId: message.id,
|
||||
// emoji: event.emoji,
|
||||
// conversationJid: message.conversationJid,
|
||||
// ),
|
||||
// awaitable: false,
|
||||
// );
|
||||
}
|
||||
|
||||
Future<void> _onStickerSent(StickerSentEvent event, Emitter<ConversationState> emit) async {
|
||||
|
@ -15,7 +15,6 @@ class ConversationState with _$ConversationState {
|
||||
@Default('') String messageText,
|
||||
@Default(defaultSendButtonState) SendButtonState sendButtonState,
|
||||
@Default(null) Message? quotedMessage,
|
||||
@Default(<Message>[]) List<Message> messages,
|
||||
@Default(null) Conversation? conversation,
|
||||
@Default('') String backgroundPath,
|
||||
@Default(false) bool pickerVisible,
|
||||
|
114
lib/ui/controller/conversation_controller.dart
Normal file
114
lib/ui/controller/conversation_controller.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
|
||||
class BidirectionalConversationController {
|
||||
BidirectionalConversationController(this.conversationJid) {
|
||||
_controller.addListener(_handleScroll);
|
||||
}
|
||||
|
||||
/// The list of messages we know about
|
||||
final List<Message> _messageCache = List<Message>.empty(growable: true);
|
||||
|
||||
/// Flag indicating whether we are currently fetching messages.
|
||||
bool isFetchingMessages = false;
|
||||
final StreamController<bool> _isFetchingStreamController = StreamController();
|
||||
Stream<bool> get isFetchingStream => _isFetchingStreamController.stream;
|
||||
|
||||
/// Flag indicating whether we have newer messages we could request from the database
|
||||
bool hasNewerMessages = false;
|
||||
|
||||
/// Flag indicating whether we have older messages we could request from the database
|
||||
bool hasOlderMessages = true;
|
||||
|
||||
/// Flag indicating whether messages have been fetched once
|
||||
bool hasFetchedOnce = false;
|
||||
|
||||
/// Scroll controller for managing things like loading newer and older messages
|
||||
final ScrollController _controller = ScrollController();
|
||||
ScrollController get scrollController => _controller;
|
||||
|
||||
/// Stream for message updates
|
||||
final StreamController<List<Message>> _messageStreamController = StreamController();
|
||||
Stream<List<Message>> get messageStream => _messageStreamController.stream;
|
||||
|
||||
/// The JID of the current chat
|
||||
final String conversationJid;
|
||||
|
||||
void _setIsFetching(bool state) {
|
||||
isFetchingMessages = state;
|
||||
_isFetchingStreamController.add(state);
|
||||
}
|
||||
|
||||
void _handleScroll() {
|
||||
if (!_controller.hasClients) return;
|
||||
|
||||
// Fetch older messages when we reach the top edge of the list
|
||||
if (_controller.offset >= _controller.position.maxScrollExtent - 20 && !isFetchingMessages) {
|
||||
unawaited(fetchOlderMessages());
|
||||
}
|
||||
}
|
||||
|
||||
void animateToBottom() {
|
||||
_controller.animateTo(
|
||||
_controller.position.minScrollExtent,
|
||||
curve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
|
||||
void onMessageSent(Message message) {
|
||||
if (hasNewerMessages) {
|
||||
_messageCache.add(message);
|
||||
|
||||
_messageStreamController.add(_messageCache);
|
||||
|
||||
Future<void>.delayed(const Duration(milliseconds: 300))
|
||||
.then((_) => animateToBottom());
|
||||
} else {
|
||||
// TODO(PapaTutuWawa): Load the newest page and scroll to it
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchOlderMessages() async {
|
||||
if (isFetchingMessages ||
|
||||
_messageCache.isEmpty && hasFetchedOnce) return;
|
||||
if (!hasOlderMessages) return;
|
||||
|
||||
_setIsFetching(true);
|
||||
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetPagedMessagesCommand(
|
||||
conversationJid: conversationJid,
|
||||
oldestMessageTimestamp: !hasFetchedOnce ?
|
||||
null :
|
||||
_messageCache.first.timestamp,
|
||||
),
|
||||
) as PagedMessagesResultEvent;
|
||||
|
||||
_setIsFetching(false);
|
||||
hasFetchedOnce = true;
|
||||
hasOlderMessages = result.hasOlderMessages;
|
||||
|
||||
if (result.messages.length == 0) {
|
||||
hasOlderMessages = false;
|
||||
return;
|
||||
} else if (result.messages.length < paginatedMessageFetchAmount) {
|
||||
// This means we reached the end of messages we can fetch
|
||||
hasOlderMessages = false;
|
||||
}
|
||||
|
||||
_messageCache.insertAll(0, result.messages.reversed);
|
||||
_messageStreamController.add(_messageCache);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/timer.dart';
|
||||
@ -93,6 +94,7 @@ class ConversationBottomRow extends StatefulWidget {
|
||||
this.controller,
|
||||
this.tabController,
|
||||
this.focusNode,
|
||||
this.conversationController,
|
||||
this.speedDialValueNotifier, {
|
||||
super.key,
|
||||
}
|
||||
@ -101,6 +103,7 @@ class ConversationBottomRow extends StatefulWidget {
|
||||
final TabController tabController;
|
||||
final FocusNode focusNode;
|
||||
final ValueNotifier<bool> speedDialValueNotifier;
|
||||
final BidirectionalConversationController conversationController;
|
||||
|
||||
@override
|
||||
ConversationBottomRowState createState() => ConversationBottomRowState();
|
||||
@ -298,6 +301,7 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageSentEvent(),
|
||||
);
|
||||
widget.conversationController.animateToBottom();
|
||||
widget.controller.text = '';
|
||||
return;
|
||||
case SendButtonState.multi:
|
||||
|
@ -13,6 +13,7 @@ import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/shared/warning_types.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/blink.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/bottom.dart';
|
||||
@ -25,14 +26,13 @@ import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
|
||||
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
|
||||
|
||||
class ConversationPage extends StatefulWidget {
|
||||
const ConversationPage({ super.key });
|
||||
const ConversationPage({
|
||||
required this.conversationJid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const ConversationPage(),
|
||||
settings: const RouteSettings(
|
||||
name: conversationRoute,
|
||||
),
|
||||
);
|
||||
/// The JID of the current conversation
|
||||
final String conversationJid;
|
||||
|
||||
@override
|
||||
ConversationPageState createState() => ConversationPageState();
|
||||
@ -40,7 +40,6 @@ class ConversationPage extends StatefulWidget {
|
||||
|
||||
class ConversationPageState extends State<ConversationPage> with TickerProviderStateMixin {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late final AnimationController _animationController;
|
||||
late final AnimationController _overviewAnimationController;
|
||||
late final TabController _tabController;
|
||||
@ -50,6 +49,8 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
late FocusNode _textfieldFocus;
|
||||
final ValueNotifier<bool> _isSpeedDialOpen = ValueNotifier(false);
|
||||
|
||||
late final BidirectionalConversationController _conversationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -58,7 +59,13 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
vsync: this,
|
||||
);
|
||||
_textfieldFocus = FocusNode();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// TODO
|
||||
_conversationController = BidirectionalConversationController(
|
||||
widget.conversationJid,
|
||||
);
|
||||
_conversationController.scrollController.addListener(_onScroll);
|
||||
_conversationController.fetchOlderMessages();
|
||||
|
||||
_overviewAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@ -80,9 +87,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_controller.dispose();
|
||||
_scrollController
|
||||
..removeListener(_onScroll)
|
||||
..dispose();
|
||||
_conversationController.dispose();
|
||||
_animationController.dispose();
|
||||
_overviewAnimationController.dispose();
|
||||
_textfieldFocus.dispose();
|
||||
@ -133,10 +138,13 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
|
||||
final start = index - 1 < 0 ?
|
||||
true :
|
||||
isSent(state.messages[index - 1], state.jid) != isSent(item, state.jid);
|
||||
final end = index + 1 >= state.messages.length ?
|
||||
true :
|
||||
isSent(state.messages[index + 1], state.jid) != isSent(item, state.jid);
|
||||
false;
|
||||
// isSent(state.messages[index - 1], state.jid) != isSent(item, state.jid);
|
||||
// final end = index + 1 >= state.messages.length ?
|
||||
// true :
|
||||
// false;
|
||||
final end = true;
|
||||
// isSent(state.messages[index + 1], state.jid) != isSent(item, state.jid);
|
||||
final between = !start && !end;
|
||||
final sentBySelf = isSent(message, state.jid);
|
||||
|
||||
@ -401,9 +409,9 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
|
||||
/// Taken from https://bloclibrary.dev/#/flutterinfinitelisttutorial
|
||||
bool _isScrolledToBottom() {
|
||||
if (!_scrollController.hasClients) return false;
|
||||
if (!_conversationController.scrollController.hasClients) return false;
|
||||
|
||||
return _scrollController.offset <= 10;
|
||||
return _conversationController.scrollController.offset <= 10;
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
@ -495,17 +503,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
},
|
||||
),
|
||||
|
||||
BlocBuilder<ConversationBloc, ConversationState>(
|
||||
// NOTE: We don't need to update when the jid changes as it should
|
||||
// be static over the entire lifetime of the BLoC.
|
||||
buildWhen: (prev, next) => prev.messages != next.messages || prev.conversation?.encrypted != next.conversation?.encrypted,
|
||||
builder: (context, state) => Expanded(
|
||||
// Inspired by https://github.com/SimformSolutionsPvtLtd/flutter_chatview/blob/main/lib/src/widgets/chat_groupedlist_widget.dart
|
||||
child: SingleChildScrollView(
|
||||
Expanded(
|
||||
child: StreamBuilder<List<Message>>(
|
||||
initialData: [],
|
||||
stream: _conversationController.messageStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return SingleChildScrollView(
|
||||
reverse: true,
|
||||
controller: _scrollController,
|
||||
controller: _conversationController.scrollController,
|
||||
child: GroupedListView<Message, DateTime>(
|
||||
elements: state.messages,
|
||||
elements: snapshot.data!,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
groupBy: (message) {
|
||||
@ -525,13 +533,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
],
|
||||
),
|
||||
indexedItemBuilder: (context, message, index) => _renderBubble(
|
||||
state,
|
||||
context.read<ConversationBloc>().state,
|
||||
message,
|
||||
index,
|
||||
maxWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CircularProgressIndicator();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -543,6 +555,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
_controller,
|
||||
_tabController,
|
||||
_textfieldFocus,
|
||||
_conversationController,
|
||||
_isSpeedDialOpen,
|
||||
),
|
||||
),
|
||||
@ -570,7 +583,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
heroTag: 'fabScrollDown',
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
onPressed: () {
|
||||
_scrollController.jumpTo(0);
|
||||
_conversationController.animateToBottom();
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.arrow_downward,
|
||||
|
Loading…
Reference in New Issue
Block a user