feat: Drag&Drop to send multiple files on desktop and web

This commit is contained in:
Krille Fear 2021-11-28 11:43:36 +01:00
parent 0e984677b8
commit 2737768a60
10 changed files with 247 additions and 189 deletions

View File

@ -27,6 +27,7 @@
- feat: Send reactions to multiple events - feat: Send reactions to multiple events
- feat: Speed up app start - feat: Speed up app start
- feat: Use SalomonBottomBar - feat: Use SalomonBottomBar
- feat: Drag&Drop to send multiple files on desktop and web
- fix: Adjust color - fix: Adjust color
- fix: Automatic key requests - fix: Automatic key requests
- fix: Bootstrap loop - fix: Bootstrap loop

View File

@ -6,6 +6,7 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -29,8 +30,8 @@ import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
import '../../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; import '../../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart'; import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
import '../new_private_chat/send_file_dialog.dart'; import 'send_file_dialog.dart';
import '../new_private_chat/send_location_dialog.dart'; import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart'; import 'sticker_picker_dialog.dart';
class Chat extends StatefulWidget { class Chat extends StatefulWidget {
@ -60,6 +61,28 @@ class ChatController extends State<Chat> {
Timer typingCoolDown; Timer typingCoolDown;
Timer typingTimeout; Timer typingTimeout;
bool currentlyTyping = false; bool currentlyTyping = false;
bool dragging = false;
void onDragEntered(_) => setState(() => dragging = true);
void onDragExited(_) => setState(() => dragging = false);
void onDragDone(DropDoneDetails details) async {
setState(() => dragging = false);
for (final url in details.urls) {
final file = File.fromUri(url);
final bytes = await file.readAsBytes();
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendFileDialog(
file: MatrixFile(
bytes: bytes,
name: file.path.split('/').last,
).detectFileType,
room: room,
),
);
}
}
List<Event> selectedEvents = []; List<Event> selectedEvents = [];

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -168,202 +169,223 @@ class ChatView extends StatelessWidget {
) )
: null, : null,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: Stack( body: DropTarget(
children: <Widget>[ onDragDone: controller.onDragDone,
if (Matrix.of(context).wallpaper != null) onDragEntered: controller.onDragEntered,
Image.file( onDragExited: controller.onDragExited,
Matrix.of(context).wallpaper, child: Stack(
width: double.infinity, children: <Widget>[
height: double.infinity, if (Matrix.of(context).wallpaper != null)
fit: BoxFit.cover, Image.file(
), Matrix.of(context).wallpaper,
SafeArea( width: double.infinity,
child: Column( height: double.infinity,
children: <Widget>[ fit: BoxFit.cover,
TombstoneDisplay(controller), ),
Expanded( SafeArea(
child: FutureBuilder<bool>( child: Column(
future: controller.getTimeline(), children: <Widget>[
builder: (BuildContext context, snapshot) { TombstoneDisplay(controller),
if (snapshot.hasError) { Expanded(
SentryController.captureException( child: FutureBuilder<bool>(
snapshot.error, future: controller.getTimeline(),
StackTrace.current, builder: (BuildContext context, snapshot) {
); if (snapshot.hasError) {
} SentryController.captureException(
if (controller.timeline == null) { snapshot.error,
return const Center( StackTrace.current,
child: CircularProgressIndicator.adaptive( );
strokeWidth: 2), }
); if (controller.timeline == null) {
} return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2),
);
}
// create a map of eventId --> index to greatly improve performance of // create a map of eventId --> index to greatly improve performance of
// ListView's findChildIndexCallback // ListView's findChildIndexCallback
final thisEventsKeyMap = <String, int>{}; final thisEventsKeyMap = <String, int>{};
for (var i = 0; for (var i = 0;
i < controller.filteredEvents.length; i < controller.filteredEvents.length;
i++) { i++) {
thisEventsKeyMap[ thisEventsKeyMap[
controller.filteredEvents[i].eventId] = i; controller.filteredEvents[i].eventId] = i;
} }
return ListView.custom( return ListView.custom(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 16, top: 16,
bottom: 4, bottom: 4,
left: horizontalPadding, left: horizontalPadding,
right: horizontalPadding, right: horizontalPadding,
), ),
reverse: true, reverse: true,
controller: controller.scrollController, controller: controller.scrollController,
keyboardDismissBehavior: PlatformInfos.isIOS keyboardDismissBehavior: PlatformInfos.isIOS
? ScrollViewKeyboardDismissBehavior.onDrag ? ScrollViewKeyboardDismissBehavior.onDrag
: ScrollViewKeyboardDismissBehavior.manual, : ScrollViewKeyboardDismissBehavior.manual,
childrenDelegate: SliverChildBuilderDelegate( childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int i) { (BuildContext context, int i) {
return i == controller.filteredEvents.length + 1 return i ==
? controller.timeline.isRequestingHistory controller.filteredEvents.length + 1
? const Center( ? controller.timeline.isRequestingHistory
child: CircularProgressIndicator ? const Center(
.adaptive(strokeWidth: 2), child: CircularProgressIndicator
) .adaptive(strokeWidth: 2),
: controller.canLoadMore )
? Center( : controller.canLoadMore
child: OutlinedButton( ? Center(
style: child: OutlinedButton(
OutlinedButton.styleFrom( style: OutlinedButton
backgroundColor: Theme.of( .styleFrom(
context) backgroundColor: Theme.of(
.scaffoldBackgroundColor, context)
.scaffoldBackgroundColor,
),
onPressed: controller
.requestHistory,
child: Text(L10n.of(context)
.loadMore),
), ),
onPressed: )
controller.requestHistory, : Container()
child: Text(L10n.of(context) : i == 0
.loadMore), ? Column(
), mainAxisSize: MainAxisSize.min,
) children: [
: Container() SeenByRow(controller),
: i == 0 TypingIndicators(controller),
? Column( ],
mainAxisSize: MainAxisSize.min, )
children: [ : AutoScrollTag(
SeenByRow(controller),
TypingIndicators(controller),
],
)
: AutoScrollTag(
key: ValueKey(controller
.filteredEvents[i - 1].eventId),
index: i - 1,
controller:
controller.scrollController,
child: Swipeable(
key: ValueKey(controller key: ValueKey(controller
.filteredEvents[i - 1] .filteredEvents[i - 1]
.eventId), .eventId),
background: const Padding( index: i - 1,
padding: EdgeInsets.symmetric( controller:
horizontal: 12.0), controller.scrollController,
child: Center( child: Swipeable(
child: Icon( key: ValueKey(controller
Icons.reply_outlined), .filteredEvents[i - 1]
.eventId),
background: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.0),
child: Center(
child: Icon(
Icons.reply_outlined),
),
), ),
), direction:
direction: SwipeDirection.endToStart,
SwipeDirection.endToStart, onSwipe: (direction) =>
onSwipe: (direction) => controller.replyAction(
controller.replyAction( replyTo: controller
replyTo: controller .filteredEvents[
.filteredEvents[ i - 1]),
i - 1]), child: Message(
child: Message( controller
controller .filteredEvents[i - 1],
.filteredEvents[i - 1], onInfoTab: controller
onInfoTab: .showEventInfo,
controller.showEventInfo, onAvatarTab: (Event event) =>
onAvatarTab: (Event event) => showModalBottomSheet(
showModalBottomSheet( context: context,
context: context, builder: (c) =>
builder: (c) => UserBottomSheet(
UserBottomSheet( user: event.sender,
user: event.sender, outerContext:
outerContext: context, context,
onMention: () => controller onMention: () => controller
.sendController .sendController
.text += .text +=
'${event.sender.mention} ', '${event.sender.mention} ',
),
), ),
), unfold: controller.unfold,
unfold: controller.unfold, onSelect: controller
onSelect: controller .onSelectMessage,
.onSelectMessage, scrollToEventId:
scrollToEventId: (String eventId) => controller (String eventId) =>
.scrollToEventId(eventId), controller.scrollToEventId(
longPressSelect: controller eventId),
.selectedEvents.isEmpty, longPressSelect: controller
selected: controller.selectedEvents.any((e) => .selectedEvents.isEmpty,
e.eventId == selected: controller
controller .selectedEvents
.filteredEvents[i - 1] .any((e) =>
.eventId), e.eventId ==
timeline: controller.timeline, controller
nextEvent: i < .filteredEvents[i - 1]
controller .eventId),
.filteredEvents timeline: controller.timeline,
.length nextEvent: i < controller.filteredEvents.length ? controller.filteredEvents[i] : null),
? controller.filteredEvents[i] ),
: null), );
), },
); childCount:
}, controller.filteredEvents.length + 2,
childCount: controller.filteredEvents.length + 2, findChildIndexCallback: (key) =>
findChildIndexCallback: (key) => controller.findChildIndexCallback(
controller.findChildIndexCallback( key, thisEventsKeyMap),
key, thisEventsKeyMap), ),
);
},
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5),
alignment: Alignment.center,
child: Material(
borderRadius: const BorderRadius.only(
bottomLeft:
Radius.circular(AppConfig.borderRadius),
bottomRight:
Radius.circular(AppConfig.borderRadius),
),
elevation: 6,
shadowColor: Theme.of(context)
.secondaryHeaderColor
.withAlpha(100),
clipBehavior: Clip.hardEdge,
color:
Theme.of(context).appBarTheme.backgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
), ),
);
},
),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5),
alignment: Alignment.center,
child: Material(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(AppConfig.borderRadius),
bottomRight:
Radius.circular(AppConfig.borderRadius),
),
elevation: 6,
shadowColor: Theme.of(context)
.secondaryHeaderColor
.withAlpha(100),
clipBehavior: Clip.hardEdge,
color: Theme.of(context).appBarTheme.backgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
), ),
), ),
), ],
], ),
), ),
), if (controller.dragging)
], Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.9),
alignment: Alignment.center,
child: const Icon(
Icons.upload_outlined,
size: 100,
),
),
],
),
), ),
), ),
), ),

View File

@ -13,7 +13,7 @@ import 'package:fluffychat/utils/room_status_extension.dart';
import '../../utils/date_time_extension.dart'; import '../../utils/date_time_extension.dart';
import '../../widgets/avatar.dart'; import '../../widgets/avatar.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
import '../new_private_chat/send_file_dialog.dart'; import '../chat/send_file_dialog.dart';
enum ArchivedRoomAction { delete, rejoin } enum ArchivedRoomAction { delete, rejoin }

View File

@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage/flutter_secure_storage_plugin.h> #include <flutter_secure_storage/flutter_secure_storage_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
file_selector_linux file_selector_linux
flutter_secure_storage flutter_secure_storage
url_launcher_linux url_launcher_linux

View File

@ -246,6 +246,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.3"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
desktop_notifications: desktop_notifications:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -14,6 +14,7 @@ dependencies:
cached_network_image: ^3.1.0 cached_network_image: ^3.1.0
chewie: ^1.2.2 chewie: ^1.2.2
cupertino_icons: any cupertino_icons: any
desktop_drop: ^0.2.0
desktop_notifications: ">=0.4.0 <0.5.0" # Version 0.5.0 breaks web builds: https://github.com/canonical/dbus.dart/issues/250 desktop_notifications: ">=0.4.0 <0.5.0" # Version 0.5.0 breaks web builds: https://github.com/canonical/dbus.dart/issues/250
email_validator: ^2.0.1 email_validator: ^2.0.1
emoji_picker_flutter: ^1.0.7 emoji_picker_flutter: ^1.0.7
@ -23,7 +24,6 @@ dependencies:
file_picker_cross: ^4.5.0 file_picker_cross: ^4.5.0
flutter: flutter:
sdk: flutter sdk: flutter
# From this fix: https://github.com/g123k/flutter_app_badger/pull/47
flutter_app_badger: ^1.3.0 flutter_app_badger: ^1.3.0
flutter_app_lock: ^2.0.0 flutter_app_lock: ^2.0.0
flutter_blurhash: ^0.6.0 flutter_blurhash: ^0.6.0