Skip to content

Commit

Permalink
TW-731: paste images in web, mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
sherlockvn committed Oct 10, 2023
1 parent f1d119c commit ec53400
Show file tree
Hide file tree
Showing 10 changed files with 549 additions and 94 deletions.
4 changes: 3 additions & 1 deletion assets/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2760,5 +2760,7 @@
"thisMessageHasBeenEncrypted": "This message has been encrypted",
"roomCreationFailed": "Room creation failed",
"errorGettingPdf": "Error getting PDF",
"errorPreviewingFile": "Error previewing file"
"errorPreviewingFile": "Error previewing file",
"paste": "Paste",
"cut": "Cut"
}
39 changes: 36 additions & 3 deletions lib/pages/chat/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import 'package:fluffychat/presentation/mixins/media_picker_mixin.dart';
import 'package:fluffychat/presentation/mixins/send_files_mixin.dart';
import 'package:fluffychat/presentation/model/forward/forward_argument.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/clipboard.dart';
import 'package:fluffychat/utils/extension/build_context_extension.dart';
import 'package:fluffychat/utils/extension/value_notifier_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
Expand All @@ -47,7 +48,7 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart';
import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/services.dart' as flutter;
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
Expand Down Expand Up @@ -777,8 +778,40 @@ class ChatController extends State<Chat>
return copyString;
}

void copySingleEventAction() async {
if (selectedEvents.length == 1) {
final event = selectedEvents.first;
final matrixFile = event.getMatrixFile() ??
await event.downloadAndDecryptAttachment(
getThumbnail: true,
);
if (event.messageType == MessageTypes.Image) {
if (matrixFile.filePath != null) {
Clipboard.instance.copyImageAsStream(
File(matrixFile.filePath!),
mimeType: event.mimeType,
);
} else if (matrixFile.bytes != null) {
Clipboard.instance.copyImageAsBytes(
matrixFile.bytes!,
mimeType: event.mimeType,
);
} else {
Logs().e(
'copySingleEventAction(): failed to copy file ${matrixFile.name}',
);
}
}
}
}

void copyEventsAction() {
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
flutter.Clipboard.setData(
flutter.ClipboardData(
text: _getSelectedEventString(),
),
);

showEmojiPickerNotifier.value = false;
setState(() {
selectedEvents.clear();
Expand Down Expand Up @@ -1469,7 +1502,7 @@ class ChatController extends State<Chat>
break;
case ChatContextMenuActions.copyMessage:
onSelectMessage(event);
copyEventsAction();
copySingleEventAction();
break;
case ChatContextMenuActions.pinMessage:
onSelectMessage(event);
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/chat/chat_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ChatView extends StatelessWidget {
TwakeIconButton(
icon: Icons.copy_outlined,
tooltip: L10n.of(context)!.copy,
onTap: controller.copyEventsAction,
onTap: controller.copySingleEventAction,
),
if (controller.canRedactSelectedEvents)
TwakeIconButton(
Expand Down
230 changes: 191 additions & 39 deletions lib/pages/chat/input_bar.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
import 'package:fluffychat/presentation/enum/chat/popup_menu_item_web_enum.dart';
import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart';
import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart';
import 'package:fluffychat/utils/clipboard.dart';
import 'package:fluffychat/utils/extension/raw_key_event_extension.dart';
import 'package:fluffychat/widgets/avatar/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' as flutter;

import 'package:emojis/emoji.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
Expand All @@ -15,7 +22,7 @@ import 'package:fluffychat/widgets/mxc_image.dart';

import 'command_hints.dart';

class InputBar extends StatelessWidget {
class InputBar extends StatelessWidget with PasteImageMixin {
final Room? room;
final int? minLines;
final int? maxLines;
Expand Down Expand Up @@ -311,52 +318,197 @@ class InputBar extends StatelessWidget {
onSubmitted?.call(controller?.text ?? '');
}
},
child: TypeAheadField<Map<String, String?>>(
direction: AxisDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
keepSuggestionsOnSuggestionSelected: true,
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
textFieldConfiguration: TextFieldConfiguration(
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
style: InputBarStyle.getTypeAheadTextStyle(context),
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
child: CallbackShortcuts(
bindings: {
const SingleActivator(
flutter.LogicalKeyboardKey.keyV,
meta: true,
): () async {
if (await Clipboard.instance.isReadableImageFormat()) {
await pasteImage(context, room!);
} else if (controller != null) {
await controller!.pasteText();
}
},
controller: controller,
decoration: decoration!,
focusNode: focusNode,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
const SingleActivator(
flutter.LogicalKeyboardKey.keyC,
meta: true,
): () {
if (controller != null) {
controller!.copyText();
}
},
textCapitalization: TextCapitalization.sentences,
),
suggestionsCallback: getSuggestions,
itemBuilder: (context, suggestion) => SuggestionTile(
suggestion: suggestion,
client: Matrix.of(context).client,
},
child: Listener(
onPointerDown: (PointerDownEvent event) async {
if (event.kind == PointerDeviceKind.mouse &&
event.buttons == kSecondaryMouseButton) {
final screenSize = MediaQuery.of(context).size;
final offset = event.position;
final position = RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy,
);
final menuItem = await showMenu<PopupMenuItemWebEnum>(
useRootNavigator: PlatformInfos.isWeb,
context: context,
items: [
PopupMenuItem(
value: PopupMenuItemWebEnum.copy,
child: Text(L10n.of(context)!.copy),
),
PopupMenuItem(
value: PopupMenuItemWebEnum.cut,
child: Text(L10n.of(context)!.cut),
),
PopupMenuItem(
value: PopupMenuItemWebEnum.paste,
child: Text(L10n.of(context)!.paste),
),
],
position: position,
);

if (menuItem == null) {
return;
}

if (controller == null) {
return;
}

switch (menuItem) {
case PopupMenuItemWebEnum.copy:
controller!.copyText();
break;
case PopupMenuItemWebEnum.cut:
controller!.cutText();
break;
case PopupMenuItemWebEnum.paste:
if (await Clipboard.instance.isReadableImageFormat()) {
await pasteImage(context, room!);
} else {
await controller!.pasteText();
}
break;
}
}
},
child: TypeAheadField<Map<String, String?>>(
direction: AxisDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
keepSuggestionsOnSuggestionSelected: true,
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
textFieldConfiguration: TextFieldConfiguration(
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
style: InputBarStyle.getTypeAheadTextStyle(context),
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
},
controller: controller,
decoration: decoration!,
focusNode: focusNode,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (keyboardInsertContent) async {
if (room == null || !keyboardInsertContent.hasData) {
return;
}
await showDialog(
context: context,
builder: (context) {
return SendFileDialog(
room: room!,
files: [
MatrixFile(
name: keyboardInsertContent.uri,
bytes: keyboardInsertContent.data,
)
],
);
},
);
},
),
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
) {
return AdaptiveTextSelectionToolbar.editable(
anchors: editableTextState.contextMenuAnchors,
clipboardStatus: ClipboardStatus.pasteable,
onPaste: !PlatformInfos.isWeb
? () async {
if (room == null) {
// FIXME: need to handle the case when in draft chat
return;
}

if (await Clipboard.instance
.isReadableImageFormat()) {
await pasteImage(context, room!);
} else {
editableTextState
.pasteText(SelectionChangedCause.toolbar);
}
}
: null,
onCopy: () {
editableTextState
.copySelection(SelectionChangedCause.toolbar);
},
onCut: () {
editableTextState
.cutSelection(SelectionChangedCause.toolbar);
},
onSelectAll: () {
editableTextState.selectAll(SelectionChangedCause.toolbar);
},
);
},
),
suggestionsCallback: getSuggestions,
itemBuilder: (context, suggestion) => SuggestionTile(
suggestion: suggestion,
client: Matrix.of(context).client,
),
onSuggestionSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) => Container(),
loadingBuilder: (BuildContext context) => Container(),
// fix loading briefly flickering a dark box
noItemsFoundBuilder: (BuildContext context) =>
Container(), // fix loading briefly showing no suggestions
),
),
onSuggestionSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) => Container(),
loadingBuilder: (BuildContext context) => Container(),
// fix loading briefly flickering a dark box
noItemsFoundBuilder: (BuildContext context) =>
Container(), // fix loading briefly showing no suggestions
),
);
}
}

class PasteIntent extends Intent {
const PasteIntent();
}

class CopyIntent extends Intent {
const CopyIntent();
}

class NewLineIntent extends Intent {}

class SubmitLineIntent extends Intent {}
Expand Down
Loading

0 comments on commit ec53400

Please sign in to comment.