Add unread indicator functionality

This commit is contained in:
Nutcake 2023-05-03 20:03:46 +02:00
parent 3a4c7be758
commit bc57b20219
5 changed files with 107 additions and 30 deletions

View file

@ -25,16 +25,8 @@ class Friend extends Comparable {
} }
@override @override
int compareTo(other) { int compareTo(covariant Friend other) {
if (userStatus.onlineStatus == other.userStatus.onlineStatus) { return username.compareTo(other.username);
return userStatus.lastStatusChange.compareTo(other.userStatus.lastStatusChange);
} else {
if (userStatus.onlineStatus == OnlineStatus.online) {
return -1;
} else {
return 1;
}
}
} }
} }

View file

@ -165,3 +165,19 @@ class AudioClipContent {
); );
} }
} }
class MarkReadBatch {
final String senderId;
final List<String> 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(),
};
}
}

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/api_client.dart'; import 'package:contacts_plus_plus/api_client.dart';
@ -42,6 +43,11 @@ class NeosHub {
start(); start();
} }
void _sendData(data) {
if (_wsChannel == null) throw "Neos Hub is not connected";
_wsChannel!.add(jsonEncode(data)+eofChar);
}
Future<MessageCache> getCache(String userId) async { Future<MessageCache> getCache(String userId) async {
var cache = _messageCache[userId]; var cache = _messageCache[userId];
if (cache == null){ if (cache == null){
@ -52,6 +58,13 @@ class NeosHub {
return cache; return cache;
} }
Future<void> checkUnreads() async {
final unreads = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
for (var message in unreads) {
throw UnimplementedError();
}
}
void _onDisconnected(error) { void _onDisconnected(error) {
_logger.warning("Neos Hub connection died with error '$error', reconnecting..."); _logger.warning("Neos Hub connection died with error '$error', reconnecting...");
start(); start();
@ -161,7 +174,6 @@ class NeosHub {
} }
void sendMessage(Message message) async { void sendMessage(Message message) async {
if (_wsChannel == null) throw "Neos Hub is not connected";
final msgBody = message.toMap(); final msgBody = message.toMap();
final data = { final data = {
"type": EventType.message.index, "type": EventType.message.index,
@ -170,9 +182,21 @@ class NeosHub {
msgBody msgBody
], ],
}; };
_sendData(data);
final cache = await getCache(message.recipientId); final cache = await getCache(message.recipientId);
cache.messages.add(message); cache.messages.add(message);
_wsChannel!.add(jsonEncode(data)+eofChar);
notifyListener(message.recipientId); notifyListener(message.recipientId);
} }
void markMessagesRead(MarkReadBatch batch) {
final msgBody = batch.toMap();
final data = {
"type": EventType.message.index,
"target": "MarkMessagesRead",
"arguments": [
msgBody
],
};
_sendData(data);
}
} }

View file

@ -5,19 +5,26 @@ import 'package:contacts_plus_plus/widgets/messages.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FriendListTile extends StatelessWidget { 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 Friend friend;
final int? unreads;
final Function? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final imageUri = Aux.neosDbToHttp(friend.userProfile.iconUrl); final imageUri = Aux.neosDbToHttp(friend.userProfile.iconUrl);
final theme = Theme.of(context);
return ListTile( return ListTile(
leading: GenericAvatar(imageUri: imageUri,), 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), title: Text(friend.username),
subtitle: Text(friend.userStatus.onlineStatus.name), subtitle: Text(friend.userStatus.onlineStatus.name),
onTap: () { onTap: () async {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: friend))); Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: friend)));
await onTap?.call();
}, },
); );
} }

View file

@ -2,7 +2,9 @@ import 'dart:async';
import 'package:contacts_plus_plus/api_client.dart'; import 'package:contacts_plus_plus/api_client.dart';
import 'package:contacts_plus_plus/apis/friend_api.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/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/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/friend_list_tile.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart';
@ -20,6 +22,7 @@ class _FriendsListState extends State<FriendsList> {
ClientHolder? _clientHolder; ClientHolder? _clientHolder;
Timer? _debouncer; Timer? _debouncer;
String _searchFilter = ""; String _searchFilter = "";
final _unreads = <String, List<Message>>{};
@override @override
void dispose() { void dispose() {
@ -38,17 +41,32 @@ class _FriendsListState extends State<FriendsList> {
} }
void _refreshFriendsList() { void _refreshFriendsList() {
_friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable<Friend> value) => _friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable<Friend> value) async {
value.toList() final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.client, unreadOnly: true);
..sort((a, b) { _unreads.clear();
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {
return a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange); for (final msg in unreadMessages) {
if (msg.senderId != _clientHolder!.client.userId) {
final value = _unreads[msg.senderId];
if (value == null) {
_unreads[msg.senderId] = [msg];
} else { } else {
return a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus); 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 @override
@ -76,14 +94,34 @@ class _FriendsListState extends State<FriendsList> {
future: _friendsFuture, future: _friendsFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var data = (snapshot.data as List<Friend>); var friends = (snapshot.data as List<Friend>);
if (_searchFilter.isNotEmpty) { if (_searchFilter.isNotEmpty) {
data = data.where((element) => element.username.contains(_searchFilter)).toList(); friends = friends.where((element) => element.username.contains(_searchFilter)).toList();
data.sort((a, b) => a.username.length.compareTo(b.username.length)); friends.sort((a, b) => a.username.length.compareTo(b.username.length));
} }
return ListView.builder( return ListView.builder(
itemCount: data.length, itemCount: friends.length,
itemBuilder: (context, index) => FriendListTile(friend: data[index]), 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) { } else if (snapshot.hasError) {
FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace));