First cut at group timelines

This commit is contained in:
Hank Grabowski 2022-12-08 13:37:30 -05:00
parent 0704539a47
commit 3ede9ed04c
8 changed files with 236 additions and 78 deletions

View file

@ -8,9 +8,11 @@ import 'models/TimelineIdentifiers.dart';
import 'models/connection.dart';
import 'models/credentials.dart';
import 'models/exec_error.dart';
import 'models/group_data.dart';
import 'models/timeline_entry.dart';
import 'models/user_notification.dart';
import 'serializers/friendica/connection_friendica_extensions.dart';
import 'serializers/mastodon/group_data_mastodon_extensions.dart';
import 'serializers/mastodon/notification_mastodon_extension.dart';
import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart';
@ -66,6 +68,32 @@ class FriendicaClient {
return response.mapValue((value) => true);
}
FutureResult<List<GroupData>, ExecError> getGroups() async {
_logger.finest(() => 'Getting group (Mastodon List) data');
final url = 'https://$serverName/api/v1/lists';
final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error is ExecError
? error
: ExecError(type: ErrorType.localError, message: error.toString()));
}
FutureResult<List<GroupData>, ExecError> getMemberGroupsForConnection(
String connectionId) async {
_logger.finest(() =>
'Getting groups (Mastodon Lists) containing connection: $connectionId');
final url = 'https://$serverName/api/v1/accounts/$connectionId/lists';
final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson
.map((json) => GroupDataMastodonExtensions.fromJson(json))
.toList()))
.mapError((error) => error as ExecError);
}
FutureResult<List<TimelineEntry>, ExecError> getUserTimeline(
{String userId = '', int page = 1, int count = 10}) async {
_logger.finest(() => 'Getting user timeline for $userId');
@ -298,7 +326,8 @@ class FriendicaClient {
return 'timelines/public';
case TimelineType.local:
return 'timelines/public';
case TimelineType.tag:
case TimelineType.group:
return 'timelines/list/${type.auxData}';
case TimelineType.profile:
return '/accounts/${type.auxData}/statuses';
case TimelineType.self:
@ -311,10 +340,10 @@ class FriendicaClient {
case TimelineType.home:
case TimelineType.global:
case TimelineType.profile:
case TimelineType.group:
return '';
case TimelineType.local:
return 'local=true';
case TimelineType.tag:
case TimelineType.self:
throw UnimplementedError('These types are not supported yet');
}

View file

@ -2,9 +2,26 @@ enum TimelineType {
home,
global,
local,
tag,
group,
profile,
self,
self;
String toLabel() {
switch (this) {
case TimelineType.home:
return 'Home';
case TimelineType.global:
return 'Global';
case TimelineType.local:
return 'Local';
case TimelineType.group:
return 'Group';
case TimelineType.profile:
return 'Profile';
case TimelineType.self:
return 'Self';
}
}
}
class TimelineIdentifiers {
@ -20,7 +37,8 @@ class TimelineIdentifiers {
factory TimelineIdentifiers.home() =>
TimelineIdentifiers(timeline: TimelineType.home);
factory TimelineIdentifiers.profile(String profileId) => TimelineIdentifiers(
factory TimelineIdentifiers.profile(String profileId) =>
TimelineIdentifiers(
timeline: TimelineType.profile,
auxData: profileId,
);

View file

@ -0,0 +1,11 @@
class GroupData {
final String id;
final String name;
GroupData(this.id, this.name);
@override
String toString() {
return 'GroupData{id: $id, name: $name}';
}
}

View file

@ -77,6 +77,7 @@ class _EditorScreenState extends State<EditorScreen> {
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
@ -131,6 +132,7 @@ class _EditorScreenState extends State<EditorScreen> {
),
),
),
),
);
}

View file

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/padding.dart';
import '../controls/timeline/timeline_panel.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/group_data.dart';
import '../services/timeline_manager.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@ -18,18 +22,58 @@ class _HomeScreenState extends State<HomeScreen> {
final postText = TextEditingController();
var currentType = TimelineType.home;
GroupData? currentGroup;
final types = [
TimelineType.home,
TimelineType.global,
TimelineType.local,
TimelineType.group,
];
@override
Widget build(BuildContext context) {
_logger.finest('Build');
final groups = context
.watch<TimelineManager>()
.getGroups()
.getValueOrElse(() => [])
.toList();
return Scaffold(
appBar: AppBar(
title: Text('Home'),
backgroundColor: Theme.of(context).canvasColor,
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
DropdownButton<TimelineType>(
value: currentType,
items: types
.map((e) => DropdownMenuItem<TimelineType>(
value: e,
child: Text(e.toLabel()),
))
.toList(),
onChanged: (value) {
setState(() {
currentType = value!;
});
}),
const HorizontalPadding(),
if (currentType == TimelineType.group)
DropdownButton<GroupData>(
value: currentGroup,
items: groups
.map((g) => DropdownMenuItem<GroupData>(
value: g,
child: Text(g.name),
))
.toList(),
onChanged: (value) {
setState(() {
currentGroup = value;
});
}),
],
),
actions: [
IconButton(
onPressed: () {
@ -41,23 +85,11 @@ class _HomeScreenState extends State<HomeScreen> {
),
body: Column(
children: [
DropdownButton<TimelineType>(
value: currentType,
items: types
.map((e) => DropdownMenuItem<TimelineType>(
value: e,
child: Text(e.name),
))
.toList(),
onChanged: (value) {
setState(() {
currentType = value!;
});
}),
Expanded(
child: TimelinePanel(
timeline: TimelineIdentifiers(
timeline: currentType,
auxData: currentGroup?.id ?? '',
),
)),
],

View file

@ -0,0 +1,8 @@
import '../../models/group_data.dart';
extension GroupDataMastodonExtensions on GroupData {
static GroupData fromJson(Map<String, dynamic> json) => GroupData(
json['id'],
json['title'],
);
}

View file

@ -1,11 +1,19 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import '../globals.dart';
import '../models/connection.dart';
import '../models/exec_error.dart';
import '../models/group_data.dart';
import 'auth_service.dart';
class ConnectionsManager {
class ConnectionsManager extends ChangeNotifier {
static final _logger = Logger('$ConnectionsManager');
final _connectionsById = <String, Connection>{};
final _connectionsByName = <String, Connection>{};
final _connectionsByProfileUrl = <Uri, Connection>{};
final _listsForConnection = <String, List<GroupData>>{};
int get length => _connectionsById.length;
@ -13,6 +21,7 @@ class ConnectionsManager {
_connectionsById.clear();
_connectionsByName.clear();
_connectionsByProfileUrl.clear();
_listsForConnection.clear();
}
bool addConnection(Connection connection) {
@ -36,6 +45,28 @@ class ConnectionsManager {
return result;
}
Result<List<GroupData>, ExecError> getListsForUser(String id) {
final result = _listsForConnection[id] ?? [];
_refreshConnectionListData(id);
return Result.ok(result);
}
Future<void> _refreshConnectionListData(String id) async {
_logger.finest('Refreshing member list data for Connection $id');
await getIt<AuthService>()
.currentClient
.andThenAsync((client) => client.getMemberGroupsForConnection(id))
.match(
onSuccess: (lists) {
_listsForConnection[id] = lists;
notifyListeners();
},
onError: (error) {
_logger.severe('Error getting list data for $id: $error');
},
);
}
Result<Connection, String> getById(String id) {
final result = _connectionsById[id];

View file

@ -6,8 +6,10 @@ import '../globals.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/entry_tree_item.dart';
import '../models/exec_error.dart';
import '../models/group_data.dart';
import '../models/timeline.dart';
import '../models/timeline_entry.dart';
import 'auth_service.dart';
import 'entry_manager_service.dart';
enum TimelineRefreshType {
@ -21,12 +23,43 @@ class TimelineManager extends ChangeNotifier {
final cachedTimelines = <TimelineIdentifiers, Timeline>{};
final _groups = <String, GroupData>{};
void clear() {
cachedTimelines.clear();
_groups.clear();
getIt<EntryManagerService>().clear();
notifyListeners();
}
Result<List<GroupData>, ExecError> getGroups() {
if (_groups.isEmpty) {
_refreshGroupData();
return Result.ok([]);
}
return Result.ok(_groups.values.toList());
}
Future<void> _refreshGroupData() async {
_logger.finest('Refreshing member group data ');
await getIt<AuthService>()
.currentClient
.andThenAsync((client) => client.getGroups())
.match(
onSuccess: (groups) {
_groups.clear();
for (final group in groups) {
_groups[group.id] = group;
}
notifyListeners();
},
onError: (error) {
_logger.severe('Error getting list data: $error');
},
);
}
FutureResult<bool, ExecError> createNewStatus(String text,
{String spoilerText = '', String inReplyToId = ''}) async {
final result = await getIt<EntryManagerService>().createNewStatus(
@ -110,7 +143,9 @@ class TimelineManager extends ChangeNotifier {
late final int highestId;
switch (refreshType) {
case TimelineRefreshType.refresh:
lowestId = timeline.highestStatusId + 1;
lowestId = timeline.highestStatusId == 0
? timeline.highestStatusId
: timeline.highestStatusId + 1;
highestId = 0;
break;
case TimelineRefreshType.loadOlder:
@ -153,16 +188,8 @@ class TimelineManager extends ChangeNotifier {
notifyListeners();
return result;
}
// All statuses get dumped into the entity mangager and get full assembled posts out of it
// Timeline keeps track of posts level only so can query timeline manager for those
// Should put backing store on timelines and entity manager so can recover from restart faster
// Have a purge caches button to start that over from scratch
// Should have a contacts manager with backing store as well
// Timeline view is new control that knows how to load timeline, scrolling around with refresh and get more
// Timeline Item view displays itself and children
// Has "Add Comment" value
// Has like/dislke
// Has reshare/quote reshare (if can get that working somehow)
// If our own has delete
}