Merge branch 'circle-vs-channel-awareness' into 'main'

Circle vs channel awareness

See merge request mysocialportal/relatica!61
This commit is contained in:
HankG 2024-08-31 00:39:18 +00:00
commit 19cca28692
19 changed files with 353 additions and 150 deletions

View file

@ -13,12 +13,18 @@ if (flutterRoot == null) {
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '3'
} }
def flutterVersionName = localProperties.getProperty('flutter.versionName') def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) { if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '0.9.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
@ -53,11 +59,27 @@ android {
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
if (System.getenv()["CI"]) { // CI=true is exported by Codemagic
storeFile file(System.getenv()["CM_KEYSTORE_PATH"])
storePassword System.getenv()["CM_KEYSTORE_PASSWORD"]
keyAlias System.getenv()["CM_KEY_ALIAS"]
keyPassword System.getenv()["CM_KEY_PASSWORD"]
} else {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug signingConfig signingConfigs.release
} }
} }
} }

50
codemagic.yaml Normal file
View file

@ -0,0 +1,50 @@
workflows:
android-workflow:
name: Android Workflow
instance_type: mac_mini_m1
max_build_duration: 120
environment:
android_signing:
- codemagickeystore
groups:
- google_play # <-- (Includes GCLOUD_SERVICE_ACCOUNT_CREDENTIALS)
vars:
PACKAGE_NAME: "social.myportal.relatica" # <-- Put your package name here
GOOGLE_PLAY_TRACK: "internal"
flutter: stable
scripts:
- name: Set up local.properties
script: |
echo "flutter.sdk=$HOME/programs/flutter" > "$CM_BUILD_DIR/android/local.properties"
- name: Get Flutter packages
script: |
flutter packages pub get
# - name: Flutter analyze
# script: |
# flutter analyze
# ignore_failure: true
- name: Flutter unit tests
script: |
flutter test
ignore_failure: true
- name: Build AAB with Flutter
script: |
BUILD_NUMBER=$(($(google-play get-latest-build-number --package-name "$PACKAGE_NAME" --tracks="$GOOGLE_PLAY_TRACK") + 1))
flutter build appbundle --release \
--build-name=0.9.0 \
--build-number=$PROJECT_BUILD_NUMBER
artifacts:
- build/**/outputs/**/*.aab
- build/**/outputs/**/mapping.txt
- flutter_drive.log
publishing:
email:
recipients:
- codemagic@myportal.social
notify:
success: true
failure: false
google_play:
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
track: $GOOGLE_PLAY_TRACK
submit_as_draft: true

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../models/visibility.dart' as v; import '../../models/visibility.dart' as v;
import '../models/timeline_grouping_list_data.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
Future<bool?> showVisibilityDialog( Future<bool?> showVisibilityDialog(
@ -8,7 +9,9 @@ Future<bool?> showVisibilityDialog(
ConnectionsManager cm, ConnectionsManager cm,
v.Visibility visibility, v.Visibility visibility,
) async { ) async {
final circlesMap = {for (var item in cm.getMyCircles()) item.id: item}; final circlesMap = {
for (var item in cm.getGroupingListData(GroupingType.circle)) item.id: item
};
final allowedCircles = visibility.allowedCircleIds.map((c) { final allowedCircles = visibility.allowedCircleIds.map((c) {
if (c == '~') { if (c == '~') {

View file

@ -1,27 +1,31 @@
import 'package:result_monad/result_monad.dart'; import 'package:result_monad/result_monad.dart';
import '../../models/circle_data.dart';
import '../../models/connection.dart'; import '../../models/connection.dart';
import '../../models/exec_error.dart'; import '../../models/exec_error.dart';
import '../../models/timeline_grouping_list_data.dart';
abstract class ICirclesRepo { abstract class ICirclesRepo {
void clear(); void clear();
void addAllCircles(List<CircleData> circles); void addAllCircles(List<TimelineGroupingListData> circles);
void addConnectionToCircle(CircleData circle, Connection connection); void addConnectionToCircle(
TimelineGroupingListData circle, Connection connection);
void clearMyCircles(); void clearMyCircles();
void upsertCircle(CircleData circle); void upsertCircle(TimelineGroupingListData circle);
void deleteCircle(CircleData circle); void deleteCircle(TimelineGroupingListData circle);
List<CircleData> getMyCircles(); List<TimelineGroupingListData> getMyCircles();
Result<List<Connection>, ExecError> getCircleMembers(CircleData circle); Result<List<Connection>, ExecError> getCircleMembers(
TimelineGroupingListData circle);
Result<List<CircleData>, ExecError> getCirclesForUser(String id); Result<List<TimelineGroupingListData>, ExecError> getCirclesForUser(
String id);
bool updateConnectionCircleData(String id, List<CircleData> currentCircless); bool updateConnectionCircleData(
String id, List<TimelineGroupingListData> currentCircless);
} }

View file

@ -1,14 +1,14 @@
import 'package:result_monad/result_monad.dart'; import 'package:result_monad/result_monad.dart';
import '../../models/circle_data.dart';
import '../../models/connection.dart'; import '../../models/connection.dart';
import '../../models/exec_error.dart'; import '../../models/exec_error.dart';
import '../../models/timeline_grouping_list_data.dart';
import '../interfaces/circles_repo_intf.dart'; import '../interfaces/circles_repo_intf.dart';
class MemoryCirclesRepo implements ICirclesRepo { class MemoryCirclesRepo implements ICirclesRepo {
final _circlesForConnection = <String, List<CircleData>>{}; final _circlesForConnection = <String, List<TimelineGroupingListData>>{};
final _connectionsForCircle = <String, Set<Connection>>{}; final _connectionsForCircle = <String, Set<Connection>>{};
final _myCircles = <CircleData>{}; final _myCircles = <TimelineGroupingListData>{};
@override @override
void clear() { void clear() {
@ -18,7 +18,8 @@ class MemoryCirclesRepo implements ICirclesRepo {
} }
@override @override
Result<List<CircleData>, ExecError> getCirclesForUser(String id) { Result<List<TimelineGroupingListData>, ExecError> getCirclesForUser(
String id) {
if (!_circlesForConnection.containsKey(id)) { if (!_circlesForConnection.containsKey(id)) {
return Result.error(ExecError( return Result.error(ExecError(
type: ErrorType.notFound, type: ErrorType.notFound,
@ -30,12 +31,13 @@ class MemoryCirclesRepo implements ICirclesRepo {
} }
@override @override
List<CircleData> getMyCircles() { List<TimelineGroupingListData> getMyCircles() {
return _myCircles.toList(); return _myCircles.toList();
} }
@override @override
Result<List<Connection>, ExecError> getCircleMembers(CircleData circle) { Result<List<Connection>, ExecError> getCircleMembers(
TimelineGroupingListData circle) {
if (_connectionsForCircle.containsKey(circle.id)) { if (_connectionsForCircle.containsKey(circle.id)) {
return Result.ok(_connectionsForCircle[circle.id]!.toList()); return Result.ok(_connectionsForCircle[circle.id]!.toList());
} }
@ -52,31 +54,33 @@ class MemoryCirclesRepo implements ICirclesRepo {
} }
@override @override
void addConnectionToCircle(CircleData circle, Connection connection) { void addConnectionToCircle(
TimelineGroupingListData circle, Connection connection) {
_connectionsForCircle.putIfAbsent(circle.id, () => {}).add(connection); _connectionsForCircle.putIfAbsent(circle.id, () => {}).add(connection);
_circlesForConnection[connection.id]?.add(circle); _circlesForConnection[connection.id]?.add(circle);
} }
@override @override
void addAllCircles(List<CircleData> circle) { void addAllCircles(List<TimelineGroupingListData> circle) {
_myCircles.addAll(circle); _myCircles.addAll(circle);
} }
@override @override
bool updateConnectionCircleData(String id, List<CircleData> currentCircles) { bool updateConnectionCircleData(
String id, List<TimelineGroupingListData> currentCircles) {
_circlesForConnection[id] = currentCircles; _circlesForConnection[id] = currentCircles;
return true; return true;
} }
@override @override
void upsertCircle(CircleData circle) { void upsertCircle(TimelineGroupingListData circle) {
_connectionsForCircle.putIfAbsent(circle.id, () => {}); _connectionsForCircle.putIfAbsent(circle.id, () => {});
_myCircles.remove(circle); _myCircles.remove(circle);
_myCircles.add(circle); _myCircles.add(circle);
} }
@override @override
void deleteCircle(CircleData circle) { void deleteCircle(TimelineGroupingListData circle) {
for (final conCircles in _circlesForConnection.values) { for (final conCircles in _circlesForConnection.values) {
conCircles.remove(circle); conCircles.remove(circle);
} }

View file

@ -9,7 +9,6 @@ import 'package:result_monad/result_monad.dart';
import '../friendica_client/paged_response.dart'; import '../friendica_client/paged_response.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/auth/profile.dart'; import '../models/auth/profile.dart';
import '../models/circle_data.dart';
import '../models/connection.dart'; import '../models/connection.dart';
import '../models/direct_message.dart'; import '../models/direct_message.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
@ -21,6 +20,7 @@ import '../models/media_attachment_uploads/image_types_enum.dart';
import '../models/search_results.dart'; import '../models/search_results.dart';
import '../models/search_types.dart'; import '../models/search_types.dart';
import '../models/timeline_entry.dart'; import '../models/timeline_entry.dart';
import '../models/timeline_grouping_list_data.dart';
import '../models/timeline_identifiers.dart'; import '../models/timeline_identifiers.dart';
import '../models/user_notification.dart'; import '../models/user_notification.dart';
import '../models/visibility.dart'; import '../models/visibility.dart';
@ -28,13 +28,13 @@ import '../serializers/friendica/direct_message_friendica_extensions.dart';
import '../serializers/friendica/gallery_data_friendica_extensions.dart'; import '../serializers/friendica/gallery_data_friendica_extensions.dart';
import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../serializers/friendica/image_entry_friendica_extensions.dart';
import '../serializers/friendica/visibility_friendica_extensions.dart'; import '../serializers/friendica/visibility_friendica_extensions.dart';
import '../serializers/mastodon/circle_data_mastodon_extensions.dart';
import '../serializers/mastodon/connection_mastodon_extensions.dart'; import '../serializers/mastodon/connection_mastodon_extensions.dart';
import '../serializers/mastodon/follow_request_mastodon_extensions.dart'; import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
import '../serializers/mastodon/instance_info_mastodon_extensions.dart'; import '../serializers/mastodon/instance_info_mastodon_extensions.dart';
import '../serializers/mastodon/notification_mastodon_extension.dart'; import '../serializers/mastodon/notification_mastodon_extension.dart';
import '../serializers/mastodon/search_result_mastodon_extensions.dart'; import '../serializers/mastodon/search_result_mastodon_extensions.dart';
import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart'; import '../serializers/mastodon/timeline_entry_mastodon_extensions.dart';
import '../serializers/mastodon/timeline_grouping_list_data.dart';
import '../serializers/mastodon/visibility_mastodon_extensions.dart'; import '../serializers/mastodon/visibility_mastodon_extensions.dart';
import '../services/fediverse_server_validator.dart'; import '../services/fediverse_server_validator.dart';
import '../services/network_status_service.dart'; import '../services/network_status_service.dart';
@ -168,18 +168,20 @@ class GalleryClient extends FriendicaClient {
} }
} }
class CirclesClient extends FriendicaClient { class TimelineGroupingListClient extends FriendicaClient {
static final _logger = Logger('$CirclesClient'); static final _logger = Logger('$TimelineGroupingListClient');
CirclesClient(super.credentials) : super(); TimelineGroupingListClient(super.credentials) : super();
FutureResult<List<CircleData>, ExecError> getCircles() async { FutureResult<List<TimelineGroupingListData>, ExecError>
_logger.finest(() => 'Getting circle (Mastodon List) data'); getTimelineGroupingListData() async {
_logger.finest(() => 'Getting timeline grouping data (Mastodon List) data');
final url = 'https://$serverName/api/v1/lists'; final url = 'https://$serverName/api/v1/lists';
final request = Uri.parse(url); final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync( return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson.data (listsJson) async => listsJson.data
.map((json) => CircleDataMastodonExtensions.fromJson(json)) .map((json) =>
TimelineGroupingListDataMastodonExtensions.fromJson(json))
.toList())) .toList()))
.mapError((error) => error is ExecError .mapError((error) => error is ExecError
? error ? error
@ -187,7 +189,7 @@ class CirclesClient extends FriendicaClient {
} }
FutureResult<PagedResponse<List<Connection>>, ExecError> getCircleMembers( FutureResult<PagedResponse<List<Connection>>, ExecError> getCircleMembers(
CircleData circleData, TimelineGroupingListData circleData,
PagingData page, PagingData page,
) async { ) async {
_networkStatusService.startConnectionUpdateStatus(); _networkStatusService.startConnectionUpdateStatus();
@ -206,7 +208,8 @@ class CirclesClient extends FriendicaClient {
.execErrorCast(); .execErrorCast();
} }
FutureResult<CircleData, ExecError> createCircle(String title) async { FutureResult<TimelineGroupingListData, ExecError> createCircle(
String title) async {
_logger.finest(() => 'Creating circle (Mastodon List) of name $title'); _logger.finest(() => 'Creating circle (Mastodon List) of name $title');
final url = 'https://$serverName/api/v1/lists'; final url = 'https://$serverName/api/v1/lists';
final body = { final body = {
@ -217,11 +220,11 @@ class CirclesClient extends FriendicaClient {
body, body,
headers: _headers, headers: _headers,
).andThenSuccessAsync((data) async => ).andThenSuccessAsync((data) async =>
CircleDataMastodonExtensions.fromJson(jsonDecode(data))); TimelineGroupingListDataMastodonExtensions.fromJson(jsonDecode(data)));
return result.execErrorCast(); return result.execErrorCast();
} }
FutureResult<CircleData, ExecError> renameCircle( FutureResult<TimelineGroupingListData, ExecError> renameCircle(
String id, String title) async { String id, String title) async {
_logger.finest(() => 'Reanming circle (Mastodon List) to name $title'); _logger.finest(() => 'Reanming circle (Mastodon List) to name $title');
final url = 'https://$serverName/api/v1/lists/$id'; final url = 'https://$serverName/api/v1/lists/$id';
@ -234,12 +237,13 @@ class CirclesClient extends FriendicaClient {
headers: _headers, headers: _headers,
).andThenSuccessAsync((data) async { ).andThenSuccessAsync((data) async {
final json = jsonDecode(data); final json = jsonDecode(data);
return CircleDataMastodonExtensions.fromJson(json); return TimelineGroupingListDataMastodonExtensions.fromJson(json);
}); });
return result.execErrorCast(); return result.execErrorCast();
} }
FutureResult<bool, ExecError> deleteCircle(CircleData circleData) async { FutureResult<bool, ExecError> deleteCircle(
TimelineGroupingListData circleData) async {
_logger.finest( _logger.finest(
() => 'Reanming circle (Mastodon List) to name ${circleData.name}'); () => 'Reanming circle (Mastodon List) to name ${circleData.name}');
final url = 'https://$serverName/api/v1/lists/${circleData.id}'; final url = 'https://$serverName/api/v1/lists/${circleData.id}';
@ -247,21 +251,22 @@ class CirclesClient extends FriendicaClient {
return result.mapValue((_) => true).execErrorCast(); return result.mapValue((_) => true).execErrorCast();
} }
FutureResult<List<CircleData>, ExecError> getMemberCirclesForConnection( FutureResult<List<TimelineGroupingListData>, ExecError>
String connectionId) async { getMemberCirclesForConnection(String connectionId) async {
_logger.finest(() => _logger.finest(() =>
'Getting circles (Mastodon Lists) containing connection: $connectionId'); 'Getting circles (Mastodon Lists) containing connection: $connectionId');
final url = 'https://$serverName/api/v1/accounts/$connectionId/lists'; final url = 'https://$serverName/api/v1/accounts/$connectionId/lists';
final request = Uri.parse(url); final request = Uri.parse(url);
return (await _getApiListRequest(request).andThenSuccessAsync( return (await _getApiListRequest(request).andThenSuccessAsync(
(listsJson) async => listsJson.data (listsJson) async => listsJson.data
.map((json) => CircleDataMastodonExtensions.fromJson(json)) .map((json) =>
TimelineGroupingListDataMastodonExtensions.fromJson(json))
.toList())) .toList()))
.mapError((error) => error as ExecError); .mapError((error) => error as ExecError);
} }
FutureResult<bool, ExecError> addConnectionToCircle( FutureResult<bool, ExecError> addConnectionToCircle(
CircleData circle, TimelineGroupingListData circle,
Connection connection, Connection connection,
) async { ) async {
_logger.finest(() => 'Adding connection to circle'); _logger.finest(() => 'Adding connection to circle');
@ -275,7 +280,7 @@ class CirclesClient extends FriendicaClient {
} }
FutureResult<bool, ExecError> removeConnectionFromCircle( FutureResult<bool, ExecError> removeConnectionFromCircle(
CircleData circle, TimelineGroupingListData circle,
Connection connection, Connection connection,
) async { ) async {
_logger.finest(() => 'Adding connection to circle'); _logger.finest(() => 'Adding connection to circle');

View file

@ -1,22 +0,0 @@
class CircleData {
static final followersPseudoCircle = CircleData('~', 'Followers');
final String id;
final String name;
CircleData(this.id, this.name);
@override
String toString() {
return 'CircleData{id: $id, name: $name}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CircleData && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}

View file

@ -0,0 +1,35 @@
enum GroupingType {
channel,
circle,
group,
}
class TimelineGroupingListData {
static const followersPseudoCircle =
TimelineGroupingListData('~', 'Followers', GroupingType.circle);
static const empty = TimelineGroupingListData('', '', GroupingType.circle);
final String id;
final String name;
final GroupingType groupingType;
const TimelineGroupingListData(this.id, this.name, this.groupingType);
@override
String toString() {
return 'CircleData{id: $id, name: $name, type: ${groupingType.name}}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TimelineGroupingListData &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}

View file

@ -8,9 +8,9 @@ import '../controls/linear_status_indicator.dart';
import '../controls/responsive_max_width.dart'; import '../controls/responsive_max_width.dart';
import '../controls/status_and_refresh_button.dart'; import '../controls/status_and_refresh_button.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/circle_data.dart';
import '../models/connection.dart'; import '../models/connection.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../models/timeline_grouping_list_data.dart';
import '../routes.dart'; import '../routes.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
import '../services/network_status_service.dart'; import '../services/network_status_service.dart';
@ -29,15 +29,17 @@ class CircleAddUsersScreen extends StatefulWidget {
class _CircleAddUsersScreenState extends State<CircleAddUsersScreen> { class _CircleAddUsersScreenState extends State<CircleAddUsersScreen> {
static final _logger = Logger('$CircleAddUsersScreen'); static final _logger = Logger('$CircleAddUsersScreen');
var filterText = ''; var filterText = '';
late CircleData circleData; late TimelineGroupingListData circleData;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final manager = final manager =
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value; getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value;
circleData = circleData = manager
manager.getMyCircles().where((g) => g.id == widget.circleId).first; .getGroupingListData(GroupingType.circle)
.where((g) => g.id == widget.circleId)
.first;
} }
Future<void> addUserToCircle( Future<void> addUserToCircle(

View file

@ -9,8 +9,8 @@ import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart'; import '../controls/standard_appbar.dart';
import '../controls/status_and_refresh_button.dart'; import '../controls/status_and_refresh_button.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/circle_data.dart';
import '../models/connection.dart'; import '../models/connection.dart';
import '../models/timeline_grouping_list_data.dart';
import '../routes.dart'; import '../routes.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
import '../services/network_status_service.dart'; import '../services/network_status_service.dart';
@ -31,7 +31,7 @@ class _CircleEditorScreenState extends State<CircleEditorScreen> {
var processingUpdate = false; var processingUpdate = false;
var allowNameEditing = false; var allowNameEditing = false;
var filterText = ''; var filterText = '';
late CircleData circleData; late TimelineGroupingListData circleData;
Future<void> updateCircleName( Future<void> updateCircleName(
BuildContext context, ConnectionsManager manager) async { BuildContext context, ConnectionsManager manager) async {
@ -93,7 +93,7 @@ class _CircleEditorScreenState extends State<CircleEditorScreen> {
final manager = final manager =
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value; getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value;
circleData = manager circleData = manager
.getMyCircles() .getGroupingListData(GroupingType.circle)
.where( .where(
(g) => g.id == widget.circleId, (g) => g.id == widget.circleId,
) )

View file

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../controls/responsive_max_width.dart'; import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart'; import '../controls/standard_appbar.dart';
import '../models/timeline_grouping_list_data.dart';
import '../routes.dart'; import '../routes.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
import '../utils/active_profile_selector.dart'; import '../utils/active_profile_selector.dart';
@ -17,7 +18,7 @@ class CircleManagementScreen extends StatelessWidget {
.watch<ActiveProfileSelector<ConnectionsManager>>() .watch<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry .activeEntry
.value; .value;
final circles = manager.getMyCircles(); final circles = manager.getGroupingListData(GroupingType.circle);
circles.sort((g1, g2) => g1.name.compareTo(g2.name)); circles.sort((g1, g2) => g1.name.compareTo(g2.name));
return Scaffold( return Scaffold(
appBar: StandardAppBar.build( appBar: StandardAppBar.build(

View file

@ -17,12 +17,12 @@ import '../controls/standard_appbar.dart';
import '../controls/timeline/status_header_control.dart'; import '../controls/timeline/status_header_control.dart';
import '../controls/visibility_dialog.dart'; import '../controls/visibility_dialog.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/circle_data.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../models/image_entry.dart'; import '../models/image_entry.dart';
import '../models/link_preview_data.dart'; import '../models/link_preview_data.dart';
import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/media_attachment_uploads/new_entry_media_items.dart';
import '../models/timeline_entry.dart'; import '../models/timeline_entry.dart';
import '../models/timeline_grouping_list_data.dart';
import '../models/visibility.dart'; import '../models/visibility.dart';
import '../serializers/friendica/link_preview_friendica_extensions.dart'; import '../serializers/friendica/link_preview_friendica_extensions.dart';
import '../services/connections_manager.dart'; import '../services/connections_manager.dart';
@ -58,7 +58,7 @@ class _EditorScreenState extends State<EditorScreen> {
final existingMediaItems = <ImageEntry>[]; final existingMediaItems = <ImageEntry>[];
final focusNode = FocusNode(); final focusNode = FocusNode();
Visibility visibility = Visibility.public(); Visibility visibility = Visibility.public();
CircleData? currentCircle; TimelineGroupingListData? currentCircle;
var isSubmitting = false; var isSubmitting = false;
@ -599,21 +599,23 @@ class _EditorScreenState extends State<EditorScreen> {
final circles = context final circles = context
.watch<ActiveProfileSelector<TimelineManager>>() .watch<ActiveProfileSelector<TimelineManager>>()
.activeEntry .activeEntry
.andThen((tm) => tm.getCircles()) .andThen((tm) => tm.getTimelineGroupingListData(GroupingType.circle))
.getValueOrElse(() => []); .getValueOrElse(() => []);
circles.sort((g1, g2) => g1.name.compareTo(g2.name)); circles.sort((g1, g2) => g1.name.compareTo(g2.name));
final circleMenuItems = <DropdownMenuItem<CircleData>>[]; final circleMenuItems = <DropdownMenuItem<TimelineGroupingListData>>[];
circleMenuItems.add(DropdownMenuItem( circleMenuItems.add(DropdownMenuItem(
value: CircleData.followersPseudoCircle, value: TimelineGroupingListData.followersPseudoCircle,
child: Text(CircleData.followersPseudoCircle.name))); child: Text(TimelineGroupingListData.followersPseudoCircle.name)));
circleMenuItems.add(DropdownMenuItem( circleMenuItems.add(const DropdownMenuItem(
value: CircleData('', ''), enabled: false, child: const Divider())); value: TimelineGroupingListData.empty,
enabled: false,
child: Divider()));
circleMenuItems.addAll(circles.map((g) => DropdownMenuItem( circleMenuItems.addAll(circles.map((g) => DropdownMenuItem(
value: g, value: g,
child: Text(g.name), child: Text(g.name),
))); )));
if (currentCircle != CircleData.followersPseudoCircle && if (currentCircle != TimelineGroupingListData.followersPseudoCircle &&
!circles.contains(currentCircle)) { !circles.contains(currentCircle)) {
currentCircle = null; currentCircle = null;
} }
@ -660,7 +662,7 @@ class _EditorScreenState extends State<EditorScreen> {
const HorizontalPadding(), const HorizontalPadding(),
if (visibility.type == VisibilityType.private) if (visibility.type == VisibilityType.private)
Expanded( Expanded(
child: DropdownButton<CircleData>( child: DropdownButton<TimelineGroupingListData>(
value: currentCircle, value: currentCircle,
isExpanded: true, isExpanded: true,
onChanged: widget.forEditing onChanged: widget.forEditing

View file

@ -3,17 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relatica/controls/focus_mode_status_headline.dart';
import 'package:relatica/riverpod_controllers/focus_mode.dart';
import '../controls/app_bottom_nav_bar.dart'; import '../controls/app_bottom_nav_bar.dart';
import '../controls/focus_mode_status_headline.dart';
import '../controls/linear_status_indicator.dart'; import '../controls/linear_status_indicator.dart';
import '../controls/login_aware_cached_network_image.dart'; import '../controls/login_aware_cached_network_image.dart';
import '../controls/responsive_max_width.dart'; import '../controls/responsive_max_width.dart';
import '../controls/standard_app_drawer.dart'; import '../controls/standard_app_drawer.dart';
import '../controls/timeline/timeline_panel.dart'; import '../controls/timeline/timeline_panel.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/timeline_grouping_list_data.dart';
import '../models/timeline_identifiers.dart'; import '../models/timeline_identifiers.dart';
import '../riverpod_controllers/focus_mode.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/network_status_service.dart'; import '../services/network_status_service.dart';
import '../services/timeline_manager.dart'; import '../services/timeline_manager.dart';
@ -114,38 +115,85 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
.activeEntry .activeEntry
.value; .value;
final circles = manager.getCircles().getValueOrElse(() => []).toList(); final circles = manager
.getTimelineGroupingListData(GroupingType.circle)
.getValueOrElse(() => [])
.toList();
circles.sort((g1, g2) => g1.name.compareTo(g2.name)); circles.sort((g1, g2) => g1.name.compareTo(g2.name));
final groups = manager
.getTimelineGroupingListData(GroupingType.group)
.getValueOrElse(() => [])
.toList();
groups.sort((g1, g2) => g1.name.compareTo(g2.name));
final channels = manager
.getTimelineGroupingListData(GroupingType.channel)
.getValueOrElse(() => [])
.toList();
channels.sort((g1, g2) => g1.name.compareTo(g2.name));
final items = [ final items = [
...standardTypes ...standardTypes
.map((t) => TimelineIdentifiers(timeline: t)) .map((t) => TimelineIdentifiers(timeline: t))
.map((e) => DropdownMenuItem(value: e, child: Text(e.toLabel()))), .map((e) => DropdownMenuItem(value: e, child: Text(e.toLabel()))),
const DropdownMenuItem( if (circles.isNotEmpty) ...[
value: null, const DropdownMenuItem(
enabled: false,
child: Divider(),
),
const DropdownMenuItem(
value: null, value: null,
enabled: false, enabled: false,
child: Text( child: Divider(),
'Circles', ),
style: TextStyle( const DropdownMenuItem(
fontWeight: FontWeight.bold, value: null,
fontStyle: FontStyle.italic, enabled: false,
decoration: TextDecoration.underline, child: Text(
), 'Circles',
)), style: TextStyle(
...circles fontWeight: FontWeight.bold,
.map((c) => TimelineIdentifiers( fontStyle: FontStyle.italic,
timeline: TimelineType.circle, auxData: c.id, label: c.name)) decoration: TextDecoration.underline,
.map((e) => DropdownMenuItem( ),
value: e, )),
child: Text( ..._timelineGroupingListDataCollectionToDropdown(circles),
e.toLabel(), ],
overflow: TextOverflow.fade, if (groups.isNotEmpty) ...[
))), const DropdownMenuItem(
value: null,
enabled: false,
child: Divider(),
),
const DropdownMenuItem(
value: null,
enabled: false,
child: Text(
'Groups',
style: TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
decoration: TextDecoration.underline,
),
)),
..._timelineGroupingListDataCollectionToDropdown(groups),
],
if (channels.isNotEmpty) ...[
const DropdownMenuItem(
value: null,
enabled: false,
child: Divider(),
),
const DropdownMenuItem(
value: null,
enabled: false,
child: Text(
'Channels',
style: TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
decoration: TextDecoration.underline,
),
)),
..._timelineGroupingListDataCollectionToDropdown(channels),
],
]; ];
if (items.where((i) => i.value == currentTimeline).isEmpty) { if (items.where((i) => i.value == currentTimeline).isEmpty) {
@ -167,3 +215,17 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
}); });
} }
} }
List<DropdownMenuItem<TimelineIdentifiers>>
_timelineGroupingListDataCollectionToDropdown(
List<TimelineGroupingListData> circles) =>
circles
.map((c) => TimelineIdentifiers(
timeline: TimelineType.circle, auxData: c.id, label: c.name))
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.toLabel(),
overflow: TextOverflow.fade,
)))
.toList();

View file

@ -6,8 +6,8 @@ import '../controls/html_text_viewer_control.dart';
import '../controls/login_aware_cached_network_image.dart'; import '../controls/login_aware_cached_network_image.dart';
import '../controls/padding.dart'; import '../controls/padding.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/circle_data.dart';
import '../models/connection.dart'; import '../models/connection.dart';
import '../models/timeline_grouping_list_data.dart';
import '../routes.dart'; import '../routes.dart';
import '../services/auth_service.dart'; import '../services/auth_service.dart';
import '../services/blocks_manager.dart'; import '../services/blocks_manager.dart';
@ -152,12 +152,12 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
Connection profile, Connection profile,
ConnectionsManager manager, ConnectionsManager manager,
) { ) {
final myCircles = manager.getMyCircles(); final myCircles = manager.getGroupingListData(GroupingType.circle);
final usersCircles = manager.getCirclesForUser(profile.id).fold( final usersCircles = manager.getCirclesForUser(profile.id).fold(
onSuccess: (circles) => circles.toSet(), onSuccess: (circles) => circles.toSet(),
onError: (error) { onError: (error) {
buildSnackbar(context, 'Error getting circle data: $error'); buildSnackbar(context, 'Error getting circle data: $error');
return <CircleData>{}; return <TimelineGroupingListData>{};
}); });
myCircles.sort((g1, g2) => g1.name.compareTo(g2.name)); myCircles.sort((g1, g2) => g1.name.compareTo(g2.name));

View file

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

View file

@ -0,0 +1,27 @@
import '../../models/timeline_grouping_list_data.dart';
extension TimelineGroupingListDataMastodonExtensions
on TimelineGroupingListData {
static TimelineGroupingListData fromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '';
final typeString = json['replies_policy']?.toString() ?? '';
late final GroupingType type;
if (typeString == 'followed') {
if (id.startsWith('channel')) {
type = GroupingType.channel;
} else if (id.startsWith('group')) {
type = GroupingType.group;
} else {
type = GroupingType.circle;
}
} else {
type = GroupingType.circle;
}
return TimelineGroupingListData(
id,
json['title'],
type,
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:relatica/models/circle_data.dart'; import '../../models/timeline_grouping_list_data.dart';
import '../../models/visibility.dart'; import '../../models/visibility.dart';
extension VisibilityMastodonExtensions on Visibility { extension VisibilityMastodonExtensions on Visibility {
@ -15,7 +14,7 @@ extension VisibilityMastodonExtensions on Visibility {
if (!onComment && hasDetails) { if (!onComment && hasDetails) {
final circleId = final circleId =
allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull; allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull;
if (circleId == CircleData.followersPseudoCircle.id) { if (circleId == TimelineGroupingListData.followersPseudoCircle.id) {
return 'private'; return 'private';
} }

View file

@ -11,9 +11,9 @@ import '../friendica_client/friendica_client.dart';
import '../friendica_client/paging_data.dart'; import '../friendica_client/paging_data.dart';
import '../globals.dart'; import '../globals.dart';
import '../models/auth/profile.dart'; import '../models/auth/profile.dart';
import '../models/circle_data.dart';
import '../models/connection.dart'; import '../models/connection.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../models/timeline_grouping_list_data.dart';
import '../utils/active_profile_selector.dart'; import '../utils/active_profile_selector.dart';
import 'persistent_info_service.dart'; import 'persistent_info_service.dart';
@ -238,16 +238,20 @@ class ConnectionsManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
List<CircleData> getMyCircles() { List<TimelineGroupingListData> getGroupingListData(GroupingType type) {
if (circlesNotInitialized) { if (circlesNotInitialized) {
circlesNotInitialized = false; circlesNotInitialized = false;
_updateMyCircles(true); _updateMyGroupingListData(true);
} }
return circlesRepo.getMyCircles(); return circlesRepo
.getMyCircles()
.where((e) => e.groupingType == type)
.toList();
} }
Result<List<Connection>, ExecError> getCircleMembers(CircleData circle) { Result<List<Connection>, ExecError> getCircleMembers(
TimelineGroupingListData circle) {
return circlesRepo return circlesRepo
.getCircleMembers(circle) .getCircleMembers(circle)
.transform( .transform(
@ -258,8 +262,9 @@ class ConnectionsManager extends ChangeNotifier {
.execErrorCast(); .execErrorCast();
} }
FutureResult<CircleData, ExecError> createCircle(String newName) async { FutureResult<TimelineGroupingListData, ExecError> createCircle(
final result = await CirclesClient(profile) String newName) async {
final result = await TimelineGroupingListClient(profile)
.createCircle(newName) .createCircle(newName)
.withResultAsync((newCircle) async { .withResultAsync((newCircle) async {
circlesRepo.upsertCircle(newCircle); circlesRepo.upsertCircle(newCircle);
@ -268,9 +273,9 @@ class ConnectionsManager extends ChangeNotifier {
return result.execErrorCast(); return result.execErrorCast();
} }
FutureResult<CircleData, ExecError> renameCircle( FutureResult<TimelineGroupingListData, ExecError> renameCircle(
String id, String newName) async { String id, String newName) async {
final result = await CirclesClient(profile) final result = await TimelineGroupingListClient(profile)
.renameCircle(id, newName) .renameCircle(id, newName)
.withResultAsync((renamedCircle) async { .withResultAsync((renamedCircle) async {
circlesRepo.upsertCircle(renamedCircle); circlesRepo.upsertCircle(renamedCircle);
@ -279,8 +284,9 @@ class ConnectionsManager extends ChangeNotifier {
return result.execErrorCast(); return result.execErrorCast();
} }
FutureResult<bool, ExecError> deleteCircle(CircleData circleData) async { FutureResult<bool, ExecError> deleteCircle(
final result = await CirclesClient(profile) TimelineGroupingListData circleData) async {
final result = await TimelineGroupingListClient(profile)
.deleteCircle(circleData) .deleteCircle(circleData)
.withResultAsync((_) async { .withResultAsync((_) async {
circlesRepo.deleteCircle(circleData); circlesRepo.deleteCircle(circleData);
@ -290,12 +296,12 @@ class ConnectionsManager extends ChangeNotifier {
} }
void refreshCircles() { void refreshCircles() {
_updateMyCircles(true); _updateMyGroupingListData(true);
} }
Future<void> refreshCircleMemberships(CircleData circle) async { Future<void> refreshCircleMemberships(TimelineGroupingListData circle) async {
var page = PagingData(limit: 50); var page = PagingData(limit: 50);
final client = CirclesClient(profile); final client = TimelineGroupingListClient(profile);
final allResults = <Connection>{}; final allResults = <Connection>{};
var moreResults = true; var moreResults = true;
while (moreResults) { while (moreResults) {
@ -319,7 +325,8 @@ class ConnectionsManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Result<List<CircleData>, ExecError> getCirclesForUser(String id) { Result<List<TimelineGroupingListData>, ExecError> getCirclesForUser(
String id) {
final result = circlesRepo.getCirclesForUser(id); final result = circlesRepo.getCirclesForUser(id);
if (result.isSuccess) { if (result.isSuccess) {
_logger.finer("Circles for user $id: $result"); _logger.finer("Circles for user $id: $result");
@ -335,9 +342,9 @@ class ConnectionsManager extends ChangeNotifier {
} }
FutureResult<bool, ExecError> addUserToCircle( FutureResult<bool, ExecError> addUserToCircle(
CircleData circle, Connection connection) async { TimelineGroupingListData circle, Connection connection) async {
_logger.finest('Adding ${connection.name} to circle: ${circle.name}'); _logger.finest('Adding ${connection.name} to circle: ${circle.name}');
return await CirclesClient(profile) return await TimelineGroupingListClient(profile)
.addConnectionToCircle(circle, connection) .addConnectionToCircle(circle, connection)
.withResultAsync((_) async => await refreshCircleMemberships(circle)) .withResultAsync((_) async => await refreshCircleMemberships(circle))
.withResult((_) => notifyListeners()) .withResult((_) => notifyListeners())
@ -349,9 +356,9 @@ class ConnectionsManager extends ChangeNotifier {
} }
FutureResult<bool, ExecError> removeUserFromCircle( FutureResult<bool, ExecError> removeUserFromCircle(
CircleData circle, Connection connection) async { TimelineGroupingListData circle, Connection connection) async {
_logger.finest('Removing ${connection.name} from circle: ${circle.name}'); _logger.finest('Removing ${connection.name} from circle: ${circle.name}');
return CirclesClient(profile) return TimelineGroupingListClient(profile)
.removeConnectionFromCircle(circle, connection) .removeConnectionFromCircle(circle, connection)
.withResultAsync((_) async => await refreshCircleMemberships(circle)) .withResultAsync((_) async => await refreshCircleMemberships(circle))
.withResult((_) => notifyListeners()) .withResult((_) => notifyListeners())
@ -395,7 +402,7 @@ class ConnectionsManager extends ChangeNotifier {
Connection connection, { Connection connection, {
bool withNotifications = true, bool withNotifications = true,
}) async { }) async {
await _updateMyCircles(false); await _updateMyGroupingListData(false);
await _refreshCircleListData(connection.id, false); await _refreshCircleListData(connection.id, false);
await _refreshConnection(connection, false); await _refreshConnection(connection, false);
if (withNotifications) { if (withNotifications) {
@ -405,7 +412,9 @@ class ConnectionsManager extends ChangeNotifier {
Future<void> _refreshCircleListData(String id, bool withNotification) async { Future<void> _refreshCircleListData(String id, bool withNotification) async {
_logger.finest('Refreshing member list data for Connection $id'); _logger.finest('Refreshing member list data for Connection $id');
await CirclesClient(profile).getMemberCirclesForConnection(id).match( await TimelineGroupingListClient(profile)
.getMemberCirclesForConnection(id)
.match(
onSuccess: (circles) { onSuccess: (circles) {
circlesRepo.updateConnectionCircleData(id, circles); circlesRepo.updateConnectionCircleData(id, circles);
if (withNotification) { if (withNotification) {
@ -436,9 +445,11 @@ class ConnectionsManager extends ChangeNotifier {
); );
} }
Future<void> _updateMyCircles(bool withNotification) async { Future<void> _updateMyGroupingListData(bool withNotification) async {
_logger.finest('Refreshing my circles list'); _logger.finest('Refreshing my circles list');
await CirclesClient(profile).getCircles().match( await TimelineGroupingListClient(profile)
.getTimelineGroupingListData()
.match(
onSuccess: (circles) { onSuccess: (circles) {
_logger.finest('Got updated circles:${circles.map((e) => e.name)}'); _logger.finest('Got updated circles:${circles.map((e) => e.name)}');
circlesRepo.clearMyCircles(); circlesRepo.clearMyCircles();

View file

@ -5,13 +5,13 @@ import 'package:result_monad/result_monad.dart';
import '../data/interfaces/circles_repo_intf.dart'; import '../data/interfaces/circles_repo_intf.dart';
import '../friendica_client/friendica_client.dart'; import '../friendica_client/friendica_client.dart';
import '../models/auth/profile.dart'; import '../models/auth/profile.dart';
import '../models/circle_data.dart';
import '../models/entry_tree_item.dart'; import '../models/entry_tree_item.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../models/image_entry.dart'; import '../models/image_entry.dart';
import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/media_attachment_uploads/new_entry_media_items.dart';
import '../models/timeline.dart'; import '../models/timeline.dart';
import '../models/timeline_entry.dart'; import '../models/timeline_entry.dart';
import '../models/timeline_grouping_list_data.dart';
import '../models/timeline_identifiers.dart'; import '../models/timeline_identifiers.dart';
import '../models/visibility.dart'; import '../models/visibility.dart';
import 'entry_manager_service.dart'; import 'entry_manager_service.dart';
@ -42,19 +42,25 @@ class TimelineManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Result<List<CircleData>, ExecError> getCircles() { Result<List<TimelineGroupingListData>, ExecError> getTimelineGroupingListData(
GroupingType type) {
if (circlesNotInitialized) { if (circlesNotInitialized) {
_refreshCircleData(); _refreshCircleData();
circlesNotInitialized = false; circlesNotInitialized = false;
return Result.ok([]); return Result.ok([]);
} }
return Result.ok(circlesRepo.getMyCircles()); return Result.ok(circlesRepo
.getMyCircles()
.where((e) => e.groupingType == type)
.toList());
} }
Future<void> _refreshCircleData() async { Future<void> _refreshCircleData() async {
_logger.finer('Refreshing member circle data '); _logger.finer('Refreshing member circle data ');
await CirclesClient(profile).getCircles().match( await TimelineGroupingListClient(profile)
.getTimelineGroupingListData()
.match(
onSuccess: (circles) { onSuccess: (circles) {
circlesRepo.addAllCircles(circles); circlesRepo.addAllCircles(circles);
notifyListeners(); notifyListeners();