Commit 5c67a574 authored by Wilko Manger's avatar Wilko Manger

Refactor to use flutter_bloc

parent 11b1c238
......@@ -15,11 +15,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'src/app.dart';
import 'src/app_bloc.dart';
Future<void> main() async => (await AppBloc.create()).wrap(() => runApp(App()));
Future<void> main() => App.main();
......@@ -17,83 +17,86 @@
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/redirect.dart';
import 'package:provider/provider.dart';
import 'auth/bloc.dart';
import 'matrix.dart';
import 'notifications/bloc.dart';
import 'resources/localizations.dart';
import 'resources/theme.dart';
import 'section/initial/initial_page.dart';
import 'section/main/chat/chat_page.dart';
import 'section/main/chat/image/image_page.dart';
import 'section/main/chat/settings/chat_settings_page.dart';
import 'section/main/overview/chat_overview_page.dart';
import 'section/main/overview/create/group/create_group_details_page.dart';
import 'section/main/overview/create/group/create_group_members_page.dart';
import 'section/main/chat/page.dart';
import 'section/main/chat/image/page.dart';
import 'section/main/chat/settings/page.dart';
import 'section/main/chats/page.dart';
import 'section/main/chats/create/group/details_page.dart';
import 'section/main/chats/create/group/members_page.dart';
import 'section/main/settings/appearance_page.dart';
import 'section/main/settings/name_page.dart';
import 'section/main/settings/profile_page.dart';
import 'section/main/settings/settings_page.dart';
import 'section/main/settings/page.dart';
import 'section/start/advanced_page.dart';
import 'section/start/phase/identity/username_page.dart';
import 'section/start/phase/key/password_page.dart';
import 'section/start/start_page.dart';
import 'section/start/login/username/page.dart';
import 'sentry/bloc.dart';
final routes = {
Routes.root: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.root),
builder: (context) => InitialPage(),
builder: (context) => Redirect(),
),
Routes.settings: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settings),
builder: (context) => SettingsPage(),
builder: (context) => SettingsPage.withBloc(),
),
Routes.settingsProfile: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settingsProfile),
builder: (context) => ProfilePage(),
builder: (context) => ProfilePage.withGivenBloc(params),
),
Routes.settingsProfileName: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settingsProfileName),
builder: (context) => NamePage(),
builder: (context) => NamePage.withGivenBloc(params),
),
Routes.settingsAppearance: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settingsAppearance),
builder: (context) => AppearancePage(),
builder: (context) => AppearancePage.withGivenBloc(params),
),
Routes.chats: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chats),
builder: (context) =>
arguments is Room ? ChatPage(arguments) : ChatOverviewPage(),
builder: (context) => arguments is Room
? ChatPage.withBloc(arguments)
: ChatsPage.withBloc(),
),
Routes.chatsSettings: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chatsSettings),
builder: (context) => ChatSettingsPage(arguments),
builder: (context) => ChatSettingsPage.withBloc(arguments),
),
Routes.chatsNew: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chatsNew),
builder: (context) => CreateGroupMembersPage(),
builder: (context) => CreateGroupMembersPage.withBloc(),
),
Routes.chatsNewDetails: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chatsNewDetails),
builder: (context) => CreateGroupDetailsPage(),
builder: (context) => CreateGroupDetailsPage.withGivenBloc(arguments),
),
Routes.image: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.image),
builder: (context) => ImagePage(arguments)),
Routes.start: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.start),
builder: (context) => ImagePage.withBloc(arguments)),
Routes.login: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.login),
builder: (context) => StartPage(),
),
Routes.startAdvanced: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startAdvanced),
builder: (context) => AdvancedPage(),
Routes.loginAdvanced: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.loginAdvanced),
builder: (context) => AdvancedPage(bloc: params),
),
Routes.startUsername: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startUsername),
builder: (context) => UsernamePage(),
),
Routes.startPassword: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.startPassword),
builder: (context) => PasswordPage(),
Routes.loginUsername: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.loginUsername),
builder: (context) => UsernameLoginPage.withBloc(),
),
};
......@@ -109,17 +112,23 @@ class Routes {
static const chatsSettings = '/chats/settings';
static const image = '/image';
static const start = '/start';
static const startAdvanced = '/start/advanced';
static const startUsername = '/start/username';
static const startPassword = '/start/password';
static const login = '/login';
static const loginAdvanced = '/login/advanced';
static const loginUsername = '/login/username';
static const chatsNew = '/chats/new';
static const chatsNewDetails = '/chats/new/details';
}
class App extends StatelessWidget {
// This widget is the root of your application.
static Future<void> main() async => _sentryBloc.wrap(() => runApp(App()));
static String get buildType => DotEnv().env['BUILD_TYPE'];
static final _sentryBloc = SentryBloc();
final _authBloc = AuthBloc();
@override
Widget build(BuildContext context) {
return DynamicTheme(
......@@ -127,21 +136,45 @@ class App extends StatelessWidget {
data: (brightness) =>
brightness == Brightness.dark ? darkTheme : lightTheme,
themedWidgetBuilder: (context, theme) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => l(context).appName,
localizationsDelegates: [
const AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>.value(
value: _authBloc,
),
BlocProvider<SentryBloc>.value(
value: _sentryBloc,
),
],
initialRoute: Routes.root,
onGenerateRoute: (settings) {
return routes[settings.name](settings.arguments);
},
theme: theme,
child: Provider<Matrix>(
create: (_) => Matrix(_authBloc),
child: Builder(
builder: (BuildContext c) {
return BlocProvider<NotificationsBloc>(
create: (context) => NotificationsBloc(
matrix: Matrix.of(c),
authBloc: _authBloc,
),
child: MaterialApp(
onGenerateTitle: (BuildContext context) =>
l(context).appName,
localizationsDelegates: [
const AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
],
initialRoute: Routes.root,
onGenerateRoute: (settings) {
return routes[settings.name](settings.arguments);
},
theme: theme,
),
);
},
),
),
);
},
);
......
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:meta/meta.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/sentry.dart';
import 'package:respect_24_hour/respect_24_hour.dart';
import 'package:rxdart/rxdart.dart';
import 'package:pattle/src/di.dart' as di;
import 'notifications.dart' as notifs;
import 'sync_bloc.dart';
import 'storage.dart';
class AppBloc {
Sentry sentry;
final Storage storage;
final String _mayReportCrashesStorageKey = 'may_report_crashes';
static AppBloc _instance;
AppBloc._({@required this.storage});
factory AppBloc() => _instance;
static Future<AppBloc> create() async {
await DotEnv().load();
final use24Hour = await Respect24Hour.get24HourFormat;
di.registerUse24HourFormat(use24Hour);
await notifs.initialize();
_instance = AppBloc._(storage: await Storage.open());
return _instance;
}
Future<void> setPusher() async {
final user = di.getLocalUser();
await user.pushers.set(
HttpPusher(
appId: 'im.pattle.app',
appName: 'Pattle',
deviceName: user.currentDevice.name,
key: await notifs.getFirebaseToken(),
url: Uri.parse(DotEnv().env['PUSH_URL']),
),
);
}
final _loggedInSubj = BehaviorSubject<bool>();
Observable<bool> get loggedIn => _loggedInSubj.stream;
Future<void> checkIfLoggedIn() async {
di.registerStore();
final localUser = await LocalUser.fromStore(di.getStore());
if (localUser != null) {
di.registerLocalUser(localUser);
} else {
// Delete db if user could not be retrieved from store
await di.getStore().delete();
di.registerStore();
}
final loggedIn = localUser != null;
_loggedInSubj.add(loggedIn);
if (loggedIn) {
await notifyLogin();
}
}
Future<void> notifyLogin() async {
if (getMayReportCrashes() && sentry == null) {
sentry = await Sentry.create();
}
await syncBloc.start();
}
void wrap(Function run) => runZoned<Future<void>>(
() async => run(),
onError: (error, stackTrace) => sentry?.reportError(error, stackTrace),
);
String get build => DotEnv().env['BUILD_TYPE'];
bool _mayReportCrashes;
bool getMayReportCrashes({bool lookInStorage = true}) {
if (_mayReportCrashes == null && !lookInStorage) {
setMayReportCrashes(build != 'fdroid');
return _mayReportCrashes;
} else if (_mayReportCrashes != null) {
return _mayReportCrashes;
} else {
return storage[_mayReportCrashesStorageKey] as bool;
}
}
void setMayReportCrashes(value) {
_mayReportCrashes = value;
storage[_mayReportCrashesStorageKey] = value;
}
}
// Copyright (C) 2019 Wilko Manger
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
......@@ -15,30 +15,33 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:bloc/bloc.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:rxdart/rxdart.dart';
import 'package:pattle/src/di.dart' as di;
import 'package:pedantic/pedantic.dart';
import '../matrix.dart';
final syncBloc = SyncBloc();
import 'event.dart';
import 'state.dart';
class SyncBloc {
LocalUser _user = di.getLocalUser();
export 'event.dart';
export 'state.dart';
ReplaySubject<SyncState> _syncSubj = ReplaySubject<SyncState>(maxSize: 1);
Observable<SyncState> get stream => _syncSubj.stream;
class AuthBloc extends Bloc<AuthEvent, AuthState> {
@override
AuthState get initialState => Unchecked();
Future<void> start() async {
if (!_user.isSyncing) {
await _user.sendAllUnsent();
@override
Stream<AuthState> mapEventToState(AuthEvent event) async* {
if (event is Check) {
final user = await LocalUser.fromStore(Matrix.store);
unawaited(_user.startSync());
unawaited(_syncSubj.addStream(_user.sync));
if (user != null) {
yield Authenticated(user, fromStore: true);
} else {
yield NotAuthenticated();
}
} else if (event is LoggedIn) {
yield Authenticated(event.user, fromStore: false);
}
}
Future<void> stop() async {
_user.stopSync();
}
}
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
abstract class AuthEvent extends Equatable {
@override
List<Object> get props => [];
}
class Check extends AuthEvent {}
class LoggedIn extends AuthEvent {
final LocalUser user;
LoggedIn(this.user);
@override
List<Object> get props => [user];
}
import 'package:equatable/equatable.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:meta/meta.dart';
abstract class AuthState extends Equatable {
@override
List<Object> get props => [];
}
class Unchecked extends AuthState {}
class Authenticated extends AuthState {
final LocalUser user;
final bool fromStore;
Authenticated(this.user, {@required this.fromStore});
}
class NotAuthenticated extends AuthState {}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:matrix_sdk_sqflite/matrix_sdk_sqflite.dart';
import 'package:injector/injector.dart';
final inj = Injector();
Homeserver getHomeserver() => inj.getDependency<Homeserver>();
void registerHomeserver(Homeserver homeserver) {
inj.registerSingleton<Homeserver>((_) => homeserver, override: true);
}
Future<void> registerHomeserverWith(Uri url) async {
Homeserver hs;
try {
hs = await Homeserver.fromWellKnown(url);
} on WellKnownFailPromptException {
hs = Homeserver(url);
}
inj.registerSingleton<Homeserver>((_) => hs, override: true);
}
Store getStore() => inj.getDependency<Store>();
void registerStore() {
final store = SqfliteStore(path: 'matrix.sqlite');
inj.registerSingleton<Store>((_) => store, override: true);
}
LocalUser getLocalUser() => inj.getDependency<LocalUser>();
void registerLocalUser(LocalUser user) {
inj.registerSingleton<LocalUser>((_) => user, override: true);
registerHomeserver(user.homeserver);
}
const use24HourDependencyName = 'use24HourFormat';
bool getUse24HourFormat() =>
inj.getDependency<bool>(
dependencyName: use24HourDependencyName,
) ??
false;
void registerUse24HourFormat(bool use24HourFormat) {
inj.registerSingleton(
(_) => use24HourFormat,
override: true,
dependencyName: use24HourDependencyName,
);
}
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:matrix_sdk_sqflite/matrix_sdk_sqflite.dart';
import 'package:pattle/src/auth/bloc.dart';
import 'package:provider/provider.dart';
class Matrix {
static final store = SqfliteStore(path: 'pattle.sqlite');
// Used for listening to auth state changes
final AuthBloc _authBloc;
LocalUser _user;
LocalUser get user => _user;
Matrix(this._authBloc) {
_authBloc.listen(_processAuthState);
}
void _processAuthState(AuthState state) {
if (state is Authenticated) {
_user = state.user;
_user.startSync();
}
if (state is NotAuthenticated) {
_user?.stopSync();
_user = null;
}
}
static Matrix of(BuildContext context) => Provider.of<Matrix>(
context,
listen: false,
);
}
// Copyright (C) 2019 wilko
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/di.dart' as di;
import 'util/room.dart';
import 'util/user.dart';
import 'util/url.dart';
FirebaseMessaging _firebase;
FlutterLocalNotificationsPlugin _notifications;
Future<void> _initializeNotifications() async {
_notifications = FlutterLocalNotificationsPlugin();
await _notifications.initialize(
InitializationSettings(
AndroidInitializationSettings('ic_launcher_foreground'),
IOSInitializationSettings(),
),
);
}
final channelId = 'pattle';
final channelTitle = 'Pattle';
final channelDescription = 'Receive message from Pattle';
Future<void> initialize() async {
await _initializeNotifications();
_firebase = FirebaseMessaging()
..configure(
onMessage: _showNotification,
onBackgroundMessage: backgroundHandle,
);
}
Future<String> getFirebaseToken() => _firebase.getToken();
Future<void> _showNotification(Map<String, dynamic> message) async {
final roomId = RoomId(message['data']['room_id']);
final eventId = EventId(message['data']['event_id']);
final user = di.getLocalUser();
final room = await user.rooms[roomId];
final event = await room.timeline[eventId];
final senderName = event.sender.displayName;
final icon = await DefaultCacheManager().getSingleFile(
event.sender.avatarUrl.toThumbnailStringWith(user.homeserver),
);
final senderPerson = Person(
bot: false,
name: senderName,
icon: icon.path,
iconSource: IconSource.FilePath,
);
if (event is MessageEvent) {
final message = await fromEvent(event, senderPerson);
if (message == null) {
return;
}
await _notifications.show(
eventId.hashCode,
nameOf(room),
message.text,
NotificationDetails(
AndroidNotificationDetails(
channelId,
channelTitle,
channelDescription,
importance: Importance.Max,
priority: Priority.Max,
enableVibration: true,
playSound: true,
style: AndroidNotificationStyle.Messaging,
styleInformation: MessagingStyleInformation(
senderPerson,
conversationTitle: !room.isDirect ? await nameOf(room) : null,
groupConversation: room.isDirect,
messages: [message],
),
),
IOSNotificationDetails(),
),
);
}
}
Message fromEvent(RoomEvent event, Person person) {
if (event is EmoteMessageEvent) {
return Message(
'${event.sender.displayName} ${event.content.body}',
event.time,
person,
);
}
if (event is TextMessageEvent) {
return Message(
event.content.body,
event.time,
person,