Add unread indicator functionality
This commit is contained in:
parent
3a4c7be758
commit
bc57b20219
5 changed files with 107 additions and 30 deletions
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
} else {
|
if (msg.senderId != _clientHolder!.client.userId) {
|
||||||
return a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus);
|
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
|
@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));
|
||||||
|
|
Loading…
Reference in a new issue