diff --git a/android/app/build.gradle b/android/app/build.gradle index 6c91c85..53178d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,12 +13,18 @@ if (flutterRoot == null) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = '3' } def flutterVersionName = localProperties.getProperty('flutter.versionName') 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' @@ -53,11 +59,27 @@ android { 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 { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 0000000..6c9b267 --- /dev/null +++ b/codemagic.yaml @@ -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 diff --git a/lib/controls/visibility_dialog.dart b/lib/controls/visibility_dialog.dart index 54c4831..561178e 100644 --- a/lib/controls/visibility_dialog.dart +++ b/lib/controls/visibility_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../models/visibility.dart' as v; +import '../models/timeline_grouping_list_data.dart'; import '../services/connections_manager.dart'; Future showVisibilityDialog( @@ -8,7 +9,9 @@ Future showVisibilityDialog( ConnectionsManager cm, v.Visibility visibility, ) 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) { if (c == '~') { diff --git a/lib/data/interfaces/circles_repo_intf.dart b/lib/data/interfaces/circles_repo_intf.dart index b64f618..86330e3 100644 --- a/lib/data/interfaces/circles_repo_intf.dart +++ b/lib/data/interfaces/circles_repo_intf.dart @@ -1,27 +1,31 @@ import 'package:result_monad/result_monad.dart'; -import '../../models/circle_data.dart'; import '../../models/connection.dart'; import '../../models/exec_error.dart'; +import '../../models/timeline_grouping_list_data.dart'; abstract class ICirclesRepo { void clear(); - void addAllCircles(List circles); + void addAllCircles(List circles); - void addConnectionToCircle(CircleData circle, Connection connection); + void addConnectionToCircle( + TimelineGroupingListData circle, Connection connection); void clearMyCircles(); - void upsertCircle(CircleData circle); + void upsertCircle(TimelineGroupingListData circle); - void deleteCircle(CircleData circle); + void deleteCircle(TimelineGroupingListData circle); - List getMyCircles(); + List getMyCircles(); - Result, ExecError> getCircleMembers(CircleData circle); + Result, ExecError> getCircleMembers( + TimelineGroupingListData circle); - Result, ExecError> getCirclesForUser(String id); + Result, ExecError> getCirclesForUser( + String id); - bool updateConnectionCircleData(String id, List currentCircless); + bool updateConnectionCircleData( + String id, List currentCircless); } diff --git a/lib/data/memory/memory_circles_repo.dart b/lib/data/memory/memory_circles_repo.dart index a19133a..1551232 100644 --- a/lib/data/memory/memory_circles_repo.dart +++ b/lib/data/memory/memory_circles_repo.dart @@ -1,14 +1,14 @@ import 'package:result_monad/result_monad.dart'; -import '../../models/circle_data.dart'; import '../../models/connection.dart'; import '../../models/exec_error.dart'; +import '../../models/timeline_grouping_list_data.dart'; import '../interfaces/circles_repo_intf.dart'; class MemoryCirclesRepo implements ICirclesRepo { - final _circlesForConnection = >{}; + final _circlesForConnection = >{}; final _connectionsForCircle = >{}; - final _myCircles = {}; + final _myCircles = {}; @override void clear() { @@ -18,7 +18,8 @@ class MemoryCirclesRepo implements ICirclesRepo { } @override - Result, ExecError> getCirclesForUser(String id) { + Result, ExecError> getCirclesForUser( + String id) { if (!_circlesForConnection.containsKey(id)) { return Result.error(ExecError( type: ErrorType.notFound, @@ -30,12 +31,13 @@ class MemoryCirclesRepo implements ICirclesRepo { } @override - List getMyCircles() { + List getMyCircles() { return _myCircles.toList(); } @override - Result, ExecError> getCircleMembers(CircleData circle) { + Result, ExecError> getCircleMembers( + TimelineGroupingListData circle) { if (_connectionsForCircle.containsKey(circle.id)) { return Result.ok(_connectionsForCircle[circle.id]!.toList()); } @@ -52,31 +54,33 @@ class MemoryCirclesRepo implements ICirclesRepo { } @override - void addConnectionToCircle(CircleData circle, Connection connection) { + void addConnectionToCircle( + TimelineGroupingListData circle, Connection connection) { _connectionsForCircle.putIfAbsent(circle.id, () => {}).add(connection); _circlesForConnection[connection.id]?.add(circle); } @override - void addAllCircles(List circle) { + void addAllCircles(List circle) { _myCircles.addAll(circle); } @override - bool updateConnectionCircleData(String id, List currentCircles) { + bool updateConnectionCircleData( + String id, List currentCircles) { _circlesForConnection[id] = currentCircles; return true; } @override - void upsertCircle(CircleData circle) { + void upsertCircle(TimelineGroupingListData circle) { _connectionsForCircle.putIfAbsent(circle.id, () => {}); _myCircles.remove(circle); _myCircles.add(circle); } @override - void deleteCircle(CircleData circle) { + void deleteCircle(TimelineGroupingListData circle) { for (final conCircles in _circlesForConnection.values) { conCircles.remove(circle); } diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 74b6c38..6e1b607 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -9,7 +9,6 @@ import 'package:result_monad/result_monad.dart'; import '../friendica_client/paged_response.dart'; import '../globals.dart'; import '../models/auth/profile.dart'; -import '../models/circle_data.dart'; import '../models/connection.dart'; import '../models/direct_message.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_types.dart'; import '../models/timeline_entry.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../models/timeline_identifiers.dart'; import '../models/user_notification.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/image_entry_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/follow_request_mastodon_extensions.dart'; import '../serializers/mastodon/instance_info_mastodon_extensions.dart'; import '../serializers/mastodon/notification_mastodon_extension.dart'; import '../serializers/mastodon/search_result_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 '../services/fediverse_server_validator.dart'; import '../services/network_status_service.dart'; @@ -168,18 +168,20 @@ class GalleryClient extends FriendicaClient { } } -class CirclesClient extends FriendicaClient { - static final _logger = Logger('$CirclesClient'); +class TimelineGroupingListClient extends FriendicaClient { + static final _logger = Logger('$TimelineGroupingListClient'); - CirclesClient(super.credentials) : super(); + TimelineGroupingListClient(super.credentials) : super(); - FutureResult, ExecError> getCircles() async { - _logger.finest(() => 'Getting circle (Mastodon List) data'); + FutureResult, ExecError> + getTimelineGroupingListData() async { + _logger.finest(() => 'Getting timeline grouping data (Mastodon List) data'); final url = 'https://$serverName/api/v1/lists'; final request = Uri.parse(url); return (await _getApiListRequest(request).andThenSuccessAsync( (listsJson) async => listsJson.data - .map((json) => CircleDataMastodonExtensions.fromJson(json)) + .map((json) => + TimelineGroupingListDataMastodonExtensions.fromJson(json)) .toList())) .mapError((error) => error is ExecError ? error @@ -187,7 +189,7 @@ class CirclesClient extends FriendicaClient { } FutureResult>, ExecError> getCircleMembers( - CircleData circleData, + TimelineGroupingListData circleData, PagingData page, ) async { _networkStatusService.startConnectionUpdateStatus(); @@ -206,7 +208,8 @@ class CirclesClient extends FriendicaClient { .execErrorCast(); } - FutureResult createCircle(String title) async { + FutureResult createCircle( + String title) async { _logger.finest(() => 'Creating circle (Mastodon List) of name $title'); final url = 'https://$serverName/api/v1/lists'; final body = { @@ -217,11 +220,11 @@ class CirclesClient extends FriendicaClient { body, headers: _headers, ).andThenSuccessAsync((data) async => - CircleDataMastodonExtensions.fromJson(jsonDecode(data))); + TimelineGroupingListDataMastodonExtensions.fromJson(jsonDecode(data))); return result.execErrorCast(); } - FutureResult renameCircle( + FutureResult renameCircle( String id, String title) async { _logger.finest(() => 'Reanming circle (Mastodon List) to name $title'); final url = 'https://$serverName/api/v1/lists/$id'; @@ -234,12 +237,13 @@ class CirclesClient extends FriendicaClient { headers: _headers, ).andThenSuccessAsync((data) async { final json = jsonDecode(data); - return CircleDataMastodonExtensions.fromJson(json); + return TimelineGroupingListDataMastodonExtensions.fromJson(json); }); return result.execErrorCast(); } - FutureResult deleteCircle(CircleData circleData) async { + FutureResult deleteCircle( + TimelineGroupingListData circleData) async { _logger.finest( () => 'Reanming circle (Mastodon List) to name ${circleData.name}'); final url = 'https://$serverName/api/v1/lists/${circleData.id}'; @@ -247,21 +251,22 @@ class CirclesClient extends FriendicaClient { return result.mapValue((_) => true).execErrorCast(); } - FutureResult, ExecError> getMemberCirclesForConnection( - String connectionId) async { + FutureResult, ExecError> + getMemberCirclesForConnection(String connectionId) async { _logger.finest(() => 'Getting circles (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.data - .map((json) => CircleDataMastodonExtensions.fromJson(json)) + .map((json) => + TimelineGroupingListDataMastodonExtensions.fromJson(json)) .toList())) .mapError((error) => error as ExecError); } FutureResult addConnectionToCircle( - CircleData circle, + TimelineGroupingListData circle, Connection connection, ) async { _logger.finest(() => 'Adding connection to circle'); @@ -275,7 +280,7 @@ class CirclesClient extends FriendicaClient { } FutureResult removeConnectionFromCircle( - CircleData circle, + TimelineGroupingListData circle, Connection connection, ) async { _logger.finest(() => 'Adding connection to circle'); diff --git a/lib/models/circle_data.dart b/lib/models/circle_data.dart deleted file mode 100644 index 81cb20c..0000000 --- a/lib/models/circle_data.dart +++ /dev/null @@ -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; -} diff --git a/lib/models/timeline_grouping_list_data.dart b/lib/models/timeline_grouping_list_data.dart new file mode 100644 index 0000000..3bce48b --- /dev/null +++ b/lib/models/timeline_grouping_list_data.dart @@ -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; +} diff --git a/lib/screens/circle_add_users_screen.dart b/lib/screens/circle_add_users_screen.dart index cfbe6e6..8db937f 100644 --- a/lib/screens/circle_add_users_screen.dart +++ b/lib/screens/circle_add_users_screen.dart @@ -8,9 +8,9 @@ import '../controls/linear_status_indicator.dart'; import '../controls/responsive_max_width.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; -import '../models/circle_data.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../routes.dart'; import '../services/connections_manager.dart'; import '../services/network_status_service.dart'; @@ -29,15 +29,17 @@ class CircleAddUsersScreen extends StatefulWidget { class _CircleAddUsersScreenState extends State { static final _logger = Logger('$CircleAddUsersScreen'); var filterText = ''; - late CircleData circleData; + late TimelineGroupingListData circleData; @override void initState() { super.initState(); final manager = getIt>().activeEntry.value; - circleData = - manager.getMyCircles().where((g) => g.id == widget.circleId).first; + circleData = manager + .getGroupingListData(GroupingType.circle) + .where((g) => g.id == widget.circleId) + .first; } Future addUserToCircle( diff --git a/lib/screens/circle_editor_screen.dart b/lib/screens/circle_editor_screen.dart index 6061050..3cd83ac 100644 --- a/lib/screens/circle_editor_screen.dart +++ b/lib/screens/circle_editor_screen.dart @@ -9,8 +9,8 @@ import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; -import '../models/circle_data.dart'; import '../models/connection.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../routes.dart'; import '../services/connections_manager.dart'; import '../services/network_status_service.dart'; @@ -31,7 +31,7 @@ class _CircleEditorScreenState extends State { var processingUpdate = false; var allowNameEditing = false; var filterText = ''; - late CircleData circleData; + late TimelineGroupingListData circleData; Future updateCircleName( BuildContext context, ConnectionsManager manager) async { @@ -93,7 +93,7 @@ class _CircleEditorScreenState extends State { final manager = getIt>().activeEntry.value; circleData = manager - .getMyCircles() + .getGroupingListData(GroupingType.circle) .where( (g) => g.id == widget.circleId, ) diff --git a/lib/screens/circle_management_screen.dart b/lib/screens/circle_management_screen.dart index 7badb83..872adaf 100644 --- a/lib/screens/circle_management_screen.dart +++ b/lib/screens/circle_management_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../routes.dart'; import '../services/connections_manager.dart'; import '../utils/active_profile_selector.dart'; @@ -17,7 +18,7 @@ class CircleManagementScreen extends StatelessWidget { .watch>() .activeEntry .value; - final circles = manager.getMyCircles(); + final circles = manager.getGroupingListData(GroupingType.circle); circles.sort((g1, g2) => g1.name.compareTo(g2.name)); return Scaffold( appBar: StandardAppBar.build( diff --git a/lib/screens/editor.dart b/lib/screens/editor.dart index d0fa4f9..07e6cb3 100644 --- a/lib/screens/editor.dart +++ b/lib/screens/editor.dart @@ -17,12 +17,12 @@ import '../controls/standard_appbar.dart'; import '../controls/timeline/status_header_control.dart'; import '../controls/visibility_dialog.dart'; import '../globals.dart'; -import '../models/circle_data.dart'; import '../models/exec_error.dart'; import '../models/image_entry.dart'; import '../models/link_preview_data.dart'; import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/timeline_entry.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../models/visibility.dart'; import '../serializers/friendica/link_preview_friendica_extensions.dart'; import '../services/connections_manager.dart'; @@ -58,7 +58,7 @@ class _EditorScreenState extends State { final existingMediaItems = []; final focusNode = FocusNode(); Visibility visibility = Visibility.public(); - CircleData? currentCircle; + TimelineGroupingListData? currentCircle; var isSubmitting = false; @@ -599,21 +599,23 @@ class _EditorScreenState extends State { final circles = context .watch>() .activeEntry - .andThen((tm) => tm.getCircles()) + .andThen((tm) => tm.getTimelineGroupingListData(GroupingType.circle)) .getValueOrElse(() => []); circles.sort((g1, g2) => g1.name.compareTo(g2.name)); - final circleMenuItems = >[]; + final circleMenuItems = >[]; circleMenuItems.add(DropdownMenuItem( - value: CircleData.followersPseudoCircle, - child: Text(CircleData.followersPseudoCircle.name))); - circleMenuItems.add(DropdownMenuItem( - value: CircleData('', ''), enabled: false, child: const Divider())); + value: TimelineGroupingListData.followersPseudoCircle, + child: Text(TimelineGroupingListData.followersPseudoCircle.name))); + circleMenuItems.add(const DropdownMenuItem( + value: TimelineGroupingListData.empty, + enabled: false, + child: Divider())); circleMenuItems.addAll(circles.map((g) => DropdownMenuItem( value: g, child: Text(g.name), ))); - if (currentCircle != CircleData.followersPseudoCircle && + if (currentCircle != TimelineGroupingListData.followersPseudoCircle && !circles.contains(currentCircle)) { currentCircle = null; } @@ -660,7 +662,7 @@ class _EditorScreenState extends State { const HorizontalPadding(), if (visibility.type == VisibilityType.private) Expanded( - child: DropdownButton( + child: DropdownButton( value: currentCircle, isExpanded: true, onChanged: widget.forEditing diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 60abc59..23fd8d6 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -3,17 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.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/focus_mode_status_headline.dart'; import '../controls/linear_status_indicator.dart'; import '../controls/login_aware_cached_network_image.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_app_drawer.dart'; import '../controls/timeline/timeline_panel.dart'; import '../globals.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../models/timeline_identifiers.dart'; +import '../riverpod_controllers/focus_mode.dart'; import '../services/auth_service.dart'; import '../services/network_status_service.dart'; import '../services/timeline_manager.dart'; @@ -114,38 +115,85 @@ class _HomeScreenState extends ConsumerState { .activeEntry .value; - final circles = manager.getCircles().getValueOrElse(() => []).toList(); + final circles = manager + .getTimelineGroupingListData(GroupingType.circle) + .getValueOrElse(() => []) + .toList(); 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 = [ ...standardTypes .map((t) => TimelineIdentifiers(timeline: t)) .map((e) => DropdownMenuItem(value: e, child: Text(e.toLabel()))), - const DropdownMenuItem( - value: null, - enabled: false, - child: Divider(), - ), - const DropdownMenuItem( + if (circles.isNotEmpty) ...[ + const DropdownMenuItem( value: null, enabled: false, - child: Text( - 'Circles', - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic, - decoration: TextDecoration.underline, - ), - )), - ...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, - ))), + child: Divider(), + ), + const DropdownMenuItem( + value: null, + enabled: false, + child: Text( + 'Circles', + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + decoration: TextDecoration.underline, + ), + )), + ..._timelineGroupingListDataCollectionToDropdown(circles), + ], + 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) { @@ -167,3 +215,17 @@ class _HomeScreenState extends ConsumerState { }); } } + +List> + _timelineGroupingListDataCollectionToDropdown( + List 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(); diff --git a/lib/screens/user_profile_screen.dart b/lib/screens/user_profile_screen.dart index 2916e66..b8102c5 100644 --- a/lib/screens/user_profile_screen.dart +++ b/lib/screens/user_profile_screen.dart @@ -6,8 +6,8 @@ import '../controls/html_text_viewer_control.dart'; import '../controls/login_aware_cached_network_image.dart'; import '../controls/padding.dart'; import '../globals.dart'; -import '../models/circle_data.dart'; import '../models/connection.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../routes.dart'; import '../services/auth_service.dart'; import '../services/blocks_manager.dart'; @@ -152,12 +152,12 @@ class _UserProfileScreenState extends State { Connection profile, ConnectionsManager manager, ) { - final myCircles = manager.getMyCircles(); + final myCircles = manager.getGroupingListData(GroupingType.circle); final usersCircles = manager.getCirclesForUser(profile.id).fold( onSuccess: (circles) => circles.toSet(), onError: (error) { buildSnackbar(context, 'Error getting circle data: $error'); - return {}; + return {}; }); myCircles.sort((g1, g2) => g1.name.compareTo(g2.name)); diff --git a/lib/serializers/mastodon/circle_data_mastodon_extensions.dart b/lib/serializers/mastodon/circle_data_mastodon_extensions.dart deleted file mode 100644 index 1429d12..0000000 --- a/lib/serializers/mastodon/circle_data_mastodon_extensions.dart +++ /dev/null @@ -1,8 +0,0 @@ -import '../../models/circle_data.dart'; - -extension CircleDataMastodonExtensions on CircleData { - static CircleData fromJson(Map json) => CircleData( - json['id'], - json['title'], - ); -} diff --git a/lib/serializers/mastodon/timeline_grouping_list_data.dart b/lib/serializers/mastodon/timeline_grouping_list_data.dart new file mode 100644 index 0000000..11c0784 --- /dev/null +++ b/lib/serializers/mastodon/timeline_grouping_list_data.dart @@ -0,0 +1,27 @@ +import '../../models/timeline_grouping_list_data.dart'; + +extension TimelineGroupingListDataMastodonExtensions + on TimelineGroupingListData { + static TimelineGroupingListData fromJson(Map 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, + ); + } +} diff --git a/lib/serializers/mastodon/visibility_mastodon_extensions.dart b/lib/serializers/mastodon/visibility_mastodon_extensions.dart index 663b7a5..43748aa 100644 --- a/lib/serializers/mastodon/visibility_mastodon_extensions.dart +++ b/lib/serializers/mastodon/visibility_mastodon_extensions.dart @@ -1,5 +1,4 @@ -import 'package:relatica/models/circle_data.dart'; - +import '../../models/timeline_grouping_list_data.dart'; import '../../models/visibility.dart'; extension VisibilityMastodonExtensions on Visibility { @@ -15,7 +14,7 @@ extension VisibilityMastodonExtensions on Visibility { if (!onComment && hasDetails) { final circleId = allowedCircleIds.firstOrNull ?? allowedUserIds.firstOrNull; - if (circleId == CircleData.followersPseudoCircle.id) { + if (circleId == TimelineGroupingListData.followersPseudoCircle.id) { return 'private'; } diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 46fc351..c559950 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -11,9 +11,9 @@ import '../friendica_client/friendica_client.dart'; import '../friendica_client/paging_data.dart'; import '../globals.dart'; import '../models/auth/profile.dart'; -import '../models/circle_data.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../utils/active_profile_selector.dart'; import 'persistent_info_service.dart'; @@ -238,16 +238,20 @@ class ConnectionsManager extends ChangeNotifier { notifyListeners(); } - List getMyCircles() { + List getGroupingListData(GroupingType type) { if (circlesNotInitialized) { circlesNotInitialized = false; - _updateMyCircles(true); + _updateMyGroupingListData(true); } - return circlesRepo.getMyCircles(); + return circlesRepo + .getMyCircles() + .where((e) => e.groupingType == type) + .toList(); } - Result, ExecError> getCircleMembers(CircleData circle) { + Result, ExecError> getCircleMembers( + TimelineGroupingListData circle) { return circlesRepo .getCircleMembers(circle) .transform( @@ -258,8 +262,9 @@ class ConnectionsManager extends ChangeNotifier { .execErrorCast(); } - FutureResult createCircle(String newName) async { - final result = await CirclesClient(profile) + FutureResult createCircle( + String newName) async { + final result = await TimelineGroupingListClient(profile) .createCircle(newName) .withResultAsync((newCircle) async { circlesRepo.upsertCircle(newCircle); @@ -268,9 +273,9 @@ class ConnectionsManager extends ChangeNotifier { return result.execErrorCast(); } - FutureResult renameCircle( + FutureResult renameCircle( String id, String newName) async { - final result = await CirclesClient(profile) + final result = await TimelineGroupingListClient(profile) .renameCircle(id, newName) .withResultAsync((renamedCircle) async { circlesRepo.upsertCircle(renamedCircle); @@ -279,8 +284,9 @@ class ConnectionsManager extends ChangeNotifier { return result.execErrorCast(); } - FutureResult deleteCircle(CircleData circleData) async { - final result = await CirclesClient(profile) + FutureResult deleteCircle( + TimelineGroupingListData circleData) async { + final result = await TimelineGroupingListClient(profile) .deleteCircle(circleData) .withResultAsync((_) async { circlesRepo.deleteCircle(circleData); @@ -290,12 +296,12 @@ class ConnectionsManager extends ChangeNotifier { } void refreshCircles() { - _updateMyCircles(true); + _updateMyGroupingListData(true); } - Future refreshCircleMemberships(CircleData circle) async { + Future refreshCircleMemberships(TimelineGroupingListData circle) async { var page = PagingData(limit: 50); - final client = CirclesClient(profile); + final client = TimelineGroupingListClient(profile); final allResults = {}; var moreResults = true; while (moreResults) { @@ -319,7 +325,8 @@ class ConnectionsManager extends ChangeNotifier { notifyListeners(); } - Result, ExecError> getCirclesForUser(String id) { + Result, ExecError> getCirclesForUser( + String id) { final result = circlesRepo.getCirclesForUser(id); if (result.isSuccess) { _logger.finer("Circles for user $id: $result"); @@ -335,9 +342,9 @@ class ConnectionsManager extends ChangeNotifier { } FutureResult addUserToCircle( - CircleData circle, Connection connection) async { + TimelineGroupingListData circle, Connection connection) async { _logger.finest('Adding ${connection.name} to circle: ${circle.name}'); - return await CirclesClient(profile) + return await TimelineGroupingListClient(profile) .addConnectionToCircle(circle, connection) .withResultAsync((_) async => await refreshCircleMemberships(circle)) .withResult((_) => notifyListeners()) @@ -349,9 +356,9 @@ class ConnectionsManager extends ChangeNotifier { } FutureResult removeUserFromCircle( - CircleData circle, Connection connection) async { + TimelineGroupingListData circle, Connection connection) async { _logger.finest('Removing ${connection.name} from circle: ${circle.name}'); - return CirclesClient(profile) + return TimelineGroupingListClient(profile) .removeConnectionFromCircle(circle, connection) .withResultAsync((_) async => await refreshCircleMemberships(circle)) .withResult((_) => notifyListeners()) @@ -395,7 +402,7 @@ class ConnectionsManager extends ChangeNotifier { Connection connection, { bool withNotifications = true, }) async { - await _updateMyCircles(false); + await _updateMyGroupingListData(false); await _refreshCircleListData(connection.id, false); await _refreshConnection(connection, false); if (withNotifications) { @@ -405,7 +412,9 @@ class ConnectionsManager extends ChangeNotifier { Future _refreshCircleListData(String id, bool withNotification) async { _logger.finest('Refreshing member list data for Connection $id'); - await CirclesClient(profile).getMemberCirclesForConnection(id).match( + await TimelineGroupingListClient(profile) + .getMemberCirclesForConnection(id) + .match( onSuccess: (circles) { circlesRepo.updateConnectionCircleData(id, circles); if (withNotification) { @@ -436,9 +445,11 @@ class ConnectionsManager extends ChangeNotifier { ); } - Future _updateMyCircles(bool withNotification) async { + Future _updateMyGroupingListData(bool withNotification) async { _logger.finest('Refreshing my circles list'); - await CirclesClient(profile).getCircles().match( + await TimelineGroupingListClient(profile) + .getTimelineGroupingListData() + .match( onSuccess: (circles) { _logger.finest('Got updated circles:${circles.map((e) => e.name)}'); circlesRepo.clearMyCircles(); diff --git a/lib/services/timeline_manager.dart b/lib/services/timeline_manager.dart index fda1a2d..db9bb63 100644 --- a/lib/services/timeline_manager.dart +++ b/lib/services/timeline_manager.dart @@ -5,13 +5,13 @@ import 'package:result_monad/result_monad.dart'; import '../data/interfaces/circles_repo_intf.dart'; import '../friendica_client/friendica_client.dart'; import '../models/auth/profile.dart'; -import '../models/circle_data.dart'; import '../models/entry_tree_item.dart'; import '../models/exec_error.dart'; import '../models/image_entry.dart'; import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/timeline.dart'; import '../models/timeline_entry.dart'; +import '../models/timeline_grouping_list_data.dart'; import '../models/timeline_identifiers.dart'; import '../models/visibility.dart'; import 'entry_manager_service.dart'; @@ -42,19 +42,25 @@ class TimelineManager extends ChangeNotifier { notifyListeners(); } - Result, ExecError> getCircles() { + Result, ExecError> getTimelineGroupingListData( + GroupingType type) { if (circlesNotInitialized) { _refreshCircleData(); circlesNotInitialized = false; return Result.ok([]); } - return Result.ok(circlesRepo.getMyCircles()); + return Result.ok(circlesRepo + .getMyCircles() + .where((e) => e.groupingType == type) + .toList()); } Future _refreshCircleData() async { _logger.finer('Refreshing member circle data '); - await CirclesClient(profile).getCircles().match( + await TimelineGroupingListClient(profile) + .getTimelineGroupingListData() + .match( onSuccess: (circles) { circlesRepo.addAllCircles(circles); notifyListeners();