chore: Follow up new reactions picker
This commit is contained in:
parent
37d313c271
commit
13f27eda9f
5 changed files with 488 additions and 354 deletions
|
|
@ -24,7 +24,7 @@ abstract class AppConfig {
|
||||||
static String _privacyUrl =
|
static String _privacyUrl =
|
||||||
'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md';
|
'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md';
|
||||||
|
|
||||||
static const Set<String> defaultReactions = {'👍', '❤️', '😊'};
|
static const Set<String> defaultReactions = {'👍', '❤️', '😂', '😮', '😢'};
|
||||||
|
|
||||||
static String get privacyUrl => _privacyUrl;
|
static String get privacyUrl => _privacyUrl;
|
||||||
static const String website = 'https://fluffychat.im';
|
static const String website = 'https://fluffychat.im';
|
||||||
|
|
|
||||||
|
|
@ -891,6 +891,16 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get canEditSelectedEvents {
|
||||||
|
if (isArchived ||
|
||||||
|
selectedEvents.length != 1 ||
|
||||||
|
!selectedEvents.first.status.isSent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return currentRoomBundle
|
||||||
|
.any((cl) => selectedEvents.first.senderId == cl!.userID);
|
||||||
|
}
|
||||||
|
|
||||||
void forwardEventsAction() async {
|
void forwardEventsAction() async {
|
||||||
if (selectedEvents.isEmpty) return;
|
if (selectedEvents.isEmpty) return;
|
||||||
await showScaffoldDialog(
|
await showScaffoldDialog(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class ChatInputRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
const height = 48.0;
|
const height = 48.0;
|
||||||
|
|
||||||
if (!controller.room.otherPartyCanReceiveMessages) {
|
if (!controller.room.otherPartyCanReceiveMessages) {
|
||||||
|
|
@ -39,231 +40,290 @@ class ChatInputRow extends StatelessWidget {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: controller.selectMode
|
||||||
const SizedBox(width: 4),
|
? <Widget>[
|
||||||
AnimatedContainer(
|
if (controller.selectedEvents
|
||||||
duration: FluffyThemes.animationDuration,
|
.every((event) => event.status == EventStatus.error))
|
||||||
curve: FluffyThemes.animationCurve,
|
SizedBox(
|
||||||
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
height: height,
|
||||||
height: height,
|
child: TextButton(
|
||||||
alignment: Alignment.center,
|
style: TextButton.styleFrom(
|
||||||
decoration: const BoxDecoration(),
|
foregroundColor: theme.colorScheme.error,
|
||||||
clipBehavior: Clip.hardEdge,
|
),
|
||||||
child: PopupMenuButton<String>(
|
onPressed: controller.deleteErrorEventsAction,
|
||||||
useRootNavigator: true,
|
child: Row(
|
||||||
enabled: !controller.selectMode,
|
children: <Widget>[
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
const Icon(Icons.delete),
|
||||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
Text(L10n.of(context).delete),
|
||||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
],
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
),
|
||||||
if (PlatformInfos.isMobile)
|
),
|
||||||
PopupMenuItem<String>(
|
)
|
||||||
value: 'location',
|
else
|
||||||
child: ListTile(
|
SizedBox(
|
||||||
leading: CircleAvatar(
|
height: height,
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
child: TextButton(
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
onPressed: controller.forwardEventsAction,
|
||||||
child: const Icon(Icons.gps_fixed_outlined),
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(Icons.keyboard_arrow_left_outlined),
|
||||||
|
Text(L10n.of(context).forward),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context).shareLocation),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem<String>(
|
controller.selectedEvents.length == 1
|
||||||
value: 'image',
|
? controller.selectedEvents.first
|
||||||
child: ListTile(
|
.getDisplayEvent(controller.timeline!)
|
||||||
leading: CircleAvatar(
|
.status
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
.isSent
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
? SizedBox(
|
||||||
child: const Icon(Icons.photo_outlined),
|
height: height,
|
||||||
),
|
child: TextButton(
|
||||||
title: Text(L10n.of(context).sendImage),
|
onPressed: controller.replyAction,
|
||||||
contentPadding: const EdgeInsets.all(0),
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(L10n.of(context).reply),
|
||||||
|
const Icon(Icons.keyboard_arrow_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: controller.sendAgainAction,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(L10n.of(context).tryToSendAgain),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(Icons.send_outlined, size: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
]
|
||||||
|
: <Widget>[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: FluffyThemes.animationDuration,
|
||||||
|
curve: FluffyThemes.animationCurve,
|
||||||
|
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
||||||
|
height: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: const BoxDecoration(),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: PopupMenuButton<String>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||||
|
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
<PopupMenuEntry<String>>[
|
||||||
|
if (PlatformInfos.isMobile)
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'location',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.gps_fixed_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).shareLocation),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'image',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.photo_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).sendImage),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'video',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.video_camera_back_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).sendVideo),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'file',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.attachment_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).sendFile),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem<String>(
|
if (PlatformInfos.isMobile)
|
||||||
value: 'video',
|
AnimatedContainer(
|
||||||
child: ListTile(
|
duration: FluffyThemes.animationDuration,
|
||||||
leading: CircleAvatar(
|
curve: FluffyThemes.animationCurve,
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
height: height,
|
||||||
child: const Icon(Icons.video_camera_back_outlined),
|
alignment: Alignment.center,
|
||||||
|
decoration: const BoxDecoration(),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: PopupMenuButton(
|
||||||
|
useRootNavigator: true,
|
||||||
|
icon: const Icon(Icons.camera_alt_outlined),
|
||||||
|
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||||
|
iconColor: theme.colorScheme.onPrimaryContainer,
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'camera-video',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.videocam_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).recordAVideo),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'camera',
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme.onPrimaryContainer,
|
||||||
|
foregroundColor: theme.colorScheme.primaryContainer,
|
||||||
|
child: const Icon(Icons.camera_alt_outlined),
|
||||||
|
),
|
||||||
|
title: Text(L10n.of(context).takeAPhoto),
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context).sendVideo),
|
),
|
||||||
contentPadding: const EdgeInsets.all(0),
|
Container(
|
||||||
|
height: height,
|
||||||
|
width: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: L10n.of(context).emojis,
|
||||||
|
color: theme.colorScheme.onPrimaryContainer,
|
||||||
|
icon: PageTransitionSwitcher(
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> primaryAnimation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: primaryAnimation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
transitionType: SharedAxisTransitionType.scaled,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
controller.showEmojiPicker
|
||||||
|
? Icons.keyboard
|
||||||
|
: Icons.add_reaction_outlined,
|
||||||
|
key: ValueKey(controller.showEmojiPicker),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: controller.emojiPickerAction,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem<String>(
|
if (Matrix.of(context).isMultiAccount &&
|
||||||
value: 'file',
|
Matrix.of(context).hasComplexBundles &&
|
||||||
child: ListTile(
|
Matrix.of(context).currentBundle!.length > 1)
|
||||||
leading: CircleAvatar(
|
Container(
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
width: height,
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
height: height,
|
||||||
child: const Icon(Icons.attachment_outlined),
|
alignment: Alignment.center,
|
||||||
),
|
child: _ChatAccountPicker(controller),
|
||||||
title: Text(L10n.of(context).sendFile),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||||
|
child: InputBar(
|
||||||
|
room: controller.room,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 8,
|
||||||
|
autofocus: !PlatformInfos.isMobile,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
textInputAction:
|
||||||
|
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
|
||||||
|
? TextInputAction.send
|
||||||
|
: null,
|
||||||
|
onSubmitted: controller.onInputBarSubmitted,
|
||||||
|
onSubmitImage: controller.sendImageFromClipBoard,
|
||||||
|
focusNode: controller.inputFocus,
|
||||||
|
controller: controller.sendController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 6.0,
|
||||||
|
right: 6.0,
|
||||||
|
bottom: 6.0,
|
||||||
|
top: 3.0,
|
||||||
|
),
|
||||||
|
hintText: L10n.of(context).writeAMessage,
|
||||||
|
hintMaxLines: 1,
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
filled: false,
|
||||||
|
),
|
||||||
|
onChanged: controller.onInputBarChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: height,
|
||||||
|
width: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: PlatformInfos.platformCanRecord &&
|
||||||
|
controller.sendController.text.isEmpty
|
||||||
|
? FloatingActionButton.small(
|
||||||
|
tooltip: L10n.of(context).voiceMessage,
|
||||||
|
onPressed: controller.voiceMessageAction,
|
||||||
|
elevation: 0,
|
||||||
|
heroTag: null,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(height),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.bubbleColor,
|
||||||
|
foregroundColor: theme.onBubbleColor,
|
||||||
|
child: const Icon(Icons.mic_none_outlined),
|
||||||
|
)
|
||||||
|
: FloatingActionButton.small(
|
||||||
|
tooltip: L10n.of(context).send,
|
||||||
|
onPressed: controller.send,
|
||||||
|
elevation: 0,
|
||||||
|
heroTag: null,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(height),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.bubbleColor,
|
||||||
|
foregroundColor: theme.onBubbleColor,
|
||||||
|
child: const Icon(Icons.send_outlined),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
if (PlatformInfos.isMobile)
|
|
||||||
AnimatedContainer(
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
width: controller.sendController.text.isNotEmpty ? 0 : height,
|
|
||||||
height: height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: const BoxDecoration(),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
child: PopupMenuButton(
|
|
||||||
enabled: !controller.selectMode,
|
|
||||||
useRootNavigator: true,
|
|
||||||
icon: const Icon(Icons.camera_alt_outlined),
|
|
||||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
|
||||||
iconColor: theme.colorScheme.onPrimaryContainer,
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'camera-video',
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
|
||||||
child: const Icon(Icons.videocam_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context).recordAVideo),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem<String>(
|
|
||||||
value: 'camera',
|
|
||||||
child: ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: theme.colorScheme.onPrimaryContainer,
|
|
||||||
foregroundColor: theme.colorScheme.primaryContainer,
|
|
||||||
child: const Icon(Icons.camera_alt_outlined),
|
|
||||||
),
|
|
||||||
title: Text(L10n.of(context).takeAPhoto),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: height,
|
|
||||||
width: height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: IconButton(
|
|
||||||
tooltip: L10n.of(context).emojis,
|
|
||||||
color: theme.colorScheme.onPrimaryContainer,
|
|
||||||
icon: PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (
|
|
||||||
Widget child,
|
|
||||||
Animation<double> primaryAnimation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: primaryAnimation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.scaled,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Icon(
|
|
||||||
controller.showEmojiPicker
|
|
||||||
? Icons.keyboard
|
|
||||||
: Icons.add_reaction_outlined,
|
|
||||||
key: ValueKey(controller.showEmojiPicker),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed:
|
|
||||||
controller.selectMode ? null : controller.emojiPickerAction,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (Matrix.of(context).isMultiAccount &&
|
|
||||||
Matrix.of(context).hasComplexBundles &&
|
|
||||||
Matrix.of(context).currentBundle!.length > 1)
|
|
||||||
Container(
|
|
||||||
width: height,
|
|
||||||
height: height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: _ChatAccountPicker(controller),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
|
||||||
child: InputBar(
|
|
||||||
room: controller.room,
|
|
||||||
minLines: 1,
|
|
||||||
readOnly: controller.selectMode,
|
|
||||||
maxLines: 8,
|
|
||||||
autofocus: !PlatformInfos.isMobile,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
textInputAction:
|
|
||||||
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
|
|
||||||
? TextInputAction.send
|
|
||||||
: null,
|
|
||||||
onSubmitted: controller.onInputBarSubmitted,
|
|
||||||
onSubmitImage: controller.sendImageFromClipBoard,
|
|
||||||
focusNode: controller.inputFocus,
|
|
||||||
controller: controller.sendController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.only(
|
|
||||||
left: 6.0,
|
|
||||||
right: 6.0,
|
|
||||||
bottom: 6.0,
|
|
||||||
top: 3.0,
|
|
||||||
),
|
|
||||||
hintText: L10n.of(context).writeAMessage,
|
|
||||||
hintMaxLines: 1,
|
|
||||||
border: InputBorder.none,
|
|
||||||
enabledBorder: InputBorder.none,
|
|
||||||
filled: false,
|
|
||||||
),
|
|
||||||
onChanged: controller.onInputBarChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Opacity(
|
|
||||||
opacity: controller.selectMode ? 0.66 : 1,
|
|
||||||
child: Container(
|
|
||||||
height: height,
|
|
||||||
width: height,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: PlatformInfos.platformCanRecord &&
|
|
||||||
controller.sendController.text.isEmpty
|
|
||||||
? FloatingActionButton.small(
|
|
||||||
tooltip: L10n.of(context).voiceMessage,
|
|
||||||
onPressed: controller.selectMode
|
|
||||||
? null
|
|
||||||
: controller.voiceMessageAction,
|
|
||||||
elevation: 0,
|
|
||||||
heroTag: null,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(height),
|
|
||||||
),
|
|
||||||
backgroundColor: theme.bubbleColor,
|
|
||||||
foregroundColor: theme.onBubbleColor,
|
|
||||||
child: const Icon(Icons.mic_none_outlined),
|
|
||||||
)
|
|
||||||
: FloatingActionButton.small(
|
|
||||||
tooltip: L10n.of(context).send,
|
|
||||||
onPressed: controller.selectMode ? null : controller.send,
|
|
||||||
elevation: 0,
|
|
||||||
heroTag: null,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(height),
|
|
||||||
),
|
|
||||||
backgroundColor: theme.bubbleColor,
|
|
||||||
foregroundColor: theme.onBubbleColor,
|
|
||||||
child: const Icon(Icons.send_outlined),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ class ChatView extends StatelessWidget {
|
||||||
List<Widget> _appBarActions(BuildContext context) {
|
List<Widget> _appBarActions(BuildContext context) {
|
||||||
if (controller.selectMode) {
|
if (controller.selectMode) {
|
||||||
return [
|
return [
|
||||||
|
if (controller.canEditSelectedEvents)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
tooltip: L10n.of(context).edit,
|
||||||
|
onPressed: controller.editSelectedEventAction,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.copy_outlined),
|
icon: const Icon(Icons.copy_outlined),
|
||||||
tooltip: L10n.of(context).copy,
|
tooltip: L10n.of(context).copy,
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@ class Message extends StatelessWidget {
|
||||||
final showReceiptsRow =
|
final showReceiptsRow =
|
||||||
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
||||||
|
|
||||||
|
final showReactionPicker =
|
||||||
|
singleSelected && event.room.canSendDefaultMessages;
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Swipeable(
|
child: Swipeable(
|
||||||
key: ValueKey(event.eventId),
|
key: ValueKey(event.eventId),
|
||||||
|
|
@ -263,6 +266,7 @@ class Message extends StatelessWidget {
|
||||||
child: animateIn
|
child: animateIn
|
||||||
? const SizedBox(height: 0, width: double.infinity)
|
? const SizedBox(height: 0, width: double.infinity)
|
||||||
: Stack(
|
: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -290,7 +294,7 @@ class Message extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: rowMainAxisAlignment,
|
mainAxisAlignment: rowMainAxisAlignment,
|
||||||
children: [
|
children: [
|
||||||
if (longPressSelect)
|
if (longPressSelect && !event.redacted)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
width: Avatar.defaultSize,
|
width: Avatar.defaultSize,
|
||||||
|
|
@ -413,8 +417,10 @@ class Message extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
padding:
|
padding: EdgeInsets.only(
|
||||||
const EdgeInsets.only(left: 8),
|
left: 8,
|
||||||
|
bottom: showReactionPicker ? 40 : 0,
|
||||||
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onLongPress: longPressSelect
|
onLongPress: longPressSelect
|
||||||
? null
|
? null
|
||||||
|
|
@ -625,156 +631,208 @@ class Message extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
left:
|
||||||
|
ownMessage ? null : Avatar.defaultSize + 8,
|
||||||
|
right: ownMessage ? 0 : null,
|
||||||
|
bottom: 0,
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: FluffyThemes.animationDuration,
|
||||||
|
curve: FluffyThemes.animationCurve,
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: showReactionPicker
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 4.0,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius,
|
||||||
|
),
|
||||||
|
shadowColor: theme
|
||||||
|
.colorScheme.surface
|
||||||
|
.withAlpha(128),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
...AppConfig.defaultReactions
|
||||||
|
.map(
|
||||||
|
(emoji) => IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: sentReactions
|
||||||
|
.contains(emoji)
|
||||||
|
? 0.33
|
||||||
|
: 1,
|
||||||
|
child: Text(
|
||||||
|
emoji,
|
||||||
|
style:
|
||||||
|
const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
textAlign:
|
||||||
|
TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: sentReactions
|
||||||
|
.contains(emoji)
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
onSelect(event);
|
||||||
|
event.room
|
||||||
|
.sendReaction(
|
||||||
|
event.eventId,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_reaction_outlined,
|
||||||
|
),
|
||||||
|
tooltip: L10n.of(context)
|
||||||
|
.customReaction,
|
||||||
|
onPressed: () async {
|
||||||
|
final emoji =
|
||||||
|
await showDialog<
|
||||||
|
String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.min,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
CloseButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
L10n.of(context)
|
||||||
|
.customReaction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
titlePadding:
|
||||||
|
const EdgeInsets
|
||||||
|
.all(8),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets
|
||||||
|
.all(0),
|
||||||
|
clipBehavior:
|
||||||
|
Clip.hardEdge,
|
||||||
|
content: SizedBox(
|
||||||
|
width: 350,
|
||||||
|
height: 350,
|
||||||
|
child: EmojiPicker(
|
||||||
|
onEmojiSelected: (
|
||||||
|
_,
|
||||||
|
emoji,
|
||||||
|
) =>
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(
|
||||||
|
emoji.emoji,
|
||||||
|
),
|
||||||
|
config: Config(
|
||||||
|
emojiViewConfig:
|
||||||
|
const EmojiViewConfig(
|
||||||
|
backgroundColor:
|
||||||
|
Colors
|
||||||
|
.transparent,
|
||||||
|
),
|
||||||
|
bottomActionBarConfig:
|
||||||
|
const BottomActionBarConfig(
|
||||||
|
enabled: false,
|
||||||
|
),
|
||||||
|
categoryViewConfig:
|
||||||
|
CategoryViewConfig(
|
||||||
|
initCategory:
|
||||||
|
Category
|
||||||
|
.SMILEYS,
|
||||||
|
backspaceColor: theme
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
iconColor: theme
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withAlpha(
|
||||||
|
128,
|
||||||
|
),
|
||||||
|
iconColorSelected:
|
||||||
|
theme
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
indicatorColor: theme
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
backgroundColor:
|
||||||
|
theme
|
||||||
|
.colorScheme
|
||||||
|
.surface,
|
||||||
|
),
|
||||||
|
skinToneConfig:
|
||||||
|
SkinToneConfig(
|
||||||
|
dialogBackgroundColor:
|
||||||
|
Color.lerp(
|
||||||
|
theme
|
||||||
|
.colorScheme
|
||||||
|
.surface,
|
||||||
|
theme
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer,
|
||||||
|
0.75,
|
||||||
|
)!,
|
||||||
|
indicatorColor: theme
|
||||||
|
.colorScheme
|
||||||
|
.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (emoji == null) return;
|
||||||
|
if (sentReactions.contains(
|
||||||
|
emoji,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelect(event);
|
||||||
|
|
||||||
|
await event.room
|
||||||
|
.sendReaction(
|
||||||
|
event.eventId,
|
||||||
|
emoji,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: Avatar.defaultSize + 8.0),
|
|
||||||
child: AnimatedSize(
|
|
||||||
duration: FluffyThemes.animationDuration,
|
|
||||||
curve: FluffyThemes.animationCurve,
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: singleSelected && event.room.canSendDefaultMessages
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
|
||||||
child: Material(
|
|
||||||
elevation: 4,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(AppConfig.borderRadius),
|
|
||||||
shadowColor: theme.appBarTheme.shadowColor,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.reply_outlined),
|
|
||||||
tooltip: L10n.of(context).reply,
|
|
||||||
onPressed: onSwipe,
|
|
||||||
),
|
|
||||||
if (ownMessage)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit_outlined),
|
|
||||||
tooltip: L10n.of(context).edit,
|
|
||||||
onPressed: onEdit,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.add_reaction_outlined),
|
|
||||||
tooltip: L10n.of(context).customReaction,
|
|
||||||
onPressed: () async {
|
|
||||||
final emoji = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
CloseButton(
|
|
||||||
onPressed: () =>
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop(null),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
L10n.of(context).customReaction,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
titlePadding: const EdgeInsets.all(8),
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
content: SizedBox(
|
|
||||||
width: 350,
|
|
||||||
height: 350,
|
|
||||||
child: EmojiPicker(
|
|
||||||
onEmojiSelected: (_, emoji) =>
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop(emoji.emoji),
|
|
||||||
config: Config(
|
|
||||||
emojiViewConfig:
|
|
||||||
const EmojiViewConfig(
|
|
||||||
backgroundColor:
|
|
||||||
Colors.transparent,
|
|
||||||
),
|
|
||||||
bottomActionBarConfig:
|
|
||||||
const BottomActionBarConfig(
|
|
||||||
enabled: false,
|
|
||||||
),
|
|
||||||
categoryViewConfig:
|
|
||||||
CategoryViewConfig(
|
|
||||||
initCategory: Category.SMILEYS,
|
|
||||||
backspaceColor:
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
iconColor: theme
|
|
||||||
.colorScheme.primary
|
|
||||||
.withAlpha(128),
|
|
||||||
iconColorSelected:
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
indicatorColor:
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme.surface,
|
|
||||||
),
|
|
||||||
skinToneConfig: SkinToneConfig(
|
|
||||||
dialogBackgroundColor:
|
|
||||||
Color.lerp(
|
|
||||||
theme.colorScheme.surface,
|
|
||||||
theme.colorScheme
|
|
||||||
.primaryContainer,
|
|
||||||
0.75,
|
|
||||||
)!,
|
|
||||||
indicatorColor:
|
|
||||||
theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (emoji == null) return;
|
|
||||||
if (sentReactions.contains(emoji)) return;
|
|
||||||
|
|
||||||
await event.room.sendReaction(
|
|
||||||
event.eventId,
|
|
||||||
emoji,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
...AppConfig.defaultReactions.map(
|
|
||||||
(emoji) => IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: Center(
|
|
||||||
child: Opacity(
|
|
||||||
opacity: sentReactions.contains(emoji)
|
|
||||||
? 0.33
|
|
||||||
: 1,
|
|
||||||
child: Text(
|
|
||||||
emoji,
|
|
||||||
style: const TextStyle(fontSize: 20),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: sentReactions.contains(emoji)
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
onSelect(event);
|
|
||||||
event.room.sendReaction(
|
|
||||||
event.eventId,
|
|
||||||
emoji,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
duration: FluffyThemes.animationDuration,
|
duration: FluffyThemes.animationDuration,
|
||||||
curve: FluffyThemes.animationCurve,
|
curve: FluffyThemes.animationCurve,
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
child: !showReceiptsRow
|
child: !showReceiptsRow
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Padding(
|
: Padding(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue