feat(meta): Paginate message requests

This commit is contained in:
PapaTutuWawa 2023-02-18 21:02:55 +01:00
parent 28591a6787
commit de2e2f3987
11 changed files with 355 additions and 166 deletions

View File

@ -265,6 +265,16 @@ files:
stickerPack: stickerPack:
type: StickerPack type: StickerPack
deserialise: true deserialise: true
# Returned by [GetPagedMessagesCommand]
- name: PagedMessagesResultEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
messages:
type: List<Message>
deserialise: true
hasOlderMessages: bool
generate_builder: true generate_builder: true
builder_name: "Event" builder_name: "Event"
builder_baseclass: "BackgroundEvent" builder_baseclass: "BackgroundEvent"
@ -545,6 +555,13 @@ files:
implements: implements:
- JsonImplementation - JsonImplementation
attributes: attributes:
- name: GetPagedMessagesCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
conversationJid: String
oldestMessageTimestamp: int?
generate_builder: true generate_builder: true
# get${builder_Name}FromJson # get${builder_Name}FromJson
builder_name: "Command" builder_name: "Command"

View File

@ -261,7 +261,9 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
case conversationRoute: return PageTransition<dynamic>( case conversationRoute: return PageTransition<dynamic>(
type: PageTransitionType.rightToLeft, type: PageTransitionType.rightToLeft,
settings: settings, settings: settings,
child: const ConversationPage(), child: ConversationPage(
conversationJid: settings.arguments as String,
),
); );
case sharedMediaRoute: return SharedMediaPage.route; case sharedMediaRoute: return SharedMediaPage.route;
case blocklistRoute: return BlocklistPage.route; case blocklistRoute: return BlocklistPage.route;

View File

@ -41,6 +41,7 @@ import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/omemo/omemo.dart'; import 'package:moxxyv2/service/omemo/omemo.dart';
import 'package:moxxyv2/service/omemo/types.dart'; import 'package:moxxyv2/service/omemo/types.dart';
import 'package:moxxyv2/service/roster.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/conversation.dart';
import 'package:moxxyv2/shared/models/media.dart'; import 'package:moxxyv2/shared/models/media.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
@ -287,6 +288,39 @@ class DatabaseService {
return messages; 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. /// Updates the conversation with JID [jid] inside the database.
Future<Conversation> updateConversation(String jid, { Future<Conversation> updateConversation(String jid, {
int? lastChangeTimestamp, int? lastChangeTimestamp,

View File

@ -82,6 +82,7 @@ void setupBackgroundEventHandler() {
EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack), EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack),
EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall), EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall),
EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist), EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist),
EventTypeMatcher<GetPagedMessagesCommand>(performGetPagedMessages),
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
@ -1012,3 +1013,21 @@ Future<void> performGetBlocklist(GetBlocklistCommand command, { dynamic extra })
id: id, 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,
);
}

View File

@ -14,9 +14,11 @@ import 'package:moxxyv2/shared/models/media.dart';
import 'package:moxxyv2/shared/models/message.dart'; import 'package:moxxyv2/shared/models/message.dart';
class MessageService { class MessageService {
MessageService() : _messageCache = HashMap(), _log = Logger('MessageService'); // TODO(PapaTutuWawa): Maybe remove the cache
final HashMap<String, List<Message>> _messageCache; final HashMap<String, List<Message>> _messageCache = HashMap();
final Logger _log;
/// Logger
final Logger _log = Logger('MessageService');
/// Returns the messages for [jid], either from cache or from the database. /// Returns the messages for [jid], either from cache or from the database.
Future<List<Message>> getMessagesForJid(String jid) async { Future<List<Message>> getMessagesForJid(String jid) async {
@ -33,6 +35,13 @@ class MessageService {
return messages; 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. /// Wrapper around [DatabaseService]'s addMessageFromData that updates the cache.
Future<Message> addMessageFromData( Future<Message> addMessageFromData(
String body, String body,

View File

@ -1 +1,4 @@
const int timestampNever = -1; const int timestampNever = -1;
/// The amount of messages that are fetched by a paginated message request
const int paginatedMessageFetchAmount = 30;

View File

@ -44,7 +44,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
on<JidBlockedEvent>(_onJidBlocked); on<JidBlockedEvent>(_onJidBlocked);
on<JidAddedEvent>(_onJidAdded); on<JidAddedEvent>(_onJidAdded);
on<CurrentConversationResetEvent>(_onCurrentConversationReset); on<CurrentConversationResetEvent>(_onCurrentConversationReset);
on<MessageAddedEvent>(_onMessageAdded);
on<MessageUpdatedEvent>(_onMessageUpdated); on<MessageUpdatedEvent>(_onMessageUpdated);
on<ConversationUpdatedEvent>(_onConversationUpdated); on<ConversationUpdatedEvent>(_onConversationUpdated);
on<AppStateChanged>(_onAppStateChanged); on<AppStateChanged>(_onAppStateChanged);
@ -160,25 +159,23 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
final navEvent = event.removeUntilConversations ? ( final navEvent = event.removeUntilConversations ? (
PushedNamedAndRemoveUntilEvent( PushedNamedAndRemoveUntilEvent(
const NavigationDestination(conversationRoute), NavigationDestination(
conversationRoute,
arguments: event.jid,
),
ModalRoute.withName(conversationsRoute), ModalRoute.withName(conversationsRoute),
) )
) : ( ) : (
PushedNamedEvent( PushedNamedEvent(
const NavigationDestination(conversationRoute), NavigationDestination(
conversationRoute,
arguments: event.jid,
),
) )
); );
GetIt.I.get<NavigationBloc>().add(navEvent); 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( await MoxplatformPlugin.handler.getDataSender().sendData(
SetOpenConversationCommand(jid: event.jid), SetOpenConversationCommand(jid: event.jid),
awaitable: false, awaitable: false,
@ -302,7 +299,6 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
conversation: null, conversation: null,
messageText: '', messageText: '',
quotedMessage: null, 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 { Future<void> _onMessageUpdated(MessageUpdatedEvent event, Emitter<ConversationState> emit) async {
if (!_isMessageForConversation(event.message)) return; 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 { 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 { Future<void> _onReactionAdded(ReactionAddedEvent event, Emitter<ConversationState> emit) async {
// TODO: Fix
// Check if such a reaction already exists // Check if such a reaction already exists
final message = state.messages[event.index]; // final message = state.messages[event.index];
final msgs = List<Message>.from(state.messages); // final msgs = List<Message>.from(state.messages);
final reactionIndex = message.reactions.indexWhere( // final reactionIndex = message.reactions.indexWhere(
(Reaction r) => r.emoji == event.emoji, // (Reaction r) => r.emoji == event.emoji,
); // );
if (reactionIndex != -1) { // if (reactionIndex != -1) {
// Ignore the request when the reaction would be invalid // // Ignore the request when the reaction would be invalid
final reaction = message.reactions[reactionIndex]; // final reaction = message.reactions[reactionIndex];
if (reaction.reactedBySelf) return; // if (reaction.reactedBySelf) return;
final reactions = List<Reaction>.from(message.reactions); // final reactions = List<Reaction>.from(message.reactions);
reactions[reactionIndex] = reaction.copyWith( // reactions[reactionIndex] = reaction.copyWith(
reactedBySelf: true, // reactedBySelf: true,
); // );
msgs[event.index] = message.copyWith( // msgs[event.index] = message.copyWith(
reactions: reactions, // reactions: reactions,
); // );
} else { // } else {
// The reaction is new // // The reaction is new
msgs[event.index] = message.copyWith( // msgs[event.index] = message.copyWith(
reactions: [ // reactions: [
...message.reactions, // ...message.reactions,
Reaction( // Reaction(
[], // [],
event.emoji, // event.emoji,
true, // true,
), // ),
], // ],
); // );
} // }
emit( // emit(
state.copyWith( // state.copyWith(
messages: msgs, // messages: msgs,
), // ),
); // );
await MoxplatformPlugin.handler.getDataSender().sendData( // await MoxplatformPlugin.handler.getDataSender().sendData(
AddReactionToMessageCommand( // AddReactionToMessageCommand(
messageId: message.id, // messageId: message.id,
emoji: event.emoji, // emoji: event.emoji,
conversationJid: message.conversationJid, // conversationJid: message.conversationJid,
), // ),
awaitable: false, // awaitable: false,
); // );
} }
Future<void> _onReactionRemoved(ReactionRemovedEvent event, Emitter<ConversationState> emit) async { Future<void> _onReactionRemoved(ReactionRemovedEvent event, Emitter<ConversationState> emit) async {
final message = state.messages[event.index]; // TODO
final msgs = List<Message>.from(state.messages); // final message = state.messages[event.index];
final reactionIndex = message.reactions.indexWhere( // final msgs = List<Message>.from(state.messages);
(Reaction r) => r.emoji == event.emoji, // final reactionIndex = message.reactions.indexWhere(
); // (Reaction r) => r.emoji == event.emoji,
// );
// We assume that reactionIndex >= 0 // // We assume that reactionIndex >= 0
assert(reactionIndex >= 0, 'The reaction must be found'); // assert(reactionIndex >= 0, 'The reaction must be found');
final reactions = List<Reaction>.from(message.reactions); // final reactions = List<Reaction>.from(message.reactions);
if (message.reactions[reactionIndex].senders.isEmpty) { // if (message.reactions[reactionIndex].senders.isEmpty) {
reactions.removeAt(reactionIndex); // reactions.removeAt(reactionIndex);
} else { // } else {
reactions[reactionIndex] = reactions[reactionIndex].copyWith( // reactions[reactionIndex] = reactions[reactionIndex].copyWith(
reactedBySelf: false, // reactedBySelf: false,
); // );
} // }
msgs[event.index] = message.copyWith(reactions: reactions); // msgs[event.index] = message.copyWith(reactions: reactions);
emit( // emit(
state.copyWith( // state.copyWith(
messages: msgs, // messages: msgs,
), // ),
); // );
await MoxplatformPlugin.handler.getDataSender().sendData( // await MoxplatformPlugin.handler.getDataSender().sendData(
RemoveReactionFromMessageCommand( // RemoveReactionFromMessageCommand(
messageId: message.id, // messageId: message.id,
emoji: event.emoji, // emoji: event.emoji,
conversationJid: message.conversationJid, // conversationJid: message.conversationJid,
), // ),
awaitable: false, // awaitable: false,
); // );
} }
Future<void> _onStickerSent(StickerSentEvent event, Emitter<ConversationState> emit) async { Future<void> _onStickerSent(StickerSentEvent event, Emitter<ConversationState> emit) async {

View File

@ -15,7 +15,6 @@ class ConversationState with _$ConversationState {
@Default('') String messageText, @Default('') String messageText,
@Default(defaultSendButtonState) SendButtonState sendButtonState, @Default(defaultSendButtonState) SendButtonState sendButtonState,
@Default(null) Message? quotedMessage, @Default(null) Message? quotedMessage,
@Default(<Message>[]) List<Message> messages,
@Default(null) Conversation? conversation, @Default(null) Conversation? conversation,
@Default('') String backgroundPath, @Default('') String backgroundPath,
@Default(false) bool pickerVisible, @Default(false) bool pickerVisible,

View 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();
}
}

View File

@ -10,6 +10,7 @@ import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart'; import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
import 'package:moxxyv2/ui/constants.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/helpers.dart';
import 'package:moxxyv2/ui/pages/conversation/blink.dart'; import 'package:moxxyv2/ui/pages/conversation/blink.dart';
import 'package:moxxyv2/ui/pages/conversation/timer.dart'; import 'package:moxxyv2/ui/pages/conversation/timer.dart';
@ -93,6 +94,7 @@ class ConversationBottomRow extends StatefulWidget {
this.controller, this.controller,
this.tabController, this.tabController,
this.focusNode, this.focusNode,
this.conversationController,
this.speedDialValueNotifier, { this.speedDialValueNotifier, {
super.key, super.key,
} }
@ -101,6 +103,7 @@ class ConversationBottomRow extends StatefulWidget {
final TabController tabController; final TabController tabController;
final FocusNode focusNode; final FocusNode focusNode;
final ValueNotifier<bool> speedDialValueNotifier; final ValueNotifier<bool> speedDialValueNotifier;
final BidirectionalConversationController conversationController;
@override @override
ConversationBottomRowState createState() => ConversationBottomRowState(); ConversationBottomRowState createState() => ConversationBottomRowState();
@ -298,6 +301,7 @@ class ConversationBottomRowState extends State<ConversationBottomRow> {
context.read<ConversationBloc>().add( context.read<ConversationBloc>().add(
MessageSentEvent(), MessageSentEvent(),
); );
widget.conversationController.animateToBottom();
widget.controller.text = ''; widget.controller.text = '';
return; return;
case SendButtonState.multi: case SendButtonState.multi:

View File

@ -13,6 +13,7 @@ import 'package:moxxyv2/shared/models/message.dart';
import 'package:moxxyv2/shared/warning_types.dart'; import 'package:moxxyv2/shared/warning_types.dart';
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart'; import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
import 'package:moxxyv2/ui/constants.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/helpers.dart';
import 'package:moxxyv2/ui/pages/conversation/blink.dart'; import 'package:moxxyv2/ui/pages/conversation/blink.dart';
import 'package:moxxyv2/ui/pages/conversation/bottom.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'; import 'package:moxxyv2/ui/widgets/overview_menu.dart';
class ConversationPage extends StatefulWidget { class ConversationPage extends StatefulWidget {
const ConversationPage({ super.key }); const ConversationPage({
required this.conversationJid,
super.key,
});
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>( /// The JID of the current conversation
builder: (context) => const ConversationPage(), final String conversationJid;
settings: const RouteSettings(
name: conversationRoute,
),
);
@override @override
ConversationPageState createState() => ConversationPageState(); ConversationPageState createState() => ConversationPageState();
@ -40,7 +40,6 @@ class ConversationPage extends StatefulWidget {
class ConversationPageState extends State<ConversationPage> with TickerProviderStateMixin { class ConversationPageState extends State<ConversationPage> with TickerProviderStateMixin {
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
late final AnimationController _animationController; late final AnimationController _animationController;
late final AnimationController _overviewAnimationController; late final AnimationController _overviewAnimationController;
late final TabController _tabController; late final TabController _tabController;
@ -50,6 +49,8 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
late FocusNode _textfieldFocus; late FocusNode _textfieldFocus;
final ValueNotifier<bool> _isSpeedDialOpen = ValueNotifier(false); final ValueNotifier<bool> _isSpeedDialOpen = ValueNotifier(false);
late final BidirectionalConversationController _conversationController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -58,7 +59,13 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
vsync: this, vsync: this,
); );
_textfieldFocus = FocusNode(); _textfieldFocus = FocusNode();
_scrollController.addListener(_onScroll);
// TODO
_conversationController = BidirectionalConversationController(
widget.conversationJid,
);
_conversationController.scrollController.addListener(_onScroll);
_conversationController.fetchOlderMessages();
_overviewAnimationController = AnimationController( _overviewAnimationController = AnimationController(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@ -80,9 +87,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
void dispose() { void dispose() {
_tabController.dispose(); _tabController.dispose();
_controller.dispose(); _controller.dispose();
_scrollController _conversationController.dispose();
..removeListener(_onScroll)
..dispose();
_animationController.dispose(); _animationController.dispose();
_overviewAnimationController.dispose(); _overviewAnimationController.dispose();
_textfieldFocus.dispose(); _textfieldFocus.dispose();
@ -133,10 +138,13 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
final start = index - 1 < 0 ? final start = index - 1 < 0 ?
true : true :
isSent(state.messages[index - 1], state.jid) != isSent(item, state.jid); false;
final end = index + 1 >= state.messages.length ? // isSent(state.messages[index - 1], state.jid) != isSent(item, state.jid);
true : // final end = index + 1 >= state.messages.length ?
isSent(state.messages[index + 1], state.jid) != isSent(item, state.jid); // true :
// false;
final end = true;
// isSent(state.messages[index + 1], state.jid) != isSent(item, state.jid);
final between = !start && !end; final between = !start && !end;
final sentBySelf = isSent(message, state.jid); final sentBySelf = isSent(message, state.jid);
@ -401,9 +409,9 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
/// Taken from https://bloclibrary.dev/#/flutterinfinitelisttutorial /// Taken from https://bloclibrary.dev/#/flutterinfinitelisttutorial
bool _isScrolledToBottom() { bool _isScrolledToBottom() {
if (!_scrollController.hasClients) return false; if (!_conversationController.scrollController.hasClients) return false;
return _scrollController.offset <= 10; return _conversationController.scrollController.offset <= 10;
} }
void _onScroll() { void _onScroll() {
@ -495,17 +503,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
}, },
), ),
BlocBuilder<ConversationBloc, ConversationState>( Expanded(
// NOTE: We don't need to update when the jid changes as it should child: StreamBuilder<List<Message>>(
// be static over the entire lifetime of the BLoC. initialData: [],
buildWhen: (prev, next) => prev.messages != next.messages || prev.conversation?.encrypted != next.conversation?.encrypted, stream: _conversationController.messageStream,
builder: (context, state) => Expanded( builder: (context, snapshot) {
// Inspired by https://github.com/SimformSolutionsPvtLtd/flutter_chatview/blob/main/lib/src/widgets/chat_groupedlist_widget.dart if (snapshot.hasData) {
child: SingleChildScrollView( return SingleChildScrollView(
reverse: true, reverse: true,
controller: _scrollController, controller: _conversationController.scrollController,
child: GroupedListView<Message, DateTime>( child: GroupedListView<Message, DateTime>(
elements: state.messages, elements: snapshot.data!,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
groupBy: (message) { groupBy: (message) {
@ -525,13 +533,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
], ],
), ),
indexedItemBuilder: (context, message, index) => _renderBubble( indexedItemBuilder: (context, message, index) => _renderBubble(
state, context.read<ConversationBloc>().state,
message, message,
index, index,
maxWidth, maxWidth,
), ),
), ),
), );
}
return CircularProgressIndicator();
},
), ),
), ),
@ -543,6 +555,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
_controller, _controller,
_tabController, _tabController,
_textfieldFocus, _textfieldFocus,
_conversationController,
_isSpeedDialOpen, _isSpeedDialOpen,
), ),
), ),
@ -570,7 +583,7 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
heroTag: 'fabScrollDown', heroTag: 'fabScrollDown',
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
onPressed: () { onPressed: () {
_scrollController.jumpTo(0); _conversationController.animateToBottom();
}, },
child: const Icon( child: const Icon(
Icons.arrow_downward, Icons.arrow_downward,