diff --git a/lib/chat/bootstrap/view/key_verification_dialog.dart b/lib/chat/bootstrap/view/key_verification_dialog.dart index 06737ba..b800589 100644 --- a/lib/chat/bootstrap/view/key_verification_dialog.dart +++ b/lib/chat/bootstrap/view/key_verification_dialog.dart @@ -10,6 +10,7 @@ import 'package:matrix/matrix.dart'; import 'package:yaru/yaru.dart'; import '../../../common/view/build_context_x.dart'; +import '../../../common/view/ui_constants.dart'; import '../../../l10n/l10n.dart'; import '../../common/view/chat_avatar.dart'; import '../../chat_master/view/chat_master_detail_page.dart'; @@ -218,8 +219,8 @@ class KeyVerificationPageState extends State { avatarUri: user?.avatarUrl, ), const SizedBox( - width: 38, - height: 38, + width: kAvatarDefaultSize, + height: kAvatarDefaultSize, child: CircularProgressIndicator(strokeWidth: 2), ), ], diff --git a/lib/chat/chat_master/view/chat_space_control_panel.dart b/lib/chat/chat_master/view/chat_space_control_panel.dart index d88c22c..17ac8c8 100644 --- a/lib/chat/chat_master/view/chat_space_control_panel.dart +++ b/lib/chat/chat_master/view/chat_space_control_panel.dart @@ -27,7 +27,7 @@ class ChatSpaceControlPanel extends StatelessWidget with WatchItMixin { spacing: kMediumPadding, children: [ SizedBox.square( - dimension: 38, + dimension: kAvatarDefaultSize, child: OutlinedButton( style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, @@ -55,7 +55,7 @@ class ChatSpaceControlPanel extends StatelessWidget with WatchItMixin { ), ), SizedBox.square( - dimension: 38, + dimension: kAvatarDefaultSize, child: OutlinedButton( style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, diff --git a/lib/chat/chat_room/common/view/chat_room_page.dart b/lib/chat/chat_room/common/view/chat_room_page.dart index 21f6d75..c546b1a 100644 --- a/lib/chat/chat_room/common/view/chat_room_page.dart +++ b/lib/chat/chat_room/common/view/chat_room_page.dart @@ -107,7 +107,6 @@ class _ChatRoomPageState extends State { padding: const EdgeInsets.only(bottom: kMediumPadding), child: ChatRoomTimelineList( timeline: snapshot.data!, - room: widget.room, listKey: _roomListKey, ), ); diff --git a/lib/chat/chat_room/common/view/chat_room_timeline_list.dart b/lib/chat/chat_room/common/view/chat_room_timeline_list.dart index b677063..c2459f6 100644 --- a/lib/chat/chat_room/common/view/chat_room_timeline_list.dart +++ b/lib/chat/chat_room/common/view/chat_room_timeline_list.dart @@ -4,14 +4,17 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../../../common/date_time_x.dart'; import '../../../../common/view/build_context_x.dart'; import '../../../../common/view/theme.dart'; import '../../../../common/view/ui_constants.dart'; -import '../../../events/view/chat_event_column.dart'; +import '../../../../l10n/l10n.dart'; +import '../../../common/event_x.dart'; +import '../../../events/view/chat_event_tile.dart'; +import '../../../settings/settings_model.dart'; import '../../titlebar/chat_room_title_bar.dart'; import '../timeline_model.dart'; import 'chat_seen_by_indicator.dart'; -import 'chat_typing_indicator.dart'; class ChatRoomTimelineList extends StatefulWidget with WatchItStatefulWidgetMixin { @@ -19,11 +22,9 @@ class ChatRoomTimelineList extends StatefulWidget super.key, required this.timeline, required this.listKey, - required this.room, }); final Timeline timeline; - final Room room; final GlobalKey listKey; @override @@ -52,67 +53,82 @@ class _ChatRoomTimelineListState extends State { @override Widget build(BuildContext context) { final theme = context.theme; + final showAvatarChanges = + watchPropertyValue((SettingsModel m) => m.showChatAvatarChanges); + final showDisplayNameChanges = + watchPropertyValue((SettingsModel m) => m.showChatDisplaynameChanges); return Stack( children: [ - Column( - children: [ - Expanded( - child: NotificationListener( - onNotification: onScroll, - child: AnimatedList( - controller: _controller, - padding: const EdgeInsets.symmetric( - horizontal: kMediumPadding, - ), - key: widget.listKey, - reverse: true, - initialItemCount: widget.timeline.events.length, - itemBuilder: (context, i, animation) { - final event = widget.timeline.events[i]; - - final maybePreviousEvent = - widget.timeline.events.elementAtOrNull(i + 1); - - if (i == 0 && !widget.room.isArchived) { - widget.timeline.setReadMarker(); - } - - return AutoScrollTag( - index: i, - controller: _controller, - key: ValueKey('${event.eventId}tag'), - child: FadeTransition( - opacity: animation, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ChatEventColumn( - key: ValueKey('${event.eventId}column'), - event: event, - maybePreviousEvent: maybePreviousEvent, - jump: _jump, - showSeenByIndicator: i == 0, - timeline: widget.timeline, - room: widget.room, - ), - if (i == 0) - ChatEventSeenByIndicator( - key: ValueKey( - '${event.eventId}${widget.timeline.events.length}', - ), - event: event, - ), - ], + NotificationListener( + onNotification: onScroll, + child: AnimatedList( + controller: _controller, + padding: const EdgeInsets.symmetric( + horizontal: kMediumPadding, + vertical: kSmallPadding, + ), + key: widget.listKey, + reverse: true, + initialItemCount: widget.timeline.events.length, + itemBuilder: (context, i, animation) { + final event = widget.timeline.events[i]; + + if (event.hideEventInTimeline( + showAvatarChanges: showAvatarChanges, + showDisplayNameChanges: showDisplayNameChanges, + )) { + return SizedBox.shrink( + key: ValueKey(ValueKey(event.eventId)), + ); + } + + final previous = widget.timeline.events.elementAtOrNull(i + 1); + + if (i == 0 && !widget.timeline.room.isArchived) { + widget.timeline.setReadMarker(); + } + + return AutoScrollTag( + index: i, + controller: _controller, + key: ValueKey('${event.eventId}tag'), + child: FadeTransition( + opacity: animation, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (previous != null && + event.originServerTs.toLocal().day != + previous.originServerTs.toLocal().day) + Text( + previous.originServerTs + .toLocal() + .formatAndLocalizeDay(context.l10n), + textAlign: TextAlign.center, + style: theme.textTheme.labelSmall, ), + ChatEventTile( + key: ValueKey('${event.eventId}column'), + event: event, + partOfMessageCohort: + event.partOfMessageCohort(previous), + onReplyOriginClick: (event) => _jump(event), + timeline: widget.timeline, ), - ); - }, + if (i == 0) + ChatEventSeenByIndicator( + key: ValueKey( + '${event.eventId}${widget.timeline.events.length}', + ), + event: event, + ), + ], + ), ), - ), - ), - ChatTypingIndicator(room: widget.room), - ], + ); + }, + ), ), if (_showScrollButton) Positioned( @@ -127,7 +143,8 @@ class _ChatRoomTimelineListState extends State { backgroundColor: getMonochromeBg(theme: theme, darkFactor: 5), onPressed: () => showDialog( context: context, - builder: (context) => ChatRoomSearchDialog(room: widget.room), + builder: (context) => + ChatRoomSearchDialog(room: widget.timeline.room), ), child: Icon( YaruIcons.search, @@ -190,7 +207,7 @@ class _ChatRoomTimelineListState extends State { retryCount--; } await _maybeScrollTo(index); - if (!widget.room.isArchived) { + if (!widget.timeline.room.isArchived) { widget.timeline.setReadMarker(eventId: event.eventId); } } diff --git a/lib/chat/chat_room/common/view/chat_seen_by_indicator.dart b/lib/chat/chat_room/common/view/chat_seen_by_indicator.dart index a75aead..c9dfe12 100644 --- a/lib/chat/chat_room/common/view/chat_seen_by_indicator.dart +++ b/lib/chat/chat_room/common/view/chat_seen_by_indicator.dart @@ -38,11 +38,7 @@ class ChatEventSeenByIndicator extends StatelessWidget with WatchItMixin { return Container( width: double.infinity, - alignment: event.room.isDirectChat || - di().isUserEvent(event) && - event.type != EventTypes.Reaction - ? Alignment.centerRight - : Alignment.centerLeft, + alignment: Alignment.center, child: AnimatedContainer( padding: const EdgeInsets.symmetric( vertical: kSmallPadding, diff --git a/lib/chat/chat_room/common/view/chat_typing_indicator.dart b/lib/chat/chat_room/common/view/chat_typing_indicator.dart index a9d5217..fdafca0 100644 --- a/lib/chat/chat_room/common/view/chat_typing_indicator.dart +++ b/lib/chat/chat_room/common/view/chat_typing_indicator.dart @@ -26,7 +26,7 @@ class ChatTypingIndicator extends StatelessWidget with WatchItMixin { []; return AnimatedContainer( - height: typingUsers.isEmpty ? 0 : kTypingAvatarSize + kMediumPadding, + height: typingUsers.isEmpty ? 0 : kTypingAvatarSize, duration: kAvatarAnimationDuration, curve: kAvatarAnimationCurve, alignment: Alignment.centerLeft, @@ -34,7 +34,6 @@ class ChatTypingIndicator extends StatelessWidget with WatchItMixin { decoration: const BoxDecoration(), padding: const EdgeInsets.symmetric( horizontal: kBigPadding, - vertical: kSmallPadding, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -55,7 +54,7 @@ class ChatTypingIndicator extends StatelessWidget with WatchItMixin { ), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: kMediumPadding, + horizontal: kSmallPadding, ), child: typingUsers.isEmpty ? null : const _TypingDots(), ), @@ -105,27 +104,27 @@ class __TypingDotsState extends State<_TypingDots> { @override Widget build(BuildContext context) { final theme = context.theme; - const size = 8.0; - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (var i = 1; i <= 3; i++) - AnimatedContainer( - duration: animationDuration * 1.5, - curve: Curves.bounceIn, - width: size, - height: _tick == i ? size * 2 : size, - margin: EdgeInsets.symmetric( - horizontal: 2, - vertical: _tick == i ? 4 : 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(size * 2), - color: theme.colorScheme.secondary, + const size = kTypingAvatarSize / 3; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: kSmallPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 1; i <= 3; i++) + AnimatedContainer( + duration: animationDuration * 1.5, + curve: Curves.bounceIn, + width: size, + height: _tick == i ? size * 3 : size, + margin: const EdgeInsets.symmetric(horizontal: size / 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(size * 2), + color: theme.colorScheme.primary, + ), ), - ), - ], + ], + ), ); } } diff --git a/lib/chat/chat_room/input/view/chat_input.dart b/lib/chat/chat_room/input/view/chat_input.dart index 92ab91c..9a16200 100644 --- a/lib/chat/chat_room/input/view/chat_input.dart +++ b/lib/chat/chat_room/input/view/chat_input.dart @@ -11,6 +11,7 @@ import '../../../../common/view/snackbars.dart'; import '../../../../common/view/ui_constants.dart'; import '../../../../l10n/l10n.dart'; import '../../../common/chat_model.dart'; +import '../../common/view/chat_typing_indicator.dart'; import '../draft_model.dart'; import 'chat_attachment_draft_panel.dart'; import 'chat_emoji_picker.dart'; @@ -100,127 +101,138 @@ class _ChatInputState extends State { child: Icon(YaruIcons.send_filled), ), ); - return Material( - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (draftFiles.isNotEmpty) const Divider(height: 1), - if (draftFiles.isNotEmpty) - ChatAttachmentDraftPanel(roomId: widget.room.id), - if (replyEvent != null || editEvent != null) - Padding( - padding: const EdgeInsets.only( - left: kMediumPadding, - right: kMediumPadding, - top: kMediumPadding, - ), - child: YaruInfoBox( - trailing: IconButton( - onPressed: () => di() - ..setReplyEvent(null) - ..setEditEvent(roomId: widget.room.id, event: null) - ..setDraft( - roomId: widget.room.id, - draft: '', - notify: true, + return Stack( + clipBehavior: Clip.none, + children: [ + Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (draftFiles.isNotEmpty) const Divider(height: 1), + if (draftFiles.isNotEmpty) + ChatAttachmentDraftPanel(roomId: widget.room.id), + if (replyEvent != null || editEvent != null) + Padding( + padding: const EdgeInsets.only( + left: kMediumPadding, + right: kMediumPadding, + top: kMediumPadding, + ), + child: YaruInfoBox( + trailing: IconButton( + onPressed: () => di() + ..setReplyEvent(null) + ..setEditEvent(roomId: widget.room.id, event: null) + ..setDraft( + roomId: widget.room.id, + draft: '', + notify: true, + ), + icon: const Icon( + YaruIcons.trash, + ), + ), + yaruInfoType: editEvent != null + ? YaruInfoType.warning + : YaruInfoType.information, + icon: Icon( + editEvent != null ? YaruIcons.pen : YaruIcons.reply, + ), + subtitle: Text( + (editEvent ?? replyEvent)!.plaintextBody, + overflow: TextOverflow.ellipsis, + maxLines: 3, + ), + title: Text( + '${context.l10n.reply} (${(editEvent ?? replyEvent)!.senderFromMemoryOrFallback.displayName}):', ), - icon: const Icon( - YaruIcons.trash, ), ), - yaruInfoType: editEvent != null - ? YaruInfoType.warning - : YaruInfoType.information, - icon: Icon(editEvent != null ? YaruIcons.pen : YaruIcons.reply), - subtitle: Text( - (editEvent ?? replyEvent)!.plaintextBody, - overflow: TextOverflow.ellipsis, - maxLines: 3, - ), - title: Text( - '${context.l10n.reply} (${(editEvent ?? replyEvent)!.senderFromMemoryOrFallback.displayName}):', - ), - ), - ), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.all(kMediumPadding), - child: TextField( - minLines: 1, - maxLines: 10, - focusNode: _sendNode, - controller: _sendController, - enabled: watchPropertyValue((ChatModel m) => !m.archiveActive), - autofocus: true, - onChanged: (v) { - draftModel.setDraft( - roomId: widget.room.id, - draft: v, - notify: false, - ); - widget.room.setTyping(v.isNotEmpty, timeout: 500); - }, - // onSubmitted: (_) => send.call(), - decoration: InputDecoration( - hintText: context.l10n.sendAMessage, - prefixIcon: Padding( - padding: const EdgeInsets.all(kSmallPadding), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - padding: EdgeInsets.zero, - onPressed: attaching - ? null - : () => draftModel.addAttachment( - widget.room.id, - onFail: (error) => - showErrorSnackBar(context, error), - ), - icon: attaching - ? const Center( - child: SizedBox.square( - dimension: 15, - child: Progress( - strokeWidth: 1, + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(kMediumPadding), + child: TextField( + minLines: 1, + maxLines: 10, + focusNode: _sendNode, + controller: _sendController, + enabled: + watchPropertyValue((ChatModel m) => !m.archiveActive), + autofocus: true, + onChanged: (v) { + draftModel.setDraft( + roomId: widget.room.id, + draft: v, + notify: false, + ); + widget.room.setTyping(v.isNotEmpty, timeout: 500); + }, + decoration: InputDecoration( + hintText: context.l10n.sendAMessage, + prefixIcon: Padding( + padding: const EdgeInsets.all(kSmallPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + padding: EdgeInsets.zero, + onPressed: attaching + ? null + : () => draftModel.addAttachment( + widget.room.id, + onFail: (error) => + showErrorSnackBar(context, error), + ), + icon: attaching + ? const Center( + child: SizedBox.square( + dimension: 15, + child: Progress( + strokeWidth: 1, + ), + ), + ) + : const Icon( + YaruIcons.plus, ), - ), - ) - : const Icon( - YaruIcons.plus, - ), + ), + ChatInputEmojiMenu( + onEmojiSelected: (cat, emo) { + _sendController.text = + _sendController.text + emo.emoji; + draftModel.setDraft( + roomId: widget.room.id, + draft: _sendController.text, + notify: true, + ); + _sendNode.requestFocus(); + }, + ), + ], ), - ChatInputEmojiMenu( - onEmojiSelected: (cat, emo) { - _sendController.text = - _sendController.text + emo.emoji; - draftModel.setDraft( - roomId: widget.room.id, - draft: _sendController.text, - notify: true, - ); - _sendNode.requestFocus(); - }, - ), - ], - ), - ), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - padding: EdgeInsets.zero, - icon: transform, - onPressed: send, ), - ], + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + padding: EdgeInsets.zero, + icon: transform, + onPressed: send, + ), + ], + ), + ), ), ), - ), + ], ), - ], - ), + ), + Positioned( + top: -kTypingAvatarSize + kSmallPadding, + child: ChatTypingIndicator(room: widget.room), + ), + ], ); } } diff --git a/lib/chat/common/event_x.dart b/lib/chat/common/event_x.dart index 48765b4..202327f 100644 --- a/lib/chat/common/event_x.dart +++ b/lib/chat/common/event_x.dart @@ -20,4 +20,37 @@ extension EventX on Event { EventTypes.GuestAccess, EventTypes.Encryption, }.contains(type); + + bool hideEventInTimeline({ + required bool showAvatarChanges, + required bool showDisplayNameChanges, + }) { + if (type == EventTypes.RoomMember && + roomMemberChangeType == RoomMemberChangeType.avatar && + !showAvatarChanges) { + return true; + } + if (type == EventTypes.RoomMember && + roomMemberChangeType == RoomMemberChangeType.displayname && + !showDisplayNameChanges) { + return true; + } + + if ({RelationshipTypes.edit, RelationshipTypes.reaction} + .contains(relationshipType)) { + return true; + } + + return { + EventTypes.Redaction, + EventTypes.Reaction, + }.contains(type); + } + + bool partOfMessageCohort(Event? maybePreviousEvent) { + return maybePreviousEvent != null && + !maybePreviousEvent.showAsBadge && + !maybePreviousEvent.isImage && + maybePreviousEvent.senderId == senderId; + } } diff --git a/lib/chat/common/view/chat_avatar.dart b/lib/chat/common/view/chat_avatar.dart index 1b08980..61dc7d5 100644 --- a/lib/chat/common/view/chat_avatar.dart +++ b/lib/chat/common/view/chat_avatar.dart @@ -4,12 +4,13 @@ import 'package:yaru/yaru.dart'; import '../../../common/view/avatar_vignette.dart'; import '../../../common/view/safe_network_image.dart'; +import '../../../common/view/ui_constants.dart'; import '../remote_image_model.dart'; class ChatAvatar extends StatefulWidget with WatchItStatefulWidgetMixin { const ChatAvatar({ super.key, - this.dimension = 38, + this.dimension = kAvatarDefaultSize, this.fallBackIcon, this.fallBackIconSize, this.avatarUri, diff --git a/lib/chat/events/view/chat_event_column.dart b/lib/chat/events/view/chat_event_column.dart deleted file mode 100644 index e8db941..0000000 --- a/lib/chat/events/view/chat_event_column.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../../common/date_time_x.dart'; -import '../../../common/view/build_context_x.dart'; -import '../../../l10n/l10n.dart'; -import '../../common/event_x.dart'; -import '../../settings/settings_model.dart'; -import 'chat_event_tile.dart'; - -class ChatEventColumn extends StatelessWidget with WatchItMixin { - const ChatEventColumn({ - super.key, - required this.event, - this.maybePreviousEvent, - required this.jump, - required this.showSeenByIndicator, - required this.timeline, - required this.room, - }); - - final Event event; - final Timeline timeline; - final Room room; - final Event? maybePreviousEvent; - final Future Function(Event event) jump; - final bool showSeenByIndicator; - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final showChatAvatarChanges = - watchPropertyValue((SettingsModel m) => m.showChatAvatarChanges); - final showChatDisplaynameChanges = - watchPropertyValue((SettingsModel m) => m.showChatDisplaynameChanges); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (maybePreviousEvent != null && - event.originServerTs.toLocal().day != - maybePreviousEvent?.originServerTs.toLocal().day) - Text( - maybePreviousEvent!.originServerTs - .toLocal() - .formatAndLocalizeDay(context.l10n), - textAlign: TextAlign.center, - style: theme.textTheme.labelSmall, - ), - if (!hideEventInTimeline( - event: event, - showAvatarChanges: showChatAvatarChanges, - showDisplayNameChanges: showChatDisplaynameChanges, - )) - ChatEventTile( - event: event, - timeline: timeline, - onReplyOriginClick: jump, - partOfMessageCohort: _partOfMessageCohort( - event, - maybePreviousEvent, - ), - ), - ], - ); - } - - bool hideEventInTimeline({ - required Event event, - required bool showAvatarChanges, - required bool showDisplayNameChanges, - }) { - if (event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.avatar && - !showAvatarChanges) { - return true; - } - if (event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.displayname && - !showDisplayNameChanges) { - return true; - } - - if ({RelationshipTypes.edit, RelationshipTypes.reaction} - .contains(event.relationshipType)) { - return true; - } - - return { - EventTypes.Redaction, - EventTypes.Reaction, - }.contains(event.type); - } - - bool _partOfMessageCohort(Event event, Event? maybePreviousEvent) { - return maybePreviousEvent != null && - !maybePreviousEvent.showAsBadge && - !maybePreviousEvent.isImage && - maybePreviousEvent.senderId == event.senderId; - } -} diff --git a/lib/chat/events/view/chat_event_status_icon.dart b/lib/chat/events/view/chat_event_status_icon.dart index b659b7b..c85c9f8 100644 --- a/lib/chat/events/view/chat_event_status_icon.dart +++ b/lib/chat/events/view/chat_event_status_icon.dart @@ -71,7 +71,7 @@ class ChatEventStatusIcon extends StatelessWidget { ); return Padding( - padding: padding ?? const EdgeInsets.all(kMediumPadding), + padding: padding ?? const EdgeInsets.all(kSmallPadding), child: SizedBox( height: iconSize, child: Row( @@ -94,7 +94,9 @@ class ChatEventStatusIcon extends StatelessWidget { ), ), Text( - event.originServerTs.toLocal().formatAndLocalize(context.l10n), + event.originServerTs + .toLocal() + .formatAndLocalizeTime(context.l10n), textAlign: TextAlign.start, style: style, overflow: TextOverflow.ellipsis, diff --git a/lib/chat/events/view/chat_image.dart b/lib/chat/events/view/chat_image.dart index 8ee9849..491879a 100644 --- a/lib/chat/events/view/chat_image.dart +++ b/lib/chat/events/view/chat_image.dart @@ -50,9 +50,12 @@ class ChatImage extends StatelessWidget with WatchItMixin { watchPropertyValue((LocalImageModel m) => m.get(event.eventId)); return Padding( - padding: const EdgeInsets.symmetric( - vertical: kBigPadding, - horizontal: kMediumPadding, + padding: EdgeInsets.only( + top: kBigPadding, + bottom: kBigPadding, + right: kMediumPadding, + left: kMediumPadding + + (isUserMessage ? 0 : kAvatarDefaultSize + kBigPadding), ), child: Align( alignment: isUserMessage ? Alignment.centerRight : Alignment.centerLeft, @@ -80,15 +83,16 @@ class ChatImage extends StatelessWidget with WatchItMixin { ), ), ), - Positioned( - left: kSmallPadding, - bottom: kSmallPadding, - child: ChatMessageReactions( - key: ValueKey('${event.eventId}reactions'), - event: event, - timeline: timeline, + if (!event.redacted) + Positioned( + left: kSmallPadding, + bottom: kSmallPadding, + child: ChatMessageReactions( + key: ValueKey('${event.eventId}reactions'), + event: event, + timeline: timeline, + ), ), - ), Positioned( top: kSmallPadding, right: 10 * kSmallPadding, diff --git a/lib/chat/events/view/chat_message_bubble.dart b/lib/chat/events/view/chat_message_bubble.dart index 38dc5ee..e97e45b 100644 --- a/lib/chat/events/view/chat_message_bubble.dart +++ b/lib/chat/events/view/chat_message_bubble.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; import '../../../common/view/build_context_x.dart'; import '../../../common/view/theme.dart'; @@ -39,75 +40,45 @@ class ChatMessageBubble extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { - final theme = context.theme; final isUserMessage = di().isUserEvent(event); - return Stack( - children: [ - Align( - alignment: - isUserMessage ? Alignment.centerRight : Alignment.centerLeft, - child: Stack( - children: [ - ChatMessageMenu( - event: event, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: ChatMessageBubble.width, - minWidth: 205, - ), - child: Container( - margin: tilePadding(partOfMessageCohort), - padding: const EdgeInsets.all(kSmallPadding), - decoration: BoxDecoration( - color: getTileColor( - di().isUserEvent(event), - theme, - ), - borderRadius: messageBubbleShape - .getBorderRadius(partOfMessageCohort), - ), - child: _ChatMessageBubbleContent( - event: event, - timeline: timeline, - onReplyOriginClick: onReplyOriginClick, - hideAvatar: partOfMessageCohort, - ), - ), - ), - ), - if (!event.redacted) - Positioned( - key: ValueKey('${event.eventId}reactions'), - left: kSmallPadding, - bottom: kSmallPadding, - child: ChatMessageReactions( - event: event, - timeline: timeline, - ), - ), - Positioned( + return Align( + alignment: isUserMessage ? Alignment.centerRight : Alignment.centerLeft, + child: Stack( + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: ChatMessageBubble.width, + minWidth: 205, + ), + child: Container( + margin: tilePadding(partOfMessageCohort), + padding: const EdgeInsets.only( + top: kSmallPadding, bottom: kSmallPadding, - right: kSmallPadding, - child: ChatEventStatusIcon( - event: event, - timeline: timeline, - ), ), - Positioned( - top: kBigPadding, - right: kBigPadding, - child: event.attachmentMxcUrl == null - ? const SizedBox.shrink() - : InkWell( - onTap: () => di().safeFile(event), - child: ChatMessageAttachmentIndicator(event: event), - ), + child: _ChatMessageBubbleContent( + partOfMessageCohort: partOfMessageCohort, + messageBubbleShape: messageBubbleShape, + event: event, + timeline: timeline, + onReplyOriginClick: onReplyOriginClick, + hideAvatar: partOfMessageCohort, ), - ], + ), ), - ), - ], + if (!event.redacted) + Positioned( + key: ValueKey('${event.eventId}reactions'), + left: kMediumPlusPadding + 38, + bottom: kTinyPadding, + child: ChatMessageReactions( + event: event, + timeline: timeline, + ), + ), + ], + ), ); } } @@ -118,12 +89,16 @@ class _ChatMessageBubbleContent extends StatelessWidget { required this.timeline, required this.onReplyOriginClick, required this.hideAvatar, + required this.messageBubbleShape, + required this.partOfMessageCohort, }); final Event event; final Timeline timeline; final Future Function(Event event) onReplyOriginClick; final bool hideAvatar; + final ChatMessageBubbleShape messageBubbleShape; + final bool partOfMessageCohort; @override Widget build(BuildContext context) { @@ -135,82 +110,117 @@ class _ChatMessageBubbleContent extends StatelessWidget { final messageStyle = textTheme.bodyMedium; final displayEvent = event.getDisplayEvent(timeline); - return Material( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - spacing: kSmallPadding, - children: [ - Padding( - padding: const EdgeInsets.all(kSmallPadding), - child: hideAvatar && event.messageType == MessageTypes.Text - ? const SizedBox.shrink() - : ChatMessageBubbleLeading( - event: event, - ), - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: kSmallPadding, + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: kSmallPadding, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: kSmallPadding), + child: hideAvatar && event.messageType == MessageTypes.Text + ? const SizedBox.square( + dimension: kAvatarDefaultSize, + ) + : ChatMessageBubbleLeading( + event: event, ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - event.senderFromMemoryOrFallback.calcDisplayname(), - style: textTheme.labelSmall, - ), + ), + Flexible( + child: ChatMessageMenu( + event: event, + child: Stack( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: kMediumPadding), + decoration: BoxDecoration( + color: getTileColor( + di().isUserEvent(event), + context.theme, ), - if (!event.redacted) - Flexible( - child: ChatMessageReplyHeader( - event: event, - timeline: timeline, - onReplyOriginClick: onReplyOriginClick, - ), + borderRadius: + messageBubbleShape.getBorderRadius(partOfMessageCohort), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: kSmallPadding, ), - ], - ), - Opacity( - opacity: event.redacted ? 0.5 : 1, - child: event.redacted - ? LocalizedDisplayEventText( - displayEvent: displayEvent, - style: messageStyle?.copyWith( - decoration: TextDecoration.lineThrough, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + event.senderFromMemoryOrFallback + .calcDisplayname(), + style: textTheme.labelSmall, + ), ), - ) - : event.isRichMessage - ? HtmlMessage( - html: html, - room: timeline.room, - defaultTextColor: context.colorScheme.onSurface, - ) - : SelectableText.rich( - TextSpan( - style: messageStyle, - text: displayEvent.body, + if (!event.redacted) + Flexible( + child: ChatMessageReplyHeader( + event: event, + timeline: timeline, + onReplyOriginClick: onReplyOriginClick, ), - style: messageStyle, ), + ], + ), + Opacity( + opacity: event.redacted ? 0.5 : 1, + child: event.redacted + ? LocalizedDisplayEventText( + displayEvent: displayEvent, + style: messageStyle?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ) + : event.isRichMessage + ? HtmlMessage( + html: html, + room: timeline.room, + defaultTextColor: + context.colorScheme.onSurface, + ) + : SelectableText.rich( + TextSpan( + style: messageStyle, + text: displayEvent.body, + ), + style: messageStyle, + ), + ), + const SizedBox( + height: kBigPadding, + ), + ], + ), ), - const SizedBox( - height: kBigPadding, + Positioned( + top: kSmallPadding, + right: kSmallPadding, + child: event.attachmentMxcUrl == null + ? const SizedBox.shrink() + : InkWell( + onTap: () => di().safeFile(event), + child: ChatMessageAttachmentIndicator(event: event), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: ChatEventStatusIcon( + event: event, + timeline: timeline, + ), ), ], ), ), - const SizedBox( - height: kSmallPadding, - ), - ], - ), + ), + ], ); } } @@ -223,7 +233,7 @@ class ChatMessageBubbleLeading extends StatelessWidget { @override Widget build(BuildContext context) { if (event.messageType == MessageTypes.BadEncrypted) { - return const SizedBox.shrink(); + return const SizedBox.square(dimension: kAvatarDefaultSize); } else if (event.messageType != MessageTypes.Text && event.messageType != MessageTypes.Notice) { return ChatMessageMediaAvatar(event: event); @@ -235,11 +245,8 @@ class ChatMessageBubbleLeading extends StatelessWidget { context: context, builder: (context) => ChatProfileDialog(userId: event.senderId), ), - fallBackColor: getMonochromeBg( - theme: context.theme, - factor: 10, - darkFactor: yaru ? 1 : null, - ), + fallBackColor: + avatarFallbackColor(context.colorScheme).scale(saturation: -1), ); } } diff --git a/lib/chat/events/view/chat_message_media_avatar.dart b/lib/chat/events/view/chat_message_media_avatar.dart index b9430cf..6d47ab3 100644 --- a/lib/chat/events/view/chat_message_media_avatar.dart +++ b/lib/chat/events/view/chat_message_media_avatar.dart @@ -4,7 +4,10 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../../common/view/build_context_x.dart'; import '../../../common/view/confirm.dart'; +import '../../../common/view/theme.dart'; +import '../../../common/view/ui_constants.dart'; import '../../../l10n/l10n.dart'; import '../chat_download_model.dart'; @@ -55,13 +58,14 @@ class ChatMessageMediaAvatar extends StatelessWidget { ), ), child: CircleAvatar( - radius: 38 / 2, + backgroundColor: avatarFallbackColor(context.colorScheme), + radius: kAvatarDefaultSize / 2, child: switch (event.messageType) { MessageTypes.Audio => const Icon(YaruIcons.media_play), MessageTypes.Video => const Icon(YaruIcons.video_filled), MessageTypes.Location => const Icon(YaruIcons.location), MessageTypes.File => const Icon(YaruIcons.document_filled), - _ => const Icon(YaruIcons.question), + _ => const Icon(YaruIcons.document_filled), }, ), ), diff --git a/lib/chat/events/view/chat_message_menu.dart b/lib/chat/events/view/chat_message_menu.dart index 0d09048..4d82005 100644 --- a/lib/chat/events/view/chat_message_menu.dart +++ b/lib/chat/events/view/chat_message_menu.dart @@ -8,7 +8,6 @@ import '../../../common/view/snackbars.dart'; import '../../../common/view/ui_constants.dart'; import '../../../l10n/l10n.dart'; import '../../chat_room/input/draft_model.dart'; -import '../../common/event_x.dart'; import '../../data/emojis.dart'; class ChatMessageMenu extends StatefulWidget { @@ -37,8 +36,7 @@ class _ChatMessageMenuState extends State { child: MenuAnchor( controller: _controller, consumeOutsideTap: true, - alignmentOffset: - Offset(widget.event.isImage ? 0 : kMediumPadding, -kSmallPadding), + alignmentOffset: const Offset(0, -kSmallPadding), menuChildren: [ if (widget.event.canRedact) MenuItemButton( diff --git a/lib/common/date_time_x.dart b/lib/common/date_time_x.dart index e9d6076..c030e12 100644 --- a/lib/common/date_time_x.dart +++ b/lib/common/date_time_x.dart @@ -37,4 +37,12 @@ extension DateTimeX on DateTime { locale.countryCode, ).format(this); } + + String formatAndLocalizeTime(AppLocalizations l10n) { + final locale = WidgetsBinding.instance.platformDispatcher.locale; + + return DateFormat.Hm( + locale.countryCode, + ).format(this); + } } diff --git a/lib/common/view/theme.dart b/lib/common/view/theme.dart index 7231762..5b0ee9f 100644 --- a/lib/common/view/theme.dart +++ b/lib/common/view/theme.dart @@ -43,7 +43,7 @@ Color getTileColor( saturation: theme.colorScheme.isLight ? (yaru ? -0.3 : -0.6) : -0.6, lightness: theme.colorScheme.isLight ? 0.65 : (yaru ? -0.5 : -0.7), ) - : getMonochromeBg(theme: theme, factor: 6, darkFactor: yaru ? 12 : 15); + : getMonochromeBg(theme: theme, factor: 6, darkFactor: 15); Color getPanelBg(ThemeData theme) => getMonochromeBg(theme: theme, darkFactor: 3); @@ -67,18 +67,10 @@ Color getEventBadgeColor(ThemeData theme) => Color getEventBadgeTextColor(ThemeData theme) => theme.colorScheme.onSurface; EdgeInsets tilePadding(bool partOfMessageCohort) { - return partOfMessageCohort - ? const EdgeInsets.only( - left: kMediumPadding, - right: kMediumPadding, - bottom: kSmallPadding, - ) - : const EdgeInsets.only( - left: kMediumPadding, - right: kMediumPadding, - top: kMediumPadding, - bottom: kSmallPadding, - ); + return const EdgeInsets.symmetric( + horizontal: kSmallPadding, + vertical: kTinyPadding, + ); } ButtonStyle get textFieldSuffixStyle => IconButton.styleFrom( @@ -89,3 +81,6 @@ ButtonStyle get textFieldSuffixStyle => IconButton.styleFrom( ), ), ); + +Color avatarFallbackColor(ColorScheme colorScheme) => + colorScheme.primary.withValues(alpha: 0.3); diff --git a/lib/common/view/ui_constants.dart b/lib/common/view/ui_constants.dart index 84192e6..db19f93 100644 --- a/lib/common/view/ui_constants.dart +++ b/lib/common/view/ui_constants.dart @@ -23,7 +23,8 @@ const kLoginFormWidth = 350.0; const Duration kAvatarAnimationDuration = Duration(milliseconds: 250); const Curve kAvatarAnimationCurve = Curves.easeInOut; -const kTypingAvatarSize = 24.0; +const kTypingAvatarSize = 15.0; +const kAvatarDefaultSize = 38.0; const kBubbleRadius = Radius.circular(4.0); const kBigBubbleRadius = Radius.circular(8.0);