mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 15:53:32 +00:00
First cut at group timelines
This commit is contained in:
parent
0704539a47
commit
3ede9ed04c
8 changed files with 236 additions and 78 deletions
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
11
lib/models/group_data.dart
Normal file
11
lib/models/group_data.dart
Normal 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}';
|
||||
}
|
||||
}
|
|
@ -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> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ?? '',
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
|
|
@ -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'],
|
||||
);
|
||||
}
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue