feat(ui): Remove ConversationsListItem completely
This commit is contained in:
@@ -5,11 +5,12 @@ import 'package:move_to_background/move_to_background.dart';
|
|||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
import 'package:moxxyv2/ui/helpers.dart';
|
import 'package:moxxyv2/ui/helpers.dart';
|
||||||
import 'package:moxxyv2/ui/state/navigation.dart' as navigation;
|
import 'package:moxxyv2/ui/state/navigation.dart' as navigation;
|
||||||
import 'package:moxxyv2/ui/state/share_selection.dart';
|
import 'package:moxxyv2/ui/state/share_selection.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/conversation.dart';
|
import 'package:moxxyv2/ui/widgets/conversation_card.dart';
|
||||||
|
|
||||||
class ShareSelectionPage extends StatelessWidget {
|
class ShareSelectionPage extends StatelessWidget {
|
||||||
const ShareSelectionPage({super.key});
|
const ShareSelectionPage({super.key});
|
||||||
@@ -31,13 +32,13 @@ class ShareSelectionPage extends StatelessWidget {
|
|||||||
prev.type != next.type;
|
prev.type != next.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData? _getSuffixIcon(ShareListItem item) {
|
Widget? _getSuffixIcon(ShareListItem item) {
|
||||||
if (item.pseudoRosterItem) {
|
if (item.pseudoRosterItem) {
|
||||||
return Icons.smartphone;
|
return const Icon(Icons.smartphone);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isEncrypted) {
|
if (item.isEncrypted) {
|
||||||
return Icons.lock;
|
return const Icon(Icons.lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -68,11 +69,22 @@ class ShareSelectionPage extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = state.items[index];
|
final item = state.items[index];
|
||||||
|
|
||||||
return ConversationsListRow(
|
return ConversationCard(
|
||||||
Conversation(
|
conversation: Conversation(
|
||||||
'',
|
'',
|
||||||
item.titleWithOptionalContact,
|
item.titleWithOptionalContact,
|
||||||
null,
|
Message(
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
item.jid,
|
||||||
|
0,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
item.avatarPath,
|
item.avatarPath,
|
||||||
item.avatarHash,
|
item.avatarHash,
|
||||||
item.jid,
|
item.jid,
|
||||||
@@ -89,11 +101,10 @@ class ShareSelectionPage extends StatelessWidget {
|
|||||||
contactAvatarPath: item.contactAvatarPath,
|
contactAvatarPath: item.contactAvatarPath,
|
||||||
contactDisplayName: item.contactDisplayName,
|
contactDisplayName: item.contactDisplayName,
|
||||||
),
|
),
|
||||||
false,
|
|
||||||
titleSuffixIcon: _getSuffixIcon(item),
|
titleSuffixIcon: _getSuffixIcon(item),
|
||||||
showTimestamp: false,
|
showTimestamp: false,
|
||||||
isSelected: state.selection.contains(index),
|
selected: state.selection.contains(index),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
context.read<ShareSelectionCubit>().selectionToggled(index);
|
context.read<ShareSelectionCubit>().selectionToggled(index);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,477 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
|
||||||
import 'package:moxxyv2/shared/constants.dart';
|
|
||||||
import 'package:moxxyv2/shared/helpers.dart';
|
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
|
||||||
import 'package:moxxyv2/ui/state/account.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/avatar.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/chat/typing.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/contact_helper.dart';
|
|
||||||
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
|
||||||
|
|
||||||
class AnimatedMaterialColor extends StatefulWidget {
|
|
||||||
const AnimatedMaterialColor({
|
|
||||||
required this.color,
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The color attribute of the [Material] widget.
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
/// The child widget of the [Material] widget.
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
AnimatedMaterialColorState createState() => AnimatedMaterialColorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimatedMaterialColorState extends State<AnimatedMaterialColor>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late final AnimationController _controller;
|
|
||||||
late final ColorTween _tween;
|
|
||||||
late final Animation<Color?> _animation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_tween = ColorTween(
|
|
||||||
begin: widget.color,
|
|
||||||
end: widget.color,
|
|
||||||
);
|
|
||||||
|
|
||||||
_animation = _tween.animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _controller,
|
|
||||||
curve: Curves.easeInOutCubic,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(AnimatedMaterialColor oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
|
|
||||||
if (oldWidget.color != widget.color) {
|
|
||||||
_tween
|
|
||||||
..begin = oldWidget.color
|
|
||||||
..end = widget.color;
|
|
||||||
|
|
||||||
_controller
|
|
||||||
..reset()
|
|
||||||
..forward();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _animation,
|
|
||||||
builder: (_, child) => Material(
|
|
||||||
color: _animation.value,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(Unknown): Make this widget less reliant on a [Conversation], so that we can use
|
|
||||||
// it to build the icon entry in other pages, like the newconversation page.
|
|
||||||
class ConversationsListRow extends StatefulWidget {
|
|
||||||
const ConversationsListRow(
|
|
||||||
this.conversation,
|
|
||||||
this.update, {
|
|
||||||
required this.isSelected,
|
|
||||||
this.showTimestamp = true,
|
|
||||||
this.titleSuffixIcon,
|
|
||||||
this.enableAvatarOnTap = false,
|
|
||||||
this.avatarWidget,
|
|
||||||
this.onPressed,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
final Conversation conversation;
|
|
||||||
final bool update; // Should a timer run to update the timestamp
|
|
||||||
final IconData? titleSuffixIcon;
|
|
||||||
final bool showTimestamp;
|
|
||||||
final bool enableAvatarOnTap;
|
|
||||||
final Widget? avatarWidget;
|
|
||||||
|
|
||||||
/// Flag indicating whether the conversation row is selected (true) or not (false).
|
|
||||||
final bool isSelected;
|
|
||||||
|
|
||||||
/// Callback for when the row has been tapped.
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConversationsListRowState createState() => ConversationsListRowState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConversationsListRowState extends State<ConversationsListRow> {
|
|
||||||
late String _timestampString;
|
|
||||||
late Timer? _updateTimer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final initNow = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
|
|
||||||
_timestampString = formatConversationTimestamp(
|
|
||||||
widget.conversation.lastChangeTimestamp,
|
|
||||||
initNow,
|
|
||||||
);
|
|
||||||
|
|
||||||
// NOTE: We could also check and run the timer hourly, but who has a messenger on the
|
|
||||||
// conversation screen open for hours on end?
|
|
||||||
if (widget.update &&
|
|
||||||
widget.conversation.lastChangeTimestamp > -1 &&
|
|
||||||
initNow - widget.conversation.lastChangeTimestamp >=
|
|
||||||
60 * Duration.millisecondsPerMinute) {
|
|
||||||
_updateTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
|
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
setState(() {
|
|
||||||
_timestampString = formatConversationTimestamp(
|
|
||||||
widget.conversation.lastChangeTimestamp,
|
|
||||||
now,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (now - widget.conversation.lastChangeTimestamp >=
|
|
||||||
60 * Duration.millisecondsPerMinute) {
|
|
||||||
_updateTimer!.cancel();
|
|
||||||
_updateTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_updateTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
if (_updateTimer != null) {
|
|
||||||
_updateTimer!.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAvatar() {
|
|
||||||
return RebuildOnContactIntegrationChange(
|
|
||||||
builder: () {
|
|
||||||
final avatar = CachingXMPPAvatar(
|
|
||||||
borderRadius: 35,
|
|
||||||
size: 70,
|
|
||||||
jid: widget.conversation.jid,
|
|
||||||
hash: widget.conversation.avatarHash,
|
|
||||||
path: widget.conversation.avatarPathWithOptionalContact,
|
|
||||||
hasContactId: widget.conversation.contactId != null,
|
|
||||||
isGroupchat: widget.conversation.isGroupchat,
|
|
||||||
altIcon: widget.conversation.type == ConversationType.note
|
|
||||||
? Icons.notes
|
|
||||||
: null,
|
|
||||||
shouldRequest: widget.conversation.type != ConversationType.note,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.enableAvatarOnTap &&
|
|
||||||
widget.conversation.avatarPathWithOptionalContact != null &&
|
|
||||||
widget.conversation.avatarPathWithOptionalContact!.isNotEmpty) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return IgnorePointer(
|
|
||||||
child: Image.file(
|
|
||||||
File(widget.conversation.avatarPathWithOptionalContact!),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
child: avatar,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return avatar;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLastMessagePreview() {
|
|
||||||
Widget? preview;
|
|
||||||
if (widget.conversation.lastMessage!.stickerPackId != null) {
|
|
||||||
if (widget.conversation.lastMessage!.fileMetadata!.path != null) {
|
|
||||||
preview = SharedImageWidget(
|
|
||||||
widget.conversation.lastMessage!.fileMetadata!.path!,
|
|
||||||
borderRadius: 5,
|
|
||||||
size: 30,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
preview = Icon(
|
|
||||||
PhosphorIcons.regular.sticker,
|
|
||||||
size: 30,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (widget.conversation.lastMessage!.fileMetadata!.mimeType!
|
|
||||||
.startsWith('image/')) {
|
|
||||||
if (widget.conversation.lastMessage!.fileMetadata!.path == null) {
|
|
||||||
preview = const SizedBox();
|
|
||||||
} else {
|
|
||||||
preview = SharedImageWidget(
|
|
||||||
widget.conversation.lastMessage!.fileMetadata!.path!,
|
|
||||||
borderRadius: 5,
|
|
||||||
size: 30,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (widget.conversation.lastMessage!.fileMetadata!.mimeType!
|
|
||||||
.startsWith('video/')) {
|
|
||||||
if (widget.conversation.lastMessage!.fileMetadata!.path == null) {
|
|
||||||
preview = const SizedBox();
|
|
||||||
} else {
|
|
||||||
preview = SharedVideoWidget(
|
|
||||||
widget.conversation.lastMessage!.fileMetadata!.path!,
|
|
||||||
widget.conversation.jid,
|
|
||||||
widget.conversation.lastMessage!.fileMetadata!.mimeType!,
|
|
||||||
borderRadius: 5,
|
|
||||||
size: 30,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: preview,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLastMessageBody() {
|
|
||||||
if (widget.conversation.isTyping) {
|
|
||||||
return const TypingIndicatorWidget(Colors.black, Colors.white);
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastMessage = widget.conversation.lastMessage;
|
|
||||||
String body;
|
|
||||||
if (lastMessage == null) {
|
|
||||||
body = '';
|
|
||||||
} else {
|
|
||||||
if (lastMessage.isRetracted) {
|
|
||||||
body = t.messages.retracted;
|
|
||||||
} else if (lastMessage.isMedia) {
|
|
||||||
// If the file is thumbnailable, we display a small preview on the left of the
|
|
||||||
// body, so we don't need the emoji then.
|
|
||||||
if (lastMessage.stickerPackId != null) {
|
|
||||||
body = t.messages.sticker;
|
|
||||||
} else if (lastMessage.isThumbnailable) {
|
|
||||||
body = mimeTypeToName(lastMessage.fileMetadata!.mimeType);
|
|
||||||
} else {
|
|
||||||
body = mimeTypeToEmoji(lastMessage.fileMetadata!.mimeType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body = widget.conversation.lastMessage!.body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
body,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _getLastMessageIcon(bool sentBySelf) {
|
|
||||||
final lastMessage = widget.conversation.lastMessage;
|
|
||||||
if (lastMessage == null) return const SizedBox();
|
|
||||||
|
|
||||||
Widget? icon;
|
|
||||||
if (sentBySelf) {
|
|
||||||
if (lastMessage.displayed) {
|
|
||||||
icon = Icon(
|
|
||||||
Icons.done_all,
|
|
||||||
color: Colors.blue.shade700,
|
|
||||||
);
|
|
||||||
} else if (lastMessage.received) {
|
|
||||||
icon = const Icon(Icons.done_all);
|
|
||||||
} else if (lastMessage.acked) {
|
|
||||||
icon = const Icon(Icons.done);
|
|
||||||
} else if (lastMessage.hasError) {
|
|
||||||
icon = const Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.red,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (lastMessage.isEdited) {
|
|
||||||
icon = const Icon(Icons.edit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (icon != null) {
|
|
||||||
if (widget.conversation.unreadCounter > 0) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: icon,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Figure out the display name of the last message's sender.
|
|
||||||
/// [sentBySelf] indicates that the last message was sent by us.
|
|
||||||
/// Requires that [widget.conversation.lastMessage] is non-null.
|
|
||||||
String _getSenderName(bool sentBySelf) {
|
|
||||||
if (sentBySelf) {
|
|
||||||
return t.messages.you;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(
|
|
||||||
widget.conversation.lastMessage != null,
|
|
||||||
'The conversation must have a last message',
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
widget.conversation.isGroupchat,
|
|
||||||
'The sender name should not be displayed for non-groupchat conversations',
|
|
||||||
);
|
|
||||||
return widget.conversation.lastMessage!.senderJid.resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final badgeText = widget.conversation.unreadCounter > 99
|
|
||||||
? '99+'
|
|
||||||
: widget.conversation.unreadCounter.toString();
|
|
||||||
final showTimestamp =
|
|
||||||
widget.conversation.lastChangeTimestamp != timestampNever &&
|
|
||||||
widget.showTimestamp;
|
|
||||||
final sentBySelf = widget.conversation.lastMessage?.sender ==
|
|
||||||
GetIt.I.get<AccountCubit>().state.account.jid;
|
|
||||||
|
|
||||||
final showBadge = widget.conversation.unreadCounter > 0;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(radiusLargeSize),
|
|
||||||
child: AnimatedMaterialColor(
|
|
||||||
color: widget.isSelected ? Colors.blue : Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: widget.onPressed ?? () {},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildAvatar(),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
RebuildOnContactIntegrationChange(
|
|
||||||
builder: () => Text(
|
|
||||||
widget.conversation.titleWithOptionalContact,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 17,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.titleSuffixIcon != null)
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 6),
|
|
||||||
child: Icon(
|
|
||||||
widget.titleSuffixIcon,
|
|
||||||
size: 17,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showTimestamp) const Spacer(),
|
|
||||||
if (showTimestamp) Text(_timestampString),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (!widget.conversation.isTyping &&
|
|
||||||
widget.conversation.lastMessage != null &&
|
|
||||||
(sentBySelf ||
|
|
||||||
widget.conversation.isGroupchat))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${_getSenderName(sentBySelf)}:',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if ((widget.conversation.lastMessage
|
|
||||||
?.isThumbnailable ??
|
|
||||||
false) &&
|
|
||||||
!widget.conversation.isTyping)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: _buildLastMessagePreview(),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
Expanded(
|
|
||||||
child: _buildLastMessageBody(),
|
|
||||||
),
|
|
||||||
_getLastMessageIcon(sentBySelf),
|
|
||||||
// Off-stage the badge if not visible to prevent the invisible
|
|
||||||
// badge taking up space.
|
|
||||||
Offstage(
|
|
||||||
offstage: !showBadge,
|
|
||||||
child: badges.Badge(
|
|
||||||
badgeContent: Text(badgeText),
|
|
||||||
showBadge: showBadge,
|
|
||||||
badgeStyle: const badges.BadgeStyle(
|
|
||||||
badgeColor: bubbleColorSent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,84 @@ IconData? _messageStateToIcon(Message msg) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AnimatedMaterialColor extends StatefulWidget {
|
||||||
|
const AnimatedMaterialColor({
|
||||||
|
required this.color,
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The color attribute of the [Material] widget.
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// The child widget of the [Material] widget.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AnimatedMaterialColorState createState() => AnimatedMaterialColorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimatedMaterialColorState extends State<AnimatedMaterialColor>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
late final ColorTween _tween;
|
||||||
|
late final Animation<Color?> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_tween = ColorTween(
|
||||||
|
begin: widget.color,
|
||||||
|
end: widget.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
_animation = _tween.animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(AnimatedMaterialColor oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (oldWidget.color != widget.color) {
|
||||||
|
_tween
|
||||||
|
..begin = oldWidget.color
|
||||||
|
..end = widget.color;
|
||||||
|
|
||||||
|
_controller
|
||||||
|
..reset()
|
||||||
|
..forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (_, child) => Material(
|
||||||
|
color: _animation.value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RowIcon extends StatelessWidget {
|
class _RowIcon extends StatelessWidget {
|
||||||
const _RowIcon(
|
const _RowIcon(
|
||||||
this.icon, {
|
this.icon, {
|
||||||
@@ -139,6 +217,7 @@ class ConversationCard extends StatelessWidget {
|
|||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.highlightWord,
|
this.highlightWord,
|
||||||
this.showTimestamp = true,
|
this.showTimestamp = true,
|
||||||
|
this.selected = false,
|
||||||
this.titleSuffixIcon,
|
this.titleSuffixIcon,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -158,6 +237,10 @@ class ConversationCard extends StatelessWidget {
|
|||||||
/// Icon to show after the conversation title.
|
/// Icon to show after the conversation title.
|
||||||
final Widget? titleSuffixIcon;
|
final Widget? titleSuffixIcon;
|
||||||
|
|
||||||
|
/// Flag indicating whether we should show a selection indicator (true) or
|
||||||
|
/// not (false).
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
Widget _buildLastMessagePreview() {
|
Widget _buildLastMessagePreview() {
|
||||||
Widget? preview;
|
Widget? preview;
|
||||||
if (conversation.lastMessage!.stickerPackId != null) {
|
if (conversation.lastMessage!.stickerPackId != null) {
|
||||||
@@ -283,8 +366,10 @@ class ConversationCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sentBySelf = conversation.lastMessage?.sender ==
|
final sentBySelf = conversation.lastMessage?.sender ==
|
||||||
GetIt.I.get<AccountCubit>().state.account.jid;
|
GetIt.I.get<AccountCubit>().state.account.jid;
|
||||||
return Material(
|
return AnimatedMaterialColor(
|
||||||
color: Colors.transparent,
|
color: selected
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|||||||
Reference in New Issue
Block a user