Remove unneeded screens and types

This commit is contained in:
Hank Grabowski 2022-01-17 11:30:33 -05:00
parent 9bf45e42ba
commit 3547537619
13 changed files with 3 additions and 1262 deletions

View file

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_message.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
import 'package:provider/provider.dart';
import 'facebook_link_elements_component.dart';
import 'facebook_media_timeline_component.dart';
import 'facebook_media_wrapper_component.dart';
class ConversationMessageCard extends StatelessWidget {
final FacebookMessengerMessage message;
const ConversationMessageCard({Key? key, required this.message})
: super(key: key);
@override
Widget build(BuildContext context) {
if (Scrollable.recommendDeferredLoadingForContext(context)) {
return const SizedBox();
}
const double spacingHeight = 5.0;
const double stickerSize = 64.0;
final settings = Provider.of<SettingsController>(context);
final formatter = settings.dateTimeFormatter;
final mapper = Provider.of<PathMappingService>(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Tooltip(
message: formatter
.format(DateTime.fromMillisecondsSinceEpoch(message.timestampMS)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tooltip(
message: 'Copy text version of line to clipboard',
child: IconButton(
onPressed: () async => await copyToClipboard(
context: context,
text: message.toHumanString(mapper, formatter),
snackbarMessage:
'Copied Messenger line to clipboard'),
icon: const Icon(Icons.copy)),
),
Text('${message.from}: ',
style: const TextStyle(fontWeight: FontWeight.bold)),
Expanded(
child: Text(
message.message,
)),
]),
if (message.media.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
FacebookMediaTimelineComponent(mediaAttachments: message.media)
],
if (message.stickers.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: message.stickers
.map((s) => FacebookMediaWrapperComponent(
mediaAttachment: s,
preferredWidth: stickerSize,
preferredHeight: stickerSize,
))
.toList(),
)
],
if (message.links.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
FacebookLinkElementsComponent(links: message.links)
],
],
),
),
);
}
}

View file

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_event.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_location_data.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
import 'package:provider/provider.dart';
class EventCard extends StatelessWidget {
final FacebookEvent event;
const EventCard({Key? key, required this.event}) : super(key: key);
@override
Widget build(BuildContext context) {
const double spacingHeight = 5.0;
final formatter =
Provider.of<SettingsController>(context).dateTimeFormatter;
final copyButton = Tooltip(
message: 'Copy text version of event to clipboard',
child: IconButton(
onPressed: () async => await copyToClipboard(
context: context,
text: event.toHumanString(formatter),
snackbarMessage: 'Copied Event to clipboard'),
icon: const Icon(Icons.copy)));
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
event.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
copyButton,
],
),
if (event.description.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
Text(event.description)
],
_buildStatusLine('You are:', _eventStatusToString(event.eventStatus)),
const SizedBox(height: spacingHeight),
_buildStatusLine(
'Starts: ',
formatter.format(DateTime.fromMillisecondsSinceEpoch(
event.startTimestamp * 1000))),
if (event.endTimestamp != 0) ...[
const SizedBox(height: spacingHeight),
_buildStatusLine(
'Stops: ',
formatter.format(DateTime.fromMillisecondsSinceEpoch(
event.endTimestamp * 1000))),
],
const SizedBox(height: spacingHeight),
if (event.location.hasData()) event.location.toWidget(spacingHeight),
],
),
);
}
Widget _buildStatusLine(String label, String status) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 2),
Text(status),
],
);
}
String _eventStatusToString(FacebookEventStatus status) {
switch (status) {
case FacebookEventStatus.declined:
return 'Declined';
case FacebookEventStatus.interested:
return 'Interested';
case FacebookEventStatus.invited:
return 'Invited';
case FacebookEventStatus.joined:
return 'Joined';
case FacebookEventStatus.owner:
return 'Owner';
case FacebookEventStatus.unknown:
return 'Unknown';
}
}
}

View file

@ -1,50 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_conversation.dart';
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';
class FacebookConversationHistoryComponent extends StatefulWidget {
static final FacebookMessengerConversation noConversationSelected =
FacebookMessengerConversation.empty();
final FacebookMessengerConversation conversation;
const FacebookConversationHistoryComponent(
{Key? key, required this.conversation})
: super(key: key);
@override
State<FacebookConversationHistoryComponent> createState() =>
_FacebookConversationHistoryComponentState();
}
class _FacebookConversationHistoryComponentState
extends State<FacebookConversationHistoryComponent> {
@override
Widget build(BuildContext context) {
if (widget.conversation ==
FacebookConversationHistoryComponent.noConversationSelected) {
return const StandInStatusScreen(
title: 'No conversation selected',
subTitle: 'Select a conversation to display here',
);
}
return ListView.separated(
primary: false,
restorationId: 'facebookConversationPane',
itemCount: widget.conversation.messages.length,
itemBuilder: (context, index) {
final message = widget.conversation.messages[index];
return Text(
'${message.from}: ${message.message}',
softWrap: true,
);
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black,
thickness: 0.2,
);
});
}
}

View file

@ -1,92 +0,0 @@
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'facebook_location_data.dart';
import 'model_utils.dart';
enum FacebookEventStatus {
declined,
interested,
invited,
joined,
owner,
unknown,
}
class FacebookEvent {
static final _logger = Logger('$FacebookEvent');
final String name;
final String description;
final int creationTimestamp;
final int startTimestamp;
final int endTimestamp;
final FacebookLocationData location;
final FacebookEventStatus eventStatus;
FacebookEvent(
{required this.name,
required this.description,
required this.creationTimestamp,
required this.startTimestamp,
required this.endTimestamp,
required this.location,
required this.eventStatus});
@override
String toString() {
return 'FacebookEvent{name: $name, description: $description, creationTimestamp: $creationTimestamp, startTimestamp: $startTimestamp, endTimestamp: $endTimestamp, location: $location, eventStatus: $eventStatus}';
}
String toHumanString(DateFormat formatter) {
final creationDateString = formatter.format(
DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000)
.toLocal());
final startTimeString = formatter.format(
DateTime.fromMillisecondsSinceEpoch(startTimestamp * 1000).toLocal());
final endTimeString = formatter.format(
DateTime.fromMillisecondsSinceEpoch(endTimestamp * 1000).toLocal());
return [
if (name.isNotEmpty) 'Name: $name',
if (description.isNotEmpty) 'Description:\n$description',
'Creation At: $creationDateString',
if (startTimestamp != 0) 'Start Time: $startTimeString',
if (endTimestamp != 0) 'End Time: $endTimeString',
'Your Status: $eventStatus',
if (location.hasPosition) location.toHumanString(),
].join('\n');
}
static FacebookEvent fromJson(Map<String, dynamic> json,
{FacebookEventStatus statusType = FacebookEventStatus.unknown}) {
final knownTopLevelKeys = [
'name',
'start_timestamp',
'end_timestamp',
'place',
'description',
'create_timestamp'
];
logAdditionalKeys(knownTopLevelKeys, json.keys, _logger, Level.WARNING,
'Unknown top level event keys');
final name = json['name'] ?? '';
final description = json['description'] ?? '';
final int creationTimestamp = json['create_timestamp'] ?? 0;
final int startTimestamp = json['start_timestamp'] ?? 0;
final int endTimestamp = json['end_timestamp'] ?? 0;
final FacebookLocationData location = json.containsKey('place')
? FacebookLocationData.fromJson(json['place'])
: const FacebookLocationData();
return FacebookEvent(
name: name,
description: description,
creationTimestamp: creationTimestamp,
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
location: location,
eventStatus: statusType);
}
}

View file

@ -1,95 +0,0 @@
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
import 'facebook_messenger_message.dart';
import 'model_utils.dart';
class Copy<T> {
T? copy() => null;
}
class FacebookMessengerConversation with Copy<FacebookMessengerConversation> {
static final _logger = Logger('$FacebookMessengerConversation');
final String id;
final Set<String> participants;
final List<FacebookMessengerMessage> messages;
final String title;
FacebookMessengerConversation(
{required this.id,
required this.participants,
required this.messages,
required this.title});
factory FacebookMessengerConversation.empty() =>
FacebookMessengerConversation(
id: '', participants: {}, messages: [], title: '');
@override
FacebookMessengerConversation copy() => FacebookMessengerConversation(
id: id,
participants: {...participants},
messages: [...messages],
title: title);
@override
String toString() {
return 'FacebookMessengerConversation{participants: $participants, messages: $messages, title: $title}';
}
int earliestTimestampMS() => messages.isEmpty ? 0 : messages.last.timestampMS;
int latestTimestampMS() => messages.isEmpty ? 0 : messages.first.timestampMS;
bool hasImages() => messages.where((m) => m.hasImages()).isNotEmpty;
bool hasVideos() => messages.where((m) => m.hasVideos()).isNotEmpty;
FacebookMessengerConversation.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
participants = {...json['participants'] as List<dynamic>? ?? []},
messages = (json['messages'] as List<dynamic>? ?? [])
.map((j) => FacebookMessengerMessage.fromJson(j))
.toList(),
title = json['title'] ?? '';
Map<String, dynamic> toJson() => {
'id': id,
'participants': participants.toList(),
'messages': messages.map((m) => m.toJson()).toList(),
'title': title,
};
static FacebookMessengerConversation fromFacebookJson(
Map<String, dynamic> json) {
final id = json['thread_path'] ?? const Uuid().v4();
const knownTopLevelKeys = [
'participants',
'messages',
'title',
'is_still_participant',
'thread_type',
'thread_path',
'magic_words',
];
logAdditionalKeys(knownTopLevelKeys, json.keys, _logger, Level.WARNING,
'Unknown top level conversation keys: ');
final title = json['title'] ?? '';
final participants = <String>{};
final messages = <FacebookMessengerMessage>[];
for (Map<String, dynamic> p in json['messages'] ?? <Map, dynamic>{}) {
messages.add(FacebookMessengerMessage.fromFacebookJson(p));
}
for (Map<String, dynamic> p in json['participants'] ?? <Map, dynamic>{}) {
participants.add(p['name'] ?? '');
}
return FacebookMessengerConversation(
id: id, participants: participants, messages: messages, title: title);
}
}

View file

@ -1,179 +0,0 @@
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'facebook_media_attachment.dart';
import 'model_utils.dart';
class FacebookMessengerMessage {
static final _logger = Logger('$FacebookMessengerMessage');
final String from;
final String message;
final int timestampMS;
final List<FacebookMediaAttachment> media;
final List<FacebookMediaAttachment> stickers;
final List<Uri> links;
final Map<String, String> reactions;
FacebookMessengerMessage(
{required this.from,
required this.message,
required this.timestampMS,
this.media = const [],
this.stickers = const [],
this.links = const [],
this.reactions = const {}});
@override
String toString() {
return 'FacebookMessengerMessage{from: $from, message: $message, timestampMS: $timestampMS, media: $media, stickers: $stickers, links: $links, reactions: $reactions}';
}
String toHumanString(PathMappingService mapper, DateFormat formatter) {
final creationDateString = formatter
.format(DateTime.fromMillisecondsSinceEpoch(timestampMS).toLocal());
return [
'Creation At: $creationDateString',
if (message.isNotEmpty) 'Message: $message',
'',
if (links.isNotEmpty) 'Links:',
...links.map((e) => e.toString()),
'',
if (stickers.isNotEmpty) 'Stickers:',
...stickers.map((e) => e.toHumanString(mapper)),
if (media.isNotEmpty) 'Media:',
...media.map((e) => e.toHumanString(mapper)),
].join('\n');
}
FacebookMessengerMessage copy(
{String? from,
String? message,
int? timestampMS,
List<FacebookMediaAttachment>? media,
List<FacebookMediaAttachment>? stickers,
List<Uri>? links,
Map<String, String>? reactions}) {
return FacebookMessengerMessage(
from: from ?? this.from,
message: message ?? this.message,
timestampMS: timestampMS ?? this.timestampMS,
media: media ?? this.media,
stickers: stickers ?? this.stickers,
links: links ?? this.links,
reactions: reactions ?? this.reactions,
);
}
FacebookMessengerMessage.fromJson(Map<String, dynamic> json)
: from = json['from'] ?? '',
message = json['message'] ?? '',
timestampMS = json['timestampMS'] ?? '',
media = (json['media'] as List<dynamic>? ?? [])
.map((j) => FacebookMediaAttachment.fromJson(j))
.toList(),
stickers = (json['stickers'] as List<dynamic>? ?? [])
.map((j) => FacebookMediaAttachment.fromJson(j))
.toList(),
links = (json['links'] as List<dynamic>? ?? [])
.map((j) => Uri.parse(j))
.toList(),
reactions = (json['reactions'] as Map<String, dynamic>? ?? {})
.map((key, value) => MapEntry(key, value.toString()));
Map<String, dynamic> toJson() => {
'from': from,
'message': message,
'timestampMS': timestampMS,
'media': media.map((m) => m.toJson()).toList(),
'stickers': stickers.map((m) => m.toJson()).toList(),
'links': links.map((e) => e.toString()).toList(),
'reactions': reactions,
};
bool hasImages() => media
.where((element) =>
element.estimatedType() == FacebookAttachmentMediaType.image)
.isNotEmpty;
bool hasVideos() => media
.where((element) =>
element.estimatedType() == FacebookAttachmentMediaType.video)
.isNotEmpty;
static FacebookMessengerMessage fromFacebookJson(Map<String, dynamic> json) {
const knownTopLevelKeys = [
'sender_name',
'timestamp_ms',
'photos',
'reactions',
'gifs',
'content',
'type',
'share',
'videos',
'users',
'sticker',
'files',
'call_duration',
'missed',
'audio_files',
'is_unsent',
'ip',
];
logAdditionalKeys(knownTopLevelKeys, json.keys, _logger, Level.WARNING,
'Unknown top level message keys: ');
final from = json['sender_name'] ?? '';
final timestamp = json['timestamp_ms'] ?? 0;
final message = json['content'] ?? '';
final type = json['Generic'] ?? 'Generic';
if (!['Generic', 'Share'].contains(type)) {
_logger.severe("New message type: $type");
}
final links = <Uri>[];
final String linkString = json['share']?['link'] ?? '';
if (linkString.isNotEmpty) {
links.add(Uri.parse(linkString));
}
// TODO Add Reactions
List<FacebookMediaAttachment> mediaAttachments = [];
for (Map<String, dynamic> photo in json['photos'] ?? []) {
final media = FacebookMediaAttachment.fromFacebookJson(photo);
mediaAttachments.add(media);
}
for (Map<String, dynamic> video in json['videos'] ?? []) {
final media = FacebookMediaAttachment.fromFacebookJson(video);
mediaAttachments.add(media);
}
for (Map<String, dynamic> audioFile in json['audio_files'] ?? []) {
final path = audioFile['uri'];
links.add(Uri.file(path));
}
for (Map<String, dynamic> gif in json['gifs'] ?? []) {
final media = FacebookMediaAttachment.fromFacebookJson(gif);
mediaAttachments.add(media);
}
final stickers = <FacebookMediaAttachment>[];
final String path = json['sticker']?['uri'] ?? '';
if (path.isNotEmpty) {
stickers.add(FacebookMediaAttachment.fromUriOnly(Uri.file(path)));
}
return FacebookMessengerMessage(
from: from,
message: message,
timestampMS: timestamp,
media: mediaAttachments,
stickers: stickers,
links: links);
}
}

View file

@ -1,232 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/components/conversation_message_card.dart';
import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_conversation.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_message.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.dart';
import 'package:friendica_archive_browser/src/screens/error_screen.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../screens/loading_status_screen.dart';
import '../../screens/standin_status_screen.dart';
class FacebookConversationScreen extends StatelessWidget {
static final _logger = Logger('$FacebookConversationScreen');
const FacebookConversationScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final service = Provider.of<FacebookArchiveDataService>(context);
_logger.info('Build Facebook Conversation Screen');
return FutureBuilder<
Result<List<FacebookMessengerConversation>, ExecError>>(
future: service.getConvos(),
builder: (context, snapshot) {
_logger.fine('Future Conversation builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
_logger.finer('No data yet, just return status screen');
return const LoadingStatusScreen(
title: 'Loading Conversations',
subTitle:
'This can take several minutes the first time loading the archive.',
);
}
final convoResult = snapshot.requireData;
if (convoResult.isFailure) {
return ErrorScreen(error: convoResult.error);
}
_logger.finer(
'Now have data! ${snapshot.requireData.value.length} conversations');
final conversations = convoResult.value;
if (conversations.isEmpty) {
return const StandInStatusScreen(
title: 'No conversations were found');
}
return _FacebookConversionsFilteredWidget(
conversations: conversations);
});
}
}
class _FacebookConversionsFilteredWidget extends StatelessWidget {
final List<FacebookMessengerConversation> conversations;
const _FacebookConversionsFilteredWidget(
{Key? key, required this.conversations})
: super(key: key);
@override
Widget build(BuildContext context) {
return FilterControl<FacebookMessengerConversation,
FacebookMessengerMessage>(
allItems: conversations,
imagesOnlyFilterFunction: (convo) => convo.hasImages(),
videosOnlyFilterFunction: (convo) => convo.hasVideos(),
textSearchFilterFunction: (convo, text) =>
convo.title.contains(text) ||
convo.messages
.map((e) => e.message)
.where((element) => element.contains(text))
.isNotEmpty,
itemToDateTimeFunction: (convo) =>
DateTime.fromMillisecondsSinceEpoch(convo.latestTimestampMS()),
dateRangeFilterFunction: (convo, start, stop) =>
timestampInRange(convo.earliestTimestampMS(), start, stop) ||
timestampInRange(convo.latestTimestampMS(), start, stop),
getSecondary: (convo) => convo.messages,
copyPrimary: (convo) => convo.copy(),
secondaryItemToDateTimeFunction: (message) =>
DateTime.fromMillisecondsSinceEpoch(message.timestampMS),
secondaryDateRangeFilterFunction: (message, start, stop) =>
timestampInRange(message.timestampMS, start, stop),
secondaryImagesOnlyFilterFunction: (message) =>
message.hasImages() || message.stickers.isNotEmpty,
secondaryVideosOnlyFilterFunction: (message) => message.hasVideos(),
secondaryTextSearchFilterFunction: (message, text) =>
message.message.contains(text),
builder: (context, conversations) {
return _FacebookConversationsScreenWidget(
conversations: conversations);
});
}
}
class _FacebookConversationsScreenWidget extends StatefulWidget {
final List<FacebookMessengerConversation> conversations;
const _FacebookConversationsScreenWidget(
{Key? key, required this.conversations})
: super(key: key);
@override
State<_FacebookConversationsScreenWidget> createState() =>
_FacebookConversationsScreenWidgetState();
}
class _FacebookConversationsScreenWidgetState
extends State<_FacebookConversationsScreenWidget> {
static final _logger = Logger('$_FacebookConversationsScreenWidget');
static final FacebookMessengerConversation noConversationSelected =
FacebookMessengerConversation.empty();
FacebookMessengerConversation _currentConversation = noConversationSelected;
final ItemScrollController _scrollController = ItemScrollController();
_setConversation(int index) {
if (index > widget.conversations.length) {
_logger.severe(
'Requested participants index greater then max: $index > ${widget.conversations.length}');
return;
}
final conversation =
index < 0 ? noConversationSelected : widget.conversations[index];
if (conversation == _currentConversation) {
return;
}
_logger.finer('Jumping to $index');
final scrollToIndex = index > 0 ? index - 1 : 0;
_scrollController.scrollTo(
index: scrollToIndex, duration: const Duration(seconds: 1));
setState(() {
_currentConversation = conversation;
});
}
@override
Widget build(BuildContext context) {
_logger.fine('Build _FacebookConversationsScreenWidget');
if (!widget.conversations.contains(_currentConversation)) {
final selectedIndex = widget.conversations
.indexWhere((c) => c.id == _currentConversation.id);
_setConversation(selectedIndex);
}
return Row(
children: [
SizedBox(
width: 200,
child:
_buildConversationParticipantsList(context, widget.conversations),
),
SizedBox(width: 1, child: Container(color: Colors.grey)),
Expanded(child: _buildConversationPanel(context)),
],
);
}
Widget _buildConversationParticipantsList(
BuildContext context, List<FacebookMessengerConversation> conversations) {
_logger.fine('Build _buildConversationParticipantsList');
final textTheme = Theme.of(context).textTheme;
return ScrollablePositionedList.separated(
itemScrollController: _scrollController,
itemCount: conversations.length,
itemBuilder: (context, index) {
final conversation = conversations[index];
return TextButton(
onPressed: () => _setConversation(index),
style: _currentConversation == conversation
? TextButton.styleFrom(
backgroundColor:
textTheme.bodyText1?.decorationColor ?? Colors.blue)
: null,
child: Align(
alignment: Alignment.centerLeft,
child: Text(conversation.title,
softWrap: true,
textAlign: TextAlign.start,
style: _currentConversation == conversation
? textTheme.bodyText1
: textTheme.bodyText2),
));
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black,
thickness: 0.2,
);
});
}
Widget _buildConversationPanel(BuildContext context) {
_logger.fine('Build _buildConversationPanel');
if (_currentConversation == noConversationSelected) {
return const StandInStatusScreen(
title: 'No conversation selected',
subTitle: 'Select a conversation to display here',
);
}
final settings = Provider.of<SettingsController>(context);
final username = settings.facebookName;
return ListView.separated(
primary: false,
restorationId: 'facebookConversationPane',
itemCount: _currentConversation.messages.length,
itemBuilder: (context, index) {
final msg = _currentConversation.messages[index];
return ConversationMessageCard(
message: msg.from == username ? msg.copy(from: 'You') : msg);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 5);
});
}
}

View file

@ -1,100 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/components/event_card.dart';
import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_event.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.dart';
import 'package:friendica_archive_browser/src/screens/error_screen.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../../screens/loading_status_screen.dart';
import '../../screens/standin_status_screen.dart';
class FacebookEventsScreen extends StatelessWidget {
static final _logger = Logger('$FacebookEventsScreen');
const FacebookEventsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final service = Provider.of<FacebookArchiveDataService>(context);
_logger.fine('Build FacebookEventsScreen');
return FutureBuilder<Result<List<FacebookEvent>, ExecError>>(
future: service.getEvents(),
builder: (context, snapshot) {
_logger.fine('Future Events builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const LoadingStatusScreen(title: 'Loading events');
}
final eventsResult = snapshot.requireData;
if (eventsResult.isFailure) {
return ErrorScreen(error: eventsResult.error);
}
final events = eventsResult.value;
if (events.isEmpty) {
return const StandInStatusScreen(title: 'No events were found');
}
_logger.fine('Build events ListView');
return _FacebookEventsScreenWidget(events: events);
});
}
}
class _FacebookEventsScreenWidget extends StatelessWidget {
static final _logger = Logger('$_FacebookEventsScreenWidget');
final List<FacebookEvent> events;
const _FacebookEventsScreenWidget({Key? key, required this.events})
: super(key: key);
@override
Widget build(BuildContext context) {
return FilterControl<FacebookEvent, dynamic>(
allItems: events,
textSearchFilterFunction: (event, text) =>
event.name.contains(text) ||
event.description.contains(text) ||
event.location.name.contains(text) ||
event.location.address.contains(text),
itemToDateTimeFunction: (event) {
if (event.endTimestamp == 0) {
return DateTime.fromMillisecondsSinceEpoch(
event.startTimestamp * 1000);
}
return DateTime.fromMillisecondsSinceEpoch(event.endTimestamp * 1000);
},
dateRangeFilterFunction: (event, start, stop) =>
timestampInRange(event.startTimestamp * 1000, start, stop) ||
timestampInRange(event.endTimestamp * 1000, start, stop),
builder: (context, items) {
if (items.isEmpty) {
return const StandInStatusScreen(
title: 'No events meet filter criteria');
}
return ListView.separated(
primary: false,
restorationId: 'facebookEventsListView',
itemCount: items.length,
itemBuilder: (context, index) {
_logger.finer('Rendering Facebook Event List Item');
return EventCard(event: items[index]);
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black,
thickness: 0.2,
);
});
});
}
}

View file

@ -4,9 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_comment.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_event.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_friend.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_conversation.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_post.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_saved_item.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_timeline_type.dart';
@ -228,148 +226,6 @@ class FacebookArchiveFolderReader extends ChangeNotifier {
return Result.ok(allFriends);
}
FutureResult<List<FacebookEvent>, ExecError> readEvents() async {
final basePath = '$rootDirectoryPath/events';
final invitationsFile = File('$basePath/event_invitations.json');
final responsesFile = File('$basePath/your_event_responses.json');
final yourEventsFile = File('$basePath/your_events.json');
final events = <FacebookEvent>[];
if (!Directory(basePath).existsSync()) {
_logger.severe('Events base folder does not exist: $basePath');
return Result.error(
ExecError(errorMessage: 'Events data does not exist'));
}
if (invitationsFile.existsSync()) {
final json = (await _getJson(invitationsFile.path)).fold(
onSuccess: (json) => json,
onError: (error) {
_logger.severe(
'Error $error reading json for ${invitationsFile.path}');
return <String, dynamic>{};
});
final List<dynamic> invited =
json['events_invited_v2'] ?? <Map<String, dynamic>>[];
try {
events.addAll(invited.map((e) => FacebookEvent.fromJson(e,
statusType: FacebookEventStatus.invited)));
} catch (e) {
_logger.severe(
'Error $e processing JSON invitations file: ${invitationsFile.path}');
}
} else {
_logger.info('Invitations file does not exist; ${invitationsFile.path}');
}
if (responsesFile.existsSync()) {
final json = (await _getJson(responsesFile.path)).fold(
onSuccess: (json) => json,
onError: (error) {
_logger.severe(
'Error $error responses json for ${responsesFile.path}');
return <String, dynamic>{};
});
final Map<String, dynamic> responses =
json['event_responses_v2'] ?? <String, dynamic>{};
final List<dynamic> joined = responses['events_joined'] ?? [];
try {
events.addAll(joined.map((e) =>
FacebookEvent.fromJson(e, statusType: FacebookEventStatus.joined)));
} catch (e) {
_logger.severe(
'Error $e processing JSON joined events file: ${invitationsFile.path}');
}
final List<dynamic> declined = responses['events_declined'] ?? [];
try {
events.addAll(declined.map((e) => FacebookEvent.fromJson(e,
statusType: FacebookEventStatus.declined)));
} catch (e) {
_logger.severe(
'Error $e processing JSON declined events file: ${invitationsFile.path}');
}
final List<dynamic> interested = responses['events_interested'] ?? [];
try {
events.addAll(interested.map((e) => FacebookEvent.fromJson(e,
statusType: FacebookEventStatus.declined)));
} catch (e) {
_logger.severe(
'Error $e processing JSON interested events file: ${invitationsFile.path}');
}
} else {
_logger.info('Responses file does not exist; ${responsesFile.path}');
}
if (yourEventsFile.existsSync()) {
final json = (await _getJson(yourEventsFile.path)).fold(
onSuccess: (json) => json,
onError: (error) {
_logger.severe(
'Error $error your events file json for ${responsesFile.path}');
return <String, dynamic>{};
});
final List<dynamic> yourEvents =
json['your_events_v2'] ?? <Map<String, dynamic>>[];
try {
events.addAll(yourEvents.map((e) =>
FacebookEvent.fromJson(e, statusType: FacebookEventStatus.owner)));
} catch (e) {
_logger.severe(
'Error $e processing JSON your events file: ${yourEventsFile.path}');
}
} else {
_logger.info('Your events file does not exist ${yourEventsFile.path}');
}
events.sort((e1, e2) => -e1.startTimestamp.compareTo(e2.startTimestamp));
return Result.ok(events);
}
FutureResult<List<FacebookMessengerConversation>, ExecError>
readConversations() async {
final path = '$rootDirectoryPath/messages';
final folder = Directory(path);
final conversations = <String, FacebookMessengerConversation>{};
if (!folder.existsSync()) {
_logger.severe('Messages folder does not exist; $path');
return Result.ok([]);
}
await for (var entity in folder.list(recursive: true)) {
if (entity.path.toLowerCase().endsWith('json')) {
if (entity is Directory) {
continue;
}
try {
final jsonResult = await _getJson(entity.path, level: Level.FINEST);
if (jsonResult.isFailure) {
_logger.severe(
'Error ${jsonResult.error} reading JSON data for ${entity.path}');
continue;
}
final conversation =
FacebookMessengerConversation.fromFacebookJson(jsonResult.value);
if (conversations.containsKey(conversation.id)) {
final existingConvo = conversations[conversation.id]!;
existingConvo.messages.addAll(conversation.messages);
existingConvo.messages
.sort((m1, m2) => -m1.timestampMS.compareTo(m2.timestampMS));
} else {
conversations[conversation.id] = conversation;
}
} catch (e) {
_logger.severe('Error $e processing conversation ${entity.path}');
}
}
}
return Result.ok(conversations.values.toList());
}
FutureResult<List<FacebookSavedItem>, ExecError> readSavedItems() async {
final path =
'$rootDirectoryPath/saved_items_and_collections/saved_items_and_collections.json';

View file

@ -1,13 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_comment.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_event.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_friend.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_media_attachment.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_messenger_conversation.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_post.dart';
import 'package:friendica_archive_browser/src/friendica/models/facebook_saved_item.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
@ -25,9 +22,7 @@ class FacebookArchiveDataService extends ChangeNotifier {
final List<FacebookAlbum> albums = [];
final List<FacebookPost> posts = [];
final List<FacebookComment> comments = [];
final List<FacebookEvent> events = [];
final List<FacebookFriend> friends = [];
final List<FacebookMessengerConversation> convos = [];
final List<FacebookSavedItem> savedItems = [];
bool canUseConvoCacheFile = true;
@ -42,8 +37,6 @@ class FacebookArchiveDataService extends ChangeNotifier {
albums.clear();
posts.clear();
comments.clear();
events.clear();
convos.clear();
friends.clear();
savedItems.clear();
notifyListeners();
@ -111,28 +104,6 @@ class FacebookArchiveDataService extends ChangeNotifier {
return Result.ok(List.unmodifiable(comments));
}
FutureResult<List<FacebookEvent>, ExecError> getEvents() async {
_logger.fine('Request for events');
if (events.isNotEmpty) {
_logger.fine(
'Events already loaded, returning existing ${events.length} events');
return Result.ok(List.unmodifiable(events));
}
_logger.finer('No previously pulled events reading from disk');
final eventsResult = await _readAllEvents();
eventsResult.match(
onSuccess: (newEvents) {
events.clear();
events.addAll(newEvents);
events.sort((e1, e2) =>
-e1.creationTimestamp.compareTo(e2.creationTimestamp));
},
onError: (error) => _logger.severe('Error loading events: $error'));
_logger.fine('Returning ${comments.length} events');
return Result.ok(List.unmodifiable(events));
}
FutureResult<List<FacebookFriend>, ExecError> getFriends() async {
_logger.fine('Request for friends');
if (friends.isNotEmpty) {
@ -183,62 +154,6 @@ class FacebookArchiveDataService extends ChangeNotifier {
return Result.ok(List.unmodifiable(albums));
}
FutureResult<List<FacebookMessengerConversation>, ExecError>
getConvos() async {
_logger.fine('Request for conversations');
if (convos.isNotEmpty) {
_logger.fine(
'Conversations already loaded, returning existing ${convos.length} posts');
return Result.ok(List.unmodifiable(convos));
}
final convoCacheFile = File(_conversationCachePath);
try {
if (canUseConvoCacheFile && convoCacheFile.existsSync()) {
_logger.finer(
'Attempt to load conversations from: $_conversationCachePath');
final newConvosTextResult = await convoCacheFile.readAsString();
if (newConvosTextResult.isNotEmpty) {
final newConvosData =
jsonDecode(newConvosTextResult) as List<dynamic>;
final newConvos = newConvosData
.map((json) => FacebookMessengerConversation.fromJson(json))
.toList();
convos.clear();
convos.addAll(newConvos);
_logger.fine(
'${newConvos.length} conversations loaded from disk. Returning ${convos.length} conversations');
return Result.ok(List.unmodifiable(convos));
}
}
} catch (e) {
_logger.severe('Exception thrown trying to read from cache, $e');
}
_logger.finer('No cache data available so reading from original archive');
final conversationsResult = await _readAllConvos();
conversationsResult.match(onSuccess: (newConversations) {
convos.clear();
convos.addAll(newConversations);
convos.sort((c1, c2) =>
-c1.latestTimestampMS().compareTo(c2.latestTimestampMS()));
}, onError: (error) {
_logger.severe('Error loading posts: $error');
});
try {
_logger.finer(
'Writing ${convos.length} to conversation cache file $_conversationCachePath');
String json = jsonEncode(convos);
await convoCacheFile.writeAsString(json, flush: true);
} catch (e) {
_logger.severe('Error trying to write to cache file, $e');
}
_logger.fine('Returning ${convos.length} conversations');
return Result.ok(List.unmodifiable(convos));
}
FutureResult<List<FacebookSavedItem>, ExecError> getSavedItems() async {
_logger.fine('Request for saved items');
if (savedItems.isNotEmpty) {
@ -319,34 +234,6 @@ class FacebookArchiveDataService extends ChangeNotifier {
'Unable to find any comment JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FacebookEvent>, ExecError> _readAllEvents() async {
final allEvents = <FacebookEvent>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse event JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final eventsResult = await reader.readEvents();
eventsResult.match(
onSuccess: (newEvents) {
allEvents.addAll(newEvents);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read events, $e');
}
}
if (hadSuccess) {
return Result.ok(allEvents);
}
return Result.error(ExecError.message(
'Unable to find any event JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FacebookFriend>, ExecError> _readAllFriends() async {
final allFriends = <FacebookFriend>[];
bool hadSuccess = false;
@ -403,35 +290,6 @@ class FacebookArchiveDataService extends ChangeNotifier {
'Unable to find any album JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FacebookMessengerConversation>, ExecError>
_readAllConvos() async {
final allConvos = <FacebookMessengerConversation>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse conversation JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final convosResult = await reader.readConversations();
convosResult.match(
onSuccess: (newConvos) {
allConvos.addAll(newConvos);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read conversations, $e');
}
}
if (hadSuccess) {
return Result.ok(allConvos);
}
return Result.error(ExecError.message(
'Unable to find any event JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FacebookSavedItem>, ExecError> _readAllSavedItems() async {
final allSavedItems = <FacebookSavedItem>[];
bool hadSuccess = false;

View file

@ -1,7 +1,7 @@
// ignore_for_file: avoid_print
import 'package:flutter_test/flutter_test.dart';
import 'package:friendica_archive_browser/src/facebook/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:logging/logging.dart';
void main() {

View file

@ -1,8 +1,7 @@
// ignore_for_file: avoid_print
import 'package:flutter_test/flutter_test.dart';
import 'package:friendica_archive_browser/src/facebook/models/facebook_event.dart';
import 'package:friendica_archive_browser/src/facebook/services/facebook_archive_reader.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_reader.dart';
import 'package:logging/logging.dart';
void main() {
@ -41,16 +40,6 @@ void main() {
});
});
group('Test Read Events JSON', () {
test('Read from events disk', () async {
final events = await FacebookArchiveFolderReader(rootPath).readEvents();
expect(events.value.length, equals(11));
events.value
.where((element) => element.eventStatus == FacebookEventStatus.owner)
.forEach(print);
});
});
group('Test Read Friends JSON', () {
test('Read from friends disk', () async {
final friendsResult =
@ -61,15 +50,6 @@ void main() {
});
});
group('Test Read Conversation JSON', () {
test('Read conversations from disk', () async {
final conversations =
await FacebookArchiveFolderReader(rootPath).readConversations();
expect(conversations.value.length, equals(1));
conversations.value.forEach(print);
});
});
group('Test Read Saved Items JSON', () {
test('Read savedItems from disk', () async {
final savedItems =

View file

@ -3,7 +3,7 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:friendica_archive_browser/src/facebook/services/facebook_file_reader.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_file_reader.dart';
import 'package:logging/logging.dart';
void main() {
@ -15,24 +15,6 @@ void main() {
'${event.level.name} - ${event.loggerName} @ ${event.time}: ${event.message}');
});
// group('Test Facebook Reading timing', () {
// test('Normal Read vs. proper encoded read', () async {
// final expected = [
// 'This is malformed and should be Polish diacritical character ą.',
// 'This should be a heart ❤.',
// 'This should be a five stars ★★ ★★ ★.',
// ];
// const path = '$rootPath/mangled.txt';
// final result = await File(path).readFacebookEncodedFileAsString();
// expect(result.isSuccess, true);
// final lines = result.get().split('\n');
// expect(lines.length, equals(expected.length));
// for(var i = 0; i < lines.length; i++) {
// //expect(lines[i], equals(expected[i]));
// print('|${lines[i]}| ?= |${expected[i]}|');
// }
// });
group('Test Facebook Reading', () {
test('Read encoded text from disk', () async {
final expected = [