diff --git a/lib/views/invitation_selection.dart b/lib/views/invitation_selection.dart index ad9e90a..c89e9e0 100644 --- a/lib/views/invitation_selection.dart +++ b/lib/views/invitation_selection.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/avatar.dart'; @@ -8,13 +10,24 @@ import 'package:toast/toast.dart'; import 'chat_list.dart'; -class InvitationSelection extends StatelessWidget { +class InvitationSelection extends StatefulWidget { final Room room; const InvitationSelection(this.room, {Key key}) : super(key: key); + @override + _InvitationSelectionState createState() => _InvitationSelectionState(); +} + +class _InvitationSelectionState extends State { + TextEditingController controller = TextEditingController(); + String currentSearchTerm; + bool loading = false; + List> foundProfiles = []; + Timer coolDown; + Future> getContacts(BuildContext context) async { final Client client = Matrix.of(context).client; - List participants = await room.requestParticipants(); + List participants = await widget.room.requestParticipants(); List contacts = []; Map userMap = {}; for (int i = 0; i < client.rooms.length; i++) { @@ -36,7 +49,7 @@ class InvitationSelection extends StatelessWidget { void inviteAction(BuildContext context, String id) async { final success = await Matrix.of(context).tryRequestWithLoadingDialog( - room.invite(id), + widget.room.invite(id), ); if (success != false) { Toast.show( @@ -47,40 +60,139 @@ class InvitationSelection extends StatelessWidget { } } + void searchUserWithCoolDown(BuildContext context, String text) async { + coolDown?.cancel(); + coolDown = Timer( + Duration(seconds: 1), + () => searchUser(context, text), + ); + } + + void searchUser(BuildContext context, String text) async { + coolDown?.cancel(); + if (text.isEmpty) { + setState(() { + foundProfiles = []; + }); + } + currentSearchTerm = text; + if (currentSearchTerm.isEmpty) return; + if (loading) return; + setState(() => loading = true); + final MatrixState matrix = Matrix.of(context); + final response = await matrix.tryRequestWithErrorToast( + matrix.client.jsonRequest( + type: HTTPType.POST, + action: "/client/r0/user_directory/search", + data: { + "search_term": text, + "limit": 10, + }), + ); + setState(() => loading = false); + if (response == false || + !(response is Map) || + (response["results"] == null)) return; + setState(() { + foundProfiles = List>.from(response["results"]); + if ("@$text".isValidMatrixId && + foundProfiles + .indexWhere((profile) => "@$text" == profile["user_id"]) == + -1) { + setState(() => foundProfiles = [ + {"user_id": "@$text"} + ]); + } + foundProfiles.removeWhere((profile) => + widget.room + .getParticipants() + .indexWhere((u) => u.id == profile["user_id"]) != + -1); + }); + } + @override Widget build(BuildContext context) { - final String groupName = - room.name?.isEmpty ?? false ? I18n.of(context).group : room.name; + final String groupName = widget.room.name?.isEmpty ?? false + ? I18n.of(context).group + : widget.room.name; return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(activeChat: room.id), + firstScaffold: ChatList(activeChat: widget.room.id), secondScaffold: Scaffold( appBar: AppBar( - title: Text(I18n.of(context).inviteContactToGroup(groupName)), - ), - body: FutureBuilder>( - future: getContacts(context), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - List contacts = snapshot.data; - return ListView.builder( - itemCount: contacts.length, - itemBuilder: (BuildContext context, int i) => ListTile( - leading: Avatar( - contacts[i].avatarUrl, - contacts[i].calcDisplayname(), + title: Text(I18n.of(context).inviteContact), + bottom: PreferredSize( + preferredSize: Size.fromHeight(68), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.search, + onChanged: (String text) => + searchUserWithCoolDown(context, text), + onSubmitted: (String text) => searchUser(context, text), + decoration: InputDecoration( + border: OutlineInputBorder(), + prefixText: "@", + hintText: I18n.of(context).username, + labelText: I18n.of(context).inviteContactToGroup(groupName), + suffixIcon: loading + ? Container( + padding: const EdgeInsets.all(8.0), + width: 12, + height: 12, + child: CircularProgressIndicator(), + ) + : Icon(Icons.search), ), - title: Text(contacts[i].calcDisplayname()), - subtitle: Text(contacts[i].id), - onTap: () => inviteAction(context, contacts[i].id), ), - ); - }, - )), + ), + ), + ), + body: foundProfiles.isNotEmpty + ? ListView.builder( + itemCount: foundProfiles.length, + itemBuilder: (BuildContext context, int i) => ListTile( + leading: Avatar( + MxContent(foundProfiles[i]["avatar_url"] ?? ""), + foundProfiles[i]["display_name"] ?? + foundProfiles[i]["user_id"], + ), + title: Text( + foundProfiles[i]["display_name"] ?? + (foundProfiles[i]["user_id"] as String).localpart, + ), + subtitle: Text(foundProfiles[i]["user_id"]), + onTap: () => + inviteAction(context, foundProfiles[i]["user_id"]), + ), + ) + : FutureBuilder>( + future: getContacts(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + List contacts = snapshot.data; + return ListView.builder( + itemCount: contacts.length, + itemBuilder: (BuildContext context, int i) => ListTile( + leading: Avatar( + contacts[i].avatarUrl, + contacts[i].calcDisplayname(), + ), + title: Text(contacts[i].calcDisplayname()), + subtitle: Text(contacts[i].id), + onTap: () => inviteAction(context, contacts[i].id), + ), + ); + }, + )), ); } }