Merge pull request #1504 from krille-chan/krille/new-share-file-dialog

refactor: Improved share / forward dialog
pull/1505/head
Krille-chan 11 months ago committed by GitHub
commit b65d3dbd16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -35,6 +35,7 @@ import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/log_view.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
abstract class AppRoutes { abstract class AppRoutes {
static FutureOr<String?> loggedInRedirect( static FutureOr<String?> loggedInRedirect(
@ -318,15 +319,25 @@ abstract class AppRoutes {
), ),
GoRoute( GoRoute(
path: ':roomid', path: ':roomid',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) {
context, final body = state.uri.queryParameters['body'];
state, var shareItems = state.extra is List<ShareItem>
ChatPage( ? state.extra as List<ShareItem>
roomId: state.pathParameters['roomid']!, : null;
shareText: state.uri.queryParameters['body'], if (body != null && body.isNotEmpty) {
eventId: state.uri.queryParameters['event'], shareItems ??= [];
), shareItems.add(TextShareItem(body));
), }
return defaultPageBuilder(
context,
state,
ChatPage(
roomId: state.pathParameters['roomid']!,
shareItems: shareItems,
eventId: state.uri.queryParameters['event'],
),
);
},
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
routes: [ routes: [
GoRoute( GoRoute(

@ -32,8 +32,10 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/show_scaffold_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import '../../utils/account_bundles.dart'; import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
import 'send_file_dialog.dart'; import 'send_file_dialog.dart';
@ -41,14 +43,14 @@ import 'send_location_dialog.dart';
class ChatPage extends StatelessWidget { class ChatPage extends StatelessWidget {
final String roomId; final String roomId;
final String? shareText; final List<ShareItem>? shareItems;
final String? eventId; final String? eventId;
const ChatPage({ const ChatPage({
super.key, super.key,
required this.roomId, required this.roomId,
this.eventId, this.eventId,
this.shareText, this.shareItems,
}); });
@override @override
@ -69,7 +71,7 @@ class ChatPage extends StatelessWidget {
return ChatPageWithRoom( return ChatPageWithRoom(
key: Key('chat_page_${roomId}_$eventId'), key: Key('chat_page_${roomId}_$eventId'),
room: room, room: room,
shareText: shareText, shareItems: shareItems,
eventId: eventId, eventId: eventId,
); );
} }
@ -77,13 +79,13 @@ class ChatPage extends StatelessWidget {
class ChatPageWithRoom extends StatefulWidget { class ChatPageWithRoom extends StatefulWidget {
final Room room; final Room room;
final String? shareText; final List<ShareItem>? shareItems;
final String? eventId; final String? eventId;
const ChatPageWithRoom({ const ChatPageWithRoom({
super.key, super.key,
required this.room, required this.room,
this.shareText, this.shareItems,
this.eventId, this.eventId,
}); });
@ -224,18 +226,42 @@ class ChatController extends State<ChatPageWithRoom>
void _loadDraft() async { void _loadDraft() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final draft = widget.shareText ?? prefs.getString('draft_$roomId'); final draft = prefs.getString('draft_$roomId');
if (draft != null && draft.isNotEmpty) { if (draft != null && draft.isNotEmpty) {
sendController.text = draft; sendController.text = draft;
} }
} }
void _shareItems([_]) {
final shareItems = widget.shareItems;
if (shareItems == null || shareItems.isEmpty) return;
for (final item in shareItems) {
if (item is FileShareItem) continue;
if (item is TextShareItem) room.sendTextEvent(item.value);
if (item is ContentShareItem) room.sendEvent(item.value);
}
final files = shareItems
.whereType<FileShareItem>()
.map((item) => item.value)
.toList();
if (files.isEmpty) return;
showAdaptiveDialog(
context: context,
builder: (c) => SendFileDialog(
files: files,
room: room,
outerContext: context,
),
);
}
@override @override
void initState() { void initState() {
scrollController.addListener(_updateScrollController); scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener); inputFocus.addListener(_inputFocusListener);
_loadDraft(); _loadDraft();
WidgetsBinding.instance.addPostFrameCallback(_shareItems);
super.initState(); super.initState();
_displayChatDetailsColumn = ValueNotifier( _displayChatDetailsColumn = ValueNotifier(
Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
@ -821,17 +847,17 @@ class ChatController extends State<ChatPageWithRoom>
} }
void forwardEventsAction() async { void forwardEventsAction() async {
if (selectedEvents.length == 1) { if (selectedEvents.isEmpty) return;
Matrix.of(context).shareContent = await showScaffoldDialog(
selectedEvents.first.getDisplayEvent(timeline!).content; context: context,
} else { builder: (context) => ShareScaffoldDialog(
Matrix.of(context).shareContent = { items: selectedEvents
'msgtype': 'm.text', .map((event) => ContentShareItem(event.content))
'body': _getSelectedEventString(), .toList(),
}; ),
} );
if (!mounted) return;
setState(() => selectedEvents.clear()); setState(() => selectedEvents.clear());
context.go('/rooms');
} }
void sendAgainAction() { void sendAgainAction() {

@ -15,14 +15,15 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.dart'; import 'package:uni_links/uni_links.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/show_scaffold_dialog.dart';
import 'package:fluffychat/utils/show_update_snackbar.dart'; import 'package:fluffychat/utils/show_update_snackbar.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import '../../../utils/account_bundles.dart'; import '../../../utils/account_bundles.dart';
import '../../config/setting_keys.dart'; import '../../config/setting_keys.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
@ -34,11 +35,6 @@ import '../bootstrap/bootstrap_dialog.dart';
import 'package:fluffychat/utils/tor_stub.dart' import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
enum SelectMode {
normal,
share,
}
enum PopupMenuAction { enum PopupMenuAction {
settings, settings,
invite, invite,
@ -191,42 +187,6 @@ class ChatListController extends State<ChatList>
setActiveSpace(room.id); setActiveSpace(room.id);
return; return;
} }
// Share content into this room
final shareContent = Matrix.of(context).shareContent;
if (shareContent != null) {
final shareFile = shareContent.tryGet<XFile>('file');
if (shareContent.tryGet<String>('msgtype') == 'chat.fluffy.shared_file' &&
shareFile != null) {
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendFileDialog(
files: [shareFile],
room: room,
outerContext: context,
),
);
Matrix.of(context).shareContent = null;
} else {
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).forward,
message: L10n.of(context).forwardMessageTo(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
okLabel: L10n.of(context).forward,
cancelLabel: L10n.of(context).cancel,
);
if (consent == OkCancelResult.cancel) {
Matrix.of(context).shareContent = null;
return;
}
if (consent == OkCancelResult.ok) {
room.sendEvent(shareContent);
Matrix.of(context).shareContent = null;
}
}
}
context.go('/rooms/${room.id}'); context.go('/rooms/${room.id}');
} }
@ -420,53 +380,27 @@ class ChatListController extends State<ChatList>
String? get activeChat => widget.activeChat; String? get activeChat => widget.activeChat;
SelectMode get selectMode => Matrix.of(context).shareContent != null
? SelectMode.share
: SelectMode.normal;
void _processIncomingSharedMedia(List<SharedMediaFile> files) { void _processIncomingSharedMedia(List<SharedMediaFile> files) {
if (files.isEmpty) return; if (files.isEmpty) return;
if (files.length > 1) { showScaffoldDialog(
Logs().w( context: context,
'Received ${files.length} incoming shared media but app can only handle the first one', builder: (context) => ShareScaffoldDialog(
); items: files
} .map(
(file) => switch (file.type) {
// We only handle the first file currently SharedMediaType.file => FileShareItem(
final sharedMedia = files.first; XFile(
file.path.replaceFirst('file://', ''),
// Handle URIs and Texts, which are also passed in path mimeType: file.mimeType,
if (sharedMedia.type case SharedMediaType.text || SharedMediaType.url) { ),
return _processIncomingSharedText(sharedMedia.path); ),
} _ => TextShareItem(file.path),
},
final file = XFile( )
sharedMedia.path.replaceFirst('file://', ''), .toList(),
mimeType: sharedMedia.mimeType, ),
); );
Matrix.of(context).shareContent = {
'msgtype': 'chat.fluffy.shared_file',
'file': file,
if (sharedMedia.message != null) 'body': sharedMedia.message,
};
context.go('/rooms');
}
void _processIncomingSharedText(String? text) {
if (text == null) return;
if (text.toLowerCase().startsWith(AppConfig.deepLinkPrefix) ||
text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) ||
(text.toLowerCase().startsWith(AppConfig.schemePrefix) &&
!RegExp(r'\s').hasMatch(text))) {
return _processIncomingUris(text);
}
Matrix.of(context).shareContent = {
'msgtype': 'm.text',
'body': text,
};
context.go('/rooms');
} }
void _processIncomingUris(String? text) async { void _processIncomingUris(String? text) async {
@ -871,12 +805,6 @@ class ChatListController extends State<ChatList>
} }
} }
void cancelAction() {
if (selectMode == SelectMode.share) {
setState(() => Matrix.of(context).shareContent = null);
}
}
void setActiveFilter(ActiveFilter filter) { void setActiveFilter(ActiveFilter filter) {
setState(() { setState(() {
activeFilter = filter; activeFilter = filter;

@ -21,112 +21,84 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final selectMode = controller.selectMode;
return SliverAppBar( return SliverAppBar(
floating: true, floating: true,
toolbarHeight: 72, toolbarHeight: 72,
pinned: pinned: FluffyThemes.isColumnMode(context),
FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, scrolledUnderElevation: 0,
scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, backgroundColor: Colors.transparent,
backgroundColor:
selectMode == SelectMode.normal ? Colors.transparent : null,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: selectMode == SelectMode.normal title: TextField(
? null controller: controller.searchController,
: IconButton( focusNode: controller.searchFocusNode,
tooltip: L10n.of(context).cancel, textInputAction: TextInputAction.search,
icon: const Icon(Icons.close_outlined), onChanged: (text) => controller.onSearchEnter(
onPressed: controller.cancelAction, text,
color: theme.colorScheme.primary, globalSearch: globalSearch,
), ),
title: selectMode == SelectMode.share decoration: InputDecoration(
? Text( filled: true,
L10n.of(context).share, fillColor: theme.colorScheme.secondaryContainer,
key: const ValueKey(SelectMode.share), border: OutlineInputBorder(
) borderSide: BorderSide.none,
: TextField( borderRadius: BorderRadius.circular(99),
controller: controller.searchController, ),
focusNode: controller.searchFocusNode, contentPadding: EdgeInsets.zero,
textInputAction: TextInputAction.search, hintText: L10n.of(context).searchChatsRooms,
onChanged: (text) => controller.onSearchEnter( hintStyle: TextStyle(
text, color: theme.colorScheme.onPrimaryContainer,
globalSearch: globalSearch, fontWeight: FontWeight.normal,
), ),
decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true, prefixIcon: controller.isSearchMode
fillColor: theme.colorScheme.secondaryContainer, ? IconButton(
border: OutlineInputBorder( tooltip: L10n.of(context).cancel,
borderSide: BorderSide.none, icon: const Icon(Icons.close_outlined),
borderRadius: BorderRadius.circular(99), onPressed: controller.cancelSearch,
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context).searchChatsRooms,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal, )
: IconButton(
onPressed: controller.startSearch,
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
), ),
floatingLabelBehavior: FloatingLabelBehavior.never, suffixIcon: controller.isSearchMode && globalSearch
prefixIcon: controller.isSearchMode ? controller.isSearching
? IconButton( ? const Padding(
tooltip: L10n.of(context).cancel, padding: EdgeInsets.symmetric(
icon: const Icon(Icons.close_outlined), vertical: 10.0,
onPressed: controller.cancelSearch, horizontal: 12,
color: theme.colorScheme.onPrimaryContainer, ),
) child: SizedBox.square(
: IconButton( dimension: 24,
onPressed: controller.startSearch, child: CircularProgressIndicator.adaptive(
icon: Icon( strokeWidth: 2,
Icons.search_outlined, ),
color: theme.colorScheme.onPrimaryContainer, ),
)
: TextButton.icon(
onPressed: controller.setServer,
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
), ),
textStyle: const TextStyle(fontSize: 12),
), ),
suffixIcon: controller.isSearchMode && globalSearch icon: const Icon(Icons.edit_outlined, size: 16),
? controller.isSearching label: Text(
? const Padding( controller.searchServer ??
padding: EdgeInsets.symmetric( Matrix.of(context).client.homeserver!.host,
vertical: 10.0, maxLines: 2,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: TextButton.icon(
onPressed: controller.setServer,
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
),
textStyle: const TextStyle(fontSize: 12),
),
icon: const Icon(Icons.edit_outlined, size: 16),
label: Text(
controller.searchServer ??
Matrix.of(context).client.homeserver!.host,
maxLines: 2,
),
)
: SizedBox(
width: 0,
child: ClientChooserButton(controller),
), ),
), )
), : SizedBox(
actions: selectMode == SelectMode.share width: 0,
? [ child: ClientChooserButton(controller),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
), ),
child: ClientChooserButton(controller), ),
), ),
]
: null,
); );
} }

@ -22,140 +22,124 @@ class ChatListView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
return StreamBuilder<Object?>( return PopScope(
stream: Matrix.of(context).onShareContentChanged.stream, canPop: !controller.isSearchMode && controller.activeSpaceId == null,
builder: (_, __) { onPopInvokedWithResult: (pop, _) {
final selectMode = controller.selectMode; if (pop) return;
return PopScope( if (controller.activeSpaceId != null) {
canPop: controller.selectMode == SelectMode.normal && controller.clearActiveSpace();
!controller.isSearchMode && return;
controller.activeSpaceId == null, }
onPopInvokedWithResult: (pop, _) { if (controller.isSearchMode) {
if (pop) return; controller.cancelSearch();
if (controller.activeSpaceId != null) { return;
controller.clearActiveSpace(); }
return; },
} child: Row(
final selMode = controller.selectMode; children: [
if (controller.isSearchMode) { if (FluffyThemes.isColumnMode(context) &&
controller.cancelSearch(); controller.widget.displayNavigationRail) ...[
return; StreamBuilder(
} key: ValueKey(
if (selMode != SelectMode.normal) { client.userID.toString(),
controller.cancelAction(); ),
return; stream: client.onSync.stream
} .where((s) => s.hasRoomUpdate)
}, .rateLimit(const Duration(seconds: 1)),
child: Row( builder: (context, _) {
children: [ final allSpaces = Matrix.of(context)
if (FluffyThemes.isColumnMode(context) && .client
controller.widget.displayNavigationRail) ...[ .rooms
StreamBuilder( .where((room) => room.isSpace);
key: ValueKey( final rootSpaces = allSpaces
client.userID.toString(), .where(
), (space) => !allSpaces.any(
stream: client.onSync.stream (parentSpace) => parentSpace.spaceChildren
.where((s) => s.hasRoomUpdate) .any((child) => child.roomId == space.id),
.rateLimit(const Duration(seconds: 1)),
builder: (context, _) {
final allSpaces = Matrix.of(context)
.client
.rooms
.where((room) => room.isSpace);
final rootSpaces = allSpaces
.where(
(space) => !allSpaces.any(
(parentSpace) => parentSpace.spaceChildren
.any((child) => child.roomId == space.id),
),
)
.toList();
return SizedBox(
width: FluffyThemes.navRailWidth,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: rootSpaces.length + 2,
itemBuilder: (context, i) {
if (i == 0) {
return NaviRailItem(
isSelected: controller.activeSpaceId == null,
onTap: controller.clearActiveSpace,
icon: const Icon(Icons.forum_outlined),
selectedIcon: const Icon(Icons.forum),
toolTip: L10n.of(context).chats,
unreadBadgeFilter: (room) => true,
);
}
i--;
if (i == rootSpaces.length) {
return NaviRailItem(
isSelected: false,
onTap: () => context.go('/rooms/newspace'),
icon: const Icon(Icons.add),
toolTip: L10n.of(context).createNewSpace,
);
}
final space = rootSpaces[i];
final displayname =
rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
final spaceChildrenIds =
space.spaceChildren.map((c) => c.roomId).toSet();
return NaviRailItem(
toolTip: displayname,
isSelected: controller.activeSpaceId == space.id,
onTap: () =>
controller.setActiveSpace(rootSpaces[i].id),
unreadBadgeFilter: (room) =>
spaceChildrenIds.contains(room.id),
icon: Avatar(
mxContent: rootSpaces[i].avatar,
name: displayname,
size: 32,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
);
},
), ),
); )
}, .toList();
),
Container( return SizedBox(
color: Theme.of(context).dividerColor, width: FluffyThemes.navRailWidth,
width: 1, child: ListView.builder(
), scrollDirection: Axis.vertical,
], itemCount: rootSpaces.length + 2,
Expanded( itemBuilder: (context, i) {
child: GestureDetector( if (i == 0) {
onTap: FocusManager.instance.primaryFocus?.unfocus, return NaviRailItem(
excludeFromSemantics: true, isSelected: controller.activeSpaceId == null,
behavior: HitTestBehavior.translucent, onTap: controller.clearActiveSpace,
child: Scaffold( icon: const Icon(Icons.forum_outlined),
body: ChatListViewBody(controller), selectedIcon: const Icon(Icons.forum),
floatingActionButton: selectMode == SelectMode.normal && toolTip: L10n.of(context).chats,
!controller.isSearchMode && unreadBadgeFilter: (room) => true,
controller.activeSpaceId == null );
? FloatingActionButton.extended( }
onPressed: () => i--;
context.go('/rooms/newprivatechat'), if (i == rootSpaces.length) {
icon: const Icon(Icons.add_outlined), return NaviRailItem(
label: Text( isSelected: false,
L10n.of(context).chat, onTap: () => context.go('/rooms/newspace'),
overflow: TextOverflow.fade, icon: const Icon(Icons.add),
), toolTip: L10n.of(context).createNewSpace,
) );
: const SizedBox.shrink(), }
final space = rootSpaces[i];
final displayname = rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
final spaceChildrenIds =
space.spaceChildren.map((c) => c.roomId).toSet();
return NaviRailItem(
toolTip: displayname,
isSelected: controller.activeSpaceId == space.id,
onTap: () =>
controller.setActiveSpace(rootSpaces[i].id),
unreadBadgeFilter: (room) =>
spaceChildrenIds.contains(room.id),
icon: Avatar(
mxContent: rootSpaces[i].avatar,
name: displayname,
size: 32,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
);
},
), ),
), );
},
),
Container(
color: Theme.of(context).dividerColor,
width: 1,
),
],
Expanded(
child: GestureDetector(
onTap: FocusManager.instance.primaryFocus?.unfocus,
excludeFromSemantics: true,
behavior: HitTestBehavior.translucent,
child: Scaffold(
body: ChatListViewBody(controller),
floatingActionButton: !controller.isSearchMode &&
controller.activeSpaceId == null
? FloatingActionButton.extended(
onPressed: () => context.go('/rooms/newprivatechat'),
icon: const Icon(Icons.add_outlined),
label: Text(
L10n.of(context).chat,
overflow: TextOverflow.fade,
),
)
: const SizedBox.shrink(),
), ),
], ),
), ),
); ],
}, ),
); );
} }
} }

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/utils/show_scaffold_dialog.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import '../../utils/matrix_sdk_extensions/event_extension.dart'; import '../../utils/matrix_sdk_extensions/event_extension.dart';
class ImageViewer extends StatefulWidget { class ImageViewer extends StatefulWidget {
@ -20,11 +20,12 @@ class ImageViewer extends StatefulWidget {
class ImageViewerController extends State<ImageViewer> { class ImageViewerController extends State<ImageViewer> {
/// Forward this image to another room. /// Forward this image to another room.
void forwardAction() { void forwardAction() => showScaffoldDialog(
Matrix.of(widget.outerContext).shareContent = widget.event.content; context: context,
Navigator.of(context).pop(); builder: (context) => ShareScaffoldDialog(
widget.outerContext.go('/rooms'); items: [ContentShareItem(widget.event.content)],
} ),
);
/// Save this file with a system call. /// Save this file with a system call.
void saveFileAction(BuildContext context) => widget.event.saveFile(context); void saveFileAction(BuildContext context) => widget.event.saveFile(context);

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
Future<void> showScaffoldDialog({
required BuildContext context,
Color? barrierColor,
Color? containerColor,
double maxWidth = 480,
double maxHeight = 720,
required Widget Function(BuildContext context) builder,
}) =>
showDialog(
context: context,
builder: FluffyThemes.isColumnMode(context)
? (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color: containerColor ??
Theme.of(context).scaffoldBackgroundColor,
),
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(16),
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
),
child: builder(context),
),
)
: builder,
);

@ -176,18 +176,6 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
Client? getClientByName(String name) => Client? getClientByName(String name) =>
widget.clients.firstWhereOrNull((c) => c.clientName == name); widget.clients.firstWhereOrNull((c) => c.clientName == name);
Map<String, dynamic>? get shareContent => _shareContent;
set shareContent(Map<String, dynamic>? content) {
_shareContent = content;
onShareContentChanged.add(_shareContent);
}
Map<String, dynamic>? _shareContent;
final StreamController<Map<String, dynamic>?> onShareContentChanged =
StreamController.broadcast();
final onRoomKeyRequestSub = <String, StreamSubscription>{}; final onRoomKeyRequestSub = <String, StreamSubscription>{};
final onKeyVerificationRequestSub = <String, StreamSubscription>{}; final onKeyVerificationRequestSub = <String, StreamSubscription>{};
final onNotification = <String, StreamSubscription>{}; final onNotification = <String, StreamSubscription>{};

@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
abstract class ShareItem {}
class TextShareItem extends ShareItem {
final String value;
TextShareItem(this.value);
}
class ContentShareItem extends ShareItem {
final Map<String, Object?> value;
ContentShareItem(this.value);
}
class FileShareItem extends ShareItem {
final XFile value;
FileShareItem(this.value);
}
class ShareScaffoldDialog extends StatefulWidget {
final List<ShareItem> items;
const ShareScaffoldDialog({required this.items, super.key});
@override
State<ShareScaffoldDialog> createState() => _ShareScaffoldDialogState();
}
class _ShareScaffoldDialogState extends State<ShareScaffoldDialog> {
final TextEditingController _filterController = TextEditingController();
String? selectedRoomId;
bool isLoading = false;
void _toggleRoom(String roomId) {
setState(() {
selectedRoomId = roomId;
});
}
void _forwardAction() async {
final roomId = selectedRoomId;
if (roomId == null) {
throw Exception(
'Started forward action before room was selected. This should never happen.',
);
}
context.pop();
context.go('/rooms/$roomId', extra: widget.items);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final rooms = Matrix.of(context)
.client
.rooms
.where(
(room) =>
room.canSendDefaultMessages &&
!room.isSpace &&
room.membership == Membership.join,
)
.toList();
final filter = _filterController.text.trim().toLowerCase();
return Scaffold(
appBar: AppBar(
leading: Center(child: CloseButton(onPressed: context.pop)),
title: Text(L10n.of(context).share),
),
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
toolbarHeight: 72,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
title: TextField(
controller: _filterController,
onChanged: (_) => setState(() {}),
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context).search,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
),
),
),
),
SliverList.builder(
itemCount: rooms.length,
itemBuilder: (context, i) {
final room = rooms[i];
final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
final value = selectedRoomId == room.id;
final filterOut = !displayname.toLowerCase().contains(filter);
if (!value && filterOut) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Opacity(
opacity: filterOut ? 0.5 : 1,
child: CheckboxListTile.adaptive(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
secondary: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 0.75,
),
title: Text(displayname),
value: value,
onChanged: filterOut || isLoading
? null
: (_) => _toggleRoom(room.id),
checkboxShape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
),
),
);
},
),
],
),
bottomNavigationBar: AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: selectedRoomId == null && !isLoading
? const SizedBox.shrink()
: Material(
elevation: 8,
shadowColor: theme.appBarTheme.shadowColor,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: isLoading ? null : _forwardAction,
child: isLoading
? const LinearProgressIndicator()
: Text(L10n.of(context).forward),
),
),
),
),
);
}
}
Loading…
Cancel
Save