Merge pull request #1504 from krille-chan/krille/new-share-file-dialog
refactor: Improved share / forward dialog
This commit is contained in:
commit
b65d3dbd16
9 changed files with 491 additions and 364 deletions
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
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),
|
||||
)
|
||||
: TextButton.icon(
|
||||
onPressed: controller.setServer,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: selectMode == SelectMode.share
|
||||
? [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
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),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
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),
|
||||
),
|
||||
)
|
||||
.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);
|
||||
|
|
|
|||
36
lib/utils/show_scaffold_dialog.dart
Normal file
36
lib/utils/show_scaffold_dialog.dart
Normal file
|
|
@ -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>{};
|
||||
|
|
|
|||
181
lib/widgets/share_scaffold_dialog.dart
Normal file
181
lib/widgets/share_scaffold_dialog.dart
Normal file
|
|
@ -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…
Add table
Reference in a new issue