import 'package:flutter/material.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/l10n/l10n.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../widgets/qr_code_viewer.dart'; class NewPrivateChatView extends StatelessWidget { final NewPrivateChatController controller; const NewPrivateChatView(this.controller, {super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final searchResponse = controller.searchResponse; final client = Matrix.of(context).client; final userId = client.userID!; final dmRoomContactList = client.rooms .where((room) => room.isDirectChat) .map( (room) => room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!), ) .map( (user) => Profile( userId: user.id, displayName: user.displayName, avatarUrl: user.avatarUrl, ), ) .toList(); return Scaffold( appBar: AppBar( scrolledUnderElevation: 0, leading: const Center(child: BackButton()), title: Text(L10n.of(context).newMessage), backgroundColor: theme.scaffoldBackgroundColor, actions: [ TextButton( onPressed: UrlLauncher( context, AppConfig.startChatTutorial, ).launchUrl, child: Text(L10n.of(context).help), ), ], ), body: MaxWidthBody( withScrolling: false, innerPadding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: TextField( controller: controller.controller, onChanged: controller.searchUsers, decoration: InputDecoration( hintText: L10n.of(context).searchForUsers, filled: true, fillColor: theme.colorScheme.secondaryContainer, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(99), ), hintStyle: TextStyle( color: theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.normal, ), prefixIcon: searchResponse == null ? const Icon(Icons.search_outlined) : FutureBuilder( future: searchResponse, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Padding( padding: EdgeInsets.all(10.0), child: SizedBox.square( dimension: 24, child: CircularProgressIndicator.adaptive( strokeWidth: 1, ), ), ); } return const Icon(Icons.search_outlined); }, ), suffixIcon: controller.controller.text.isEmpty ? null : IconButton( icon: const Icon(Icons.clear_outlined), onPressed: () { controller.controller.clear(); controller.searchUsers(); }, ), ), ), ), Expanded( child: AnimatedSwitcher( duration: FluffyThemes.animationDuration, child: searchResponse == null ? ListView( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 18.0, ), child: SelectableText.rich( TextSpan( children: [ TextSpan( text: L10n.of(context).yourGlobalUserIdIs, ), TextSpan( text: Matrix.of(context).client.userID, style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), style: TextStyle( color: theme.colorScheme.onSurface, fontSize: 12, ), ), ), const SizedBox(height: 8), ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.tertiaryContainer, foregroundColor: theme.colorScheme.onTertiaryContainer, child: const Icon(Icons.group_add_outlined), ), title: Text(L10n.of(context).createGroup), onTap: () => context.go('/rooms/newgroup'), ), ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.secondaryContainer, foregroundColor: theme.colorScheme.onSecondaryContainer, child: Icon(Icons.adaptive.share_outlined), ), title: Text(L10n.of(context).shareInviteLink), trailing: IconButton( icon: Icon(Icons.qr_code_outlined), onPressed: () => showQrCodeViewer(context, userId), ), onTap: controller.inviteAction, ), if (PlatformInfos.isMobile) ListTile( leading: CircleAvatar( backgroundColor: theme.colorScheme.primaryContainer, foregroundColor: theme.colorScheme.onPrimaryContainer, child: const Icon( Icons.qr_code_scanner_outlined, ), ), title: Text(L10n.of(context).scanQrCode), onTap: controller.openScannerAction, ), ListView.builder( shrinkWrap: true, itemCount: dmRoomContactList.length, itemBuilder: (context, i) { if (i == 0 || dmRoomContactList[i] .calcDisplayname() .substring(0, 1) .toUpperCase() != dmRoomContactList[i - 1] .calcDisplayname() .substring(0, 1) .toUpperCase()) { return Column( mainAxisSize: .min, children: [ ListTile( leading: CircleAvatar( backgroundColor: Colors.transparent, child: Text( dmRoomContactList[i] .calcDisplayname() .toUpperCase() .substring(0, 1), ), ), ), ProfileListTile( profile: dmRoomContactList[i], onTap: controller.openUserModal, ), ], ); } return ProfileListTile( profile: dmRoomContactList[i], onTap: controller.openUserModal, ); }, ), ], ) : FutureBuilder( future: searchResponse, builder: (context, snapshot) { final result = snapshot.data; final error = snapshot.error; if (error != null) { return Column( mainAxisAlignment: .center, children: [ Text( error.toLocalizedString(context), textAlign: TextAlign.center, style: TextStyle( color: theme.colorScheme.error, ), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: controller.searchUsers, icon: const Icon(Icons.refresh_outlined), label: Text(L10n.of(context).tryAgain), ), ], ); } if (result == null) { return const Center( child: CircularProgressIndicator.adaptive(), ); } if (result.isEmpty) { return Column( mainAxisAlignment: .center, children: [ const Icon(Icons.search_outlined, size: 86), Padding( padding: const EdgeInsets.all(16.0), child: Text( L10n.of(context).noUsersFoundWithQuery( controller.controller.text, ), style: TextStyle( color: theme.colorScheme.primary, ), textAlign: TextAlign.center, ), ), ], ); } return ListView.builder( itemCount: result.length, itemBuilder: (context, i) => ProfileListTile( profile: result[i], onTap: controller.openUserModal, ), ); }, ), ), ), ], ), ), ); } } extension on Profile { String calcDisplayname() => displayName ?? userId.localpart ?? userId; } class ProfileListTile extends StatelessWidget { final Profile profile; final void Function(Profile) onTap; const ProfileListTile({ super.key, required this.profile, required this.onTap, }); @override Widget build(BuildContext context) { final displayname = profile.calcDisplayname(); return ListTile( leading: Avatar( name: displayname, mxContent: profile.avatarUrl, presenceUserId: profile.userId, ), title: Text(displayname), subtitle: Text(profile.userId), onTap: () => onTap(profile), ); } }