refactor: Improved share / forward dialog

pull/1504/head
krille-chan 11 months ago
parent 69d4c50b13
commit 88e63d127a
No known key found for this signature in database

@ -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/log_view.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
abstract class AppRoutes {
static FutureOr<String?> loggedInRedirect(
@ -318,15 +319,25 @@ abstract class AppRoutes {
),
GoRoute(
path: ':roomid',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
ChatPage(
roomId: state.pathParameters['roomid']!,
shareText: state.uri.queryParameters['body'],
eventId: state.uri.queryParameters['event'],
),
),
pageBuilder: (context, state) {
final body = state.uri.queryParameters['body'];
var shareItems = state.extra is List<ShareItem>
? state.extra as List<ShareItem>
: null;
if (body != null && body.isNotEmpty) {
shareItems ??= [];
shareItems.add(TextShareItem(body));
}
return defaultPageBuilder(
context,
state,
ChatPage(
roomId: state.pathParameters['roomid']!,
shareItems: shareItems,
eventId: state.uri.queryParameters['event'],
),
);
},
redirect: loggedOutRedirect,
routes: [
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/matrix_locals.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/matrix.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart';
import 'send_file_dialog.dart';
@ -41,14 +43,14 @@ import 'send_location_dialog.dart';
class ChatPage extends StatelessWidget {
final String roomId;
final String? shareText;
final List<ShareItem>? shareItems;
final String? eventId;
const ChatPage({
super.key,
required this.roomId,
this.eventId,
this.shareText,
this.shareItems,
});
@override
@ -69,7 +71,7 @@ class ChatPage extends StatelessWidget {
return ChatPageWithRoom(
key: Key('chat_page_${roomId}_$eventId'),
room: room,
shareText: shareText,
shareItems: shareItems,
eventId: eventId,
);
}
@ -77,13 +79,13 @@ class ChatPage extends StatelessWidget {
class ChatPageWithRoom extends StatefulWidget {
final Room room;
final String? shareText;
final List<ShareItem>? shareItems;
final String? eventId;
const ChatPageWithRoom({
super.key,
required this.room,
this.shareText,
this.shareItems,
this.eventId,
});
@ -224,18 +226,42 @@ class ChatController extends State<ChatPageWithRoom>
void _loadDraft() async {
final prefs = await SharedPreferences.getInstance();
final draft = widget.shareText ?? prefs.getString('draft_$roomId');
final draft = prefs.getString('draft_$roomId');
if (draft != null && draft.isNotEmpty) {
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
void initState() {
scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
_loadDraft();
WidgetsBinding.instance.addPostFrameCallback(_shareItems);
super.initState();
_displayChatDetailsColumn = ValueNotifier(
Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
@ -821,17 +847,17 @@ class ChatController extends State<ChatPageWithRoom>
}
void forwardEventsAction() async {
if (selectedEvents.length == 1) {
Matrix.of(context).shareContent =
selectedEvents.first.getDisplayEvent(timeline!).content;
} else {
Matrix.of(context).shareContent = {
'msgtype': 'm.text',
'body': _getSelectedEventString(),
};
}
if (selectedEvents.isEmpty) return;
await showScaffoldDialog(
context: context,
builder: (context) => ShareScaffoldDialog(
items: selectedEvents
.map((event) => ContentShareItem(event.content))
.toList(),
),
);
if (!mounted) return;
setState(() => selectedEvents.clear());
context.go('/rooms');
}
void sendAgainAction() {

@ -15,14 +15,15 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.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/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.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/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import '../../../utils/account_bundles.dart';
import '../../config/setting_keys.dart';
import '../../utils/url_launcher.dart';
@ -34,11 +35,6 @@ import '../bootstrap/bootstrap_dialog.dart';
import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
enum SelectMode {
normal,
share,
}
enum PopupMenuAction {
settings,
invite,
@ -191,42 +187,6 @@ class ChatListController extends State<ChatList>
setActiveSpace(room.id);
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}');
}
@ -420,53 +380,27 @@ class ChatListController extends State<ChatList>
String? get activeChat => widget.activeChat;
SelectMode get selectMode => Matrix.of(context).shareContent != null
? SelectMode.share
: SelectMode.normal;
void _processIncomingSharedMedia(List<SharedMediaFile> files) {
if (files.isEmpty) return;
if (files.length > 1) {
Logs().w(
'Received ${files.length} incoming shared media but app can only handle the first one',
);
}
// We only handle the first file currently
final sharedMedia = files.first;
// Handle URIs and Texts, which are also passed in path
if (sharedMedia.type case SharedMediaType.text || SharedMediaType.url) {
return _processIncomingSharedText(sharedMedia.path);
}
final file = XFile(
sharedMedia.path.replaceFirst('file://', ''),
mimeType: sharedMedia.mimeType,
showScaffoldDialog(
context: context,
builder: (context) => ShareScaffoldDialog(
items: files
.map(
(file) => switch (file.type) {
SharedMediaType.file => FileShareItem(
XFile(
file.path.replaceFirst('file://', ''),
mimeType: file.mimeType,
),
),
_ => TextShareItem(file.path),
},
)
.toList(),
),
);
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 {
@ -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) {
setState(() {
activeFilter = filter;

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

@ -22,140 +22,124 @@ class ChatListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
return StreamBuilder<Object?>(
stream: Matrix.of(context).onShareContentChanged.stream,
builder: (_, __) {
final selectMode = controller.selectMode;
return PopScope(
canPop: controller.selectMode == SelectMode.normal &&
!controller.isSearchMode &&
controller.activeSpaceId == null,
onPopInvokedWithResult: (pop, _) {
if (pop) return;
if (controller.activeSpaceId != null) {
controller.clearActiveSpace();
return;
}
final selMode = controller.selectMode;
if (controller.isSearchMode) {
controller.cancelSearch();
return;
}
if (selMode != SelectMode.normal) {
controller.cancelAction();
return;
}
},
child: Row(
children: [
if (FluffyThemes.isColumnMode(context) &&
controller.widget.displayNavigationRail) ...[
StreamBuilder(
key: ValueKey(
client.userID.toString(),
),
stream: client.onSync.stream
.where((s) => s.hasRoomUpdate)
.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,
),
),
);
},
return PopScope(
canPop: !controller.isSearchMode && controller.activeSpaceId == null,
onPopInvokedWithResult: (pop, _) {
if (pop) return;
if (controller.activeSpaceId != null) {
controller.clearActiveSpace();
return;
}
if (controller.isSearchMode) {
controller.cancelSearch();
return;
}
},
child: Row(
children: [
if (FluffyThemes.isColumnMode(context) &&
controller.widget.displayNavigationRail) ...[
StreamBuilder(
key: ValueKey(
client.userID.toString(),
),
stream: client.onSync.stream
.where((s) => s.hasRoomUpdate)
.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),
),
);
},
),
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: selectMode == SelectMode.normal &&
!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(),
)
.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,
),
),
);
},
),
),
);
},
),
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:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/image_viewer/image_viewer_view.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';
class ImageViewer extends StatefulWidget {
@ -20,11 +20,12 @@ class ImageViewer extends StatefulWidget {
class ImageViewerController extends State<ImageViewer> {
/// Forward this image to another room.
void forwardAction() {
Matrix.of(widget.outerContext).shareContent = widget.event.content;
Navigator.of(context).pop();
widget.outerContext.go('/rooms');
}
void forwardAction() => showScaffoldDialog(
context: context,
builder: (context) => ShareScaffoldDialog(
items: [ContentShareItem(widget.event.content)],
),
);
/// Save this file with a system call.
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) =>
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 onKeyVerificationRequestSub = <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