diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 9c8242dd2e..8b7c2796b9 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2792,6 +2792,7 @@ "downloadImageSuccess": "Image saved to Pictures", "@downloadImageSuccess": {}, "downloadImageError": "Error saving image", + "downloadFileError": "Error downloading file", "@downloadImageError": {}, "downloadFileInWeb": "File saved to {directory}", "@downloadFileInWeb": { diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index ab65dd7510..fc63e52540 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -2696,6 +2696,7 @@ "@acceptInvite": {}, "downloadImageError": "Erreur d'enregistrement de l'image", "@downloadImageError": {}, + "downloadFileError": "Erreur de téléchargement du fichier", "externalContactMessage": "Certains des utilisateurs que vous souhaitez ajouter ne figurent pas dans vos contacts. Voulez-vous les inviter ?", "@externalContactMessage": {}, "appLanguage": "Langue de l'application", diff --git a/docs/adr/0021-listen-to-presence-status.md b/docs/adr/0022-listen-to-presence-status.md similarity index 98% rename from docs/adr/0021-listen-to-presence-status.md rename to docs/adr/0022-listen-to-presence-status.md index ca5c2da4aa..9c9eed261e 100644 --- a/docs/adr/0021-listen-to-presence-status.md +++ b/docs/adr/0022-listen-to-presence-status.md @@ -1,4 +1,4 @@ -# 21. Listen to presence status +# 22. Listen to presence status Date: 2024-04-08 @@ -61,4 +61,4 @@ Here `lastActivePresence` is updated for each items in `sync.presence` list if i if (lastActivePresence != null) { onlatestPresenceChanged.add(lastActivePresence); } -``` \ No newline at end of file +``` diff --git a/docs/adr/0023-change-open-file-package.md b/docs/adr/0023-change-open-file-package.md new file mode 100644 index 0000000000..803739a973 --- /dev/null +++ b/docs/adr/0023-change-open-file-package.md @@ -0,0 +1,16 @@ +# 23. Change open file package + +Date: 2024-05-13 + +## Status + +Accepted + +## Context + +The package `open_file` has been used to open files on mobile versions of the app. The problem is that this package is not compatible with desktop platforms. Especially on Linux where it caused some errors and does not work. That said we could use the method `Process.run()` for each desktop platform but that might complexify a lot the process. + +## Decision + +A fork of `open_file` has been made, named `open_file_app` (https://pub.dev/packages/open_app_file). A fix has been made for Linux https://github.com/yendoplan/open_app_file/pull/5 which is the branch we will use until it will be merged by the maintainers. +Since it's a fork, the methods are the same than `open_file` and works the same way. diff --git a/docs/adr/0024-oidc-mechanism-on-desktop.md b/docs/adr/0024-oidc-mechanism-on-desktop.md new file mode 100644 index 0000000000..3e6608f074 --- /dev/null +++ b/docs/adr/0024-oidc-mechanism-on-desktop.md @@ -0,0 +1,37 @@ +# 24. OIDC mechanism on desktop + +Date: 2024-05-13 + +## Status + +Accepted + +## Context + +Currently OIDC is handled for web and mobile versions of the application. For web `FlutterWebAuth2` uses an `iframe` to watch the result of the log in process wether it's a success, or a timeout. For the case of mobile app, the app registered a url scheme which looks like `myapp://auth`. This scheme is where the OIDC server will send its result which will be retrieved by the app like a deeplink. + +But we can't use an `iframe` or register a custom url on desktop applications (at least for linux and windows). So we have to find an other solution to catch the result from the browser where the user log in. + +## Decision + +To achieve that the app (via `FlutterWebAuth2`) sets a light webserver on the user's device. This server's URI, which looks like `http://localhost:port`, uses on a random open port and is sent to OIDC server as form url encoded content. This port is found using this method: + +```dart + Future findFreePort() async { + // launch a local light web server + // to find a random open open, we set port as 0 + final tmpServer = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final port = tmpServer.port; + // when an open port as been found tmp server is closed and port is returned + await tmpServer.close(); + return port; + } +``` + +The app listens to this server, looking for a result. If log in succeed, the OIDC server `POST` the access token to this server which send it to the app. +As soon as the log in process is done (wether it's successful or not), the webserver is closed. + +More details: +- https://github.com/ThexXTURBOXx/flutter_web_auth_2/blob/b48b6f5c866b8c1018cc138b2b11acb3b6188e0b/flutter_web_auth_2/lib/src/server.dart +- https://blog.logto.io/redirect-uri-in-authorization-code-flow/ +- https://openid.net/developers/how-connect-works/ diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index d585d5dd32..980c5f4ba1 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -78,7 +78,7 @@ abstract class AppRoutes { path: '/home', pageBuilder: (context, state) => defaultPageBuilder( context, - PlatformInfos.isMobile + PlatformInfos.isMobile || PlatformInfos.isDesktop ? const TwakeWelcome() : AutoHomeserverPicker( loggedOut: state.extra is bool ? state.extra as bool? : null, diff --git a/lib/domain/model/extensions/xfile_extension.dart b/lib/domain/model/extensions/xfile_extension.dart new file mode 100644 index 0000000000..684045c614 --- /dev/null +++ b/lib/domain/model/extensions/xfile_extension.dart @@ -0,0 +1,24 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:matrix/matrix.dart'; + +extension XFileExtension on XFile { + Future toMatrixFile() async { + return MatrixFile.fromMimeType( + bytes: await readAsBytes(), + mimeType: mimeType, + name: name, + filePath: path, + sizeInBytes: await length(), + ); + } + + Future toPlatformFile() async { + return PlatformFile.fromMap({ + 'name': name, + 'path': path, + 'bytes': await readAsBytes(), + 'size': await length(), + }); + } +} diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index bd5d9ad3f3..0a8c5f8964 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -375,7 +375,7 @@ class ChatController extends State void handleDragDone(DropDoneDetails details) async { final matrixFiles = await onDragDone(details); - sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); + openSendFileDialogAction(context, room: room, matrixFilesList: matrixFiles); } void _handleReceivedShareFiles() { @@ -1284,9 +1284,12 @@ class ChatController extends State void onSendFileClick(BuildContext context) async { if (PlatformInfos.isMobile) { _showMediaPicker(context); + } else if (PlatformInfos.isDesktop) { + final matrixFiles = await pickFilesFromDesktop(); + openSendFileDialogAction(context, room: room, matrixFilesList: matrixFiles); } else { final matrixFiles = await pickFilesFromSystem(); - sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); + openSendFileDialogAction(context, room: room, matrixFilesList: matrixFiles); } } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 1514143825..275d77602e 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -241,7 +241,7 @@ class SelectionTextContainer extends StatelessWidget { @override Widget build(BuildContext context) { - if (!PlatformInfos.isWeb) { + if (PlatformInfos.isMobile) { return child; } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0557ad104b..3e6765f8c6 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -5,10 +5,12 @@ import 'package:fluffychat/pages/chat/chat_input_row_web.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/shortcuts.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:matrix/matrix.dart'; @@ -98,35 +100,57 @@ class ChatInputRow extends StatelessWidget { ); } - InputBar _buildInputBar(BuildContext context) { - return InputBar( - typeAheadKey: controller.chatComposerTypeAheadKey, - rawKeyboardFocusNode: controller.rawKeyboardListenerFocusNode, - room: controller.room!, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: null, - onSubmitted: (_) => controller.onInputBarSubmitted(), - suggestionsController: controller.suggestionsController, - typeAheadFocusNode: controller.inputFocus, - controller: controller.sendController, - focusSuggestionController: controller.focusSuggestionController, - suggestionScrollController: controller.suggestionScrollController, - showEmojiPickerNotifier: controller.showEmojiPickerNotifier, - decoration: InputDecoration( - hintText: L10n.of(context)!.chatMessage, - hintMaxLines: 1, - hintStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.merge( - Theme.of(context).inputDecorationTheme.hintStyle, - ) - .copyWith(letterSpacing: -0.15), + Widget _buildInputBar(BuildContext context) { + return Shortcuts( + shortcuts: { + LogicalKeySet( + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.keyA, + ): const SelectAllIntent(), + LogicalKeySet( + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.keyE, + ): const OnEmojiActionIntent(), + }, + child: Actions( + actions: >{ + SelectAllIntent: CallbackAction( + onInvoke: (_) => controller.selectAll(), + ), + OnEmojiActionIntent: CallbackAction( + onInvoke: (_) => controller.onEmojiAction(), + ), + }, + child: InputBar( + typeAheadKey: controller.chatComposerTypeAheadKey, + rawKeyboardFocusNode: controller.rawKeyboardListenerFocusNode, + room: controller.room!, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: null, + onSubmitted: (_) => controller.onInputBarSubmitted(), + suggestionsController: controller.suggestionsController, + typeAheadFocusNode: controller.inputFocus, + controller: controller.sendController, + focusSuggestionController: controller.focusSuggestionController, + suggestionScrollController: controller.suggestionScrollController, + showEmojiPickerNotifier: controller.showEmojiPickerNotifier, + decoration: InputDecoration( + hintText: L10n.of(context)!.chatMessage, + hintMaxLines: 1, + hintStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.merge( + Theme.of(context).inputDecorationTheme.hintStyle, + ) + .copyWith(letterSpacing: -0.15), + ), + onChanged: controller.onInputBarChanged, + ), ), - onChanged: controller.onInputBarChanged, ); } } diff --git a/lib/pages/chat/chat_input_row_mobile.dart b/lib/pages/chat/chat_input_row_mobile.dart index 0b8e310d67..c1efea26f4 100644 --- a/lib/pages/chat/chat_input_row_mobile.dart +++ b/lib/pages/chat/chat_input_row_mobile.dart @@ -2,9 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; typedef OnTapEmojiAction = void Function(); @@ -48,44 +46,34 @@ class ChatInputRowMobile extends StatelessWidget { Expanded( child: inputBar, ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyE, - }, - onKeysPressed: onEmojiAction, - helpLabel: L10n.of(context)!.emojis, - child: InkWell( - onTap: onEmojiAction, - hoverColor: Colors.transparent, - child: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, + InkWell( + onTap: onEmojiAction, + hoverColor: Colors.transparent, + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: ValueListenableBuilder( + valueListenable: emojiPickerNotifier, + builder: (context, showEmojiPicker, child) { + return TwakeIconButton( + paddingAll: + ChatInputRowStyle.chatInputRowPaddingBtnMobile, + tooltip: L10n.of(context)!.emojis, + onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, + icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, ); }, - child: ValueListenableBuilder( - valueListenable: emojiPickerNotifier, - builder: (context, showEmojiPicker, child) { - return TwakeIconButton( - paddingAll: - ChatInputRowStyle.chatInputRowPaddingBtnMobile, - tooltip: L10n.of(context)!.emojis, - onTap: - showEmojiPicker ? onKeyboardAction : onEmojiAction, - icon: - showEmojiPicker ? Icons.keyboard : Icons.tag_faces, - ); - }, - ), ), ), ), diff --git a/lib/pages/chat/chat_input_row_web.dart b/lib/pages/chat/chat_input_row_web.dart index 5448c2e5c8..5bf160c3c7 100644 --- a/lib/pages/chat/chat_input_row_web.dart +++ b/lib/pages/chat/chat_input_row_web.dart @@ -2,9 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -54,39 +52,33 @@ class ChatInputRowWeb extends StatelessWidget { Expanded( child: inputBar, ), - KeyBoardShortcuts( - keysToPress: {LogicalKeyboardKey.altLeft, LogicalKeyboardKey.keyE}, - onKeysPressed: onEmojiAction, - helpLabel: L10n.of(context)!.emojis, - child: InkWell( - onTap: onEmojiAction, - hoverColor: Colors.transparent, - child: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, + InkWell( + onTap: onEmojiAction, + hoverColor: Colors.transparent, + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: ValueListenableBuilder( + valueListenable: emojiPickerNotifier, + builder: (context, showEmojiPicker, child) { + return TwakeIconButton( + paddingAll: ChatInputRowStyle.chatInputRowPaddingBtnMobile, + tooltip: L10n.of(context)!.emojis, + onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, + icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, ); }, - child: ValueListenableBuilder( - valueListenable: emojiPickerNotifier, - builder: (context, showEmojiPicker, child) { - return TwakeIconButton( - paddingAll: - ChatInputRowStyle.chatInputRowPaddingBtnMobile, - tooltip: L10n.of(context)!.emojis, - onTap: showEmojiPicker ? onKeyboardAction : onEmojiAction, - icon: showEmojiPicker ? Icons.keyboard : Icons.tag_faces, - ); - }, - ), ), ), ), diff --git a/lib/pages/chat/events/message/multi_platform_message_container.dart b/lib/pages/chat/events/message/multi_platform_message_container.dart index d6a03bcd5d..fbe6b75919 100644 --- a/lib/pages/chat/events/message/multi_platform_message_container.dart +++ b/lib/pages/chat/events/message/multi_platform_message_container.dart @@ -18,7 +18,7 @@ class MultiPlatformsMessageContainer extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return MouseRegion( child: child, onHover: (event) { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 9e367c14d8..a61e7ea654 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -101,7 +101,7 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!PlatformInfos.isWeb) ...[ + if (PlatformInfos.isMobile) ...[ MessageDownloadContent( event, ), @@ -126,7 +126,7 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!PlatformInfos.isWeb) ...[ + if (PlatformInfos.isMobile || PlatformInfos.isDesktop) ...[ MessageDownloadContent( event, ), @@ -317,7 +317,7 @@ class _MessageImageBuilder extends StatelessWidget { return matrixFile != null && matrixFile.filePath != null && matrixFile is MatrixImageFile && - !PlatformInfos.isWeb; + !PlatformInfos.isWebOrDesktop; } } diff --git a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart index 324db4a8e4..0f69d4a881 100644 --- a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart +++ b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart @@ -87,7 +87,8 @@ class ChatAdaptiveScaffoldBuilderController builder: (_) => Stack( children: [ body!, - if (rightColumnType != null && PlatformInfos.isWeb) + if (rightColumnType != null && + PlatformInfos.isWebOrDesktop) widget.rightBuilder( this, isInStack: true, diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index 1ca9f2278d..a1dd6015ea 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -130,7 +130,7 @@ class _VideoItem extends StatelessWidget { Future _onTapVideo(BuildContext context) async { final result = await Navigator.of( context, - rootNavigator: PlatformInfos.isWeb, + rootNavigator: PlatformInfos.isWebOrDesktop, ).push( HeroPageRoute( builder: (context) { diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index e141149380..3dca912866 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -28,7 +28,7 @@ class ChatListHeader extends StatelessWidget { Container( height: ChatListHeaderStyle.searchBarContainerHeight, padding: ChatListHeaderStyle.searchInputPadding, - child: PlatformInfos.isWeb + child: PlatformInfos.isWebOrDesktop ? _normalModeWidgetWeb(context) : _normalModeWidgetsMobile(context), ), diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 61ea3c69fb..65851003ff 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -286,7 +286,7 @@ class _MessageContent extends StatelessWidget { Widget build(BuildContext context) { switch (event.messageType) { case MessageTypes.File: - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return MessageDownloadContentWeb(event, highlightText: searchWord); } else { return MessageDownloadContent(event, highlightText: searchWord); diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index c1a5cfd739..a306bbda79 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -186,7 +186,10 @@ class MyCallingPage extends State { void _playCallSound() async { const path = 'assets/sounds/call.ogg'; - if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isMacOS) { + if (kIsWeb || + PlatformInfos.isMobile || + PlatformInfos.isMacOS || + PlatformInfos.isLinux) { final player = AudioPlayer(); await player.setAsset(path); player.play(); diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index eaa40b8a17..2f8577f63b 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -45,7 +45,7 @@ class ImageViewerController extends State { @override void initState() { super.initState(); - if (!PlatformInfos.isWeb && widget.event != null) { + if (PlatformInfos.isMobile && widget.event != null) { handleDownloadFile(widget.event!); } } diff --git a/lib/pages/image_viewer/image_viewer_style.dart b/lib/pages/image_viewer/image_viewer_style.dart index 3d88427473..ecc0f9b314 100644 --- a/lib/pages/image_viewer/image_viewer_style.dart +++ b/lib/pages/image_viewer/image_viewer_style.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; class ImageViewerStyle { static const double minScaleInteractiveViewer = 1.0; static const double maxScaleInteractiveViewer = 10.0; - static double? appBarHeight = PlatformInfos.isWeb ? 56 : null; + static double? appBarHeight = PlatformInfos.isWebOrDesktop ? 56 : null; static EdgeInsetsGeometry paddingTopAppBar = EdgeInsetsDirectional.only( - top: PlatformInfos.isWeb ? 0 : 56, + top: PlatformInfos.isWebOrDesktop ? 0 : 56, ); } diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 58742ba159..b4c0445ff4 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -69,7 +69,7 @@ class ImageViewerView extends StatelessWidget { backgroundColor: Colors.black, body: GestureDetector( onTap: () { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { Navigator.of(context).pop(); } else { controller.showAppbarPreview.toggle(); @@ -101,7 +101,7 @@ class _ImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return FutureBuilder( future: event.downloadAndDecryptAttachment( getThumbnail: true, diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index b8a724bc37..aca9ace062 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -92,7 +92,7 @@ class MediaViewerAppbarView extends StatelessWidget { menuChildren: [ ContextMenuItemImageViewer( icon: Icons.file_download_outlined, - title: PlatformInfos.isWeb + title: PlatformInfos.isWebOrDesktop ? L10n.of(context)!.saveFile : L10n.of(context)!.saveToGallery, onTap: () { diff --git a/lib/pages/new_group/widget/selected_participants_list.dart b/lib/pages/new_group/widget/selected_participants_list.dart index 6ded008627..979c62f2c1 100644 --- a/lib/pages/new_group/widget/selected_participants_list.dart +++ b/lib/pages/new_group/widget/selected_participants_list.dart @@ -45,7 +45,7 @@ class _SelectedParticipantsListState extends State { padding: SelectedParticipantsListStyle.paddingAll, child: Wrap( spacing: 8.0, - runSpacing: PlatformInfos.isWeb ? 4.0 : 0.0, + runSpacing: PlatformInfos.isWebOrDesktop ? 4.0 : 0.0, children: contactsNotifier.contactsList.map((contact) { return InputChip( shape: RoundedRectangleBorder( diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 4a56c4edfd..2159c3fe9d 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -104,7 +104,7 @@ class SettingsController extends State with ConnectPageMixin { } catch (e) { Logs().e('SettingsController()::logoutAction - error: $e'); } finally { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { await tryLogoutSso(context); } } diff --git a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart index fb43918231..644f05692c 100644 --- a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart @@ -120,8 +120,7 @@ class EmotesSettingsView extends StatelessWidget { final image = controller.pack!.images[imageCode]!; final textEditingController = TextEditingController(); textEditingController.text = imageCode; - final useShortCuts = - (PlatformInfos.isWeb || PlatformInfos.isDesktop); + final useShortCuts = PlatformInfos.isWebOrDesktop; return ListTile( leading: Container( width: 180.0, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index ada07b9f99..3957b7de50 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/room/upload_content_state.dart'; import 'package:fluffychat/domain/app_state/settings/update_profile_failure.dart'; import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; +import 'package:fluffychat/domain/model/extensions/xfile_extension.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_for_web_interactor.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; @@ -24,6 +25,7 @@ import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/utils/xfile_groups.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; @@ -33,6 +35,8 @@ import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfile extends StatefulWidget { @@ -180,7 +184,39 @@ class SettingsProfileController extends State ), ); Logs().d( - 'SettingsProfile::_getImageOnWeb(): AvatarWebNotifier - $result', + 'SettingsProfile::_getImageOnWeb(): AvatarNotifier - $result', + ); + } + } + + void _getImageOnDesktop( + BuildContext context, + ) async { + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + + final XFile? result = await openFile( + initialDirectory: initialDirectory, + acceptedTypeGroups: [XFileGroups.images], + ); + + Logs().d( + 'SettingsProfile::_getImageOnDesktop(): FilePickerResult - ${result?.path}', + ); + + if (result == null) { + return; + } else { + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + settingsProfileUIState.value = Right( + GetAvatarInBytesUIStateSuccess( + filePickerResult: FilePickerResult([await result.toPlatformFile()]), + ), + ); + Logs().d( + 'SettingsProfile::_getImageOnDesktop(): AvatarNotifier - $result', ); } } @@ -190,6 +226,10 @@ class SettingsProfileController extends State _getImageOnWeb(context); return; } + if (PlatformInfos.isDesktop) { + _getImageOnDesktop(context); + return; + } final currentPermissionPhotos = await getCurrentMediaPermission(); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index c3d36bcc7b..184f03d67a 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -93,7 +93,7 @@ class SettingsProfileViewMobile extends StatelessWidget { ); } if (success is GetAvatarInBytesUIStateSuccess && - PlatformInfos.isWeb) { + PlatformInfos.isWebOrDesktop) { if (success.filePickerResult == null || success.filePickerResult?.files.single.bytes == null) { @@ -160,7 +160,7 @@ class SettingsProfileViewMobile extends StatelessWidget { ) { return GestureDetector( onTap: () { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { menuController.isOpen ? menuController.close() : menuController.open(); diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index 9f5ec79a41..369fa59f5f 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; @@ -23,7 +24,6 @@ mixin ConnectPageMixin { static const windowNameValue = '_self'; static const redirectPublicPlatformOnWeb = 'post_login_redirect_url'; - bool supportsFlow({ required BuildContext context, required String flowType, @@ -37,7 +37,8 @@ mixin ConnectPageMixin { bool supportsSso(BuildContext context) => (PlatformInfos.isMobile || PlatformInfos.isWeb || - PlatformInfos.isMacOS) && + PlatformInfos.isMacOS || + PlatformInfos.isLinux) && supportsFlow(context: context, flowType: 'm.login.sso'); bool supportsLogin(BuildContext context) => @@ -58,6 +59,9 @@ mixin ConnectPageMixin { AppConfig.homeserver.isNotEmpty; String _getRedirectUrlScheme(String redirectUrl) { + if (PlatformInfos.isLinuxOrWindows) { + return redirectUrl; + } return Uri.parse(redirectUrl).scheme; } @@ -97,7 +101,7 @@ mixin ConnectPageMixin { required BuildContext context, required String id, }) async { - final redirectUrl = _generateRedirectUrl( + final redirectUrl = await _generateRedirectUrl( Matrix.of(context).client.homeserver.toString(), ); final url = _getAuthenticateUrl( @@ -106,6 +110,7 @@ mixin ConnectPageMixin { redirectUrl: redirectUrl, ); final urlScheme = _getRedirectUrlScheme(redirectUrl); + return await FlutterWebAuth2.authenticate( url: url, callbackUrlScheme: urlScheme, @@ -161,7 +166,7 @@ mixin ConnectPageMixin { Future tryLogoutSso(BuildContext context) async { if (Matrix.of(context).loginType != LoginType.mLoginToken) return; - final redirectUrl = _generatePostLogoutRedirectUrl(); + final redirectUrl = await _generatePostLogoutRedirectUrl(); final url = _getLogoutUrl(context, redirectUrl: redirectUrl); if (url == null) return Future.value(); @@ -184,7 +189,7 @@ mixin ConnectPageMixin { required BuildContext context, required String id, }) async { - final redirectUrl = _generateRedirectUrl( + final redirectUrl = await _generateRedirectUrl( Matrix.of(context).client.homeserver.toString(), ); final url = generatePublicPlatformAuthenticationUrl( @@ -204,17 +209,22 @@ mixin ConnectPageMixin { Logs().d("ConnectPageMixin:_redirectRegistrationUrl: URI - $uri"); } - String _generatePostLogoutRedirectUrl() { + Future _generatePostLogoutRedirectUrl() async { if (kIsWeb) { if (AppConfig.issueId != null && AppConfig.issueId!.isNotEmpty) { return '${html.window.origin!}/twake-on-matrix/${AppConfig.issueId}/auth.html'; } return '${html.window.origin!}/web/auth.html'; + } else if (PlatformInfos.isLinuxOrWindows) { + return await _generateDesktopRedirectUrl(); } return '${AppConfig.appOpenUrlScheme.toLowerCase()}://redirect'; } - String _generateRedirectUrl(String homeserver) { + Future _generateRedirectUrl(String homeserver) async { + if (PlatformInfos.isLinuxOrWindows) { + return await _generateDesktopRedirectUrl(); + } if (kIsWeb) { String? homeserverParam = ''; if (homeserver.isNotEmpty) { @@ -228,6 +238,18 @@ mixin ConnectPageMixin { return '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'; } + Future _findFreePort() async { + final tmpServer = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final port = tmpServer.port; + await tmpServer.close(); + return port; + } + + Future _generateDesktopRedirectUrl() async { + final freePort = await _findFreePort(); + return 'http://localhost:$freePort/callback'; + } + List? identityProviders({ Map? rawLoginTypes, }) { diff --git a/lib/presentation/mixins/handle_clipboard_action_mixin.dart b/lib/presentation/mixins/handle_clipboard_action_mixin.dart index c203eb39b3..636cb20304 100644 --- a/lib/presentation/mixins/handle_clipboard_action_mixin.dart +++ b/lib/presentation/mixins/handle_clipboard_action_mixin.dart @@ -22,6 +22,13 @@ mixin HandleClipboardActionMixin on PasteImageMixin { ClipboardEvents.instance?.unregisterPasteEventListener(_onPasteEvent); } + void selectAll() { + sendController.selection = TextSelection( + baseOffset: 0, + extentOffset: sendController.text.length, + ); + } + void _onPasteEvent(ClipboardReadEvent event) async { if (chatFocusNode.hasFocus != true) { return; diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart index 9bd069b01f..9c0e9ed9c2 100644 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart @@ -151,7 +151,7 @@ mixin MediaViewerAppBarMixin on SaveMediaToGalleryAndroidMixin { BuildContext context, Event? event, ) { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { event?.saveFile(context); } else { if (event != null) { diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 507c948c28..6a5cc77889 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -29,11 +29,12 @@ mixin PlayVideoActionMixin { }, ); if (isReplacement) { - Navigator.of(context, rootNavigator: PlatformInfos.isWeb).pushReplacement( + Navigator.of(context, rootNavigator: PlatformInfos.isWebOrDesktop) + .pushReplacement( pageRoute, ); } else { - Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( + Navigator.of(context, rootNavigator: PlatformInfos.isWebOrDesktop).push( pageRoute, ); } diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 1b7fa60c87..708388d13a 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -1,12 +1,14 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/extensions/platform_file/platform_file_extension.dart'; +import 'package:fluffychat/domain/model/extensions/xfile_extension.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/images_picker/images_picker.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; @@ -68,6 +70,29 @@ mixin SendFilesMixin { return result.files.map((file) => file.toMatrixFileOnWeb()).toList(); } + Future> pickFilesFromDesktop() async { + try { + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + final List xFiles = + await openFiles(initialDirectory: initialDirectory); + + if (xFiles.isEmpty) return []; + + final matrixFiles = []; + + for (final xFile in xFiles) { + final matrixFile = await xFile.toMatrixFile(); + matrixFiles.add(matrixFile); + } + + return matrixFiles; + } on Exception catch (error) { + Logs().e('SendFilesMixin::pickFilesFromDesktop(): error: $error'); + return []; + } + } + void onPickerTypeClick({ required BuildContext context, Room? room, diff --git a/lib/presentation/mixins/send_files_with_caption_web_mixin.dart b/lib/presentation/mixins/send_files_with_caption_web_mixin.dart index 45949fa373..90d4234c2a 100644 --- a/lib/presentation/mixins/send_files_with_caption_web_mixin.dart +++ b/lib/presentation/mixins/send_files_with_caption_web_mixin.dart @@ -8,7 +8,7 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:matrix/matrix.dart'; mixin SendFilesWithCaptionWebMixin { - void sendFileOnWebAction( + void openSendFileDialogAction( BuildContext context, { Room? room, required List matrixFilesList, diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 7b9cf5418c..17cfa360e4 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -107,7 +107,8 @@ abstract class ClientManager { AuthenticationTypes.password, if (PlatformInfos.isMobile || PlatformInfos.isWeb || - PlatformInfos.isMacOS) + PlatformInfos.isMacOS || + PlatformInfos.isLinux) AuthenticationTypes.sso, }, nativeImplementations: nativeImplementations, diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index a283163d0a..6f666c8f72 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -98,6 +98,7 @@ class DownloadManager { required Event event, bool getThumbnail = false, bool isFirstPriority = false, + bool isTemporary = true, }) async { _initDownloadFileInfo(event); final streamController = _eventIdMapDownloadFileInfo[event.eventId] @@ -131,6 +132,7 @@ class DownloadManager { streamController: streamController, cancelToken: cancelToken, isFirstPriority: isFirstPriority, + isTemporary: isTemporary, ); } @@ -140,6 +142,7 @@ class DownloadManager { required StreamController> streamController, required CancelToken cancelToken, bool isFirstPriority = false, + bool isTemporary = true, }) { if (PlatformInfos.isWeb) { _addTaskToWorkerQueueWeb( @@ -157,6 +160,7 @@ class DownloadManager { streamController, cancelToken, isFirstPriority: isFirstPriority, + isTemporary: isTemporary, ); } @@ -166,6 +170,7 @@ class DownloadManager { StreamController> streamController, CancelToken cancelToken, { bool isFirstPriority = false, + bool isTemporary = true, }) { workingQueue.addTask( Task( @@ -176,6 +181,7 @@ class DownloadManager { getThumbnail: getThumbnail, downloadStreamController: streamController, cancelToken: cancelToken, + isTemporary: isTemporary, ); } catch (e) { Logs().e('DownloadManager::download(): $e'); diff --git a/lib/utils/manager/storage_directory_manager.dart b/lib/utils/manager/storage_directory_manager.dart index a45902f8f5..24ff934e02 100644 --- a/lib/utils/manager/storage_directory_manager.dart +++ b/lib/utils/manager/storage_directory_manager.dart @@ -13,7 +13,8 @@ class StorageDirectoryManager { static StorageDirectoryManager get instance => _instance; - Future getFileStoreDirectory() async { + Future getFileStoreDirectory({bool isTemporary = true}) async { + if (!isTemporary) return (await getDownloadsDirectory())!.path; try { try { return (await getTemporaryDirectory()).path; @@ -28,10 +29,14 @@ class StorageDirectoryManager { Future getFilePathInAppDownloads({ required String eventId, required String fileName, + bool isTemporary = true, }) async { - final fileStoreDirectory = - await StorageDirectoryManager.instance.getFileStoreDirectory(); - return '$fileStoreDirectory/$eventId/$fileName'; + final fileStoreDirectory = await StorageDirectoryManager.instance + .getFileStoreDirectory(isTemporary: isTemporary); + if (isTemporary) { + return '$fileStoreDirectory/$eventId/$fileName'; + } + return '$fileStoreDirectory/${AppConfig.applicationName}/$fileName'; } Future getTwakeDownloadsFolderInDevice() async { @@ -71,9 +76,13 @@ class StorageDirectoryManager { Future getDecryptedFilePath({ required String eventId, required String fileName, + bool isTemporary = true, }) async { - final fileStoreDirectory = - await StorageDirectoryManager.instance.getFileStoreDirectory(); - return '$fileStoreDirectory/$eventId/decrypted-$fileName'; + final fileStoreDirectory = await StorageDirectoryManager.instance + .getFileStoreDirectory(isTemporary: isTemporary); + if (isTemporary) { + return '$fileStoreDirectory/$eventId/decrypted-$fileName'; + } + return '$fileStoreDirectory/${AppConfig.applicationName}/decrypted-$fileName'; } } diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 1b8483cd54..924c9adcce 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -43,11 +43,13 @@ extension DownloadFileExtension on Event { bool getThumbnail = false, CancelToken? cancelToken, required String filename, + bool isTemporary = true, }) async { final attachment = File( await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), ); final downloadLink = mxcUrl.getDownloadLink(room.client); @@ -155,6 +157,7 @@ extension DownloadFileExtension on Event { required String filename, bool getThumbnail = false, StreamController>? streamController, + bool isTemporary = true, }) async { streamController?.add( const Right( @@ -168,6 +171,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), getThumbnail: getThumbnail, ); @@ -180,6 +184,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ), ).copySync(savePath); streamController?.add( @@ -260,6 +265,7 @@ extension DownloadFileExtension on Event { StreamController>? downloadStreamController, ProgressCallback? progressCallback, CancelToken? cancelToken, + bool isTemporary = true, }) async { if (!canContainAttachment()) { throw ("getFileInfo: This event has the type '$type' and so it can't contain an attachment."); @@ -289,6 +295,7 @@ extension DownloadFileExtension on Event { await StorageDirectoryManager.instance.getDecryptedFilePath( eventId: eventId, fileName: filename, + isTemporary: isTemporary, ); final decryptedFile = File(decryptedPath); @@ -308,13 +315,17 @@ extension DownloadFileExtension on Event { return downloadOrRetrieveAttachment( mxcUrl, - await StorageDirectoryManager.instance - .getFilePathInAppDownloads(eventId: eventId, fileName: filename), + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: filename, + isTemporary: isTemporary, + ), downloadStreamController: downloadStreamController, getThumbnail: getThumbnail, progressCallback: progressCallback, cancelToken: cancelToken, filename: filename, + isTemporary: isTemporary, ); } } diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 9c78247ab3..3c002d485c 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -185,7 +185,7 @@ extension LocalizedBody on Event { bool get isPinned => room.pinnedEventIds.contains(eventId); Future copy(BuildContext context, Timeline timeline) async { - if (messageType == MessageTypes.Image && PlatformInfos.isWeb) { + if (messageType == MessageTypes.Image && PlatformInfos.isWebOrDesktop) { final matrixFile = getMatrixFile() ?? await downloadAndDecryptAttachment( getThumbnail: true, diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 1930cbd93c..9edad87c96 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -16,7 +16,7 @@ import 'package:file_saver/file_saver.dart'; extension MatrixFileExtension on MatrixFile { Future downloadFile(BuildContext context) async { - if (PlatformInfos.isWeb) { + if (PlatformInfos.isWebOrDesktop) { return await downloadFileInWeb(context); } @@ -51,11 +51,21 @@ extension MatrixFileExtension on MatrixFile { name: name, bytes: bytes, ); + + TwakeSnackBar.show( + context, + L10n.of(context)!.fileSavedToDownloads, + ); return '$directory/$name'; } catch (e) { Logs().e( "MatrixFileExtension()::downloadFileInWeb()::Error: $e", ); + + TwakeSnackBar.show( + context, + L10n.of(context)!.downloadImageError, + ); } return null; } diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index d6ee76d22f..2f4ab9467d 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -32,6 +32,10 @@ abstract class PlatformInfos { static bool get isDesktop => isLinux || isWindows || isMacOS; + static bool get isLinuxOrWindows => isLinux || isWindows; + + static bool get isWebOrDesktop => isWeb || isDesktop; + static bool get usesTouchscreen => !isMobile; static bool get platformCanRecord => (isMobile || isMacOS); diff --git a/lib/utils/shortcuts.dart b/lib/utils/shortcuts.dart new file mode 100644 index 0000000000..ffa994eac8 --- /dev/null +++ b/lib/utils/shortcuts.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +class OnEmojiActionIntent extends Intent { + const OnEmojiActionIntent(); +} + +class SelectAllIntent extends Intent { + const SelectAllIntent(); +} diff --git a/lib/utils/voip/user_media_manager.dart b/lib/utils/voip/user_media_manager.dart index 874da93ee1..f6b747efe5 100644 --- a/lib/utils/voip/user_media_manager.dart +++ b/lib/utils/voip/user_media_manager.dart @@ -19,7 +19,7 @@ class UserMediaManager { Future startRingingTone() async { if (PlatformInfos.isMobile) { await FlutterRingtonePlayer.playRingtone(volume: 80); - } else if ((kIsWeb || PlatformInfos.isMacOS) && + } else if ((kIsWeb || PlatformInfos.isMacOS || PlatformInfos.isLinux) && _assetsAudioPlayer != null) { const path = 'assets/sounds/phone.ogg'; final player = _assetsAudioPlayer = AudioPlayer(); diff --git a/lib/utils/xfile_groups.dart b/lib/utils/xfile_groups.dart new file mode 100644 index 0000000000..ba18224227 --- /dev/null +++ b/lib/utils/xfile_groups.dart @@ -0,0 +1,8 @@ +import 'package:file_selector/file_selector.dart'; + +class XFileGroups { + static const XTypeGroup images = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart index 317052f76a..29448bd301 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart @@ -33,7 +33,7 @@ class AdaptiveScaffoldAppBar extends StatelessWidget { children: [ const _LeadingAppBarWidget(), if (AppConfig.appGridDashboardAvailable && - PlatformInfos.isWeb) + PlatformInfos.isWebOrDesktop) const Expanded( child: AppGridDashboard(), ), diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart index 4a2a438835..17e1ae39a4 100644 --- a/lib/widgets/mixins/download_file_on_mobile_mixin.dart +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -70,6 +71,7 @@ mixin DownloadFileOnMobileMixin on State { await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: event.eventId, fileName: event.filename, + isTemporary: !PlatformInfos.isDesktop, ); final file = File(filePath); if (await file.exists() && await file.length() == event.getFileSize()) { @@ -119,6 +121,7 @@ mixin DownloadFileOnMobileMixin on State { downloadFileStateNotifier.value = const DownloadingPresentationState(); downloadManager.download( event: event, + isTemporary: !PlatformInfos.isDesktop, ); _trySetupDownloadingStreamSubcription(); } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index 6af3307dfc..f64d073550 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_failure.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_loading.dart'; @@ -16,7 +18,7 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:open_file/open_file.dart'; +import 'package:open_app_file/open_app_file.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; @@ -174,6 +176,13 @@ mixin HandleDownloadAndPreviewFileMixin { ); return; } + + if (PlatformInfos.isDesktop) { + _openDownloadedFileOnDesktop( + filePath: filePath, + mimeType: mimeType, + ); + } } void _openDownloadedFileForPreviewAndroid({ @@ -184,9 +193,9 @@ mixin HandleDownloadAndPreviewFileMixin { await Share.shareXFiles([XFile(filePath)]); return; } - final openResults = await OpenFile.open( + final openResults = await OpenAppFile.open( filePath, - type: mimeType, + mimeType: mimeType, uti: DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]) .value, ); @@ -207,12 +216,45 @@ mixin HandleDownloadAndPreviewFileMixin { Logs().d( 'ChatController:_openDownloadedFileForPreviewIos(): $filePath', ); - await OpenFile.open( + await OpenAppFile.open( filePath, - type: mimeType, + mimeType: mimeType, ); } + void _openDownloadedFileOnDesktop({ + required String filePath, + required String? mimeType, + }) async { + Logs().d( + 'ChatController:_openDownloadedFileOnDesktop(): $filePath', + ); + final downloadDirectory = await getDownloadsDirectory(); + try { + await OpenAppFile.open( + filePath, + mimeType: mimeType, + ); + } catch (e) { + Logs().e( + 'ChatController:_openDownloadedFileOnDesktop(): $e', + ); + if (downloadDirectory == null) { + return; + } + _openFileUsingTerminal(downloadDirectory.path); + } + } + + void _openFileUsingTerminal(String path) { + if (PlatformInfos.isLinux || PlatformInfos.isMacOS) { + Process.run('open', [path]); + } + if (PlatformInfos.isWindows) { + Process.run('explorer', [path]); + } + } + Future previewPdfWeb( BuildContext context, Event event, { diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index c0ede95b7a..21498cd125 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -217,8 +217,10 @@ class _MxcImageState extends State { void _onTap(BuildContext context) async { if (widget.onTapPreview != null) { widget.onTapPreview!(); - final result = - await Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( + final result = await Navigator.of( + context, + rootNavigator: PlatformInfos.isWebOrDesktop, + ).push( HeroPageRoute( builder: (context) { return InteractiveViewerGallery( diff --git a/lib/widgets/video_viewer_style.dart b/lib/widgets/video_viewer_style.dart index 07d9425fa3..7420582ae8 100644 --- a/lib/widgets/video_viewer_style.dart +++ b/lib/widgets/video_viewer_style.dart @@ -14,7 +14,7 @@ class VideoViewerStyle { bottom: 8.0 + MediaQuery.of(context).viewPadding.bottom, ); - static EdgeInsets backButtonMargin(context) => PlatformInfos.isWeb + static EdgeInsets backButtonMargin(context) => PlatformInfos.isWebOrDesktop ? const EdgeInsets.only(top: 8.0, left: 16.0) : EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top); diff --git a/pubspec.lock b/pubspec.lock index 6d22a13ddc..30f1f8ed1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -619,7 +619,7 @@ packages: source: hosted version: "0.2.12" file_selector: - dependency: "direct overridden" + dependency: "direct main" description: name: file_selector sha256: "1d2fde93dddf634a9c3c0faa748169d7ac0d83757135555707e52f02c017ad4f" @@ -1022,26 +1022,27 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: f2afec1f1762c040a349ea2a588e32f442da5d0db3494a52a929a97c9e550bc5 + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "9.0.0" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" - url: "https://pub.dev" - source: hosted + path: flutter_secure_storage_linux + ref: develop + resolved-ref: "27d3e2e69123f0c712919ad392e15830741e4383" + url: "https://github.com/tomekit/flutter_secure_storage.git" + source: git version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: ff0768a6700ea1d9620e03518e2e25eac86a8bd07ca3556e9617bfa5ace4bd00 + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -1804,14 +1805,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - open_file: + open_app_file: dependency: "direct main" description: - name: open_file - sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20 - url: "https://pub.dev" - source: hosted - version: "3.3.2" + path: "." + ref: HEAD + resolved-ref: "7054e90c4632af0a47be93d1b8891dc499bc5d6d" + url: "git@github.com:aws1313/open_app_file.git" + source: git + version: "4.0.1" overflow_view: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 49176b0ff7..fe881ff602 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,7 +89,7 @@ dependencies: flutter_olm: ^1.2.0 flutter_openssl_crypto: ^0.1.0 flutter_ringtone_player: ^3.1.1 - flutter_secure_storage: ^7.0.1 + flutter_secure_storage: ^9.0.0 flutter_svg: ^0.22.0 flutter_typeahead: ^5.1.0 flutter_web_auth_2: ^3.1.1 @@ -150,7 +150,6 @@ dependencies: tuple: ^2.0.2 lottie: ^2.3.2 wechat_camera_picker: 4.2.1 - open_file: ^3.3.2 mime: ^1.0.4 async: ^2.11.0 cached_network_image: ^3.2.3 @@ -175,6 +174,10 @@ dependencies: flutter_portal: 1.1.4 external_path: 1.0.3 gal: 2.3.0 + open_app_file: + git: + url: git@github.com:aws1313/open_app_file.git + file_selector: ^0.9.2+2 dev_dependencies: build_runner: ^2.3.3 @@ -245,6 +248,12 @@ dependency_overrides: git: url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git ref: main + # https://github.com/mogol/flutter_secure_storage/issues/616 + flutter_secure_storage_linux: + git: + url: https://github.com/tomekit/flutter_secure_storage.git + ref: develop + path: flutter_secure_storage_linux geolocator_android: hosted: name: geolocator_android