chore: Adjust styles and animations

This commit is contained in:
Christian Kußowski 2026-03-12 12:03:03 +01:00
parent 5c88133691
commit 9724b852bb
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
5 changed files with 656 additions and 761 deletions

View file

@ -462,21 +462,18 @@ class ChatController extends State<ChatPageWithRoom>
scrollUpBannerEventId = eventId;
});
bool firstUpdateReceived = false;
void updateView() {
if (!mounted) return;
setReadMarker();
setState(() {});
setState(() {
firstUpdateReceived = true;
});
}
Future<void>? loadTimelineFuture;
int? animateInEventIndex;
void onInsert(int i) {
// setState will be called by updateView() anyway
if (timeline?.allowNewEvent == true) animateInEventIndex = i;
}
Future<void> _getTimeline({String? eventContextId}) async {
await Matrix.of(context).client.roomsLoading;
await Matrix.of(context).client.accountDataLoading;
@ -489,15 +486,11 @@ class ChatController extends State<ChatPageWithRoom>
timeline = await room.getTimeline(
onUpdate: updateView,
eventContextId: eventContextId,
onInsert: onInsert,
);
} catch (e, s) {
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
if (!mounted) return;
timeline = await room.getTimeline(
onUpdate: updateView,
onInsert: onInsert,
);
timeline = await room.getTimeline(onUpdate: updateView);
if (!mounted) return;
if (e is TimeoutException || e is IOException) {
_showScrollUpMaterialBanner(eventContextId!);

View file

@ -35,7 +35,6 @@ class ChatEventList extends StatelessWidget {
final events = timeline.events.filterByVisibleInGui(
threadId: controller.activeThreadId,
);
final animateInEventIndex = controller.animateInEventIndex;
// create a map of eventId --> index to greatly improve performance of
// ListView's findChildIndexCallback
@ -120,10 +119,7 @@ class ChatEventList extends StatelessWidget {
// The message at this index:
final event = events[i];
final animateIn =
animateInEventIndex != null &&
timeline.events.length > animateInEventIndex &&
event == timeline.events[animateInEventIndex];
final animateIn = i == 0 && controller.firstUpdateReceived;
final nextEvent = i + 1 < events.length ? events[i + 1] : null;
final previousEvent = i > 0 ? events[i - 1] : null;
@ -139,16 +135,13 @@ class ChatEventList extends StatelessWidget {
!controller.expandedEventIds.contains(event.eventId);
return AutoScrollTag(
key: ValueKey(event.eventId),
key: ValueKey(event.transactionId ?? event.eventId),
index: i,
controller: controller.scrollController,
child: Message(
event,
bigEmojis: controller.bigEmojis,
animateIn: animateIn,
resetAnimateIn: () {
controller.animateInEventIndex = null;
},
onSwipe: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onMention: () => controller.sendController.text +=

View file

@ -42,7 +42,6 @@ class Message extends StatelessWidget {
final Timeline timeline;
final bool highlightMarker;
final bool animateIn;
final void Function()? resetAnimateIn;
final bool wallpaperMode;
final ScrollController scrollController;
final List<Color> colors;
@ -67,7 +66,6 @@ class Message extends StatelessWidget {
required this.timeline,
this.highlightMarker = false,
this.animateIn = false,
this.resetAnimateIn,
this.wallpaperMode = false,
required this.onMention,
required this.scrollController,
@ -171,9 +169,6 @@ class Message extends StatelessWidget {
: theme.bubbleColor;
}
final resetAnimateIn = this.resetAnimateIn;
var animateIn = this.animateIn;
final sentReactions = <String>{};
if (singleSelected) {
sentReactions.addAll(
@ -209,7 +204,9 @@ class Message extends StatelessWidget {
final enterThread = this.enterThread;
final sender = event.senderFromMemoryOrFallback;
return Center(
return _AnimateIn(
animateIn: animateIn,
child: Center(
child: Swipeable(
key: ValueKey(event.eventId),
background: const Padding(
@ -265,24 +262,7 @@ class Message extends StatelessWidget {
),
),
),
StatefulBuilder(
builder: (context, setState) {
if (animateIn && resetAnimateIn != null) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
animateIn = false;
setState(resetAnimateIn);
});
}
return AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.none,
alignment: ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
child: animateIn
? const SizedBox(height: 0, width: double.infinity)
: Stack(
Stack(
clipBehavior: Clip.none,
children: [
Positioned(
@ -291,13 +271,9 @@ class Message extends StatelessWidget {
left: 0,
right: 0,
child: InkWell(
hoverColor: longPressSelect
? Colors.transparent
: null,
hoverColor: longPressSelect ? Colors.transparent : null,
enableFeedback: !selected,
onTap: longPressSelect
? null
: () => onSelect(event),
onTap: longPressSelect ? null : () => onSelect(event),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 2,
),
@ -306,8 +282,9 @@ class Message extends StatelessWidget {
AppConfig.borderRadius / 2,
),
color: selected || highlightMarker
? theme.colorScheme.secondaryContainer
.withAlpha(128)
? theme.colorScheme.secondaryContainer.withAlpha(
128,
)
: Colors.transparent,
),
),
@ -338,12 +315,8 @@ class Message extends StatelessWidget {
child: SizedBox(
width: 16,
height: 16,
child:
event.status == EventStatus.error
? const Icon(
Icons.error,
color: Colors.red,
)
child: event.status == EventStatus.error
? const Icon(Icons.error, color: Colors.red)
: event.fileSendingStatus != null
? const CircularProgressIndicator.adaptive(
strokeWidth: 1,
@ -360,8 +333,7 @@ class Message extends StatelessWidget {
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
onTap: () =>
showMemberActionsPopupMenu(
onTap: () => showMemberActionsPopupMenu(
context: context,
user: user,
onMention: onMention,
@ -384,22 +356,17 @@ class Message extends StatelessWidget {
left: 8.0,
bottom: 4,
),
child:
ownMessage ||
event.room.isDirectChat
child: ownMessage || event.room.isDirectChat
? const SizedBox(height: 12)
: Row(
children: [
if (sender.powerLevel >=
50)
if (sender.powerLevel >= 50)
Padding(
padding:
const EdgeInsets.only(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(
sender.powerLevel >=
100
sender.powerLevel >= 100
? Icons
.admin_panel_settings
: Icons
@ -412,31 +379,25 @@ class Message extends StatelessWidget {
),
Expanded(
child: FutureBuilder<User?>(
future: event
.fetchSenderUser(),
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname =
snapshot.data
?.calcDisplayname() ??
sender
.calcDisplayname();
sender.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontSize: 11,
fontWeight:
FontWeight
.bold,
FontWeight.bold,
color:
(theme.brightness ==
Brightness
.light
? displayname
.color
Brightness.light
? displayname.color
: displayname
.lightColorText),
shadows:
!wallpaperMode
shadows: !wallpaperMode
? null
: [
const Shadow(
@ -444,17 +405,15 @@ class Message extends StatelessWidget {
0.0,
0.0,
),
blurRadius:
3,
color:
Colors.black,
blurRadius: 3,
color: Colors
.black,
),
],
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
TextOverflow.ellipsis,
);
},
),
@ -464,9 +423,7 @@ class Message extends StatelessWidget {
),
Container(
alignment: alignment,
padding: const EdgeInsets.only(
left: 8,
),
padding: const EdgeInsets.only(left: 8),
child: GestureDetector(
onLongPress: longPressSelect
? null
@ -474,19 +431,6 @@ class Message extends StatelessWidget {
HapticFeedback.heavyImpact();
onSelect(event);
},
child: AnimatedOpacity(
opacity: animateIn
? 0
: event.messageType ==
MessageTypes
.BadEncrypted ||
event.status.isSending
? 0.5
: 1,
duration: FluffyThemes
.animationDuration,
curve:
FluffyThemes.animationCurve,
child: Container(
decoration: BoxDecoration(
color: noBubble
@ -500,69 +444,49 @@ class Message extends StatelessWidget {
ignore:
noBubble ||
!ownMessage ||
MediaQuery.highContrastOf(
context,
),
scrollController:
scrollController,
MediaQuery.highContrastOf(context),
scrollController: scrollController,
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
AppConfig
.borderRadius,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
constraints:
const BoxConstraints(
constraints: const BoxConstraints(
maxWidth:
FluffyThemes
.columnWidth *
1.5,
FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: .min,
crossAxisAlignment:
CrossAxisAlignment
.start,
CrossAxisAlignment.start,
children: <Widget>[
if (event.inReplyToEventId(
includingFallback:
false,
includingFallback: false,
) !=
null)
FutureBuilder<Event?>(
future: event
.getReplyEvent(
future: event.getReplyEvent(
timeline,
),
builder:
(
BuildContext
context,
snapshot,
) {
builder: (BuildContext context, snapshot) {
final replyEvent =
snapshot
.hasData
? snapshot
.data!
snapshot.hasData
? snapshot.data!
: Event(
eventId:
event.inReplyToEventId() ??
event
.inReplyToEventId() ??
'\$fake_event_id',
content: {
'msgtype':
'm.text',
'body':
'...',
'msgtype': 'm.text',
'body': '...',
},
senderId:
event.senderId,
type:
'm.room.message',
room:
event.room,
room: event.room,
status:
EventStatus.sent,
originServerTs:
@ -571,23 +495,20 @@ class Message extends StatelessWidget {
return Padding(
padding:
const EdgeInsets.only(
left:
16,
right:
16,
top:
8,
left: 16,
right: 16,
top: 8,
),
child: Material(
color: Colors
.transparent,
borderRadius:
ReplyContent
color: Colors.transparent,
borderRadius: ReplyContent
.borderRadius,
child: InkWell(
borderRadius:
ReplyContent.borderRadius,
onTap: () => scrollToEventId(
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent
.eventId,
),
@ -596,8 +517,7 @@ class Message extends StatelessWidget {
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
timeline: timeline,
),
),
),
@ -610,38 +530,30 @@ class Message extends StatelessWidget {
textColor: textColor,
linkColor: linkColor,
onInfoTab: onInfoTab,
borderRadius:
borderRadius,
borderRadius: borderRadius,
timeline: timeline,
selected: selected,
bigEmojis: bigEmojis,
),
if (event
.hasAggregatedEvents(
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
RelationshipTypes.edit,
))
Padding(
padding:
const EdgeInsets.only(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize:
MainAxisSize
.min,
MainAxisSize.min,
spacing: 4.0,
children: [
Icon(
Icons
.edit_outlined,
Icons.edit_outlined,
color: textColor
.withAlpha(
164,
),
.withAlpha(164),
size: 14,
),
Text(
@ -652,11 +564,8 @@ class Message extends StatelessWidget {
),
style: TextStyle(
color: textColor
.withAlpha(
164,
),
fontSize:
11,
.withAlpha(164),
fontSize: 11,
),
),
],
@ -669,76 +578,61 @@ class Message extends StatelessWidget {
),
),
),
),
Align(
alignment: ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
child: AnimatedSize(
duration:
FluffyThemes.animationDuration,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: showReactionPicker
? Padding(
padding:
const EdgeInsets.all(
4.0,
),
padding: const EdgeInsets.all(4.0),
child: Material(
elevation: 4,
borderRadius:
BorderRadius.circular(
AppConfig
.borderRadius,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
shadowColor: theme
.colorScheme
.surface
.withAlpha(128),
child: SingleChildScrollView(
scrollDirection:
Axis.horizontal,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: .min,
children: [
...AppConfig.defaultReactions.map(
(
emoji,
) => IconButton(
padding:
EdgeInsets
.zero,
(emoji) => IconButton(
padding: EdgeInsets.zero,
icon: Center(
child: Opacity(
opacity:
sentReactions.contains(
sentReactions
.contains(
emoji,
)
? 0.33
: 1,
child: Text(
emoji,
style: const TextStyle(
fontSize:
20,
style:
const TextStyle(
fontSize: 20,
),
textAlign:
TextAlign
textAlign: TextAlign
.center,
),
),
),
onPressed:
sentReactions
.contains(
emoji,
)
.contains(emoji)
? null
: () {
onSelect(
event,
);
event.room.sendReaction(
onSelect(event);
event.room
.sendReaction(
event
.eventId,
emoji,
@ -756,8 +650,7 @@ class Message extends StatelessWidget {
).customReaction,
onPressed: () async {
final emoji = await showAdaptiveBottomSheet<String>(
context:
context,
context: context,
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(
@ -766,82 +659,94 @@ class Message extends StatelessWidget {
).customReaction,
),
leading: CloseButton(
onPressed: () => Navigator.of(
onPressed: () =>
Navigator.of(
context,
).pop(null),
),
),
body: SizedBox(
height: double
.infinity,
height:
double.infinity,
child: EmojiPicker(
onEmojiSelected:
(
_,
emoji,
) =>
(_, emoji) =>
Navigator.of(
context,
).pop(
emoji.emoji,
emoji
.emoji,
),
config: Config(
locale: Localizations.localeOf(
locale:
Localizations.localeOf(
context,
),
emojiViewConfig: const EmojiViewConfig(
emojiViewConfig:
const EmojiViewConfig(
backgroundColor:
Colors.transparent,
Colors
.transparent,
),
bottomActionBarConfig: const BottomActionBarConfig(
bottomActionBarConfig:
const BottomActionBarConfig(
enabled:
false,
),
categoryViewConfig: CategoryViewConfig(
initCategory:
Category.SMILEYS,
backspaceColor:
theme.colorScheme.primary,
iconColor: theme.colorScheme.primary.withAlpha(
Category
.SMILEYS,
backspaceColor: theme
.colorScheme
.primary,
iconColor: theme
.colorScheme
.primary
.withAlpha(
128,
),
iconColorSelected:
theme.colorScheme.primary,
indicatorColor:
theme.colorScheme.primary,
backgroundColor:
theme.colorScheme.surface,
iconColorSelected: theme
.colorScheme
.primary,
indicatorColor: theme
.colorScheme
.primary,
backgroundColor: theme
.colorScheme
.surface,
),
skinToneConfig: SkinToneConfig(
dialogBackgroundColor: Color.lerp(
theme.colorScheme.surface,
theme.colorScheme.primaryContainer,
theme
.colorScheme
.surface,
theme
.colorScheme
.primaryContainer,
0.75,
)!,
indicatorColor:
theme.colorScheme.onSurface,
indicatorColor: theme
.colorScheme
.onSurface,
),
),
),
),
),
);
if (emoji ==
null) {
if (emoji == null) {
return;
}
if (sentReactions
.contains(
emoji,
)) {
.contains(emoji)) {
return;
}
onSelect(event);
await event.room
.sendReaction(
event
.eventId,
event.eventId,
emoji,
);
},
@ -861,9 +766,6 @@ class Message extends StatelessWidget {
),
],
),
);
},
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
@ -959,6 +861,7 @@ class Message extends StatelessWidget {
),
),
),
),
);
}
}
@ -1033,3 +936,37 @@ class BubblePainter extends CustomPainter {
return scrollable.position != oldScrollable?.position;
}
}
class _AnimateIn extends StatefulWidget {
final bool animateIn;
final Widget child;
const _AnimateIn({required this.animateIn, required this.child});
@override
State<_AnimateIn> createState() => __AnimateInState();
}
class __AnimateInState extends State<_AnimateIn> {
bool _animationFinished = false;
@override
Widget build(BuildContext context) {
if (!widget.animateIn) return widget.child;
if (!_animationFinished) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_animationFinished = true;
});
});
}
return AnimatedOpacity(
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(),
),
);
}
}

View file

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/file_selector.dart';
@ -111,32 +110,6 @@ class SettingsStyleController extends State<SettingsStyle> {
ThemeMode get currentTheme => ThemeController.of(context).themeMode;
Color? get currentColor => ThemeController.of(context).primaryColor;
static final List<Color?> customColors = [
null,
AppConfig.chatColor,
Colors.indigo,
Colors.blue,
Colors.blueAccent,
Colors.teal,
Colors.tealAccent,
Colors.green,
Colors.greenAccent,
Colors.yellow,
Colors.yellowAccent,
Colors.orange,
Colors.orangeAccent,
Colors.red,
Colors.redAccent,
Colors.pink,
Colors.pinkAccent,
Colors.purple,
Colors.purpleAccent,
Colors.blueGrey,
Colors.grey,
Colors.white,
Colors.black,
];
void switchTheme(ThemeMode? newTheme) {
if (newTheme == null) return;
switch (newTheme) {

View file

@ -82,14 +82,13 @@ class SettingsStyleView extends StatelessWidget {
Theme.of(context).brightness == Brightness.light
? light?.primary
: dark?.primary;
final colors = List<Color?>.from(
SettingsStyleController.customColors,
);
final colors = [null, AppConfig.chatColor, ...Colors.primaries];
if (systemColor == null) {
colors.remove(null);
}
return GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 64,
),