change: Implement contact list instead of status

This commit is contained in:
Christian Pauly 2021-02-05 08:43:44 +01:00
parent 6960618f55
commit b9be33c16b
10 changed files with 216 additions and 288 deletions

View File

@ -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<Profile>(
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}');
});
});
}
}

View File

@ -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<Profile>(
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),
),
),
],
),
),
],
);
});
}
}

View File

@ -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<Matrix> {
StreamSubscription onKeyVerificationRequestSub;
StreamSubscription onJitsiCallSub;
StreamSubscription onNotification;
StreamSubscription<Presence> onPresence;
StreamSubscription<LoginState> onLoginStateChanged;
StreamSubscription<UiaRequest> onUiaRequest;
StreamSubscription<html.Event> onFocusSub;
@ -290,10 +288,6 @@ class MatrixState extends State<Matrix> {
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<Matrix> {
}
}
Map<String, Status> 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<void> 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<Matrix> {
onNotification?.cancel();
onFocusSub?.cancel();
onBlurSub?.cancel();
onPresence?.cancel();
super.dispose();
}

View File

@ -1,7 +1,34 @@
import 'package:famedlysdk/famedlysdk.dart';
extension ClientPresenceExtension on Client {
List<Presence> get statuses => presences.values
.where((p) => p.presence.statusMsg?.isNotEmpty ?? false)
.toList();
List<Presence> 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<String, dynamic> json)
: senderId = json['sender_id'],
message = json['message'],
dateTime = DateTime.fromMillisecondsSinceEpoch(json['date_time']);
Map<String, dynamic> toJson() => <String, dynamic>{
'sender_id': senderId,
'message': message,
'date_time': dateTime.millisecondsSinceEpoch,
};
}

View File

@ -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<ChatDetails> {
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.edit_outlined),
radius: Avatar.defaultSize / 2,
)
: null,
title: Text('${L10n.of(context).groupDescription}:',

View File

@ -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<HomeView> 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<HomeView> 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<HomeView> 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,

View File

@ -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<ContactList> {
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<Object>(
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]),
);
}),
]);
}
}

View File

@ -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<StatusList> {
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<Object>(
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]),
);
}),
]);
}
}