Add notification click through flow to posts/profiles

merge-requests/67/merge
Hank Grabowski 2022-11-30 00:56:14 +00:00
rodzic 6f6fe79ac0
commit 52bc8a20e1
14 zmienionych plików z 153 dodań i 47 usunięć

Wyświetl plik

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../globals.dart';
import '../models/user_notification.dart';
import '../routes.dart';
import '../services/notifications_manager.dart';
import '../services/timeline_manager.dart';
import '../utils/dateutils.dart';
import '../utils/snackbar_builder.dart';
@ -19,12 +23,36 @@ class NotificationControl extends StatelessWidget {
Widget build(BuildContext context) {
final manager = context.watch<NotificationsManager>();
return ListTile(
tileColor: notification.seen ? null : Colors.black12,
leading: Text(notification.fromName),
title: HtmlWidget(notification.content),
tileColor: notification.dismissed ? null : Colors.black12,
leading: GestureDetector(
onTap: () async {
context.pushNamed(ScreenPaths.userProfile,
params: {'id': notification.fromId});
},
child: Text(notification.fromName),
),
title: GestureDetector(
onTap: () async {
print('Open reference status: ${notification.iid}');
final manager = getIt<TimelineManager>();
final existingPostData = manager.getPostTreeEntryBy(notification.iid);
if (existingPostData.isSuccess) {
context.push('/post/view/${existingPostData.value.id}');
return;
}
final loadedPost = await manager.refreshStatusChain(notification.iid);
if (loadedPost.isSuccess) {
context.push('/post/view/${loadedPost.value.id}');
return;
}
buildSnackbar(context,
'Error getting data for notification: ${loadedPost.error}');
},
child: HtmlWidget(notification.content),
),
subtitle:
Text(ElapsedDateUtils.epochSecondsToString(notification.timestamp)),
trailing: notification.seen
trailing: notification.dismissed
? null
: IconButton(
onPressed: () async {

Wyświetl plik

@ -85,7 +85,7 @@ class _StatusControlState extends State<StatusControl> {
),
if (isPost && hasComments)
TextButton(
onPressed: () async => await manager.refreshPost(item.id),
onPressed: () async => await manager.refreshStatusChain(item.id),
child: Text('Load Comments'),
),
if (item.totalChildren > 0) buildChildComments(context),

Wyświetl plik

@ -11,7 +11,7 @@ import 'models/exec_error.dart';
import 'models/timeline_entry.dart';
import 'models/user_notification.dart';
import 'serializers/friendica/connection_friendica_extensions.dart';
import 'serializers/friendica/notification_friendica_extension.dart';
import 'serializers/mastodon/notification_mastodon_extension.dart';
import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart';
class FriendicaClient {
@ -32,12 +32,12 @@ class FriendicaClient {
}
FutureResult<List<UserNotification>, ExecError> getNotifications() async {
final url = 'https://$serverName/api/friendica/notifications';
final url = 'https://$serverName/api/v1/notifications?include_all=true';
final request = Uri.parse(url);
_logger.finest(() => 'Getting new notifications');
return (await _getApiListRequest(request).andThenSuccessAsync(
(notificationsJson) async => notificationsJson
.map((json) => NotificationFriendicaExtension.fromJson(json))
.map((json) => NotificationMastodonExtension.fromJson(json))
.toList()))
.mapError((error) {
if (error is ExecError) {
@ -48,20 +48,18 @@ class FriendicaClient {
});
}
FutureResult<bool, ExecError> clearNotifications(
List<UserNotification> notifications) async {
var result = true;
for (final n in notifications) {
result &= (await clearNotification(n)).isSuccess;
}
return Result.ok(result);
FutureResult<bool, ExecError> clearNotifications() async {
final url = 'https://$serverName/api/v1/notifications/clear';
final request = Uri.parse(url);
_logger.finest(() => 'Clearing unread notification');
final response = await _postUrl(request, {});
return response.mapValue((value) => true);
}
FutureResult<bool, ExecError> clearNotification(
UserNotification notification) async {
final url =
'https://$serverName/api/friendica/notification/seen?id=${notification.id}';
'https://$serverName/api/v1/notifications/${notification.id}/dismiss';
final request = Uri.parse(url);
_logger.finest(() => 'Clearing unread notification for $notification');
final response = await _postUrl(request, {});

Wyświetl plik

@ -1,5 +1,9 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
@ -19,6 +23,10 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
Logger.root.level = Level.ALL;
MultipartFile.fromBytes('file', Uint8List.fromList([]),
filename: 'hello.jpg', contentType: MediaType('image', 'jpeg'));
// Logger.root.onRecord.listen((event) {
// final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
// final msg =

Wyświetl plik

@ -10,28 +10,30 @@ enum NotificationType {
class UserNotification {
final String id;
final String type;
final String fromId;
final String fromName;
final String fromUrl;
final int timestamp;
final String iid;
final bool seen;
final bool dismissed;
final String content;
final String link;
UserNotification({
required this.id,
required this.type,
required this.fromId,
required this.fromName,
required this.fromUrl,
required this.timestamp,
required this.iid,
required this.seen,
required this.dismissed,
required this.content,
required this.link,
});
@override
String toString() {
return 'UserNotification{id: $id, seen: $seen, fromName: $fromName, content: $content}';
return 'UserNotification{id: $id, seen: $dismissed, fromName: $fromName, content: $content}';
}
}

Wyświetl plik

@ -24,14 +24,13 @@ class NotificationsScreen extends StatelessWidget {
manager.updateNotifications();
title = 'Notifications';
body = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: ListView(
children: [
Text('No notifications'),
Center(child: Text('No notifications')),
],
));
} else {
final unreadCount = notifications.where((e) => !e.seen).length;
final unreadCount = notifications.where((e) => !e.dismissed).length;
title = 'Notifications ($unreadCount)';
body = ListView.separated(
itemBuilder: (context, index) {

Wyświetl plik

@ -13,9 +13,11 @@ class PostScreen extends StatelessWidget {
Widget build(BuildContext context) {
final manager = context.watch<TimelineManager>();
final body = manager.getPostTreeEntryBy(id).fold(
onSuccess: (post) => StatusControl(
originalItem: post,
showActionBar: false,
onSuccess: (post) => SingleChildScrollView(
child: StatusControl(
originalItem: post,
showActionBar: false,
),
),
onError: (error) => Text('Error getting post: $error'));
return Scaffold(

Wyświetl plik

@ -5,11 +5,12 @@ extension NotificationFriendicaExtension on UserNotification {
UserNotification(
id: json['id'].toString(),
type: json['type'].toString(),
fromId: json['uid'],
fromName: json['name'],
fromUrl: json['url'],
timestamp: int.tryParse(json['timestamp'] ?? '') ?? 0,
iid: json['iid'].toString(),
seen: json['seen'] ?? false,
dismissed: json['seen'] ?? false,
content: json['msg_html'],
link: json['link'],
);

Wyświetl plik

@ -5,7 +5,7 @@ extension ConnectionMastodonExtensions on Connection {
final name = json['display_name'] ?? '';
final id = json['id'] ?? '';
final profileUrl = Uri.parse(json['url'] ?? '');
const network = 'Mastodon';
const network = 'Unknown';
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
return Connection(

Wyświetl plik

@ -0,0 +1,45 @@
import 'package:logging/logging.dart';
import '../../globals.dart';
import '../../models/user_notification.dart';
import '../../services/connections_manager.dart';
import '../../utils/dateutils.dart';
import '../../utils/string_utils.dart';
import 'connection_mastodon_extensions.dart';
import 'timeline_entry_mastodon_extensions.dart';
final _logger = Logger('NotificationMastodonExtension');
extension NotificationMastodonExtension on UserNotification {
static UserNotification fromJson(Map<String, dynamic> json) {
final int timestamp = json.containsKey('created_at')
? OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(json['created_at'])
.fold(
onSuccess: (value) => value,
onError: (error) {
_logger.severe("Couldn't read date time string: $error");
return 0;
})
: 0;
final type = json['type'];
final from = ConnectionMastodonExtensions.fromJson(json['account']);
getIt<ConnectionsManager>().addConnection(from);
final status = TimelineEntryMastodonExtensions.fromJson(json['status']);
final referenceType = status.parentId == null ? 'post' : 'comment';
final content =
"${from.name} $type on ${status.author}'s $referenceType: ${status.body.truncate()}";
return UserNotification(
id: json['id'].toString(),
type: type,
fromId: from.id,
fromName: from.name,
fromUrl: from.profileUrl.toString(),
timestamp: timestamp,
iid: status.id,
dismissed: json['dismissed'] ?? false,
content: content,
link: status.externalLink,
);
}
}

Wyświetl plik

@ -21,10 +21,24 @@ class EntryManagerService extends ChangeNotifier {
_parentPostIds.clear();
}
_Node? _getPostRootNode(String id) {
final fromPosts = _postNodes[id];
if (fromPosts != null) {
return fromPosts;
}
final parentId = _parentPostIds[id];
if (parentId == null) {
return null;
}
return _postNodes[parentId];
}
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
_logger.finest('Getting post: $id');
final auth = getIt<AuthService>();
final postNode = _postNodes[id];
final postNode = _getPostRootNode(id);
if (postNode == null) {
return Result.error(ExecError(
type: ErrorType.notFound,
@ -74,7 +88,7 @@ class EntryManagerService extends ChangeNotifier {
} else {
rootPostId = _parentPostIds[inReplyToId];
}
await refreshPost(rootPostId);
await refreshStatusChain(rootPostId);
}
return item;
});
@ -131,6 +145,7 @@ class EntryManagerService extends ChangeNotifier {
String currentId,
FriendicaClient? client,
) async {
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
final allSeenItems = [...items];
for (final item in items) {
_entries[item.id] = item;
@ -198,7 +213,7 @@ class EntryManagerService extends ChangeNotifier {
return updatedPosts;
}
FutureResult<EntryTreeItem, ExecError> refreshPost(String id) async {
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
_logger.finest('Refreshing post: $id');
final auth = getIt<AuthService>();
final clientResult = auth.currentClient;
@ -210,18 +225,17 @@ class EntryManagerService extends ChangeNotifier {
final client = clientResult.value;
final result = await client
.getPostOrComment(id, fullContext: false)
.andThenAsync((rootItems) async => await client
.getPostOrComment(id, fullContext: true)
.andThenSuccessAsync(
(contextItems) async => [...rootItems, ...contextItems]))
.andThenSuccessAsync((items) async {
await processNewItems(items, client.credentials.username, null);
})
.andThenAsync(
(_) async => await client.getPostOrComment(id, fullContext: true))
.andThenSuccessAsync((items) async {
await processNewItems(items, client.credentials.username, null);
});
await processNewItems(items, client.credentials.username, null);
});
return result.mapValue((_) {
_logger.finest('$id post updated');
return _nodeToTreeItem(_postNodes[id]!, auth.currentId);
return _nodeToTreeItem(_getPostRootNode(id)!, auth.currentId);
}).mapError(
(error) {
_logger.finest('$id error updating: $error');

Wyświetl plik

@ -14,11 +14,11 @@ class NotificationsManager extends ChangeNotifier {
List<UserNotification> get notifications {
final result = List<UserNotification>.from(_notifications.values);
result.sort((n1, n2) {
if (n1.seen == n2.seen) {
if (n1.dismissed == n2.dismissed) {
return n2.timestamp.compareTo(n1.timestamp);
}
if (n1.seen && !n2.seen) {
if (n1.dismissed && !n2.dismissed) {
return 1;
}
@ -77,9 +77,7 @@ class NotificationsManager extends ChangeNotifier {
}
final client = clientResult.value;
final unreadNotifications =
_notifications.values.where((n) => !n.seen).toList();
final result = await client.clearNotifications(unreadNotifications);
final result = await client.clearNotifications();
if (result.isFailure) {
return result.errorCast();
}

Wyświetl plik

@ -64,9 +64,11 @@ class TimelineManager extends ChangeNotifier {
return Result.ok([]);
}
FutureResult<EntryTreeItem, ExecError> refreshPost(String id) async {
///
/// id is the id of a post or comment in the chain, including the original post.
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
_logger.finest('Refreshing post $id');
final result = await getIt<EntryManagerService>().refreshPost(id);
final result = await getIt<EntryManagerService>().refreshStatusChain(id);
if (result.isSuccess) {
for (final t in cachedTimelines.values) {
t.addOrUpdate([result.value]);

Wyświetl plik

@ -0,0 +1,9 @@
extension StringUtils on String {
String truncate({int length = 32}) {
if (length <= length) {
return this;
}
return '${substring(0, length)}...';
}
}