Add external links, likes, dislikes, and tracking known other users.

This commit is contained in:
Hank Grabowski 2022-01-19 21:35:24 -05:00
parent 33dfb509f0
commit 8a5636ab54
8 changed files with 179 additions and 133 deletions

View file

@ -79,11 +79,22 @@ class TreeEntryCard extends StatelessWidget {
snackbarMessage: 'Copied Post to clipboard'),
icon: const Icon(Icons.copy)),
),
Tooltip(
message: 'Open link to original item',
child: IconButton(
onPressed: () async {
await canLaunch(entry.externalLink)
? await launch(entry.externalLink)
: _logger.info(
'Failed to launch ${entry.externalLink}');
},
icon: const Icon(Icons.link)),
),
]),
if (entry.post.isNotEmpty) ...[
if (entry.body.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
HtmlWidget(
entry.post,
entry.body,
onTapUrl: (url) async {
bool canLaunchResult = await canLaunch(url);
if (!canLaunchResult) {
@ -100,6 +111,22 @@ class TreeEntryCard extends StatelessWidget {
},
)
],
const SizedBox(height: spacingHeight * 2),
Row(
children: [
Tooltip(
message: entry.likes.map((e) => e.name).join(', '),
child: const Icon(Icons.thumb_up_alt_outlined)),
Text('${entry.likes.length}'),
SizedBox(
width: 3,
),
Tooltip(
message: entry.dislikes.map((e) => e.name).join(', '),
child: const Icon(Icons.thumb_down_alt_outlined)),
Text('${entry.dislikes.length}'),
],
),
if (entry.locationData.hasData())
entry.locationData.toWidget(spacingHeight),
if (entry.links.isNotEmpty) ...[

View file

@ -1,123 +1,57 @@
import 'package:logging/logging.dart';
import 'model_utils.dart';
class FriendicaContact {
static final _logger = Logger('$FriendicaContact');
final ConnectionStatus status;
final FriendStatus status;
final String name;
final String contactInfo;
final int friendSinceTimestamp;
final int receivedTimestamp;
final int rejectedTimestamp;
final int removeTimestamp;
final int sentTimestamp;
final bool markedAsSpam;
final String id;
final Uri profileUrl;
final String network;
FriendicaContact(
{this.status = FriendStatus.unknown,
{required this.status,
required this.name,
this.contactInfo = '',
this.friendSinceTimestamp = 0,
this.receivedTimestamp = 0,
this.rejectedTimestamp = 0,
this.removeTimestamp = 0,
this.sentTimestamp = 0,
this.markedAsSpam = false});
required this.id,
required this.profileUrl,
required this.network});
@override
String toString() {
return 'FriendicaFriend{status: $status, name: $name, contactInfo: $contactInfo, friendSinceTimestamp: $friendSinceTimestamp, receivedTimestamp: $receivedTimestamp, rejectedTimestamp: $rejectedTimestamp, removeTimestamp: $removeTimestamp, sentTimestamp: $sentTimestamp, markedAsSpam: $markedAsSpam}';
}
static FriendicaContact fromJson(
Map<String, dynamic> json, FriendStatus status) {
final knownTopLevelKeys = [
'timestamp',
'name',
'contact_info',
'marked_as_spam'
];
int timestamp = json['timestamp'] ?? 0;
static FriendicaContact fromJson(Map<String, dynamic> json) {
final status = (json['following'] ?? '') == 'true'
? ConnectionStatus.youFollowThem
: ConnectionStatus.none;
final name = json['name'] ?? '';
final contactInfo = json['contact_info'] ?? '';
final markedAsSpam = json['marked_as_spam'] ?? false;
final id = json['id_str'] ?? '';
final profileUrl = Uri.parse(json['url'] ?? '');
final network = json['network'] ?? 'unkn';
logAdditionalKeys(knownTopLevelKeys, json.keys, _logger, Level.WARNING,
'Unknown top level friend keys');
switch (status) {
case FriendStatus.friends:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
friendSinceTimestamp: timestamp);
case FriendStatus.requestReceived:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
receivedTimestamp: timestamp);
case FriendStatus.rejectedRequest:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
rejectedTimestamp: timestamp);
case FriendStatus.removed:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
removeTimestamp: timestamp);
case FriendStatus.sentFriendRequest:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
sentTimestamp: timestamp);
case FriendStatus.unknown:
return FriendicaContact(
name: name,
status: status,
contactInfo: contactInfo,
markedAsSpam: markedAsSpam,
);
}
id: id,
profileUrl: profileUrl,
network: network);
}
}
enum FriendStatus {
friends,
requestReceived,
rejectedRequest,
removed,
sentFriendRequest,
unknown,
enum ConnectionStatus {
youFollowThem,
theyFollowYou,
mutual,
none,
}
extension FriendStatusWriter on FriendStatus {
extension FriendStatusWriter on ConnectionStatus {
String name() {
switch (this) {
case FriendStatus.friends:
return "Friends";
case FriendStatus.requestReceived:
return "Requested";
case FriendStatus.rejectedRequest:
return "Rejected";
case FriendStatus.removed:
return "Removed";
case FriendStatus.sentFriendRequest:
return "Sent Request";
case FriendStatus.unknown:
return "Unknown";
case ConnectionStatus.youFollowThem:
return "You Follow Them";
case ConnectionStatus.theyFollowYou:
return "They Follow You";
case ConnectionStatus.mutual:
return "Follow each other";
case ConnectionStatus.none:
return "Not connected";
}
}
}

View file

@ -1,4 +1,6 @@
import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/services/friendica_connections.dart';
import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
@ -6,7 +8,6 @@ import 'package:logging/logging.dart';
import 'friendica_media_attachment.dart';
import 'location_data.dart';
import 'model_utils.dart';
import 'timeline_type.dart';
class FriendicaTimelineEntry {
static final _logger = Logger('$FriendicaTimelineEntry');
@ -23,19 +24,25 @@ class FriendicaTimelineEntry {
final int modificationTimestamp;
final String post;
final String body;
final String title;
final String author;
final String authorId;
final String externalLink;
final List<FriendicaMediaAttachment> mediaAttachments;
final LocationData locationData;
final List<Uri> links;
final TimelineType timelineType;
final List<FriendicaContact> likes;
final List<FriendicaContact> dislikes;
FriendicaTimelineEntry(
{this.id = '',
@ -43,12 +50,15 @@ class FriendicaTimelineEntry {
this.creationTimestamp = 0,
this.backdatedTimestamp = 0,
this.modificationTimestamp = 0,
this.post = '',
this.body = '',
this.title = '',
this.author = '',
this.authorId = '',
this.parentAuthor = '',
this.externalLink = '',
this.locationData = const LocationData(),
required this.timelineType,
this.likes = const <FriendicaContact>[],
this.dislikes = const <FriendicaContact>[],
List<FriendicaMediaAttachment>? mediaAttachments,
List<Uri>? links})
: mediaAttachments = mediaAttachments ?? <FriendicaMediaAttachment>[],
@ -60,12 +70,15 @@ class FriendicaTimelineEntry {
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
id = randomId(),
parentId = randomId(),
post = 'Random post text ${randomId()}',
externalLink = 'Random external link ${randomId()}',
body = 'Random post text ${randomId()}',
title = 'Random title ${randomId()}',
author = 'Random author ${randomId()}',
authorId = 'Random authorId ${randomId()}',
parentAuthor = 'Random parent author ${randomId()}',
locationData = LocationData.randomBuilt(),
timelineType = TimelineType.active,
likes = const <FriendicaContact>[],
dislikes = const <FriendicaContact>[],
links = [
Uri.parse('http://localhost/${randomId()}'),
Uri.parse('http://localhost/${randomId()}')
@ -81,13 +94,16 @@ class FriendicaTimelineEntry {
int? modificationTimestamp,
String? id,
String? parentId,
String? post,
String? externalLink,
String? body,
String? title,
String? author,
String? authorId,
String? parentAuthor,
LocationData? locationData,
List<FriendicaMediaAttachment>? mediaAttachments,
TimelineType? timelineType,
List<FriendicaContact>? likes,
List<FriendicaContact>? dislikes,
List<Uri>? links}) {
return FriendicaTimelineEntry(
creationTimestamp: creationTimestamp ?? this.creationTimestamp,
@ -96,19 +112,22 @@ class FriendicaTimelineEntry {
modificationTimestamp ?? this.modificationTimestamp,
id: id ?? this.id,
parentId: parentId ?? this.parentId,
post: post ?? this.post,
externalLink: externalLink ?? this.externalLink,
body: body ?? this.body,
title: title ?? this.title,
author: author ?? this.author,
authorId: authorId ?? this.authorId,
parentAuthor: parentAuthor ?? this.parentAuthor,
locationData: locationData ?? this.locationData,
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
timelineType: timelineType ?? this.timelineType,
likes: likes ?? this.likes,
dislikes: dislikes ?? this.dislikes,
links: links ?? this.links);
}
@override
String toString() {
return 'FriendicaTimelineEntry{id: $id, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}';
return 'FriendicaTimelineEntry{id: $id, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}';
}
String toHumanString(PathMappingService mapper, DateFormat formatter) {
@ -120,7 +139,8 @@ class FriendicaTimelineEntry {
'Creation At: $creationDateString',
'Text:',
'Author: $author',
post,
if (externalLink.isNotEmpty) 'External Link: $externalLink',
body,
'',
if (parentId.isEmpty)
"Comment on post/comment by ${parentAuthor.isNotEmpty ? parentAuthor : 'unknown author'}",
@ -144,7 +164,7 @@ class FriendicaTimelineEntry {
.isNotEmpty;
static FriendicaTimelineEntry fromJson(
Map<String, dynamic> json, TimelineType timelineType) {
Map<String, dynamic> json, FriendicaConnections connections) {
final int timestamp = json.containsKey('created_at')
? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(
json['created_at'])
@ -160,7 +180,9 @@ class FriendicaTimelineEntry {
final parentAuthor = json['in_reply_to_screen_name'] ?? '';
final post = json['friendica_html'] ?? '';
final author = json['user']['name'];
final authorId = json['user']['id_str'];
final title = json['friendica_title'] ?? '';
final externalLink = json['external_url'] ?? '';
final actualLocationData = LocationData();
final modificationTimestamp = timestamp;
final backdatedTimestamp = timestamp;
@ -168,21 +190,40 @@ class FriendicaTimelineEntry {
final mediaAttachments = (json['attachments'] as List<dynamic>? ?? [])
.map((j) => FriendicaMediaAttachment.fromJson(j))
.toList();
final likes =
(json['friendica_activities']?['like'] as List<dynamic>? ?? [])
.map((json) => FriendicaContact.fromJson(json))
.toList();
final dislikes =
(json['friendica_activities']?['dislike'] as List<dynamic>? ?? [])
.map((json) => FriendicaContact.fromJson(json))
.toList();
final announce =
(json['friendica_activities']?['announce'] as List<dynamic>? ?? [])
.map((json) => FriendicaContact.fromJson(json))
.toList();
for (final contact in [...likes, ...dislikes, ...announce]) {
connections.addConnection(contact);
}
return FriendicaTimelineEntry(
creationTimestamp: timestamp,
modificationTimestamp: modificationTimestamp,
backdatedTimestamp: backdatedTimestamp,
locationData: actualLocationData,
post: post,
externalLink: externalLink,
body: post,
id: id,
parentId: parentId,
author: author,
authorId: authorId,
parentAuthor: parentAuthor,
title: title,
links: links,
likes: likes,
dislikes: dislikes,
mediaAttachments: mediaAttachments,
timelineType: timelineType,
);
}
}

View file

@ -1,5 +0,0 @@
enum TimelineType {
active,
archive,
trash,
}

View file

@ -69,7 +69,7 @@ class _FriendicaEntriesScreenWidget extends StatelessWidget {
imagesOnlyFilterFunction: (post) => post.entry.hasImages(),
videosOnlyFilterFunction: (post) => post.entry.hasVideos(),
textSearchFilterFunction: (post, text) =>
post.entry.title.contains(text) || post.entry.post.contains(text),
post.entry.title.contains(text) || post.entry.body.contains(text),
itemToDateTimeFunction: (post) => DateTime.fromMillisecondsSinceEpoch(
post.entry.creationTimestamp * 1000),
dateRangeFilterFunction: (post, start, stop) =>

View file

@ -51,7 +51,7 @@ class _StatsScreenState extends State<StatsScreen> {
hasImages: e.entry.hasImages(),
hasVideos: e.entry.hasVideos(),
title: e.entry.title,
text: e.entry.post)),
text: e.entry.body)),
onError: (error) {
_logger.severe('Error getting posts: $error');
return [];
@ -64,7 +64,7 @@ class _StatsScreenState extends State<StatsScreen> {
hasImages: e.entry.hasImages(),
hasVideos: e.entry.hasVideos(),
title: e.entry.title,
text: e.entry.post)),
text: e.entry.body)),
onError: (error) {
_logger.severe('Error getting oprhaned comments: $error');
return [];

View file

@ -4,9 +4,9 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
import 'package:friendica_archive_browser/src/friendica/models/timeline_type.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
import 'package:friendica_archive_browser/src/services/friendica_connections.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:path/path.dart' as p;
import 'package:result_monad/result_monad.dart';
@ -16,10 +16,24 @@ class FriendicaArchiveService extends ChangeNotifier {
final Map<String, ImageEntry> _imagesByRequestUrl = {};
final List<FriendicaEntryTreeItem> _postEntries = [];
final List<FriendicaEntryTreeItem> _orphanedCommentEntries = [];
final FriendicaConnections _connections = FriendicaConnections();
String _ownersName = '';
FriendicaArchiveService({required this.pathMappingService});
String get ownersName {
if (_ownersName.isNotEmpty) {
return _ownersName;
}
final uniqueNames = _postEntries.map((e) => e.entry.author).toSet();
_ownersName = uniqueNames.isNotEmpty ? uniqueNames.first : '';
return _ownersName;
}
void clearCaches() {
_connections.clearCaches();
_imagesByRequestUrl.clear();
_orphanedCommentEntries.clear();
_postEntries.clear();
@ -60,8 +74,8 @@ class FriendicaArchiveService extends ChangeNotifier {
final jsonFile = File(entriesJsonPath);
if (jsonFile.existsSync()) {
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
final entries = json
.map((j) => FriendicaTimelineEntry.fromJson(j, TimelineType.active));
final entries =
json.map((j) => FriendicaTimelineEntry.fromJson(j, _connections));
final topLevelEntries =
entries.where((element) => element.parentId.isEmpty);
final ids = entries.map((e) => e.id).toSet();
@ -88,6 +102,7 @@ class FriendicaArchiveService extends ChangeNotifier {
_postEntries.clear();
_postEntries
.addAll(entryTrees.values.where((element) => !element.isOrphaned));
_orphanedCommentEntries.clear();
_orphanedCommentEntries
.addAll(entryTrees.values.where((element) => element.isOrphaned));

View file

@ -0,0 +1,34 @@
import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart';
import 'package:result_monad/result_monad.dart';
class FriendicaConnections {
final _connectionsById = <String, FriendicaContact>{};
final _connectionsByName = <String, FriendicaContact>{};
void clearCaches() {
_connectionsById.clear();
_connectionsByName.clear();
}
bool addConnection(FriendicaContact contact) {
if (_connectionsById.containsKey(contact.id)) {
return false;
}
_connectionsById[contact.id] = contact;
_connectionsByName[contact.name] = contact;
return true;
}
Result<FriendicaContact, String> getById(String id) {
final result = _connectionsById[id];
return result != null ? Result.ok(result) : Result.error('$id not found');
}
Result<FriendicaContact, String> getByName(String name) {
final result = _connectionsByName[name];
return result != null ? Result.ok(result) : Result.error('$name not found');
}
}