kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add notification click through flow to posts/profiles
rodzic
6f6fe79ac0
commit
52bc8a20e1
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'],
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
extension StringUtils on String {
|
||||
String truncate({int length = 32}) {
|
||||
if (length <= length) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return '${substring(0, length)}...';
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue