design: Remake UX of selecting messages and chats

pull/1031/head
krille-chan 1 year ago
parent d2f12452fc
commit 384e57fbd2
No known key found for this signature in database

@ -9,7 +9,6 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart'; import '../../../config/app_config.dart';
import 'message_content.dart'; import 'message_content.dart';
@ -149,211 +148,247 @@ class Message extends StatelessWidget {
offset: Offset(0, animateIn ? 1 : 0), offset: Offset(0, animateIn ? 1 : 0),
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
child: Row( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: rowMainAxisAlignment,
children: [ children: [
if (longPressSelect) Positioned(
SizedBox( top: 0,
height: 32, bottom: 0,
width: Avatar.defaultSize, left: 0,
child: Checkbox.adaptive( right: 0,
value: selected, child: InkWell(
shape: const CircleBorder(), onTap: () => onSelect(event),
onChanged: (_) => onSelect(event), borderRadius:
), BorderRadius.circular(AppConfig.borderRadius / 2),
) child: Material(
else if (nextEventSameSender || ownMessage) borderRadius:
SizedBox( BorderRadius.circular(AppConfig.borderRadius / 2),
width: Avatar.defaultSize, color: selected
child: Center( ? Theme.of(context)
child: SizedBox( .colorScheme
width: 16, .secondaryContainer
height: 16, .withAlpha(100)
child: event.status == EventStatus.error : highlightMarker
? const Icon(Icons.error, color: Colors.red) ? Theme.of(context)
: event.fileSendingStatus != null .colorScheme
? const CircularProgressIndicator.adaptive( .tertiaryContainer
strokeWidth: 1, .withAlpha(100)
) : Colors.transparent,
: null,
),
), ),
)
else
FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final user =
snapshot.data ?? event.senderFromMemoryOrFallback;
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
presenceBackgroundColor: avatarPresenceBackgroundColor,
onTap: () => onAvatarTab(event),
);
},
), ),
Expanded( ),
child: Column( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: rowMainAxisAlignment,
children: [ children: [
if (!nextEventSameSender) if (longPressSelect)
Padding( SizedBox(
padding: const EdgeInsets.only(left: 8.0, bottom: 4), height: 32,
child: ownMessage || event.room.isDirectChat width: Avatar.defaultSize,
? const SizedBox(height: 12) child: Checkbox.adaptive(
: FutureBuilder<User?>( value: selected,
future: event.fetchSenderUser(), shape: const CircleBorder(),
builder: (context, snapshot) { onChanged: (_) => onSelect(event),
final displayname =
snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback
.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontSize: 12,
color: (Theme.of(context).brightness ==
Brightness.light
? displayname.color
: displayname.lightColorText),
),
);
},
),
), ),
Container( )
alignment: alignment, else if (nextEventSameSender || ownMessage)
padding: const EdgeInsets.only(left: 8), SizedBox(
child: GestureDetector( width: Avatar.defaultSize,
onLongPress: longPressSelect child: Center(
? null child: SizedBox(
: () { width: 16,
HapticFeedback.heavyImpact(); height: 16,
onSelect(event); child: event.status == EventStatus.error
}, ? const Icon(Icons.error, color: Colors.red)
child: AnimatedOpacity( : event.fileSendingStatus != null
opacity: animateIn ? const CircularProgressIndicator.adaptive(
? 0 strokeWidth: 1,
: event.redacted || )
event.messageType == : null,
MessageTypes.BadEncrypted || ),
event.status.isSending ),
? 0.5 )
: 1, else
duration: FluffyThemes.animationDuration, FutureBuilder<User?>(
curve: FluffyThemes.animationCurve, future: event.fetchSenderUser(),
child: Material( builder: (context, snapshot) {
color: noBubble ? Colors.transparent : color, final user =
clipBehavior: Clip.antiAlias, snapshot.data ?? event.senderFromMemoryOrFallback;
shape: RoundedRectangleBorder( return Avatar(
borderRadius: borderRadius, mxContent: user.avatarUrl,
), name: user.calcDisplayname(),
child: Container( presenceUserId: user.stateKey,
decoration: BoxDecoration( presenceBackgroundColor:
borderRadius: BorderRadius.circular( avatarPresenceBackgroundColor,
AppConfig.borderRadius, onTap: () => onAvatarTab(event),
);
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!nextEventSameSender)
Padding(
padding:
const EdgeInsets.only(left: 8.0, bottom: 4),
child: ownMessage || event.room.isDirectChat
? const SizedBox(height: 12)
: FutureBuilder<User?>(
future: event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname =
snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback
.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontSize: 12,
color:
(Theme.of(context).brightness ==
Brightness.light
? displayname.color
: displayname.lightColorText),
),
);
},
),
),
Container(
alignment: alignment,
padding: const EdgeInsets.only(left: 8),
child: GestureDetector(
onLongPress: longPressSelect
? null
: () {
HapticFeedback.heavyImpact();
onSelect(event);
},
child: AnimatedOpacity(
opacity: animateIn
? 0
: event.redacted ||
event.messageType ==
MessageTypes.BadEncrypted ||
event.status.isSending
? 0.5
: 1,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Material(
color: noBubble ? Colors.transparent : color,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
), ),
), child: Container(
padding: noBubble || noPadding decoration: BoxDecoration(
? EdgeInsets.zero borderRadius: BorderRadius.circular(
: const EdgeInsets.symmetric( AppConfig.borderRadius,
horizontal: 16,
vertical: 8,
), ),
constraints: const BoxConstraints( ),
maxWidth: FluffyThemes.columnWidth * 1.5, padding: noBubble || noPadding
), ? EdgeInsets.zero
child: Column( : const EdgeInsets.symmetric(
mainAxisSize: MainAxisSize.min, horizontal: 16,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 8,
children: <Widget>[ ),
if (event.relationshipType == constraints: const BoxConstraints(
RelationshipTypes.reply) maxWidth: FluffyThemes.columnWidth * 1.5,
FutureBuilder<Event?>( ),
future: event.getReplyEvent(timeline), child: Column(
builder: mainAxisSize: MainAxisSize.min,
(BuildContext context, snapshot) { crossAxisAlignment:
final replyEvent = snapshot.hasData CrossAxisAlignment.start,
? snapshot.data! children: <Widget>[
: Event( if (event.relationshipType ==
eventId: RelationshipTypes.reply)
event.relationshipEventId!, FutureBuilder<Event?>(
content: { future: event.getReplyEvent(timeline),
'msgtype': 'm.text', builder:
'body': '...', (BuildContext context, snapshot) {
}, final replyEvent = snapshot.hasData
senderId: event.senderId, ? snapshot.data!
type: 'm.room.message', : Event(
room: event.room, eventId: event
status: EventStatus.sent, .relationshipEventId!,
originServerTs: DateTime.now(), content: {
); 'msgtype': 'm.text',
return Padding( 'body': '...',
},
senderId: event.senderId,
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
originServerTs:
DateTime.now(),
);
return Padding(
padding: const EdgeInsets.only(
bottom: 4.0,
),
child: InkWell(
borderRadius:
ReplyContent.borderRadius,
onTap: () => scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor: textColor,
onInfoTab: onInfoTab,
borderRadius: borderRadius,
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
))
Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 4.0, top: 4.0,
), ),
child: InkWell( child: Row(
borderRadius: mainAxisSize: MainAxisSize.min,
ReplyContent.borderRadius, children: [
onTap: () => scrollToEventId( Icon(
replyEvent.eventId, Icons.edit_outlined,
), color: textColor.withAlpha(164),
child: AbsorbPointer( size: 14,
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
), ),
), Text(
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color:
textColor.withAlpha(164),
fontSize: 12,
),
),
],
), ),
); ),
}, ],
),
MessageContent(
displayEvent,
textColor: textColor,
onInfoTab: onInfoTab,
borderRadius: borderRadius,
), ),
if (event.hasAggregatedEvents( ),
timeline,
RelationshipTypes.edit,
))
Padding(
padding: const EdgeInsets.only(
top: 4.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor.withAlpha(164),
fontSize: 12,
),
),
],
),
),
],
), ),
), ),
), ),
), ),
), ],
), ),
], ),
), ],
), ),
], ],
), ),
@ -449,13 +484,7 @@ class Message extends StatelessWidget {
container = row; container = row;
} }
return Container( return Center(
alignment: Alignment.center,
color: selected
? Theme.of(context).colorScheme.secondaryContainer.withAlpha(100)
: highlightMarker
? Theme.of(context).colorScheme.tertiaryContainer.withAlpha(100)
: Colors.transparent,
child: Swipeable( child: Swipeable(
key: ValueKey(event.eventId), key: ValueKey(event.eventId),
background: const Padding( background: const Padding(
@ -466,87 +495,17 @@ class Message extends StatelessWidget {
), ),
direction: SwipeDirection.endToStart, direction: SwipeDirection.endToStart,
onSwipe: (_) => onSwipe(), onSwipe: (_) => onSwipe(),
child: HoverBuilder( child: Container(
builder: (context, hovered) => Stack( constraints: const BoxConstraints(
children: [ maxWidth: FluffyThemes.columnWidth * 2.5,
Container( ),
constraints: const BoxConstraints( padding: EdgeInsets.only(
maxWidth: FluffyThemes.columnWidth * 2.5, left: 8.0,
), right: 8.0,
padding: EdgeInsets.only( top: nextEventSameSender ? 1.0 : 4.0,
left: 8.0, bottom: previousEventSameSender ? 1.0 : 4.0,
right: 8.0,
top: nextEventSameSender ? 1.0 : 4.0,
bottom: previousEventSameSender ? 1.0 : 4.0,
),
child: container,
),
Positioned(
left: ownMessage ? null : 48,
right: ownMessage ? 4 : null,
top: displayTime ? 38 : 0,
child: AnimatedScale(
duration: Duration(
milliseconds:
(FluffyThemes.animationDuration.inMilliseconds / 2)
.floor(),
),
curve: FluffyThemes.animationCurve,
scale: !longPressSelect && hovered ? 1 : 0,
alignment: Alignment.center,
child: Material(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.9),
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ??
4,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
shadowColor: Theme.of(context).appBarTheme.shadowColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (event.room.canSendDefaultMessages)
SizedBox(
width: 32,
height: 32,
child: IconButton(
icon: Icon(
Icons.reply_outlined,
size: 16,
color: Theme.of(context)
.colorScheme
.onTertiaryContainer,
),
tooltip: L10n.of(context)!.reply,
onPressed: event.room.canSendDefaultMessages
? () => onSwipe()
: null,
),
),
SizedBox(
width: 32,
height: 32,
child: IconButton(
icon: Icon(
Icons.more_vert,
size: 16,
color: Theme.of(context)
.colorScheme
.onTertiaryContainer,
),
tooltip: L10n.of(context)!.select,
onPressed: () => onSelect(event),
),
),
],
),
),
),
),
],
), ),
child: container,
), ),
), ),
); );

@ -97,11 +97,43 @@ class ChatListItem extends StatelessWidget {
visualDensity: const VisualDensity(vertical: -0.5), visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 8),
onLongPress: onLongPress, onLongPress: onLongPress,
leading: Avatar( leading: Stack(
mxContent: room.avatar, clipBehavior: Clip.none,
name: displayname, children: [
presenceUserId: room.directChatMatrixID, HoverBuilder(
presenceBackgroundColor: backgroundColor, builder: (context, hovered) => AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: Avatar(
mxContent: room.avatar,
name: displayname,
presenceUserId: room.directChatMatrixID,
presenceBackgroundColor: backgroundColor,
onTap: onLongPress,
),
),
),
Positioned(
bottom: -2,
right: -2,
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: (hovered || selected) ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: Icon(
selected
? Icons.check_circle
: Icons.check_circle_outlined,
size: 18,
),
),
),
),
],
), ),
title: Row( title: Row(
children: <Widget>[ children: <Widget>[
@ -281,19 +313,7 @@ class ChatListItem extends StatelessWidget {
), ),
onTap: onTap, onTap: onTap,
trailing: onForget == null trailing: onForget == null
? hovered || selected ? null
? IconButton(
color: selected
? Theme.of(context).colorScheme.primary
: null,
icon: Icon(
selected
? Icons.check_circle
: Icons.check_circle_outlined,
),
onPressed: onLongPress,
)
: null
: IconButton( : IconButton(
icon: const Icon(Icons.delete_outlined), icon: const Icon(Icons.delete_outlined),
onPressed: onForget, onPressed: onForget,

Loading…
Cancel
Save