Browse Source

neon: Separate notifications from other apps

pull/121/head
jld3103 2 years ago
parent
commit
627e8890b7
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 3
      packages/neon/integration_test/screenshot_test.dart
  2. 2
      packages/neon/lib/src/apps/news/app.dart
  3. 2
      packages/neon/lib/src/apps/notifications/app.dart
  4. 22
      packages/neon/lib/src/blocs/accounts.dart
  5. 66
      packages/neon/lib/src/blocs/apps.dart
  6. 10
      packages/neon/lib/src/blocs/apps.rxb.g.dart
  7. 3
      packages/neon/lib/src/neon.dart
  8. 84
      packages/neon/lib/src/pages/home.dart
  9. 47
      packages/neon/lib/src/widgets/app_implementation_icon.dart

3
packages/neon/integration_test/screenshot_test.dart

@ -486,7 +486,8 @@ Future main() async {
),
);
await prepareScreenshot(tester, binding);
await switchPage(tester, 'app-notifications');
await tester.tap(find.byKey(const Key('app-notifications')));
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.pump();

2
packages/neon/lib/src/apps/news/app.dart

@ -71,5 +71,5 @@ class NewsApp extends AppImplementation<NewsBloc, NewsAppSpecificOptions> {
);
@override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter;
BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) => appsBloc.getAppBloc<NewsBloc>(this).unreadCounter;
}

2
packages/neon/lib/src/apps/notifications/app.dart

@ -40,6 +40,6 @@ class NotificationsApp extends AppImplementation<NotificationsBloc, Notification
);
@override
BehaviorSubject<int>? getUnreadCounter(final AppsBloc appsBloc) =>
BehaviorSubject<int> getUnreadCounter(final AppsBloc appsBloc) =>
appsBloc.getAppBloc<NotificationsBloc>(this).unreadCounter;
}

22
packages/neon/lib/src/blocs/accounts.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/blocs/user_details.dart';
import 'package:neon/src/blocs/user_status.dart';
import 'package:neon/src/models/account.dart';
@ -127,18 +128,30 @@ class AccountsBloc extends $AccountsBloc {
);
AppsBloc getAppsBloc(final Account account) {
if (_accountsAppsBlocs[account.id] != null) {
return _accountsAppsBlocs[account.id]!;
if (_appsBlocs[account.id] != null) {
return _appsBlocs[account.id]!;
}
return _accountsAppsBlocs[account.id] = AppsBloc(
return _appsBlocs[account.id] = AppsBloc(
_requestManager,
getCapabilitiesBloc(account),
this,
account,
_allAppImplementations,
);
}
CapabilitiesBloc getCapabilitiesBloc(final Account account) {
if (_capabilitiesBlocs[account.id] != null) {
return _capabilitiesBlocs[account.id]!;
}
return _capabilitiesBlocs[account.id] = CapabilitiesBloc(
_requestManager,
account.client,
);
}
UserDetailsBloc getUserDetailsBloc(final Account account) {
if (_userDetailsBlocs[account.id] != null) {
return _userDetailsBlocs[account.id]!;
@ -176,7 +189,8 @@ class AccountsBloc extends $AccountsBloc {
late final _activeAccountSubject = BehaviorSubject<Account?>.seeded(null);
late final _accountsSubject = BehaviorSubject<List<Account>>.seeded([]);
final _accountsAppsBlocs = <String, AppsBloc>{};
final _appsBlocs = <String, AppsBloc>{};
final _capabilitiesBlocs = <String, CapabilitiesBloc>{};
final _userDetailsBlocs = <String, UserDetailsBloc>{};
final _userStatusBlocs = <String, UserStatusBloc>{};

66
packages/neon/lib/src/blocs/apps.dart

@ -1,7 +1,8 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:neon/src/apps/notifications/app.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/capabilities.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart';
@ -22,6 +23,8 @@ abstract class AppsBlocStates {
BehaviorSubject<Result<List<AppImplementation>>> get appImplementations;
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation;
BehaviorSubject<String?> get activeAppID;
}
@ -29,6 +32,7 @@ abstract class AppsBlocStates {
class AppsBloc extends $AppsBloc {
AppsBloc(
this._requestManager,
this._capabilitiesBloc,
this._accountsBloc,
this._account,
this._allAppImplementations,
@ -40,28 +44,31 @@ class AppsBloc extends $AppsBloc {
if (_activeAppSubject.valueOrNull != appId) {
_activeAppSubject.add(appId);
}
} else if (appId == 'notifications') {
// TODO: Open notifications page
} else {
debugPrint('App $appId not found');
throw Exception('App $appId not found');
}
});
_appsSubject.listen((final result) {
if (result is ResultLoading) {
_appImplementationsSubject.add(Result.loading());
} else if (result is ResultError) {
_appImplementationsSubject.add(Result.error((result as ResultError).error));
} else if (result is ResultSuccess) {
} else if (result is ResultError<List<NextcloudApp>>) {
_appImplementationsSubject.add(Result.error(result.error));
} else if (result is ResultSuccess<List<NextcloudApp>>) {
_appImplementationsSubject.add(
Result.success(_filteredAppImplementations((result as ResultSuccess<List<NextcloudApp>>).data)),
Result.success(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
);
} else if (result is ResultCached && result.data != null) {
} else if (result is ResultCached<List<NextcloudApp>>) {
_appImplementationsSubject.add(
ResultCached(_filteredAppImplementations((result as ResultCached<List<NextcloudApp>>).data)),
ResultCached(_filteredAppImplementations(result.data.map((final a) => a.id).toList())),
);
}
final appImplementations =
result.data != null ? _filteredAppImplementations(result.data!) : <AppImplementation>[];
final appImplementations = result.data != null
? _filteredAppImplementations(result.data!.map((final a) => a.id).toList())
: <AppImplementation>[];
if (result.data != null) {
final options = _accountsBloc.getOptions(_account);
@ -84,16 +91,39 @@ class AppsBloc extends $AppsBloc {
}
});
_capabilitiesBloc.capabilities.listen((final result) {
if (result is ResultLoading) {
_notificationsAppImplementationSubject.add(Result.loading());
} else if (result is ResultError<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(Result.error(result.error));
} else if (result is ResultSuccess<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
Result.success(
result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null,
),
);
} else if (result is ResultCached<CoreServerCapabilities_Ocs_Data>) {
_notificationsAppImplementationSubject.add(
ResultCached(result.data.capabilities.notifications != null ? _findAppImplementation('notifications') : null),
);
}
});
_loadApps();
}
final _extraApps = ['notifications'];
T? _findAppImplementation<T extends AppImplementation>(final String id) {
final matches = _filteredAppImplementations([id]);
if (matches.isNotEmpty) {
return matches.single as T;
}
List<AppImplementation> _filteredAppImplementations(final List<NextcloudApp> apps) {
final appIds = apps.map((final a) => a.id).toList();
return _allAppImplementations.where((final a) => appIds.contains(a.id) || _extraApps.contains(a.id)).toList();
return null;
}
List<AppImplementation> _filteredAppImplementations(final List<String> appIds) =>
_allAppImplementations.where((final a) => appIds.contains(a.id)).toList();
void _loadApps() {
_requestManager
.wrapNextcloud<List<NextcloudApp>, CoreNavigationApps>(
@ -107,12 +137,14 @@ class AppsBloc extends $AppsBloc {
}
final RequestManager _requestManager;
final CapabilitiesBloc _capabilitiesBloc;
final AccountsBloc _accountsBloc;
final Account _account;
final List<AppImplementation> _allAppImplementations;
final _appsSubject = BehaviorSubject<Result<List<NextcloudApp>>>();
final _appImplementationsSubject = BehaviorSubject<Result<List<AppImplementation>>>();
final _notificationsAppImplementationSubject = BehaviorSubject<Result<NotificationsApp?>>();
late final _activeAppSubject = BehaviorSubject<String?>();
final Map<String, RxBlocBase> _blocs = {};
@ -128,6 +160,8 @@ class AppsBloc extends $AppsBloc {
@override
void dispose() {
unawaited(_appsSubject.close());
unawaited(_appImplementationsSubject.close());
unawaited(_notificationsAppImplementationSubject.close());
unawaited(_activeAppSubject.close());
for (final key in _blocs.keys) {
_blocs[key]!.dispose();
@ -142,6 +176,10 @@ class AppsBloc extends $AppsBloc {
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState() => _appImplementationsSubject;
@override
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState() =>
_notificationsAppImplementationSubject;
@override
BehaviorSubject<String?> _mapToActiveAppIDState() => _activeAppSubject;
}

10
packages/neon/lib/src/blocs/apps.rxb.g.dart

@ -32,6 +32,11 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
late final BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_appImplementationsState = _mapToAppImplementationsState();
/// The state of [notificationsAppImplementation] implemented in
/// [_mapToNotificationsAppImplementationState]
late final BehaviorSubject<Result<NotificationsApp?>> _notificationsAppImplementationState =
_mapToNotificationsAppImplementationState();
/// The state of [activeAppID] implemented in [_mapToActiveAppIDState]
late final BehaviorSubject<String?> _activeAppIDState = _mapToActiveAppIDState();
@ -48,6 +53,9 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>> get appImplementations =>
_appImplementationsState;
@override
BehaviorSubject<Result<NotificationsApp?>> get notificationsAppImplementation => _notificationsAppImplementationState;
@override
BehaviorSubject<String?> get activeAppID => _activeAppIDState;
@ -56,6 +64,8 @@ abstract class $AppsBloc extends RxBlocBase implements AppsBlocEvents, AppsBlocS
BehaviorSubject<Result<List<AppImplementation<RxBlocBase, NextcloudAppSpecificOptions>>>>
_mapToAppImplementationsState();
BehaviorSubject<Result<NotificationsApp?>> _mapToNotificationsAppImplementationState();
BehaviorSubject<String?> _mapToActiveAppIDState();
@override

3
packages/neon/lib/src/neon.dart

@ -56,9 +56,9 @@ import 'package:window_manager/window_manager.dart';
import 'package:xdg_directories/xdg_directories.dart' as xdg;
part 'app.dart';
part 'pages/account_settings.dart';
part 'pages/home.dart';
part 'pages/login.dart';
part 'pages/account_settings.dart';
part 'pages/nextcloud_app_settings.dart';
part 'pages/settings.dart';
part 'platform/abstract.dart';
@ -88,6 +88,7 @@ part 'utils/validators.dart';
part 'widgets/account_avatar.dart';
part 'widgets/account_settings_tile.dart';
part 'widgets/account_tile.dart';
part 'widgets/app_implementation_icon.dart';
part 'widgets/cached_api_image.dart';
part 'widgets/cached_image.dart';
part 'widgets/cached_url_image.dart';

84
packages/neon/lib/src/pages/home.dart

@ -30,11 +30,8 @@ class _HomePageState extends State<HomePage> {
_globalOptions = Provider.of<GlobalOptions>(context, listen: false);
_appsBloc = RxBlocProvider.of<AccountsBloc>(context).getAppsBloc(widget.account);
_capabilitiesBloc = RxBlocProvider.of<AccountsBloc>(context).getCapabilitiesBloc(widget.account);
_capabilitiesBloc = CapabilitiesBloc(
Provider.of<RequestManager>(context, listen: false),
widget.account.client,
);
_capabilitiesBloc.capabilities.listen((final result) async {
if (result.data != null) {
widget.onThemeChanged(result.data!.capabilities.theming!);
@ -161,6 +158,16 @@ class _HomePageState extends State<HomePage> {
final appsError,
final appsLoading,
final _,
) =>
StandardRxResultBuilder<AppsBloc, NotificationsApp?>(
bloc: _appsBloc,
state: (final bloc) => bloc.notificationsAppImplementation,
builder: (
final context,
final notificationsAppData,
final notificationsAppError,
final notificationsAppLoading,
final _,
) =>
RxBlocBuilder<AppsBloc, String?>(
bloc: _appsBloc,
@ -342,27 +349,10 @@ class _HomePageState extends State<HomePage> {
onPressed: () {
_appsBloc.setActiveApp(appImplementation.id);
},
icon: Stack(
alignment: Alignment.bottomRight,
children: [
Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
height: kAvatarSize,
width: kAvatarSize,
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
],
],
icon: AppImplementationIcon(
appImplementation: appImplementation,
unreadCount: unreadCount,
color: Theme.of(context).colorScheme.primary,
),
),
);
@ -501,6 +491,49 @@ class _HomePageState extends State<HomePage> {
],
),
actions: [
if (notificationsAppData != null) ...[
StreamBuilder<int>(
stream: notificationsAppData.getUnreadCounter(_appsBloc),
builder: (final context, final unreadCounterSnapshot) {
final unreadCount = unreadCounterSnapshot.data ?? 0;
return IconButton(
key: Key('app-${notificationsAppData.id}'),
icon: AppImplementationIcon(
appImplementation: notificationsAppData,
unreadCount: unreadCount,
color: unreadCount > 0
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onBackground,
width: kAvatarSize * 2 / 3,
height: kAvatarSize * 2 / 3,
),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(notificationsAppData.name(context)),
if (accounts.length > 1) ...[
Text(
account.client.humanReadableID,
style: Theme.of(context).textTheme.bodySmall!,
),
],
],
),
),
body: notificationsAppData.buildPage(context, _appsBloc),
),
),
);
},
);
},
),
],
IconButton(
icon: IntrinsicWidth(
child: AccountAvatar(
@ -573,6 +606,7 @@ class _HomePageState extends State<HomePage> {
),
),
),
),
);
}
}

47
packages/neon/lib/src/widgets/app_implementation_icon.dart

@ -0,0 +1,47 @@
part of '../neon.dart';
class AppImplementationIcon extends StatelessWidget {
const AppImplementationIcon({
required this.appImplementation,
this.unreadCount = 0,
this.color,
this.width = kAvatarSize,
this.height = kAvatarSize,
super.key,
});
final AppImplementation appImplementation;
final int unreadCount;
final Color? color;
final double width;
final double height;
@override
Widget build(final BuildContext context) => Stack(
alignment: Alignment.bottomRight,
children: [
Container(
margin: const EdgeInsets.all(5),
child: appImplementation.buildIcon(
context,
height: height,
width: width,
color: color,
),
),
if (unreadCount > 0) ...[
Text(
unreadCount.toString(),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
],
);
}
Loading…
Cancel
Save