diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 580a56add8..24dc9d132d 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -83,71 +83,68 @@ class ChatEventList extends StatelessWidget { child: SelectionTextContainer( chatController: controller, focusNode: controller.selectionFocusNode, - child: InViewNotifierList( - padding: EdgeInsets.only( - top: 16, - bottom: 8.0, - left: horizontalPadding, - right: horizontalPadding, - ), - reverse: true, - controller: controller.scrollController, - itemCount: events.length + 2, - builder: (context, index) { - // Footer to display typing indicator and read receipts: - if (index == 0) { - if (controller.timeline!.isRequestingFuture) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - } - if (controller.timeline!.canRequestFuture) { - Center( - child: OutlinedButton( - style: OutlinedButton.styleFrom( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - ), - onPressed: controller.requestFuture, - child: Text(L10n.of(context)!.loadMore), - ), - ); - } - return const SizedBox.shrink(); - } - // Request history button or progress indicator: - if (index == events.length + 1) { - if (controller.timeline!.isRequestingHistory) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - } - if (controller.timeline!.canRequestHistory) { - return Center( - child: IconButton( - onPressed: controller.requestHistory, - icon: const Icon(Icons.refresh_outlined), - ), - ); - } - return const SizedBox.shrink(); - } - final currentEventIndex = index - 1; - final event = controller.timeline!.events[currentEventIndex]; - final previousEvent = currentEventIndex > 0 - ? controller.timeline!.events[currentEventIndex - 1] - : null; - final nextEvent = index < controller.timeline!.events.length - ? controller.timeline!.events[currentEventIndex + 1] - : null; - return InViewNotifierWidget( - id: event.eventId, - builder: (context, bool isInView, _) { - if (isInView) { - controller.handleDisplayStickyTimestamp( - event.originServerTs, - ); + child: InViewNotifierListCustom( + isInViewPortCondition: controller.isInViewPortCondition, + listViewCustom: ListView.custom( + padding: EdgeInsets.only( + top: 16, + bottom: 8.0, + left: horizontalPadding, + right: horizontalPadding, + ), + reverse: true, + controller: controller.scrollController, + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + childrenDelegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + // Footer to display typing indicator and read receipts: + if (index == 0) { + if (controller.timeline!.isRequestingFuture) { + return const Center( + child: + CircularProgressIndicator.adaptive(strokeWidth: 2), + ); + } + if (controller.timeline!.canRequestFuture) { + Center( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + ), + onPressed: controller.requestFuture, + child: Text(L10n.of(context)!.loadMore), + ), + ); + } + return const SizedBox.shrink(); + } + // Request history button or progress indicator: + if (index == events.length + 1) { + if (controller.timeline!.isRequestingHistory) { + return const Center( + child: + CircularProgressIndicator.adaptive(strokeWidth: 2), + ); + } + if (controller.timeline!.canRequestHistory) { + return Center( + child: IconButton( + onPressed: controller.requestHistory, + icon: const Icon(Icons.refresh_outlined), + ), + ); + } + return const SizedBox.shrink(); } + final currentEventIndex = index - 1; + final event = controller.timeline!.events[currentEventIndex]; + final previousEvent = currentEventIndex > 0 + ? controller.timeline!.events[currentEventIndex - 1] + : null; + final nextEvent = index < controller.timeline!.events.length + ? controller.timeline!.events[currentEventIndex + 1] + : null; return AutoScrollTag( key: ValueKey(event.eventId), index: index, @@ -155,6 +152,7 @@ class ChatEventList extends StatelessWidget { highlightColor: LinagoraRefColors.material().primary[99], child: event.isVisibleInGui ? Message( + key: ValueKey(event.eventId), event, onSwipe: (direction) => controller.replyAction(replyTo: event), @@ -185,14 +183,20 @@ class ChatEventList extends StatelessWidget { controller.onHideKeyboardAndEmoji, markedUnreadLocation: controller.unreadReceivedMessageLocation, - hideTimeStamp: isInView, + timestampCallback: (event) { + controller.handleDisplayStickyTimestamp( + event.originServerTs, + ); + }, ) : const SizedBox(), ); }, - ); - }, - isInViewPortCondition: controller.isInViewPortCondition, + childCount: events.length + 2, + findChildIndexCallback: (key) => + controller.findChildIndexCallback(key, thisEventsKeyMap), + ), + ), ), ), ), diff --git a/lib/pages/chat/events/message/message.dart b/lib/pages/chat/events/message/message.dart index 1b2c385dbf..14db321e0b 100644 --- a/lib/pages/chat/events/message/message.dart +++ b/lib/pages/chat/events/message/message.dart @@ -18,6 +18,7 @@ import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/swipeable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:inview_notifier_list/inview_notifier_list.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; @@ -28,7 +29,7 @@ typedef OnMenuAction = Function( TapDownDetails, ); -class Message extends StatelessWidget { +class Message extends StatefulWidget { final Event event; final Event? previousEvent; final Event? nextEvent; @@ -47,7 +48,7 @@ class Message extends StatelessWidget { final VoidCallback? hideKeyboardChatScreen; final ContextMenuBuilder? menuChildren; final FocusNode? focusNode; - final bool? hideTimeStamp; + final void Function(Event)? timestampCallback; const Message( this.event, { @@ -69,7 +70,7 @@ class Message extends StatelessWidget { this.onMenuAction, this.markedUnreadLocation, this.focusNode, - this.hideTimeStamp = false, + this.timestampCallback, }) : super(key: key); /// Indicates wheither the user may use a mouse instead @@ -78,13 +79,67 @@ class Message extends StatelessWidget { static final responsiveUtils = getIt.get(); + @override + State createState() => _MessageState(); +} + +class _MessageState extends State { + InViewState? inViewState; + + final inviewNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + _initialInviewState(); + }); + } + + @override + void dispose() { + inViewState?.removeContext(context: context); + inviewNotifier.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(Message oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.event.eventId != widget.event.eventId) { + inViewState?.removeContext(context: context); + inViewState?.addContext(context: context, id: widget.event.eventId); + } + } + + void _initialInviewState() { + inViewState = InViewNotifierListCustom.of(context); + inViewState?.addListener(_inviewStateListener); + inViewState?.addContext(context: context, id: widget.event.eventId); + } + + void _inviewStateListener() { + _updateInViewNotifier(inViewState?.inView(widget.event.eventId)); + if (inViewState?.inView(widget.event.eventId) == true) { + widget.timestampCallback?.call(widget.event); + } + } + + void _updateInViewNotifier(bool? inView) { + try { + inviewNotifier.value = inView ?? inviewNotifier.value; + } on FlutterError catch (e) { + Logs().d('_MessageState:: _updateInViewNotifier: $e'); + } + } + @override Widget build(BuildContext context) { return MultiPlatformsMessageContainer( - onTap: hideKeyboardChatScreen, + onTap: widget.hideKeyboardChatScreen, onHover: (hover) { - if (onHover != null) { - onHover!(hover, event); + if (widget.onHover != null) { + widget.onHover!(hover, widget.event); } }, child: LayoutBuilder( @@ -94,48 +149,52 @@ class Message extends StatelessWidget { EventTypes.Sticker, EventTypes.Encrypted, EventTypes.CallInvite, - }.contains(event.type)) { - if (event.type.startsWith('m.call.')) { + }.contains(widget.event.type)) { + if (widget.event.type.startsWith('m.call.')) { return const SizedBox(); } - if (event.isJoinedByRoomCreator()) { + if (widget.event.isJoinedByRoomCreator()) { return const SizedBox(); } - return StateMessage(event); + return StateMessage(widget.event); } - if (event.type == EventTypes.Message && - event.messageType == EventTypes.KeyVerificationRequest) { - return VerificationRequestContent(event: event, timeline: timeline); + if (widget.event.type == EventTypes.Message && + widget.event.messageType == EventTypes.KeyVerificationRequest) { + return VerificationRequestContent( + event: widget.event, + timeline: widget.timeline, + ); } - final displayTime = event.type == EventTypes.RoomCreate || - nextEvent == null || - !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); - final rowMainAxisAlignment = event.isOwnMessage + final displayTime = widget.event.type == EventTypes.RoomCreate || + widget.nextEvent == null || + !widget.event.originServerTs + .sameEnvironment(widget.nextEvent!.originServerTs); + final rowMainAxisAlignment = widget.event.isOwnMessage ? MainAxisAlignment.end : MainAxisAlignment.start; final rowChildren = [ _placeHolderWidget( - event.isSameSenderWith(previousEvent), - event.isOwnMessage, - event, + widget.event.isSameSenderWith(widget.previousEvent), + widget.event.isOwnMessage, + widget.event, ), Expanded( child: MessageContentWithTimestampBuilder( - event: event, - nextEvent: nextEvent, - onSelect: onSelect, - scrollToEventId: scrollToEventId, - selected: selected, - selectMode: selectMode, - timeline: timeline, - isHoverNotifier: isHoverNotifier, - listHorizontalActionMenu: listHorizontalActionMenu, - onMenuAction: onMenuAction, - menuChildren: menuChildren, - focusNode: focusNode, + event: widget.event, + nextEvent: widget.nextEvent, + onSelect: widget.onSelect, + scrollToEventId: widget.scrollToEventId, + selected: widget.selected, + selectMode: widget.selectMode, + timeline: widget.timeline, + isHoverNotifier: widget.isHoverNotifier, + listHorizontalActionMenu: widget.listHorizontalActionMenu, + onMenuAction: widget.onMenuAction, + menuChildren: widget.menuChildren, + focusNode: widget.focusNode, ), ), ]; @@ -148,13 +207,18 @@ class Message extends StatelessWidget { return Column( children: [ if (displayTime) - StickyTimestampWidget( - content: hideTimeStamp == false - ? event.originServerTs.relativeTime(context) - : '', + ValueListenableBuilder( + valueListenable: inviewNotifier, + builder: (context, inView, _) { + return StickyTimestampWidget( + content: !inView + ? widget.event.originServerTs.relativeTime(context) + : '', + ); + }, ), - if (markedUnreadLocation != null && - markedUnreadLocation == event.eventId) ...[ + if (widget.markedUnreadLocation != null && + widget.markedUnreadLocation == widget.event.eventId) ...[ Padding( padding: MessageStyle.paddingDividerUnreadMessage, child: Divider( @@ -174,30 +238,33 @@ class Message extends StatelessWidget { ), alignment: Alignment.bottomCenter, child: SwipeableMessage( - event: event, - onSwipe: onSwipe, + event: widget.event, + onSwipe: widget.onSwipe, child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: event.isOwnMessage + crossAxisAlignment: widget.event.isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ GestureDetector( - onLongPress: () => selectMode ? onSelect!(event) : null, - onTap: () => selectMode - ? onSelect!(event) - : hideKeyboardChatScreen?.call(), + onLongPress: () => widget.selectMode + ? widget.onSelect!(widget.event) + : null, + onTap: () => widget.selectMode + ? widget.onSelect!(widget.event) + : widget.hideKeyboardChatScreen?.call(), child: Center( child: Container( padding: EdgeInsets.only( - right: selected + right: widget.selected ? 0 - : event.isOwnMessage || - responsiveUtils.isDesktop(context) + : widget.event.isOwnMessage || + Message.responsiveUtils + .isDesktop(context) ? 8.0 : 16.0, - top: selected ? 0 : 1.0, - bottom: selected ? 0 : 1.0, + top: widget.selected ? 0 : 1.0, + bottom: widget.selected ? 0 : 1.0, ), child: _messageSelectedWidget(context, row), ), @@ -215,7 +282,7 @@ class Message extends StatelessWidget { } Widget _placeHolderWidget(bool sameSender, bool ownMessage, Event event) { - if (selectMode || event.room.isDirectChat) { + if (widget.selectMode || event.room.isDirectChat) { return const SizedBox(); } @@ -229,7 +296,7 @@ class Message extends StatelessWidget { fontSize: MessageStyle.fontSize, mxContent: user.avatarUrl, name: user.calcDisplayname(), - onTap: () => onAvatarTap!(event), + onTap: () => widget.onAvatarTap!(event), ); }, ); @@ -241,9 +308,9 @@ class Message extends StatelessWidget { Widget _messageSelectedWidget(BuildContext context, Widget child) { return Container( padding: EdgeInsets.only( - left: selectMode ? 12.0 : 8.0, + left: widget.selectMode ? 12.0 : 8.0, ), - color: selected + color: widget.selected ? LinagoraSysColors.material().secondaryContainer : Theme.of(context).primaryColor.withAlpha(0), constraints: @@ -251,7 +318,7 @@ class Message extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, children: [ - if (selectMode) + if (widget.selectMode) Align( alignment: AlignmentDirectional.centerStart, child: Padding( @@ -259,8 +326,10 @@ class Message extends StatelessWidget { start: 16.0, ), child: Icon( - selected ? Icons.check_circle_rounded : Icons.circle_outlined, - color: selected + widget.selected + ? Icons.check_circle_rounded + : Icons.circle_outlined, + color: widget.selected ? LinagoraSysColors.material().primary : Colors.black, size: 20, diff --git a/pubspec.lock b/pubspec.lock index 7875c89223..6f547cc63c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1355,8 +1355,8 @@ packages: dependency: "direct main" description: path: "." - ref: support-all-listview-properties - resolved-ref: "0a4f3775ce24fb724c22d81d6487fcebe6bcbe88" + ref: master + resolved-ref: f7a29ac1c4de3ea26d749c38781a6f5e9b1b932d url: "git@github.com:linagora/inview_notifier_list.git" source: git version: "3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index c6d8be6271..f1c7d0a1fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,8 @@ dependencies: url: git@github.com:krabbenprgr/receive_sharing_intent.git ref: receive-.txt-v2 + # TODO: Android native lib build error: https://github.com/jonataslaw/VideoCompress/issues/240 + # video_compress: ^3.1.1 video_compress: git: url: git@github.com:SpectoraSoftware/VideoCompress.git @@ -38,16 +40,20 @@ dependencies: url: git@github.com:linagora/flutter_contacts.git ref: master + # TODO: Remove it after this PR merged + # https://github.com/justsoft/video_thumbnail/pull/135 video_thumbnail: git: url: https://github.com/maRci002/video_thumbnail.git ref: feat-web_implementation path: video_thumbnail + # TODO: We will change it when the PR in upstream repository will be merged + # https://github.com/rvamsikrishna/inview_notifier_list/pull/60 inview_notifier_list: git: url: git@github.com:linagora/inview_notifier_list.git - ref: support-all-listview-properties + ref: master adaptive_dialog: ^1.8.0+1 flutter_adaptive_scaffold: ^0.1.4 @@ -125,8 +131,6 @@ dependencies: universal_html: ^2.0.8 url_launcher: ^6.0.20 vibration: ^1.7.4-nullsafety.0 - # TO DO: Android native lib build error: https://github.com/jonataslaw/VideoCompress/issues/240 - # video_compress: ^3.1.1 go_router: ^10.0.0 wakelock: ^0.6.2 webrtc_interface: ^1.0.10 @@ -154,8 +158,6 @@ dependencies: image_gallery_saver: ^2.0.3 file_saver: ^0.1.1 flutter_keyboard_visibility: ^6.0.0 - # TODO: Remove it after this PR merged - # https://github.com/justsoft/video_thumbnail/pull/135 media_kit: ^1.1.7 media_kit_video: ^1.1.8 media_kit_libs_video: ^1.0.1