improved performance and battery usage on older devices
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user