From cc61d8e91a3ebfe06a710cbdfd47e8587eb235ce Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Mon, 27 Jan 2020 10:14:38 +0100 Subject: [PATCH] Redesign --- CHANGELOG.md | 7 + lib/components/dialogs/new_group_dialog.dart | 2 +- .../dialogs/new_private_chat_dialog.dart | 2 +- lib/components/list_items/chat_list_item.dart | 2 +- .../list_items/participant_list_item.dart | 2 +- lib/components/matrix.dart | 2 +- lib/utils/url_launcher.dart | 4 +- lib/views/chat.dart | 431 +++++++++--------- lib/views/chat_list.dart | 17 +- lib/views/new_group.dart | 107 +++++ lib/views/new_private_chat.dart | 240 ++++++++++ pubspec.lock | 4 +- pubspec.yaml | 2 +- 13 files changed, 593 insertions(+), 229 deletions(-) create mode 100644 lib/views/new_group.dart create mode 100644 lib/views/new_private_chat.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index bce04c8..20ee625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version 0.5.1 - 2020-MM-DD +### New features +- Refactoring under the hood +### Fixes +- Fixed the bug that when revoking permissions for a user makes the user an admin +- Fixed the Kick user when user has already left the group + # Version 0.5.0 - 2020-01-20 ### New features - Localizations diff --git a/lib/components/dialogs/new_group_dialog.dart b/lib/components/dialogs/new_group_dialog.dart index 927864a..cd629a1 100644 --- a/lib/components/dialogs/new_group_dialog.dart +++ b/lib/components/dialogs/new_group_dialog.dart @@ -37,7 +37,7 @@ class _NewGroupDialogState extends State { Navigator.push( context, MaterialPageRoute(builder: (context) { - return Chat(roomID); + return ChatView(roomID); }), ), ); diff --git a/lib/components/dialogs/new_private_chat_dialog.dart b/lib/components/dialogs/new_private_chat_dialog.dart index 9e1e6c3..5afdecc 100644 --- a/lib/components/dialogs/new_private_chat_dialog.dart +++ b/lib/components/dialogs/new_private_chat_dialog.dart @@ -42,7 +42,7 @@ class _NewPrivateChatDialogState extends State { if (roomID != null) { await Navigator.push( context, - MaterialPageRoute(builder: (context) => Chat(roomID)), + MaterialPageRoute(builder: (context) => ChatView(roomID)), ); } } diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index f731d5d..1396e51 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -76,7 +76,7 @@ class ChatListItem extends StatelessWidget { } await Navigator.pushAndRemoveUntil( context, - AppRoute.defaultRoute(context, Chat(room.id)), + AppRoute.defaultRoute(context, ChatView(room.id)), (r) => r.isFirst, ); } diff --git a/lib/components/list_items/participant_list_item.dart b/lib/components/list_items/participant_list_item.dart index bf60aaf..de3675a 100644 --- a/lib/components/list_items/participant_list_item.dart +++ b/lib/components/list_items/participant_list_item.dart @@ -35,7 +35,7 @@ class ParticipantListItem extends StatelessWidget { await Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, - Chat(roomId), + ChatView(roomId), ), (Route r) => r.isFirst); break; diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index a012d51..ad3ca9a 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -153,7 +153,7 @@ class MatrixState extends State { await Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, - Chat(roomId), + ChatView(roomId), ), (r) => r.isFirst); } catch (_) { diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index da30dd8..0e673b9 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -29,7 +29,7 @@ class UrlLauncher { if (response == false) return; await Navigator.pushAndRemoveUntil( context, - AppRoute.defaultRoute(context, Chat(response["room_id"])), + AppRoute.defaultRoute(context, ChatView(response["room_id"])), (r) => r.isFirst, ); } else if (identifier.substring(0, 1) == "@") { @@ -44,7 +44,7 @@ class UrlLauncher { if (roomID != null) { await Navigator.push( context, - MaterialPageRoute(builder: (context) => Chat(roomID)), + MaterialPageRoute(builder: (context) => ChatView(roomID)), ); } } diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 829b929..02802f5 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -17,16 +17,34 @@ import 'package:pedantic/pedantic.dart'; import 'chat_list.dart'; -class Chat extends StatefulWidget { +class ChatView extends StatelessWidget { final String id; - const Chat(this.id, {Key key}) : super(key: key); + const ChatView(this.id, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // TODO: implement build + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList( + activeChat: id, + ), + secondScaffold: _Chat(id), + ); + } +} + +class _Chat extends StatefulWidget { + final String id; + + const _Chat(this.id, {Key key}) : super(key: key); @override _ChatState createState() => _ChatState(); } -class _ChatState extends State { +class _ChatState extends State<_Chat> { Room room; Timeline timeline; @@ -202,224 +220,217 @@ class _ChatState extends State { (typingUsers.length - 1).toString()); } - return AdaptivePageLayout( - primaryPage: FocusPage.SECOND, - firstScaffold: ChatList( - activeChat: widget.id, - ), - secondScaffold: Scaffold( - appBar: AppBar( - title: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(room.getLocalizedDisplayname(context)), - AnimatedContainer( - duration: Duration(milliseconds: 500), - height: typingText.isEmpty ? 0 : 20, - child: Row( - children: [ - typingText.isEmpty - ? Container() - : Icon(Icons.edit, - color: Theme.of(context).primaryColor, size: 10), - SizedBox(width: 4), - Text( - typingText, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontStyle: FontStyle.italic, - ), + return Scaffold( + appBar: AppBar( + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(room.getLocalizedDisplayname(context)), + AnimatedContainer( + duration: Duration(milliseconds: 500), + height: typingText.isEmpty ? 0 : 20, + child: Row( + children: [ + typingText.isEmpty + ? Container() + : Icon(Icons.edit, + color: Theme.of(context).primaryColor, size: 10), + SizedBox(width: 4), + Text( + typingText, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontStyle: FontStyle.italic, ), - ], - ), + ), + ], ), - ], - ), - actions: [ChatSettingsPopupMenu(room, !room.isDirectChat)], + ), + ], ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: FutureBuilder( - future: getTimeline(), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } + actions: [ChatSettingsPopupMenu(room, !room.isDirectChat)], + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: FutureBuilder( + future: getTimeline(), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } - if (room.notificationCount != null && - room.notificationCount > 0 && - timeline != null && - timeline.events.isNotEmpty) { - room.sendReadReceipt(timeline.events.first.eventId); - } + if (room.notificationCount != null && + room.notificationCount > 0 && + timeline != null && + timeline.events.isNotEmpty) { + room.sendReadReceipt(timeline.events.first.eventId); + } - if (timeline.events.isEmpty) return Container(); + if (timeline.events.isEmpty) return Container(); - return ListView.builder( - reverse: true, - itemCount: timeline.events.length + 1, - controller: _scrollController, - itemBuilder: (BuildContext context, int i) { - return i == 0 - ? AnimatedContainer( - height: seenByText.isEmpty ? 0 : 24, - duration: seenByText.isEmpty - ? Duration(milliseconds: 0) - : Duration(milliseconds: 500), - alignment: timeline.events.first.senderId == - client.userID - ? Alignment.topRight - : Alignment.topLeft, - child: Text( - seenByText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).primaryColor, - ), + return ListView.builder( + reverse: true, + itemCount: timeline.events.length + 1, + controller: _scrollController, + itemBuilder: (BuildContext context, int i) { + return i == 0 + ? AnimatedContainer( + height: seenByText.isEmpty ? 0 : 24, + duration: seenByText.isEmpty + ? Duration(milliseconds: 0) + : Duration(milliseconds: 500), + alignment: timeline.events.first.senderId == + client.userID + ? Alignment.topRight + : Alignment.topLeft, + child: Text( + seenByText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).primaryColor, ), - padding: EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - ) - : Message(timeline.events[i - 1], - nextEvent: - i >= 2 ? timeline.events[i - 2] : null); - }); - }, - ), - ), - room.canSendDefaultMessages && room.membership == Membership.join - ? Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 1, - blurRadius: 2, - offset: Offset(0, -1), // changes position of shadow - ), - ], - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - kIsWeb - ? Container() - : PopupMenuButton( - icon: Icon(Icons.add), - onSelected: (String choice) async { - if (choice == "file") { - sendFileAction(context); - } else if (choice == "image") { - sendImageAction(context); - } - if (choice == "camera") { - openCameraAction(context); - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: "file", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment), - ), - title: Text(I18n.of(context).sendFile), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: "image", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image), - ), - title: Text(I18n.of(context).sendImage), - contentPadding: EdgeInsets.all(0), - ), - ), - PopupMenuItem( - value: "camera", - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera), - ), - title: - Text(I18n.of(context).openCamera), - contentPadding: EdgeInsets.all(0), - ), - ), - ], ), - SizedBox(width: 8), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: TextField( - minLines: 1, - maxLines: kIsWeb ? 1 : 8, - keyboardType: kIsWeb - ? TextInputType.text - : TextInputType.multiline, - onSubmitted: (String text) { - send(); - FocusScope.of(context).requestFocus(inputFocus); - }, - focusNode: inputFocus, - controller: sendController, - decoration: InputDecoration( - hintText: I18n.of(context).writeAMessage, - border: InputBorder.none, + padding: EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + ) + : Message(timeline.events[i - 1], + nextEvent: + i >= 2 ? timeline.events[i - 2] : null); + }); + }, + ), + ), + room.canSendDefaultMessages && room.membership == Membership.join + ? Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: Offset(0, -1), // changes position of shadow + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + kIsWeb + ? Container() + : PopupMenuButton( + icon: Icon(Icons.add), + onSelected: (String choice) async { + if (choice == "file") { + sendFileAction(context); + } else if (choice == "image") { + sendImageAction(context); + } + if (choice == "camera") { + openCameraAction(context); + } + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: "file", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment), + ), + title: Text(I18n.of(context).sendFile), + contentPadding: EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: "image", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image), + ), + title: Text(I18n.of(context).sendImage), + contentPadding: EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: "camera", + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera), + ), + title: Text(I18n.of(context).openCamera), + contentPadding: EdgeInsets.all(0), + ), + ), + ], ), - onChanged: (String text) { - this.typingCoolDown?.cancel(); - this.typingCoolDown = - Timer(Duration(seconds: 2), () { - this.typingCoolDown = null; - this.currentlyTyping = false; - room.sendTypingInfo(false); - }); - this.typingTimeout ??= - Timer(Duration(seconds: 30), () { - this.typingTimeout = null; - this.currentlyTyping = false; - }); - if (!this.currentlyTyping) { - this.currentlyTyping = true; - room.sendTypingInfo(true, - timeout: - Duration(seconds: 30).inMilliseconds); - } - }, + SizedBox(width: 8), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: TextField( + minLines: 1, + maxLines: kIsWeb ? 1 : 8, + keyboardType: kIsWeb + ? TextInputType.text + : TextInputType.multiline, + onSubmitted: (String text) { + send(); + FocusScope.of(context).requestFocus(inputFocus); + }, + focusNode: inputFocus, + controller: sendController, + decoration: InputDecoration( + hintText: I18n.of(context).writeAMessage, + border: InputBorder.none, ), - )), - SizedBox(width: 8), - IconButton( - icon: Icon(Icons.send), - onPressed: () => send(), + onChanged: (String text) { + this.typingCoolDown?.cancel(); + this.typingCoolDown = + Timer(Duration(seconds: 2), () { + this.typingCoolDown = null; + this.currentlyTyping = false; + room.sendTypingInfo(false); + }); + this.typingTimeout ??= + Timer(Duration(seconds: 30), () { + this.typingTimeout = null; + this.currentlyTyping = false; + }); + if (!this.currentlyTyping) { + this.currentlyTyping = true; + room.sendTypingInfo(true, + timeout: + Duration(seconds: 30).inMilliseconds); + } + }, ), - ], - ), - ) - : Container(), - ], - ), + )), + SizedBox(width: 8), + IconButton( + icon: Icon(Icons.send), + onPressed: () => send(), + ), + ], + ), + ) + : Container(), + ], ), ), ); diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index fc03145..591e4ff 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -2,14 +2,14 @@ import 'dart:async'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; -import 'package:fluffychat/components/dialogs/new_group_dialog.dart'; -import 'package:fluffychat/components/dialogs/new_private_chat_dialog.dart'; import 'package:fluffychat/components/list_items/chat_list_item.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/i18n/i18n.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/views/archive.dart'; +import 'package:fluffychat/views/new_group.dart'; +import 'package:fluffychat/views/new_private_chat.dart'; import 'package:fluffychat/views/settings.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -193,19 +193,18 @@ class _ChatListState extends State { backgroundColor: Colors.blue, label: I18n.of(context).createNewGroup, labelStyle: TextStyle(fontSize: 18.0), - onTap: () => showDialog( - context: context, - builder: (BuildContext innerContext) => NewGroupDialog(), - ), + onTap: () => Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute(context, NewGroupView()), + (r) => r.isFirst), ), SpeedDialChild( child: Icon(Icons.person_add), backgroundColor: Colors.green, label: I18n.of(context).newPrivateChat, labelStyle: TextStyle(fontSize: 18.0), - onTap: () => showDialog( - context: context, - builder: (BuildContext innerContext) => NewPrivateChatDialog()), + onTap: () => Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute(context, NewPrivateChatView()), + (r) => r.isFirst), ), ], ), diff --git a/lib/views/new_group.dart b/lib/views/new_group.dart new file mode 100644 index 0000000..4c2dc69 --- /dev/null +++ b/lib/views/new_group.dart @@ -0,0 +1,107 @@ +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/i18n/i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:pedantic/pedantic.dart'; + +import 'chat.dart'; +import 'chat_list.dart'; +import 'invitation_selection.dart'; + +class NewGroupView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: _NewGroup(), + ); + } +} + +class _NewGroup extends StatefulWidget { + @override + _NewGroupState createState() => _NewGroupState(); +} + +class _NewGroupState extends State<_NewGroup> { + TextEditingController controller = TextEditingController(); + bool publicGroup = false; + + void submitAction(BuildContext context) async { + final MatrixState matrix = Matrix.of(context); + Map params = {}; + if (publicGroup) { + params["preset"] = "public_chat"; + params["visibility"] = "public"; + if (controller.text.isNotEmpty) { + params["room_alias_name"] = controller.text; + } + } else { + params["preset"] = "private_chat"; + } + if (controller.text.isNotEmpty) params["name"] = controller.text; + final String roomID = await matrix.tryRequestWithLoadingDialog( + matrix.client.createRoom(params: params), + ); + Navigator.of(context).pop(); + if (roomID != null) { + unawaited( + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return ChatView(roomID); + }), + ), + ); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InvitationSelection( + matrix.client.getRoomById(roomID), + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(I18n.of(context).createNewGroup), + elevation: 0, + ), + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.go, + onSubmitted: (s) => submitAction(context), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: I18n.of(context).optionalGroupName, + prefixIcon: Icon(Icons.people), + hintText: I18n.of(context).enterAGroupName), + ), + ), + SwitchListTile( + title: Text(I18n.of(context).groupIsPublic), + value: publicGroup, + onChanged: (bool b) => setState(() => publicGroup = b), + ), + ], + ), + floatingActionButton: FloatingActionButton( + backgroundColor: Theme.of(context).primaryColor, + onPressed: () => submitAction(context), + child: Icon(Icons.arrow_forward), + ), + ); + } +} diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart new file mode 100644 index 0000000..60b503b --- /dev/null +++ b/lib/views/new_private_chat.dart @@ -0,0 +1,240 @@ +import 'dart:async'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/i18n/i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:share/share.dart'; + +import 'chat.dart'; +import 'chat_list.dart'; + +class NewPrivateChatView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: _NewPrivateChat(), + ); + } +} + +class _NewPrivateChat extends StatefulWidget { + @override + _NewPrivateChatState createState() => _NewPrivateChatState(); +} + +class _NewPrivateChatState extends State<_NewPrivateChat> { + TextEditingController controller = TextEditingController(); + final _formKey = GlobalKey(); + bool loading = false; + String currentSearchTerm; + List> foundProfiles = []; + Timer coolDown; + Map get foundProfile => foundProfiles.firstWhere( + (user) => user["user_id"] == "@$currentSearchTerm", + orElse: () => null); + bool get correctMxId => + foundProfiles + .indexWhere((user) => user["user_id"] == "@$currentSearchTerm") != + -1; + + void submitAction(BuildContext context) async { + if (controller.text.isEmpty) return; + if (!_formKey.currentState.validate()) return; + final MatrixState matrix = Matrix.of(context); + + if ("@" + controller.text.trim() == matrix.client.userID) return; + + final User user = User( + "@" + controller.text.trim(), + room: Room(id: "", client: matrix.client), + ); + final String roomID = + await matrix.tryRequestWithLoadingDialog(user.startDirectChat()); + Navigator.of(context).pop(); + + if (roomID != null) { + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => ChatView(roomID)), + ); + } + } + + void searchUserWithCoolDown(BuildContext context, String text) async { + coolDown?.cancel(); + coolDown = Timer( + Duration(seconds: 1), + () => searchUser(context, text), + ); + } + + void searchUser(BuildContext context, String text) async { + 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"]?.isEmpty ?? true)) return; + setState(() { + foundProfiles = List>.from(response["results"]); + }); + } + + @override + Widget build(BuildContext context) { + final String defaultDomain = Matrix.of(context).client.userID.split(":")[1]; + return Scaffold( + appBar: AppBar( + title: Text(I18n.of(context).newPrivateChat), + elevation: 0, + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: TextFormField( + controller: controller, + autofocus: true, + autocorrect: false, + onChanged: (String text) => + searchUserWithCoolDown(context, text), + textInputAction: TextInputAction.go, + onFieldSubmitted: (s) => submitAction(context), + validator: (value) { + if (value.isEmpty) { + return I18n.of(context).pleaseEnterAMatrixIdentifier; + } + final MatrixState matrix = Matrix.of(context); + String mxid = "@" + controller.text.trim(); + if (mxid == matrix.client.userID) { + return I18n.of(context).youCannotInviteYourself; + } + if (!mxid.contains("@")) { + return I18n.of(context).makeSureTheIdentifierIsValid; + } + if (!mxid.contains(":")) { + return I18n.of(context).makeSureTheIdentifierIsValid; + } + return null; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: I18n.of(context).enterAUsername, + prefixIcon: loading + ? Container( + padding: const EdgeInsets.all(8.0), + width: 12, + height: 12, + child: CircularProgressIndicator(), + ) + : correctMxId + ? Padding( + padding: const EdgeInsets.all(8.0), + child: Avatar( + MxContent(foundProfile["avatar_url"] ?? ""), + foundProfile["display_name"] ?? + foundProfile["user_id"], + size: 12, + ), + ) + : Icon(Icons.account_circle), + prefixText: "@", + hintText: + "${I18n.of(context).username.toLowerCase()}:$defaultDomain", + ), + ), + ), + ), + Divider(height: 1), + if (foundProfiles.isNotEmpty && !correctMxId) + Expanded( + child: ListView.builder( + itemCount: foundProfiles.length, + itemBuilder: (BuildContext context, int i) { + Map foundProfile = foundProfiles[i]; + return ListTile( + onTap: () { + setState(() { + controller.text = currentSearchTerm = + foundProfile["user_id"].substring(1); + }); + }, + leading: Avatar( + MxContent(foundProfile["avatar_url"] ?? ""), + foundProfile["display_name"] ?? foundProfile["user_id"], + //size: 24, + ), + title: Text( + foundProfile["display_name"] ?? + foundProfile["user_id"].split(":").first.substring(1), + style: TextStyle(), + maxLines: 1, + ), + subtitle: Text( + foundProfile["user_id"], + maxLines: 1, + style: TextStyle( + fontSize: 12, + ), + ), + ); + }, + ), + ), + if (foundProfiles.isEmpty || correctMxId) + ListTile( + trailing: Icon( + Icons.share, + size: 16, + ), + onTap: () => Share.share( + "https://matrix.to/#/${Matrix.of(context).client.userID}"), + title: Text( + "${I18n.of(context).yourOwnUsername}:", + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + subtitle: Text( + Matrix.of(context).client.userID, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + ), + ), + Divider(height: 1), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => submitAction(context), + child: Icon(Icons.arrow_forward), + backgroundColor: Theme.of(context).primaryColor, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 416e8d7..998fa79 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -110,8 +110,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1ff04785c0fb7db986c8a7b33396bf06301f1a14" - resolved-ref: "1ff04785c0fb7db986c8a7b33396bf06301f1a14" + ref: "53250618764f068aae92ecff52eaa4d968be3290" + resolved-ref: "53250618764f068aae92ecff52eaa4d968be3290" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index f14b3a7..379fc5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 1ff04785c0fb7db986c8a7b33396bf06301f1a14 + ref: 53250618764f068aae92ecff52eaa4d968be3290 localstorage: ^3.0.1+4 bubble: ^1.1.9+1