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 488 additions and 361 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/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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIcon: controller.isSearchMode && globalSearch
|
)
|
||||||
? controller.isSearching
|
: TextButton.icon(
|
||||||
? const Padding(
|
onPressed: controller.setServer,
|
||||||
padding: EdgeInsets.symmetric(
|
style: TextButton.styleFrom(
|
||||||
vertical: 10.0,
|
shape: RoundedRectangleBorder(
|
||||||
horizontal: 12,
|
borderRadius: BorderRadius.circular(99),
|
||||||
),
|
),
|
||||||
child: SizedBox.square(
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
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(
|
||||||
actions: selectMode == SelectMode.share
|
controller.searchServer ??
|
||||||
? [
|
Matrix.of(context).client.homeserver!.host,
|
||||||
Padding(
|
maxLines: 2,
|
||||||
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
|
@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;
|
|
||||||
}
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
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: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);
|
||||||
|
|
|
||||||
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) =>
|
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>{};
|
||||||
|
|
|
||||||
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
Add a link
Reference in a new issue