From 3e01544a755c78b466658c78d5936e9198071dab Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 19 Apr 2026 10:42:43 +0900 Subject: [PATCH] fix: UX feedback for sending events --- lib/pages/chat/chat.dart | 12 +- lib/pages/chat/chat_event_list.dart | 4 +- lib/pages/chat/events/message.dart | 311 ++++++++++++++++------------ 3 files changed, 182 insertions(+), 145 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 7572566a..97374d23 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -463,14 +463,17 @@ class ChatController extends State scrollUpBannerEventId = eventId; }); - bool firstUpdateReceived = false; + String? animateInEventId; + + void _insert(int index) { + if (index > 0) return; + animateInEventId = timeline?.events.firstOrNull?.eventId; + } void updateView() { if (!mounted) return; setReadMarker(); - setState(() { - firstUpdateReceived = true; - }); + setState(() {}); } Future? loadTimelineFuture; @@ -487,6 +490,7 @@ class ChatController extends State timeline?.cancelSubscriptions(); timeline = await room.getTimeline( onUpdate: updateView, + onInsert: _insert, eventContextId: eventContextId, ); } catch (e, s) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 65021ad0..d33c398c 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -117,9 +117,7 @@ class ChatEventList extends StatelessWidget { // The message at this index: final event = events[i]; - final animateIn = - event.eventId == timeline.events.first.eventId && - controller.firstUpdateReceived; + final animateIn = event.eventId == controller.animateInEventId; final nextEvent = i + 1 < events.length ? events[i + 1] : null; final previousEvent = i > 0 ? events[i - 1] : null; diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 9bcd39a9..f85b8604 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -203,6 +203,8 @@ class Message extends StatelessWidget { final enterThread = this.enterThread; final sender = event.senderFromMemoryOrFallback; + final fileSendingStatus = event.fileSendingStatus; + return _AnimateIn( animateIn: animateIn, child: Center( @@ -316,9 +318,33 @@ class Message extends StatelessWidget { height: 16, child: event.status == EventStatus.error ? const Icon(Icons.error, color: Colors.red) - : event.fileSendingStatus != null - ? const CircularProgressIndicator.adaptive( - strokeWidth: 1, + : fileSendingStatus != null + ? Stack( + children: [ + Center( + child: switch (fileSendingStatus) { + FileSendingStatus + .generatingThumbnail => + Icon( + Icons.compress_outlined, + size: 14, + ), + FileSendingStatus.encrypting => + Icon( + Icons.lock_outlined, + size: 14, + ), + FileSendingStatus.uploading => + Icon( + Icons.upload_outlined, + size: 14, + ), + }, + ), + const CircularProgressIndicator.adaptive( + strokeWidth: 1, + ), + ], ) : null, ), @@ -430,147 +456,161 @@ class Message extends StatelessWidget { HapticFeedback.heavyImpact(); onSelect(event); }, - child: Container( - decoration: BoxDecoration( - color: noBubble - ? Colors.transparent - : color, - borderRadius: borderRadius, - ), - clipBehavior: Clip.antiAlias, - child: BubbleBackground( - colors: colors, - ignore: - noBubble || - !ownMessage || - MediaQuery.highContrastOf(context), - scrollController: scrollController, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + child: AnimatedOpacity( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + opacity: + event.status.isSending || + event.type == EventTypes.Encrypted + ? 0.5 + : 1, + child: Container( + decoration: BoxDecoration( + color: noBubble + ? Colors.transparent + : color, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + child: BubbleBackground( + colors: colors, + ignore: + noBubble || + !ownMessage || + MediaQuery.highContrastOf(context), + scrollController: scrollController, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), ), - ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 1.5, - ), - child: Column( - mainAxisSize: .min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (event.inReplyToEventId( - includingFallback: false, - ) != - null) - FutureBuilder( - future: event.getReplyEvent( - timeline, - ), - builder: (BuildContext context, snapshot) { - final replyEvent = - snapshot.hasData - ? snapshot.data! - : Event( - eventId: - event - .inReplyToEventId() ?? - '\$fake_event_id', - content: { - 'msgtype': 'm.text', - 'body': '...', - }, - senderId: - event.senderId, - type: - 'm.room.message', - room: event.room, - status: - EventStatus.sent, - originServerTs: - DateTime.now(), - ); - return Padding( - padding: - const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - ), - child: Material( - color: Colors.transparent, - borderRadius: ReplyContent - .borderRadius, - child: InkWell( + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 1.5, + ), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (event.inReplyToEventId( + includingFallback: false, + ) != + null) + FutureBuilder( + future: event.getReplyEvent( + timeline, + ), + builder: (BuildContext context, snapshot) { + final replyEvent = + snapshot.hasData + ? snapshot.data! + : Event( + eventId: + event + .inReplyToEventId() ?? + '\$fake_event_id', + content: { + 'msgtype': + 'm.text', + 'body': '...', + }, + senderId: + event.senderId, + type: + 'm.room.message', + room: event.room, + status: EventStatus + .sent, + originServerTs: + DateTime.now(), + ); + return Padding( + padding: + const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + ), + child: Material( + color: + Colors.transparent, borderRadius: ReplyContent .borderRadius, - onTap: () => - scrollToEventId( - replyEvent - .eventId, + child: InkWell( + borderRadius: + ReplyContent + .borderRadius, + onTap: () => + scrollToEventId( + replyEvent + .eventId, + ), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: + ownMessage, + timeline: + timeline, ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: - ownMessage, - timeline: timeline, ), ), ), - ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - linkColor: linkColor, - onInfoTab: onInfoTab, - borderRadius: borderRadius, - timeline: timeline, - selected: selected, - bigEmojis: bigEmojis, - ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) - Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - left: 16.0, - right: 16.0, + ); + }, ), - child: Row( - mainAxisSize: - MainAxisSize.min, - spacing: 4.0, - children: [ - Icon( - Icons.edit_outlined, - color: textColor - .withAlpha(164), - size: 14, - ), - Text( - displayEvent - .originServerTs - .localizedTimeShort( - context, - ), - style: TextStyle( + MessageContent( + displayEvent, + textColor: textColor, + linkColor: linkColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + timeline: timeline, + selected: selected, + bigEmojis: bigEmojis, + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) + Padding( + padding: + const EdgeInsets.only( + bottom: 8.0, + left: 16.0, + right: 16.0, + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + spacing: 4.0, + children: [ + Icon( + Icons.edit_outlined, color: textColor .withAlpha(164), - fontSize: 11, + size: 14, ), - ), - ], + Text( + displayEvent + .originServerTs + .localizedTimeShort( + context, + ), + style: TextStyle( + color: textColor + .withAlpha(164), + fontSize: 11, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ), @@ -957,15 +997,10 @@ class __AnimateInState extends State<_AnimateIn> { }); }); } - return AnimatedOpacity( + return AnimatedSize( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - opacity: _animationFinished ? 1 : 0, - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: _animationFinished ? widget.child : const SizedBox.shrink(), - ), + child: _animationFinished ? widget.child : const SizedBox.shrink(), ); } }