import 'dart:async'; import 'package:recon/apis/user_api.dart'; import 'package:recon/client_holder.dart'; import 'package:recon/clients/messaging_client.dart'; import 'package:recon/models/users/user.dart'; import 'package:recon/widgets/default_error_widget.dart'; import 'package:recon/widgets/friends/user_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class SearchError { final String message; final IconData icon; const SearchError({required this.message, required this.icon}); } class UserSearch extends StatefulWidget { const UserSearch({super.key}); @override State createState() => _UserSearchState(); } class _UserSearchState extends State { final TextEditingController _searchInputController = TextEditingController(); Timer? _searchDebouncer; late Future?>? _usersFuture = _emptySearch; Future> get _emptySearch => Future(() => throw const SearchError(message: "Start typing to search for users", icon: Icons.search)); void _querySearch(BuildContext context, String needle) { if (needle.isEmpty) { _usersFuture = _emptySearch; return; } _usersFuture = UserApi.searchUsers(ClientHolder.of(context).apiClient, needle: needle).then((value) { final res = value.toList(); if (res.isEmpty) { throw SearchError(message: "No user found with username '$needle'", icon: Icons.search_off); } res.sort((a, b) => a.username.length.compareTo(b.username.length)); return res; }); } @override Widget build(BuildContext context) { final mClient = Provider.of(context, listen: false); return Scaffold( appBar: AppBar( title: const Text("Find Users"), ), body: SafeArea( top: false, child: Column( children: [ Expanded( child: FutureBuilder( future: _usersFuture, builder: (context, snapshot) { if (snapshot.hasData) { final users = snapshot.data as List; return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserListTile( user: user, onChanged: () { mClient.refreshFriendsList(); }, isFriend: mClient.getAsContact(user.id) != null, ); }, ); } else if (snapshot.hasError) { final err = snapshot.error; if (err is SearchError) { return DefaultErrorWidget( title: err.message, iconOverride: err.icon, ); } else { FlutterError.reportError(FlutterErrorDetails( exception: snapshot.error!, stack: snapshot.stackTrace, )); return DefaultErrorWidget( title: "${snapshot.error}", ); } } else { return const Column( children: [ LinearProgressIndicator(), ], ); } }, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: TextField( decoration: InputDecoration( isDense: true, hintText: "Search for users...", contentPadding: const EdgeInsets.all(16), border: OutlineInputBorder(borderRadius: BorderRadius.circular(24))), autocorrect: false, controller: _searchInputController, onChanged: (String value) { _searchDebouncer?.cancel(); if (value.isEmpty) { setState(() { _querySearch(context, value); }); return; } setState(() { _usersFuture = Future(() => null); }); _searchDebouncer = Timer(const Duration(milliseconds: 300), () { setState(() { _querySearch(context, value); }); }); }, ), ), ], ), ), ); } }