From bc57b2021909eded935c647fcac067d1997c68ad Mon Sep 17 00:00:00 2001 From: Nutcake Date: Wed, 3 May 2023 20:03:46 +0200 Subject: [PATCH] Add unread indicator functionality --- lib/models/friend.dart | 12 +----- lib/models/message.dart | 16 ++++++++ lib/neos_hub.dart | 28 ++++++++++++- lib/widgets/friend_list_tile.dart | 13 ++++-- lib/widgets/friends_list.dart | 68 ++++++++++++++++++++++++------- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/lib/models/friend.dart b/lib/models/friend.dart index 2ce7c2a..a8c7861 100644 --- a/lib/models/friend.dart +++ b/lib/models/friend.dart @@ -25,16 +25,8 @@ class Friend extends Comparable { } @override - int compareTo(other) { - if (userStatus.onlineStatus == other.userStatus.onlineStatus) { - return userStatus.lastStatusChange.compareTo(other.userStatus.lastStatusChange); - } else { - if (userStatus.onlineStatus == OnlineStatus.online) { - return -1; - } else { - return 1; - } - } + int compareTo(covariant Friend other) { + return username.compareTo(other.username); } } diff --git a/lib/models/message.dart b/lib/models/message.dart index dc306ed..a74adde 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -165,3 +165,19 @@ class AudioClipContent { ); } } + +class MarkReadBatch { + final String senderId; + final List ids; + final DateTime readTime; + + MarkReadBatch({required this.senderId, required this.ids, required this.readTime}); + + Map toMap() { + return { + "senderId": senderId, + "ids": ids, + "readTime": readTime.toUtc().toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/lib/neos_hub.dart b/lib/neos_hub.dart index 720251a..d65033c 100644 --- a/lib/neos_hub.dart +++ b/lib/neos_hub.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:http/http.dart' as http; import 'package:contacts_plus_plus/api_client.dart'; @@ -42,6 +43,11 @@ class NeosHub { start(); } + void _sendData(data) { + if (_wsChannel == null) throw "Neos Hub is not connected"; + _wsChannel!.add(jsonEncode(data)+eofChar); + } + Future getCache(String userId) async { var cache = _messageCache[userId]; if (cache == null){ @@ -52,6 +58,13 @@ class NeosHub { return cache; } + Future checkUnreads() async { + final unreads = await MessageApi.getUserMessages(_apiClient, unreadOnly: true); + for (var message in unreads) { + throw UnimplementedError(); + } + } + void _onDisconnected(error) { _logger.warning("Neos Hub connection died with error '$error', reconnecting..."); start(); @@ -161,7 +174,6 @@ class NeosHub { } void sendMessage(Message message) async { - if (_wsChannel == null) throw "Neos Hub is not connected"; final msgBody = message.toMap(); final data = { "type": EventType.message.index, @@ -170,9 +182,21 @@ class NeosHub { msgBody ], }; + _sendData(data); final cache = await getCache(message.recipientId); cache.messages.add(message); - _wsChannel!.add(jsonEncode(data)+eofChar); notifyListener(message.recipientId); } + + void markMessagesRead(MarkReadBatch batch) { + final msgBody = batch.toMap(); + final data = { + "type": EventType.message.index, + "target": "MarkMessagesRead", + "arguments": [ + msgBody + ], + }; + _sendData(data); + } } diff --git a/lib/widgets/friend_list_tile.dart b/lib/widgets/friend_list_tile.dart index d5f1b1f..7233b36 100644 --- a/lib/widgets/friend_list_tile.dart +++ b/lib/widgets/friend_list_tile.dart @@ -5,19 +5,26 @@ import 'package:contacts_plus_plus/widgets/messages.dart'; import 'package:flutter/material.dart'; class FriendListTile extends StatelessWidget { - const FriendListTile({required this.friend, super.key}); + const FriendListTile({required this.friend, this.unreads, this.onTap, super.key}); final Friend friend; + final int? unreads; + final Function? onTap; @override Widget build(BuildContext context) { final imageUri = Aux.neosDbToHttp(friend.userProfile.iconUrl); + final theme = Theme.of(context); return ListTile( leading: GenericAvatar(imageUri: imageUri,), + trailing: unreads != null && unreads != 0 + ? Text("+$unreads", style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.primary),) + : null, title: Text(friend.username), subtitle: Text(friend.userStatus.onlineStatus.name), - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: friend))); + onTap: () async { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: friend))); + await onTap?.call(); }, ); } diff --git a/lib/widgets/friends_list.dart b/lib/widgets/friends_list.dart index a4347d7..b387cdb 100644 --- a/lib/widgets/friends_list.dart +++ b/lib/widgets/friends_list.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:contacts_plus_plus/api_client.dart'; import 'package:contacts_plus_plus/apis/friend_api.dart'; +import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart'; @@ -20,6 +22,7 @@ class _FriendsListState extends State { ClientHolder? _clientHolder; Timer? _debouncer; String _searchFilter = ""; + final _unreads = >{}; @override void dispose() { @@ -38,17 +41,32 @@ class _FriendsListState extends State { } void _refreshFriendsList() { - _friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable value) => - value.toList() - ..sort((a, b) { - if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) { - return a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange); - } else { - return a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus); + _friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable value) async { + final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.client, unreadOnly: true); + _unreads.clear(); + + for (final msg in unreadMessages) { + if (msg.senderId != _clientHolder!.client.userId) { + final value = _unreads[msg.senderId]; + if (value == null) { + _unreads[msg.senderId] = [msg]; + } else { + value.add(msg); + } } - }, - ), - ); + } + + final friends = value.toList() + ..sort((a, b) { + var aVal = _unreads.containsKey(a.id) ? -3 : 0; + var bVal = _unreads.containsKey(b.id) ? -3 : 0; + + aVal -= a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange); + aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2; + return aVal.compareTo(bVal); + }); + return friends; + }); } @override @@ -76,14 +94,34 @@ class _FriendsListState extends State { future: _friendsFuture, builder: (context, snapshot) { if (snapshot.hasData) { - var data = (snapshot.data as List); + var friends = (snapshot.data as List); if (_searchFilter.isNotEmpty) { - data = data.where((element) => element.username.contains(_searchFilter)).toList(); - data.sort((a, b) => a.username.length.compareTo(b.username.length)); + friends = friends.where((element) => element.username.contains(_searchFilter)).toList(); + friends.sort((a, b) => a.username.length.compareTo(b.username.length)); } return ListView.builder( - itemCount: data.length, - itemBuilder: (context, index) => FriendListTile(friend: data[index]), + itemCount: friends.length, + itemBuilder: (context, index) { + final friend = friends[index]; + final unread = _unreads[friend.id] ?? []; + return FriendListTile( + friend: friend, + unreads: unread.length, + onTap: () async { + if (unread.isNotEmpty) { + final readBatch = MarkReadBatch( + senderId: _clientHolder!.client.userId, + ids: unread.map((e) => e.id).toList(), + readTime: DateTime.now(), + ); + _clientHolder!.hub.markMessagesRead(readBatch); + } + setState(() { + unread.clear(); + }); + }, + ); + }, ); } else if (snapshot.hasError) { FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));