From b9be33c16be726751ae163a8aa44f68906756739 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Fri, 5 Feb 2021 08:43:44 +0100 Subject: [PATCH] change: Implement contact list instead of status --- .../list_items/contact_list_tile.dart | 74 ++++++++++ .../list_items/status_list_tile.dart | 136 ------------------ lib/components/matrix.dart | 46 ------ lib/utils/client_presence_extension.dart | 33 ++++- lib/utils/presence_extension.dart | 12 ++ lib/utils/status.dart | 19 --- lib/views/chat_details.dart | 2 + lib/views/home_view.dart | 32 ++++- lib/views/home_view_parts/contact_list.dart | 72 ++++++++++ lib/views/home_view_parts/status_list.dart | 78 ---------- 10 files changed, 216 insertions(+), 288 deletions(-) create mode 100644 lib/components/list_items/contact_list_tile.dart delete mode 100644 lib/components/list_items/status_list_tile.dart delete mode 100644 lib/utils/status.dart create mode 100644 lib/views/home_view_parts/contact_list.dart delete mode 100644 lib/views/home_view_parts/status_list.dart diff --git a/lib/components/list_items/contact_list_tile.dart b/lib/components/list_items/contact_list_tile.dart new file mode 100644 index 00000000..26c84a1a --- /dev/null +++ b/lib/components/list_items/contact_list_tile.dart @@ -0,0 +1,74 @@ +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/avatar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../utils/presence_extension.dart'; +import '../matrix.dart'; + +class ContactListTile extends StatelessWidget { + final Presence contact; + + const ContactListTile({Key key, @required this.contact}) : super(key: key); + @override + Widget build(BuildContext context) { + var statusMsg = contact.presence?.statusMsg?.isNotEmpty ?? false + ? contact.presence.statusMsg + : null; + if (contact.senderId == '@jana:janian.de') { + statusMsg = 'Hallo Welt'; + } + return FutureBuilder( + future: + Matrix.of(context).client.getProfileFromUserId(contact.senderId), + builder: (context, snapshot) { + final displayname = + snapshot.data?.displayname ?? contact.senderId.localpart; + final avatarUrl = snapshot.data?.avatarUrl; + return ListTile( + leading: Avatar(avatarUrl, displayname), + title: Row( + children: [ + Icon(Icons.circle, color: contact.color, size: 10), + SizedBox(width: 4), + Expanded( + child: Text( + displayname, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + subtitle: statusMsg == null + ? Text(contact.getLocalizedLastActiveAgo(context)) + : Row( + children: [ + Icon(Icons.edit_outlined, + color: Theme.of(context).accentColor, size: 12), + SizedBox(width: 2), + Expanded( + child: Text( + statusMsg, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: + Theme.of(context).textTheme.bodyText1.color, + ), + ), + ), + ], + ), + onTap: () async { + if (contact.senderId == Matrix.of(context).client.userID) { + return; + } + final roomId = await User(contact.senderId, + room: Room(id: '', client: Matrix.of(context).client)) + .startDirectChat(); + await AdaptivePageLayout.of(context) + .pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}'); + }); + }); + } +} diff --git a/lib/components/list_items/status_list_tile.dart b/lib/components/list_items/status_list_tile.dart deleted file mode 100644 index a89dcf69..00000000 --- a/lib/components/list_items/status_list_tile.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:adaptive_page_layout/adaptive_page_layout.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/avatar.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/status.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../../utils/string_color.dart'; -import '../../utils/date_time_extension.dart'; -import '../matrix.dart'; - -class StatusListTile extends StatelessWidget { - final Status status; - - const StatusListTile({Key key, @required this.status}) : super(key: key); - @override - Widget build(BuildContext context) { - final text = status.message; - final isImage = text.startsWith('mxc://') && text.split(' ').length == 1; - return FutureBuilder( - future: Matrix.of(context).client.getProfileFromUserId(status.senderId), - builder: (context, snapshot) { - final displayname = - snapshot.data?.displayname ?? status.senderId.localpart; - final avatarUrl = snapshot.data?.avatarUrl; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Avatar(avatarUrl, displayname), - title: Text( - displayname, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text(status.dateTime.localizedTime(context), - style: TextStyle(fontSize: 14)), - trailing: Matrix.of(context).client.userID == status.senderId - ? null - : PopupMenuButton( - onSelected: (_) => AdaptivePageLayout.of(context) - .pushNamed('/settings/ignore', - arguments: status.senderId), - itemBuilder: (_) => [ - PopupMenuItem( - child: Text(L10n.of(context).ignore), - value: 'ignore', - ), - ], - ), - ), - isImage - ? CachedNetworkImage( - imageUrl: Uri.parse(text).getThumbnail( - Matrix.of(context).client, - width: 360, - height: 360, - method: ThumbnailMethod.scale, - ), - fit: BoxFit.cover, - width: double.infinity, - ) - : Container( - height: 256, - color: text.color, - alignment: Alignment.center, - child: SingleChildScrollView( - padding: EdgeInsets.all(12), - child: Text( - text, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 24, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 12.0, left: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(CupertinoIcons.chat_bubble), - onPressed: - Matrix.of(context).client.userID == status.senderId - ? null - : () async { - final result = await showFutureLoadingDialog( - context: context, - future: () => User( - status.senderId, - room: Room( - id: '', - client: Matrix.of(context).client), - ).startDirectChat(), - ); - if (result.error == null) { - await AdaptivePageLayout.of(context) - .pushNamed('/rooms/${result.result}'); - } - }, - ), - IconButton( - icon: Icon(Icons.ios_share), - onPressed: () => AdaptivePageLayout.of(context) - .pushNamed('/newstatus', arguments: status.message), - ), - IconButton( - icon: Icon(Icons.share_outlined), - onPressed: () => FluffyShare.share( - '$displayname: ${status.message}', - context, - ), - ), - IconButton( - icon: Icon(Icons.delete_outlined), - onPressed: () => showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .removeStatusOfUser(status.senderId), - ), - ), - ], - ), - ), - ], - ); - }); - } -} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 5fcc4f5e..ed0c76f9 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/utils/firebase_controller.dart'; import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/sentry_controller.dart'; -import 'package:fluffychat/utils/status.dart'; import 'package:flushbar/flushbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -127,7 +126,6 @@ class MatrixState extends State { StreamSubscription onKeyVerificationRequestSub; StreamSubscription onJitsiCallSub; StreamSubscription onNotification; - StreamSubscription onPresence; StreamSubscription onLoginStateChanged; StreamSubscription onUiaRequest; StreamSubscription onFocusSub; @@ -290,10 +288,6 @@ class MatrixState extends State { LoadingDialog.defaultBackLabel = L10n.of(context).close; LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context); - onPresence ??= client.onPresence.stream - .where((p) => p.presence?.statusMsg != null) - .listen(_onPresence); - onRoomKeyRequestSub ??= client.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { final room = request.room; @@ -401,45 +395,6 @@ class MatrixState extends State { } } - Map get statuses { - if (client.accountData.containsKey(Status.namespace)) { - try { - return client.accountData[Status.namespace].content - .map((k, v) => MapEntry(k, Status.fromJson(v))); - } catch (e, s) { - Logs() - .e('Unable to parse status account data. Clearing up now...', e, s); - client.setAccountData(client.userID, Status.namespace, {}); - } - } - return {}; - } - - void _onPresence(Presence presence) async { - if (statuses[presence.senderId]?.message != presence.presence.statusMsg) { - Logs().v('Update status from ${presence.senderId}'); - await client.setAccountData( - client.userID, - Status.namespace, - statuses.map((k, v) => MapEntry(k, v.toJson())) - ..[presence.senderId] = Status( - presence.senderId, - presence.presence.statusMsg, - DateTime.now(), - ), - ); - } - } - - Future removeStatusOfUser(String userId) async { - await client.setAccountData( - client.userID, - Status.namespace, - statuses.map((k, v) => MapEntry(k, v.toJson()))..remove(userId), - ); - return; - } - @override void dispose() { onRoomKeyRequestSub?.cancel(); @@ -448,7 +403,6 @@ class MatrixState extends State { onNotification?.cancel(); onFocusSub?.cancel(); onBlurSub?.cancel(); - onPresence?.cancel(); super.dispose(); } diff --git a/lib/utils/client_presence_extension.dart b/lib/utils/client_presence_extension.dart index 39e75190..5e0bf322 100644 --- a/lib/utils/client_presence_extension.dart +++ b/lib/utils/client_presence_extension.dart @@ -1,7 +1,34 @@ import 'package:famedlysdk/famedlysdk.dart'; extension ClientPresenceExtension on Client { - List get statuses => presences.values - .where((p) => p.presence.statusMsg?.isNotEmpty ?? false) - .toList(); + List get contactList { + final directChatsMxid = rooms + .where((r) => r.isDirectChat) + .map((r) => r.directChatMatrixID) + .toSet(); + final contactList = directChatsMxid + .map( + (mxid) => + presences[mxid] ?? + Presence.fromJson( + { + 'sender': mxid, + 'type': 'm.presence', + 'content': {'presence': 'online'}, + }, + ), + ) + .toList(); + contactList.addAll( + presences.values + .where((p) => + !directChatsMxid.contains(p.senderId) && + (p.presence?.statusMsg?.isNotEmpty ?? false)) + .toList(), + ); + contactList.sort((a, b) => (a.presence.lastActiveAgo?.toDouble() ?? + double.infinity) + .compareTo((b.presence.lastActiveAgo?.toDouble() ?? double.infinity))); + return contactList; + } } diff --git a/lib/utils/presence_extension.dart b/lib/utils/presence_extension.dart index 50203f87..4da61f3c 100644 --- a/lib/utils/presence_extension.dart +++ b/lib/utils/presence_extension.dart @@ -37,4 +37,16 @@ extension PresenceExtension on Presence { } return presence.presence.getLocalized(context); } + + Color get color { + switch (presence?.presence ?? PresenceType.offline) { + case PresenceType.online: + return Colors.green; + case PresenceType.offline: + return Colors.red; + case PresenceType.unavailable: + default: + return Colors.grey; + } + } } diff --git a/lib/utils/status.dart b/lib/utils/status.dart deleted file mode 100644 index 3c06ffb3..00000000 --- a/lib/utils/status.dart +++ /dev/null @@ -1,19 +0,0 @@ -class Status { - static const String namespace = 'im.fluffychat.statuses'; - final String senderId; - final String message; - final DateTime dateTime; - - Status(this.senderId, this.message, this.dateTime); - - Status.fromJson(Map json) - : senderId = json['sender_id'], - message = json['message'], - dateTime = DateTime.fromMillisecondsSinceEpoch(json['date_time']); - - Map toJson() => { - 'sender_id': senderId, - 'message': message, - 'date_time': dateTime.millisecondsSinceEpoch, - }; -} diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index 86dcf616..29ae3506 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -1,6 +1,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:fluffychat/app_config.dart'; +import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:flushbar/flushbar_helper.dart'; @@ -238,6 +239,7 @@ class _ChatDetailsState extends State { .scaffoldBackgroundColor, foregroundColor: Colors.grey, child: Icon(Icons.edit_outlined), + radius: Avatar.defaultSize / 2, ) : null, title: Text('${L10n.of(context).groupDescription}:', diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart index 2de51e8b..aa308c43 100644 --- a/lib/views/home_view.dart +++ b/lib/views/home_view.dart @@ -12,13 +12,14 @@ import 'package:fluffychat/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import '../components/matrix.dart'; import '../utils/matrix_file_extension.dart'; import '../utils/url_launcher.dart'; import 'home_view_parts/chat_list.dart'; import 'home_view_parts/settings.dart'; -import 'home_view_parts/status_list.dart'; +import 'home_view_parts/contact_list.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; enum SelectMode { normal, share, select } @@ -143,11 +144,30 @@ class _HomeViewState extends State with TickerProviderStateMixin { }); } + void _setStatus() async { + final input = await showTextInputDialog( + context: context, + title: L10n.of(context).setStatus, + textFields: [ + DialogTextField( + hintText: L10n.of(context).statusExampleMessage, + ), + ]); + if (input == null) return; + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.sendPresence( + Matrix.of(context).client.userID, + PresenceType.online, + statusMsg: input.single, + ), + ); + } + void _onFabTab() { switch (currentIndex) { case 0: - AdaptivePageLayout.of(context) - .pushNamedAndRemoveUntilIsFirst('/newstatus'); + _setStatus(); break; case 1: AdaptivePageLayout.of(context) @@ -212,7 +232,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { body: TabBarView( controller: _pageController, children: [ - StatusList(key: Key('StatusList')), + ContactList(), ChatList( onCustomAppBar: (appBar) => setState(() => this.appBar = appBar), ), @@ -246,8 +266,8 @@ class _HomeViewState extends State with TickerProviderStateMixin { }, items: [ BottomNavigationBarItem( - label: L10n.of(context).status, - icon: Icon(Icons.home_outlined), + label: L10n.of(context).contacts, + icon: Icon(Icons.people_outlined), ), BottomNavigationBarItem( label: L10n.of(context).messages, diff --git a/lib/views/home_view_parts/contact_list.dart b/lib/views/home_view_parts/contact_list.dart new file mode 100644 index 00000000..acba5378 --- /dev/null +++ b/lib/views/home_view_parts/contact_list.dart @@ -0,0 +1,72 @@ +import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/components/default_app_bar_search_field.dart'; +import 'package:fluffychat/components/list_items/contact_list_tile.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import '../../utils/client_presence_extension.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class ContactList extends StatefulWidget { + @override + _ContactListState createState() => _ContactListState(); +} + +class _ContactListState extends State { + String _searchQuery = ''; + @override + Widget build(BuildContext context) { + return ListView(children: [ + Padding( + padding: EdgeInsets.all(12), + child: DefaultAppBarSearchField( + hintText: L10n.of(context).search, + prefixIcon: Icon(Icons.search_outlined), + onChanged: (t) => setState(() => _searchQuery = t), + padding: EdgeInsets.zero, + ), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + child: Icon(Icons.add_outlined), + radius: Avatar.defaultSize / 2, + ), + title: Text('Add new contact'), + onTap: () => + AdaptivePageLayout.of(context).pushNamed('/newprivatechat'), + ), + Divider(height: 1), + StreamBuilder( + stream: Matrix.of(context).client.onSync.stream, + builder: (context, snapshot) { + final contactList = Matrix.of(context) + .client + .contactList + .where((p) => p.senderId + .toLowerCase() + .contains(_searchQuery.toLowerCase())) + .toList(); + if (contactList.isEmpty) { + return Container( + padding: EdgeInsets.all(16), + alignment: Alignment.center, + child: Text( + 'No contacts found...', + textAlign: TextAlign.center, + ), + ); + } + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.only(bottom: 24), + itemCount: contactList.length, + itemBuilder: (context, i) => + ContactListTile(contact: contactList[i]), + ); + }), + ]); + } +} diff --git a/lib/views/home_view_parts/status_list.dart b/lib/views/home_view_parts/status_list.dart deleted file mode 100644 index 17c725ab..00000000 --- a/lib/views/home_view_parts/status_list.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:fluffychat/components/list_items/status_list_tile.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:fluffychat/utils/status.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -class StatusList extends StatefulWidget { - const StatusList({Key key}) : super(key: key); - @override - _StatusListState createState() => _StatusListState(); -} - -class _StatusListState extends State { - bool _onlyContacts = false; - - @override - Widget build(BuildContext context) { - return ListView(children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RaisedButton( - elevation: _onlyContacts ? 7 : null, - color: !_onlyContacts ? null : Theme.of(context).primaryColor, - child: Text( - L10n.of(context).contacts, - style: TextStyle(color: _onlyContacts ? Colors.white : null), - ), - onPressed: () => setState(() => _onlyContacts = true), - ), - RaisedButton( - elevation: !_onlyContacts ? 7 : null, - color: _onlyContacts ? null : Theme.of(context).primaryColor, - child: Text( - L10n.of(context).all, - style: TextStyle(color: !_onlyContacts ? Colors.white : null), - ), - onPressed: () => setState(() => _onlyContacts = false), - ), - ], - ), - Divider(height: 1), - StreamBuilder( - stream: Matrix.of(context) - .client - .onAccountData - .stream - .where((a) => a.type == Status.namespace), - builder: (context, snapshot) { - final statuses = Matrix.of(context).statuses.values.toList() - ..sort((a, b) => b.dateTime.compareTo(a.dateTime)); - if (_onlyContacts) { - final client = Matrix.of(context).client; - statuses.removeWhere( - (p) => client.getDirectChatFromUserId(p.senderId) == null); - } - if (statuses.isEmpty) { - return Container( - padding: EdgeInsets.all(16), - alignment: Alignment.center, - child: Text( - L10n.of(context).noStatusesFound, - textAlign: TextAlign.center, - ), - ); - } - return ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsets.only(bottom: 24), - separatorBuilder: (_, __) => Divider(height: 1), - itemCount: statuses.length, - itemBuilder: (context, i) => StatusListTile(status: statuses[i]), - ); - }), - ]); - } -}