mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-10-31 03:57:27 +01:00 
			
		
		
		
	Merge branch 'krille/dragndrop' into 'main'
feat: Drag&Drop to send multiple files on desktop and web See merge request famedly/fluffychat!591
This commit is contained in:
		
						commit
						3b72020665
					
				| @ -27,6 +27,7 @@ | ||||
| - feat: Send reactions to multiple events | ||||
| - feat: Speed up app start | ||||
| - feat: Use SalomonBottomBar | ||||
| - feat: Drag&Drop to send multiple files on desktop and web | ||||
| - fix: Adjust color | ||||
| - fix: Automatic key requests | ||||
| - fix: Bootstrap loop | ||||
|  | ||||
| @ -6,6 +6,7 @@ import 'package:flutter/scheduler.dart'; | ||||
| import 'package:flutter/services.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:flutter_app_badger/flutter_app_badger.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/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; | ||||
| import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart'; | ||||
| import '../new_private_chat/send_file_dialog.dart'; | ||||
| import '../new_private_chat/send_location_dialog.dart'; | ||||
| import 'send_file_dialog.dart'; | ||||
| import 'send_location_dialog.dart'; | ||||
| import 'sticker_picker_dialog.dart'; | ||||
| 
 | ||||
| class Chat extends StatefulWidget { | ||||
| @ -60,6 +61,28 @@ class ChatController extends State<Chat> { | ||||
|   Timer typingCoolDown; | ||||
|   Timer typingTimeout; | ||||
|   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 = []; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| 
 | ||||
| import 'package:desktop_drop/desktop_drop.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/l10n.dart'; | ||||
| import 'package:future_loading_dialog/future_loading_dialog.dart'; | ||||
| import 'package:matrix/matrix.dart'; | ||||
| @ -168,202 +169,223 @@ class ChatView extends StatelessWidget { | ||||
|                 ) | ||||
|               : null, | ||||
|           backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|           body: Stack( | ||||
|             children: <Widget>[ | ||||
|               if (Matrix.of(context).wallpaper != null) | ||||
|                 Image.file( | ||||
|                   Matrix.of(context).wallpaper, | ||||
|                   width: double.infinity, | ||||
|                   height: double.infinity, | ||||
|                   fit: BoxFit.cover, | ||||
|                 ), | ||||
|               SafeArea( | ||||
|                 child: Column( | ||||
|                   children: <Widget>[ | ||||
|                     TombstoneDisplay(controller), | ||||
|                     Expanded( | ||||
|                       child: FutureBuilder<bool>( | ||||
|                         future: controller.getTimeline(), | ||||
|                         builder: (BuildContext context, snapshot) { | ||||
|                           if (snapshot.hasError) { | ||||
|                             SentryController.captureException( | ||||
|                               snapshot.error, | ||||
|                               StackTrace.current, | ||||
|                             ); | ||||
|                           } | ||||
|                           if (controller.timeline == null) { | ||||
|                             return const Center( | ||||
|                               child: CircularProgressIndicator.adaptive( | ||||
|                                   strokeWidth: 2), | ||||
|                             ); | ||||
|                           } | ||||
|           body: DropTarget( | ||||
|             onDragDone: controller.onDragDone, | ||||
|             onDragEntered: controller.onDragEntered, | ||||
|             onDragExited: controller.onDragExited, | ||||
|             child: Stack( | ||||
|               children: <Widget>[ | ||||
|                 if (Matrix.of(context).wallpaper != null) | ||||
|                   Image.file( | ||||
|                     Matrix.of(context).wallpaper, | ||||
|                     width: double.infinity, | ||||
|                     height: double.infinity, | ||||
|                     fit: BoxFit.cover, | ||||
|                   ), | ||||
|                 SafeArea( | ||||
|                   child: Column( | ||||
|                     children: <Widget>[ | ||||
|                       TombstoneDisplay(controller), | ||||
|                       Expanded( | ||||
|                         child: FutureBuilder<bool>( | ||||
|                           future: controller.getTimeline(), | ||||
|                           builder: (BuildContext context, snapshot) { | ||||
|                             if (snapshot.hasError) { | ||||
|                               SentryController.captureException( | ||||
|                                 snapshot.error, | ||||
|                                 StackTrace.current, | ||||
|                               ); | ||||
|                             } | ||||
|                             if (controller.timeline == null) { | ||||
|                               return const Center( | ||||
|                                 child: CircularProgressIndicator.adaptive( | ||||
|                                     strokeWidth: 2), | ||||
|                               ); | ||||
|                             } | ||||
| 
 | ||||
|                           // create a map of eventId --> index to greatly improve performance of | ||||
|                           // ListView's findChildIndexCallback | ||||
|                           final thisEventsKeyMap = <String, int>{}; | ||||
|                           for (var i = 0; | ||||
|                               i < controller.filteredEvents.length; | ||||
|                               i++) { | ||||
|                             thisEventsKeyMap[ | ||||
|                                 controller.filteredEvents[i].eventId] = i; | ||||
|                           } | ||||
|                           return ListView.custom( | ||||
|                             padding: EdgeInsets.only( | ||||
|                               top: 16, | ||||
|                               bottom: 4, | ||||
|                               left: horizontalPadding, | ||||
|                               right: horizontalPadding, | ||||
|                             ), | ||||
|                             reverse: true, | ||||
|                             controller: controller.scrollController, | ||||
|                             keyboardDismissBehavior: PlatformInfos.isIOS | ||||
|                                 ? ScrollViewKeyboardDismissBehavior.onDrag | ||||
|                                 : ScrollViewKeyboardDismissBehavior.manual, | ||||
|                             childrenDelegate: SliverChildBuilderDelegate( | ||||
|                               (BuildContext context, int i) { | ||||
|                                 return i == controller.filteredEvents.length + 1 | ||||
|                                     ? controller.timeline.isRequestingHistory | ||||
|                                         ? const Center( | ||||
|                                             child: CircularProgressIndicator | ||||
|                                                 .adaptive(strokeWidth: 2), | ||||
|                                           ) | ||||
|                                         : controller.canLoadMore | ||||
|                                             ? Center( | ||||
|                                                 child: OutlinedButton( | ||||
|                                                   style: | ||||
|                                                       OutlinedButton.styleFrom( | ||||
|                                                     backgroundColor: Theme.of( | ||||
|                                                             context) | ||||
|                                                         .scaffoldBackgroundColor, | ||||
|                             // create a map of eventId --> index to greatly improve performance of | ||||
|                             // ListView's findChildIndexCallback | ||||
|                             final thisEventsKeyMap = <String, int>{}; | ||||
|                             for (var i = 0; | ||||
|                                 i < controller.filteredEvents.length; | ||||
|                                 i++) { | ||||
|                               thisEventsKeyMap[ | ||||
|                                   controller.filteredEvents[i].eventId] = i; | ||||
|                             } | ||||
|                             return ListView.custom( | ||||
|                               padding: EdgeInsets.only( | ||||
|                                 top: 16, | ||||
|                                 bottom: 4, | ||||
|                                 left: horizontalPadding, | ||||
|                                 right: horizontalPadding, | ||||
|                               ), | ||||
|                               reverse: true, | ||||
|                               controller: controller.scrollController, | ||||
|                               keyboardDismissBehavior: PlatformInfos.isIOS | ||||
|                                   ? ScrollViewKeyboardDismissBehavior.onDrag | ||||
|                                   : ScrollViewKeyboardDismissBehavior.manual, | ||||
|                               childrenDelegate: SliverChildBuilderDelegate( | ||||
|                                 (BuildContext context, int i) { | ||||
|                                   return i == | ||||
|                                           controller.filteredEvents.length + 1 | ||||
|                                       ? controller.timeline.isRequestingHistory | ||||
|                                           ? const Center( | ||||
|                                               child: CircularProgressIndicator | ||||
|                                                   .adaptive(strokeWidth: 2), | ||||
|                                             ) | ||||
|                                           : controller.canLoadMore | ||||
|                                               ? Center( | ||||
|                                                   child: OutlinedButton( | ||||
|                                                     style: OutlinedButton | ||||
|                                                         .styleFrom( | ||||
|                                                       backgroundColor: Theme.of( | ||||
|                                                               context) | ||||
|                                                           .scaffoldBackgroundColor, | ||||
|                                                     ), | ||||
|                                                     onPressed: controller | ||||
|                                                         .requestHistory, | ||||
|                                                     child: Text(L10n.of(context) | ||||
|                                                         .loadMore), | ||||
|                                                   ), | ||||
|                                                   onPressed: | ||||
|                                                       controller.requestHistory, | ||||
|                                                   child: Text(L10n.of(context) | ||||
|                                                       .loadMore), | ||||
|                                                 ), | ||||
|                                               ) | ||||
|                                             : Container() | ||||
|                                     : i == 0 | ||||
|                                         ? Column( | ||||
|                                             mainAxisSize: MainAxisSize.min, | ||||
|                                             children: [ | ||||
|                                               SeenByRow(controller), | ||||
|                                               TypingIndicators(controller), | ||||
|                                             ], | ||||
|                                           ) | ||||
|                                         : AutoScrollTag( | ||||
|                                             key: ValueKey(controller | ||||
|                                                 .filteredEvents[i - 1].eventId), | ||||
|                                             index: i - 1, | ||||
|                                             controller: | ||||
|                                                 controller.scrollController, | ||||
|                                             child: Swipeable( | ||||
|                                                 ) | ||||
|                                               : Container() | ||||
|                                       : i == 0 | ||||
|                                           ? Column( | ||||
|                                               mainAxisSize: MainAxisSize.min, | ||||
|                                               children: [ | ||||
|                                                 SeenByRow(controller), | ||||
|                                                 TypingIndicators(controller), | ||||
|                                               ], | ||||
|                                             ) | ||||
|                                           : AutoScrollTag( | ||||
|                                               key: ValueKey(controller | ||||
|                                                   .filteredEvents[i - 1] | ||||
|                                                   .eventId), | ||||
|                                               background: const Padding( | ||||
|                                                 padding: EdgeInsets.symmetric( | ||||
|                                                     horizontal: 12.0), | ||||
|                                                 child: Center( | ||||
|                                                   child: Icon( | ||||
|                                                       Icons.reply_outlined), | ||||
|                                               index: i - 1, | ||||
|                                               controller: | ||||
|                                                   controller.scrollController, | ||||
|                                               child: Swipeable( | ||||
|                                                 key: ValueKey(controller | ||||
|                                                     .filteredEvents[i - 1] | ||||
|                                                     .eventId), | ||||
|                                                 background: const Padding( | ||||
|                                                   padding: EdgeInsets.symmetric( | ||||
|                                                       horizontal: 12.0), | ||||
|                                                   child: Center( | ||||
|                                                     child: Icon( | ||||
|                                                         Icons.reply_outlined), | ||||
|                                                   ), | ||||
|                                                 ), | ||||
|                                               ), | ||||
|                                               direction: | ||||
|                                                   SwipeDirection.endToStart, | ||||
|                                               onSwipe: (direction) => | ||||
|                                                   controller.replyAction( | ||||
|                                                       replyTo: controller | ||||
|                                                               .filteredEvents[ | ||||
|                                                           i - 1]), | ||||
|                                               child: Message( | ||||
|                                                   controller | ||||
|                                                       .filteredEvents[i - 1], | ||||
|                                                   onInfoTab: | ||||
|                                                       controller.showEventInfo, | ||||
|                                                   onAvatarTab: (Event event) => | ||||
|                                                       showModalBottomSheet( | ||||
|                                                         context: context, | ||||
|                                                         builder: (c) => | ||||
|                                                             UserBottomSheet( | ||||
|                                                           user: event.sender, | ||||
|                                                           outerContext: context, | ||||
|                                                           onMention: () => controller | ||||
|                                                                   .sendController | ||||
|                                                                   .text += | ||||
|                                                               '${event.sender.mention} ', | ||||
|                                                 direction: | ||||
|                                                     SwipeDirection.endToStart, | ||||
|                                                 onSwipe: (direction) => | ||||
|                                                     controller.replyAction( | ||||
|                                                         replyTo: controller | ||||
|                                                                 .filteredEvents[ | ||||
|                                                             i - 1]), | ||||
|                                                 child: Message( | ||||
|                                                     controller | ||||
|                                                         .filteredEvents[i - 1], | ||||
|                                                     onInfoTab: controller | ||||
|                                                         .showEventInfo, | ||||
|                                                     onAvatarTab: (Event event) => | ||||
|                                                         showModalBottomSheet( | ||||
|                                                           context: context, | ||||
|                                                           builder: (c) => | ||||
|                                                               UserBottomSheet( | ||||
|                                                             user: event.sender, | ||||
|                                                             outerContext: | ||||
|                                                                 context, | ||||
|                                                             onMention: () => controller | ||||
|                                                                     .sendController | ||||
|                                                                     .text += | ||||
|                                                                 '${event.sender.mention} ', | ||||
|                                                           ), | ||||
|                                                         ), | ||||
|                                                       ), | ||||
|                                                   unfold: controller.unfold, | ||||
|                                                   onSelect: controller | ||||
|                                                       .onSelectMessage, | ||||
|                                                   scrollToEventId: (String eventId) => controller | ||||
|                                                       .scrollToEventId(eventId), | ||||
|                                                   longPressSelect: controller | ||||
|                                                       .selectedEvents.isEmpty, | ||||
|                                                   selected: controller.selectedEvents.any((e) => | ||||
|                                                       e.eventId == | ||||
|                                                       controller | ||||
|                                                           .filteredEvents[i - 1] | ||||
|                                                           .eventId), | ||||
|                                                   timeline: controller.timeline, | ||||
|                                                   nextEvent: i < | ||||
|                                                           controller | ||||
|                                                               .filteredEvents | ||||
|                                                               .length | ||||
|                                                       ? controller.filteredEvents[i] | ||||
|                                                       : null), | ||||
|                                             ), | ||||
|                                           ); | ||||
|                               }, | ||||
|                               childCount: controller.filteredEvents.length + 2, | ||||
|                               findChildIndexCallback: (key) => | ||||
|                                   controller.findChildIndexCallback( | ||||
|                                       key, thisEventsKeyMap), | ||||
|                                                     unfold: controller.unfold, | ||||
|                                                     onSelect: controller | ||||
|                                                         .onSelectMessage, | ||||
|                                                     scrollToEventId: | ||||
|                                                         (String eventId) => | ||||
|                                                             controller.scrollToEventId( | ||||
|                                                                 eventId), | ||||
|                                                     longPressSelect: controller | ||||
|                                                         .selectedEvents.isEmpty, | ||||
|                                                     selected: controller | ||||
|                                                         .selectedEvents | ||||
|                                                         .any((e) => | ||||
|                                                             e.eventId == | ||||
|                                                             controller | ||||
|                                                                 .filteredEvents[i - 1] | ||||
|                                                                 .eventId), | ||||
|                                                     timeline: controller.timeline, | ||||
|                                                     nextEvent: i < controller.filteredEvents.length ? controller.filteredEvents[i] : null), | ||||
|                                               ), | ||||
|                                             ); | ||||
|                                 }, | ||||
|                                 childCount: | ||||
|                                     controller.filteredEvents.length + 2, | ||||
|                                 findChildIndexCallback: (key) => | ||||
|                                     controller.findChildIndexCallback( | ||||
|                                         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, | ||||
|                     ), | ||||
|                   ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|  | ||||
| @ -13,7 +13,7 @@ import 'package:fluffychat/utils/room_status_extension.dart'; | ||||
| import '../../utils/date_time_extension.dart'; | ||||
| import '../../widgets/avatar.dart'; | ||||
| import '../../widgets/matrix.dart'; | ||||
| import '../new_private_chat/send_file_dialog.dart'; | ||||
| import '../chat/send_file_dialog.dart'; | ||||
| 
 | ||||
| enum ArchivedRoomAction { delete, rejoin } | ||||
| 
 | ||||
|  | ||||
| @ -6,11 +6,15 @@ | ||||
| 
 | ||||
| #include "generated_plugin_registrant.h" | ||||
| 
 | ||||
| #include <desktop_drop/desktop_drop_plugin.h> | ||||
| #include <file_selector_linux/file_selector_plugin.h> | ||||
| #include <flutter_secure_storage/flutter_secure_storage_plugin.h> | ||||
| #include <url_launcher_linux/url_launcher_plugin.h> | ||||
| 
 | ||||
| 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 = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); | ||||
|   file_selector_plugin_register_with_registrar(file_selector_linux_registrar); | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| # | ||||
| 
 | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   desktop_drop | ||||
|   file_selector_linux | ||||
|   flutter_secure_storage | ||||
|   url_launcher_linux | ||||
|  | ||||
| @ -246,6 +246,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|  | ||||
| @ -14,6 +14,7 @@ dependencies: | ||||
|   cached_network_image: ^3.1.0 | ||||
|   chewie: ^1.2.2 | ||||
|   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 | ||||
|   email_validator: ^2.0.1 | ||||
|   emoji_picker_flutter: ^1.0.7 | ||||
| @ -23,7 +24,6 @@ dependencies: | ||||
|   file_picker_cross: ^4.5.0 | ||||
|   flutter: | ||||
|     sdk: flutter | ||||
|   # From this fix: https://github.com/g123k/flutter_app_badger/pull/47 | ||||
|   flutter_app_badger: ^1.3.0 | ||||
|   flutter_app_lock: ^2.0.0 | ||||
|   flutter_blurhash: ^0.6.0 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Krille Fear
						Krille Fear