Merge pull request #2879 from krille-chan/krille/fix-sending-indicator

fix: UX feedback for sending events
This commit is contained in:
Krille-chan 2026-04-19 11:08:07 +09:00 committed by GitHub
commit 27125f8762
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 182 additions and 145 deletions

View file

@ -463,14 +463,17 @@ class ChatController extends State<ChatPageWithRoom>
scrollUpBannerEventId = eventId; scrollUpBannerEventId = eventId;
}); });
bool firstUpdateReceived = false; String? animateInEventId;
void _insert(int index) {
if (index > 0) return;
animateInEventId = timeline?.events.firstOrNull?.eventId;
}
void updateView() { void updateView() {
if (!mounted) return; if (!mounted) return;
setReadMarker(); setReadMarker();
setState(() { setState(() {});
firstUpdateReceived = true;
});
} }
Future<void>? loadTimelineFuture; Future<void>? loadTimelineFuture;
@ -487,6 +490,7 @@ class ChatController extends State<ChatPageWithRoom>
timeline?.cancelSubscriptions(); timeline?.cancelSubscriptions();
timeline = await room.getTimeline( timeline = await room.getTimeline(
onUpdate: updateView, onUpdate: updateView,
onInsert: _insert,
eventContextId: eventContextId, eventContextId: eventContextId,
); );
} catch (e, s) { } catch (e, s) {

View file

@ -117,9 +117,7 @@ class ChatEventList extends StatelessWidget {
// The message at this index: // The message at this index:
final event = events[i]; final event = events[i];
final animateIn = final animateIn = event.eventId == controller.animateInEventId;
event.eventId == timeline.events.first.eventId &&
controller.firstUpdateReceived;
final nextEvent = i + 1 < events.length ? events[i + 1] : null; final nextEvent = i + 1 < events.length ? events[i + 1] : null;
final previousEvent = i > 0 ? events[i - 1] : null; final previousEvent = i > 0 ? events[i - 1] : null;

View file

@ -203,6 +203,8 @@ class Message extends StatelessWidget {
final enterThread = this.enterThread; final enterThread = this.enterThread;
final sender = event.senderFromMemoryOrFallback; final sender = event.senderFromMemoryOrFallback;
final fileSendingStatus = event.fileSendingStatus;
return _AnimateIn( return _AnimateIn(
animateIn: animateIn, animateIn: animateIn,
child: Center( child: Center(
@ -316,9 +318,33 @@ class Message extends StatelessWidget {
height: 16, height: 16,
child: event.status == EventStatus.error child: event.status == EventStatus.error
? const Icon(Icons.error, color: Colors.red) ? const Icon(Icons.error, color: Colors.red)
: event.fileSendingStatus != null : fileSendingStatus != null
? const CircularProgressIndicator.adaptive( ? 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, strokeWidth: 1,
),
],
) )
: null, : null,
), ),
@ -430,6 +456,14 @@ class Message extends StatelessWidget {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
onSelect(event); onSelect(event);
}, },
child: AnimatedOpacity(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
opacity:
event.status.isSending ||
event.type == EventTypes.Encrypted
? 0.5
: 1,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: noBubble color: noBubble
@ -478,7 +512,8 @@ class Message extends StatelessWidget {
.inReplyToEventId() ?? .inReplyToEventId() ??
'\$fake_event_id', '\$fake_event_id',
content: { content: {
'msgtype': 'm.text', 'msgtype':
'm.text',
'body': '...', 'body': '...',
}, },
senderId: senderId:
@ -486,8 +521,8 @@ class Message extends StatelessWidget {
type: type:
'm.room.message', 'm.room.message',
room: event.room, room: event.room,
status: status: EventStatus
EventStatus.sent, .sent,
originServerTs: originServerTs:
DateTime.now(), DateTime.now(),
); );
@ -499,8 +534,10 @@ class Message extends StatelessWidget {
top: 8, top: 8,
), ),
child: Material( child: Material(
color: Colors.transparent, color:
borderRadius: ReplyContent Colors.transparent,
borderRadius:
ReplyContent
.borderRadius, .borderRadius,
child: InkWell( child: InkWell(
borderRadius: borderRadius:
@ -516,7 +553,8 @@ class Message extends StatelessWidget {
replyEvent, replyEvent,
ownMessage: ownMessage:
ownMessage, ownMessage,
timeline: timeline, timeline:
timeline,
), ),
), ),
), ),
@ -539,7 +577,8 @@ class Message extends StatelessWidget {
RelationshipTypes.edit, RelationshipTypes.edit,
)) ))
Padding( Padding(
padding: const EdgeInsets.only( padding:
const EdgeInsets.only(
bottom: 8.0, bottom: 8.0,
left: 16.0, left: 16.0,
right: 16.0, right: 16.0,
@ -577,6 +616,7 @@ class Message extends StatelessWidget {
), ),
), ),
), ),
),
Align( Align(
alignment: ownMessage alignment: ownMessage
? Alignment.bottomRight ? Alignment.bottomRight
@ -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, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
child: _animationFinished ? widget.child : const SizedBox.shrink(), child: _animationFinished ? widget.child : const SizedBox.shrink(),
),
); );
} }
} }