improved performance and battery usage on older devices

This commit is contained in:
2026-09-25 23:58:08 +02:00
parent ed8e52f475
commit 56a497985b
70 changed files with 1631 additions and 621 deletions
+37 -13
View File
@@ -59,6 +59,13 @@ class _ChatBubbleState extends State<ChatBubble>
with SingleTickerProviderStateMixin, DownloadTrigger<ChatBubble> {
late ChatMessage message;
// The parsed message and its widget are only rebuilt when their inputs
// change. The bubble itself rebuilds on every chat emit, swipe frame and
// keyboard frame; re-running rich-object parsing, linkify/Markdown and emoji
// detection each time is what makes long chats stutter.
Object? _messageKey;
late Widget _messageWidget;
Offset _position = Offset.zero;
Offset _dragStartPosition = Offset.zero;
bool _swipeActionArmed = false;
@@ -185,12 +192,34 @@ class _ChatBubbleState extends State<ChatBubble>
}
}
@override
Widget build(BuildContext context) {
void _updateMessage(BuildContext context) {
final style = _messageTextStyle(context);
final renderMarkdown =
widget.bubbleData.markdown &&
widget.bubbleData.messageType ==
GetRoomResponseObjectMessageType.comment;
final key = (
widget.bubbleData,
widget.highlightQuery,
style,
renderMarkdown,
);
if (key == _messageKey) return;
_messageKey = key;
message = ChatMessage(
originalMessage: widget.bubbleData.message,
originalData: widget.bubbleData.messageParameters,
);
_messageWidget = message.getWidget(
highlightQuery: widget.highlightQuery,
style: style,
renderMarkdown: renderMarkdown,
);
}
@override
Widget build(BuildContext context) {
_updateMessage(context);
final showActorDisplayName =
_rendersAsCommentBubble &&
widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
@@ -277,14 +306,7 @@ class _ChatBubbleState extends State<ChatBubble>
actorText: actorText,
actorWidget: actorWidget,
timeText: timeText,
messageWidget: message.getWidget(
highlightQuery: widget.highlightQuery,
style: _messageTextStyle(context),
renderMarkdown:
widget.bubbleData.markdown &&
widget.bubbleData.messageType ==
GetRoomResponseObjectMessageType.comment,
),
messageWidget: _messageWidget,
parent: parent,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
@@ -350,10 +372,12 @@ class _BubbleContent extends StatelessWidget {
Widget build(BuildContext context) => MergeSemantics(
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxWidth: MediaQuery.sizeOf(context).width * 0.9,
minWidth: showActorDisplayName
? actorText.size.width
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
? actorText.measuredWidth(context)
: timeText.measuredWidth(context) +
(isSender ? spacing + timeIconSize : 0) +
3,
),
child: Stack(
children: [
@@ -34,7 +34,7 @@ class ChatBubbleReactions extends StatelessWidget {
return Transform.translate(
offset: const Offset(0, -10),
child: Container(
width: MediaQuery.of(context).size.width,
width: MediaQuery.sizeOf(context).width,
margin: const EdgeInsets.only(left: 15, right: 15),
child: Wrap(
alignment: isSender ? WrapAlignment.end : WrapAlignment.start,
+24 -11
View File
@@ -83,12 +83,20 @@ class _ChatTextfieldState extends State<ChatTextfield> {
);
}
/// Called per keystroke, so the text itself is saved silently; only a draft
/// appearing or disappearing notifies listeners (the chat list's marker).
void _setDraft(String text) {
final talkSettings = settings.val(write: true).talkSettings;
final drafts = settings.val().talkSettings.drafts;
final hadDraft = drafts.containsKey(widget.sendToToken);
if (text.isNotEmpty) {
talkSettings.drafts[widget.sendToToken] = text;
drafts[widget.sendToToken] = text;
} else {
talkSettings.drafts.removeWhere((key, _) => key == widget.sendToToken);
drafts.remove(widget.sendToToken);
}
if (hadDraft != text.isNotEmpty) {
settings.val(write: true);
} else {
settings.saveSilently();
}
}
@@ -276,17 +284,22 @@ class _ChatTextfieldState extends State<ChatTextfield> {
@override
Widget build(BuildContext context) {
final chatBloc = context.watch<ChatBloc>();
final chatState = chatBloc.state.data;
final chatBloc = context.read<ChatBloc>();
// Only the reply reference is rendered from the chat state; loading flags
// and paging emits don't need to rebuild the input.
final (referenceMessageId, chatResponse) = context.select(
(ChatBloc b) => (
b.state.data?.referenceMessageId,
b.state.data?.chatResponse,
),
);
Widget replyBanner = const SizedBox.shrink();
if (chatState != null &&
chatState.referenceMessageId != null &&
chatState.chatResponse != null) {
if (referenceMessageId != null && chatResponse != null) {
try {
final referenceMessage = chatState.chatResponse!
.sortByTimestamp()
.firstWhere((e) => e.id == chatState.referenceMessageId);
final referenceMessage = chatResponse.data.firstWhere(
(e) => e.id == referenceMessageId,
);
replyBanner = Row(
children: [
Expanded(
+17 -3
View File
@@ -68,15 +68,25 @@ class _ChatTileState extends State<ChatTile> {
/// One-line preview of the last message: rich-object placeholders resolved,
/// newlines flattened and — for Markdown messages — formatting stripped so
/// the list shows readable text rather than raw markers.
///
/// Memoised per last message: the tile rebuilds on every chat-list emit and
/// the Markdown strip is a full parse. Keyed by content, since every refresh
/// delivers new message objects.
String _lastMessagePreview() {
final last = widget.data.lastMessage;
final key = (last.id, last.message, last.markdown);
if (key == _previewFor) return _preview;
final text = RichObjectStringProcessor.parseToString(
last.message.replaceAll('\n', ' '),
last.messageParameters,
);
return last.markdown ? markdownToPlainText(text) : text;
_previewFor = key;
return _preview = last.markdown ? markdownToPlainText(text) : text;
}
Object? _previewFor;
String _preview = '';
Future<void> _setCurrentAsRead() async {
final token = widget.data.token;
final lastId = widget.data.lastMessage.id;
@@ -89,7 +99,11 @@ class _ChatTileState extends State<ChatTile> {
@override
Widget build(BuildContext context) {
final chatBloc = context.watch<ChatBloc>();
// Only the open token matters here (split-view highlight); watching the
// whole bloc rebuilt every tile on each message of the open chat.
final currentToken = context.select(
(ChatBloc b) => b.state.data?.currentToken,
);
final isGroup =
widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(
@@ -100,7 +114,7 @@ class _ChatTileState extends State<ChatTile> {
return ListTile(
style: ListTileStyle.list,
tileColor:
chatBloc.state.data?.currentToken == widget.data.token &&
currentToken == widget.data.token &&
TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100)
: null,