ui: Migrate [AddContactPage] to Bloc

This commit is contained in:
PapaTutuWawa 2022-03-26 18:01:15 +01:00
parent 7a999d40d8
commit 489ef364c1
13 changed files with 221 additions and 80 deletions

View File

@ -139,6 +139,19 @@ files:
removed: removed:
type: List<String> type: List<String>
default: "[]" default: "[]"
# Triggered by the service in response to an [AddContactCommand].
- name: AddContactResultEvent
extends:
- BackgroundEvent
implements:
- JsonImplementation
attributes:
conversation:
type: Conversation?
deserialise: true
# Indicate if the conversation is new (true) or modified (false).
# Does not mean anything unless conversation != null.
added: bool
generate_builder: true generate_builder: true
builder_name: "Event" builder_name: "Event"
builder_baseclass: "BackgroundEvent" builder_baseclass: "BackgroundEvent"
@ -229,6 +242,13 @@ files:
preferences: preferences:
type: PreferencesState type: PreferencesState
deserialise: true deserialise: true
- name: AddContactCommand
extends:
- BackgroundCommand
implements:
- JsonImplementation
attributes:
jid: String
generate_builder: true generate_builder: true
# get${builder_Name}FromJson # get${builder_Name}FromJson
builder_name: "Command" builder_name: "Command"

View File

@ -4,8 +4,8 @@ import "package:moxxyv2/ui/constants.dart";
import "package:moxxyv2/ui/pages/register/register.dart"; import "package:moxxyv2/ui/pages/register/register.dart";
import "package:moxxyv2/ui/pages/postregister/postregister.dart"; import "package:moxxyv2/ui/pages/postregister/postregister.dart";
import "package:moxxyv2/ui/pages/sendfiles.dart"; import "package:moxxyv2/ui/pages/sendfiles.dart";
import "package:moxxyv2/ui/pages/addcontact/addcontact.dart";
*/ */
import "package:moxxyv2/ui/pages/addcontact/addcontact.dart";
import "package:moxxyv2/ui/pages/settings/debugging.dart"; import "package:moxxyv2/ui/pages/settings/debugging.dart";
import "package:moxxyv2/ui/pages/settings/privacy.dart"; import "package:moxxyv2/ui/pages/settings/privacy.dart";
import "package:moxxyv2/ui/pages/settings/network.dart"; import "package:moxxyv2/ui/pages/settings/network.dart";
@ -29,6 +29,7 @@ import "package:moxxyv2/ui/bloc/conversation_bloc.dart";
import "package:moxxyv2/ui/bloc/blocklist_bloc.dart"; import "package:moxxyv2/ui/bloc/blocklist_bloc.dart";
import "package:moxxyv2/ui/bloc/profile_bloc.dart"; import "package:moxxyv2/ui/bloc/profile_bloc.dart";
import "package:moxxyv2/ui/bloc/preferences_bloc.dart"; import "package:moxxyv2/ui/bloc/preferences_bloc.dart";
import "package:moxxyv2/ui/bloc/addcontact_bloc.dart";
import "package:moxxyv2/ui/service/download.dart"; import "package:moxxyv2/ui/service/download.dart";
import "package:moxxyv2/service/service.dart"; import "package:moxxyv2/service/service.dart";
import "package:moxxyv2/shared/commands.dart"; import "package:moxxyv2/shared/commands.dart";
@ -62,6 +63,7 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc()); GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc());
GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc()); GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc());
GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc()); GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc());
GetIt.I.registerSingleton<AddContactBloc>(AddContactBloc());
} }
// TODO: Replace all Column(children: [ Padding(), Padding, ...]) with a // TODO: Replace all Column(children: [ Padding(), Padding, ...]) with a
@ -104,6 +106,9 @@ void main() async {
), ),
BlocProvider<PreferencesBloc>( BlocProvider<PreferencesBloc>(
create: (_) => GetIt.I.get<PreferencesBloc>() create: (_) => GetIt.I.get<PreferencesBloc>()
),
BlocProvider<AddContactBloc>(
create: (_) => GetIt.I.get<AddContactBloc>()
) )
], ],
child: MyApp(navKey) child: MyApp(navKey)
@ -235,11 +240,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
networkRoute: (context) => const NetworkPage(), networkRoute: (context) => const NetworkPage(),
privacyRoute: (context) => const PrivacyPage(), privacyRoute: (context) => const PrivacyPage(),
debuggingRoute: (context) => DebuggingPage(), debuggingRoute: (context) => DebuggingPage(),
addContactRoute: (context) => AddContactPage(),
/* /*
registrationRoute: (context) => RegistrationPage(), registrationRoute: (context) => RegistrationPage(),
postRegistrationRoute: (context) => const PostRegistrationPage(), postRegistrationRoute: (context) => const PostRegistrationPage(),
sendFilesRoute: (context) => SendFilesPage(), sendFilesRoute: (context) => SendFilesPage(),
addContactRoute: (context) => AddContactPage(),
*/ */
}, },
home: Splashscreen() home: Splashscreen()

View File

@ -7,6 +7,7 @@ import "package:moxxyv2/service/preferences.dart";
import "package:moxxyv2/service/roster.dart"; import "package:moxxyv2/service/roster.dart";
import "package:moxxyv2/service/database.dart"; import "package:moxxyv2/service/database.dart";
import "package:moxxyv2/service/blocking.dart"; import "package:moxxyv2/service/blocking.dart";
import "package:moxxyv2/service/avatars.dart";
import "package:moxxyv2/xmpp/connection.dart"; import "package:moxxyv2/xmpp/connection.dart";
import "package:moxxyv2/xmpp/settings.dart"; import "package:moxxyv2/xmpp/settings.dart";
import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/jid.dart";
@ -207,3 +208,47 @@ Future<void> performSetPreferences(BaseEvent c, { dynamic extra }) async {
final command = c as SetPreferencesCommand; final command = c as SetPreferencesCommand;
GetIt.I.get<PreferencesService>().modifyPreferences((_) => command.preferences); GetIt.I.get<PreferencesService>().modifyPreferences((_) => command.preferences);
} }
Future<void> performAddContact(BaseEvent c, { dynamic extra }) async {
final command = c as AddContactCommand;
final id = extra as String;
final jid = command.jid;
final roster = GetIt.I.get<RosterService>();
if (await roster.isInRoster(jid)) {
sendEvent(AddContactResultEvent(conversation: null, added: false), id: id);
return;
}
final db = GetIt.I.get<DatabaseService>();
final conversation = await db.getConversationByJid(jid);
if (conversation != null) {
final c = await db.updateConversation(id: conversation.id, open: true);
sendEvent(
AddContactResultEvent(conversation: c, added: false),
id: id
);
} else {
final c = await db.addConversationFromData(
jid.split("@")[0],
"",
"",
jid,
0,
-1,
[],
true
);
sendEvent(
AddContactResultEvent(conversation: c, added: true),
id: id
);
}
roster.addToRosterWrapper("", jid, jid.split("@")[0]);
// Try to figure out an avatar
await GetIt.I.get<AvatarService>().subscribeJid(jid);
GetIt.I.get<AvatarService>().fetchAndUpdateAvatarForJid(jid);
}

View File

@ -106,6 +106,20 @@ JidFormatError validateJid(String jid) {
return JidFormatError.none; return JidFormatError.none;
} }
/// Returns an error string if [jid] is not a valid JID. Returns null if everything
/// appears okay.
String? validateJidString(String jid) {
switch (validateJid(jid)) {
case JidFormatError.empty: return "XMPP-Address cannot be empty";
case JidFormatError.noSeparator:
case JidFormatError.tooManySeparators: return "XMPP-Address must contain exactly one @";
// TODO: Find a better text
case JidFormatError.noDomain: return "A domain must follow the @";
case JidFormatError.noLocalpart: return "Your username must preceed the @";
case JidFormatError.none: return null;
}
}
/// Returns the first element in [items] which is non null. /// Returns the first element in [items] which is non null.
/// Returns null if they all are null. /// Returns null if they all are null.
T? firstNotNull<T>(List<T?> items) { T? firstNotNull<T>(List<T?> items) {

View File

@ -0,0 +1,66 @@
import "package:moxxyv2/shared/commands.dart";
import "package:moxxyv2/shared/events.dart";
import "package:moxxyv2/shared/helpers.dart";
import "package:moxxyv2/shared/backgroundsender.dart";
import "package:moxxyv2/ui/bloc/conversations_bloc.dart";
import "package:moxxyv2/ui/bloc/conversation_bloc.dart";
import "package:get_it/get_it.dart";
import "package:bloc/bloc.dart";
import "package:freezed_annotation/freezed_annotation.dart";
part "addcontact_state.dart";
part "addcontact_event.dart";
part "addcontact_bloc.freezed.dart";
class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
AddContactBloc() : super(AddContactState()) {
on<AddedContactEvent>(_onContactAdded);
on<JidChangedEvent>(_onJidChanged);
}
Future<void> _onContactAdded(AddedContactEvent event, Emitter<AddContactState> emit) async {
// TODO: Remove once we can disable the custom buttom
if (state.working) return;
final validation = validateJidString(state.jid);
if (validation != null) {
emit(state.copyWith(jidError: validation));
return;
}
emit(
state.copyWith(
working: true,
jidError: null
)
);
final result = await GetIt.I.get<BackgroundServiceDataSender>().sendData(
AddContactCommand(
jid: state.jid
)
) as AddContactResultEvent;
if (result.conversation != null) {
if (result.added) {
GetIt.I.get<ConversationsBloc>().add(ConversationsAddedEvent(result.conversation!));
} else {
GetIt.I.get<ConversationsBloc>().add(ConversationsUpdatedEvent(result.conversation!));
}
}
GetIt.I.get<ConversationBloc>().add(
RequestedConversationEvent(
result.conversation!.jid,
result.conversation!.title,
result.conversation!.avatarUrl,
removeUntilConversations: true
)
);
}
Future<void> _onJidChanged(JidChangedEvent event, Emitter<AddContactState> emit) async {
emit(state.copyWith(jid: event.jid));
}
}

View File

@ -0,0 +1,13 @@
part of "addcontact_bloc.dart";
abstract class AddContactEvent {}
/// Triggered when a new contact has been added by the UI
class AddedContactEvent extends AddContactEvent {}
/// Triggered by the UI when the JID input field is changed
class JidChangedEvent extends AddContactEvent {
final String jid;
JidChangedEvent(this.jid);
}

View File

@ -0,0 +1,10 @@
part of "addcontact_bloc.dart";
@freezed
class AddContactState with _$AddContactState {
factory AddContactState({
@Default("") String jid,
@Default(null) String? jidError,
@Default(false) bool working
}) = _AddContactState;
}

View File

@ -11,6 +11,7 @@ import "package:moxxyv2/ui/bloc/conversations_bloc.dart";
import "package:get_it/get_it.dart"; import "package:get_it/get_it.dart";
import "package:bloc/bloc.dart"; import "package:bloc/bloc.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:flutter/widgets.dart";
part "conversation_state.dart"; part "conversation_state.dart";
part "conversation_event.dart"; part "conversation_event.dart";
@ -47,11 +48,18 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
) )
); );
GetIt.I.get<NavigationBloc>().add( final navEvent = event.removeUntilConversations ? (
PushedNamedAndRemoveUntilEvent(
const NavigationDestination(conversationRoute),
ModalRoute.withName(conversationsRoute)
)
) : (
PushedNamedEvent( PushedNamedEvent(
const NavigationDestination(conversationRoute) const NavigationDestination(conversationRoute)
) )
); );
GetIt.I.get<NavigationBloc>().add(navEvent);
final result = await GetIt.I.get<BackgroundServiceDataSender>().sendData( final result = await GetIt.I.get<BackgroundServiceDataSender>().sendData(
GetMessagesForJidCommand( GetMessagesForJidCommand(

View File

@ -28,8 +28,16 @@ class RequestedConversationEvent extends ConversationEvent {
final String jid; final String jid;
final String title; final String title;
final String avatarUrl; final String avatarUrl;
final bool removeUntilConversations;
RequestedConversationEvent(this.jid, this.title, this.avatarUrl); RequestedConversationEvent(
this.jid,
this.title,
this.avatarUrl,
{
this.removeUntilConversations = false
}
);
} }
/// Triggered by the UI when a message is quoted /// Triggered by the UI when a message is quoted

View File

@ -1,5 +1,3 @@
import "package:moxxyv2/shared/events.dart";
import "package:moxxyv2/shared/backgroundsender.dart";
import "package:moxxyv2/shared/models/conversation.dart"; import "package:moxxyv2/shared/models/conversation.dart";
import "package:bloc/bloc.dart"; import "package:bloc/bloc.dart";

View File

@ -14,20 +14,6 @@ part "login_state.dart";
part "login_event.dart"; part "login_event.dart";
part "login_bloc.freezed.dart"; part "login_bloc.freezed.dart";
/// Returns an error string if [jid] is not a valid JID. Returns null if everything
/// appears okay.
String? _validateJid(String jid) {
switch (validateJid(jid)) {
case JidFormatError.empty: return "XMPP-Address cannot be empty";
case JidFormatError.noSeparator:
case JidFormatError.tooManySeparators: return "XMPP-Address must contain exactly one @";
// TODO: Find a better text
case JidFormatError.noDomain: return "A domain must follow the @";
case JidFormatError.noLocalpart: return "Your username must preceed the @";
case JidFormatError.none: return null;
}
}
class LoginBloc extends Bloc<LoginEvent, LoginState> { class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(LoginState()) { LoginBloc() : super(LoginState()) {
on<LoginJidChangedEvent>(_onJidChanged); on<LoginJidChangedEvent>(_onJidChanged);
@ -49,7 +35,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
} }
Future<void> _onSubmitted(LoginSubmittedEvent event, Emitter<LoginState> emit) async { Future<void> _onSubmitted(LoginSubmittedEvent event, Emitter<LoginState> emit) async {
final jidValidity = _validateJid(state.jid); final jidValidity = validateJidString(state.jid);
if (jidValidity != null) { if (jidValidity != null) {
return emit( return emit(
state.copyWith( state.copyWith(

View File

@ -4,14 +4,12 @@ import "package:moxxyv2/shared/helpers.dart";
import "package:moxxyv2/shared/models/roster.dart"; import "package:moxxyv2/shared/models/roster.dart";
import "package:moxxyv2/shared/models/conversation.dart"; import "package:moxxyv2/shared/models/conversation.dart";
import "package:moxxyv2/shared/backgroundsender.dart"; import "package:moxxyv2/shared/backgroundsender.dart";
import "package:moxxyv2/ui/constants.dart";
import "package:moxxyv2/ui/bloc/conversations_bloc.dart"; import "package:moxxyv2/ui/bloc/conversations_bloc.dart";
import "package:moxxyv2/ui/bloc/navigation_bloc.dart"; import "package:moxxyv2/ui/bloc/conversation_bloc.dart";
import "package:bloc/bloc.dart"; import "package:bloc/bloc.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:get_it/get_it.dart"; import "package:get_it/get_it.dart";
import "package:flutter/widgets.dart";
part "newconversation_state.dart"; part "newconversation_state.dart";
part "newconversation_event.dart"; part "newconversation_event.dart";
@ -38,14 +36,12 @@ class NewConversationBloc extends Bloc<NewConversationEvent, NewConversationStat
// Guard against an unneccessary roundtrip // Guard against an unneccessary roundtrip
if (listContains(conversations.state.conversations, (Conversation c) => c.jid == event.jid)) { if (listContains(conversations.state.conversations, (Conversation c) => c.jid == event.jid)) {
// TODO: Use the [ConversationBloc] GetIt.I.get<ConversationBloc>().add(
GetIt.I.get<NavigationBloc>().add( RequestedConversationEvent(
PushedNamedAndRemoveUntilEvent( event.jid,
NavigationDestination( event.title,
conversationRoute, event.avatarUrl,
//arguments: ConversationPageArguments(event.jid) removeUntilConversations: true
),
ModalRoute.withName(conversationsRoute)
) )
); );
return; return;
@ -68,20 +64,17 @@ class NewConversationBloc extends Bloc<NewConversationEvent, NewConversationStat
conversations.add(ConversationsAddedEvent(result.conversation)); conversations.add(ConversationsAddedEvent(result.conversation));
} }
// TODO: Use the [ConversationBloc] GetIt.I.get<ConversationBloc>().add(
GetIt.I.get<NavigationBloc>().add( RequestedConversationEvent(
PushedNamedAndRemoveUntilEvent( event.jid,
NavigationDestination( event.title,
conversationRoute, event.avatarUrl,
//arguments: ConversationPageArguments(event.jid) removeUntilConversations: true
),
ModalRoute.withName(conversationsRoute)
) )
); );
} }
Future<void> _onRosterItemRemoved(NewConversationRosterItemRemovedEvent event, Emitter<NewConversationState> emit) async { Future<void> _onRosterItemRemoved(NewConversationRosterItemRemovedEvent event, Emitter<NewConversationState> emit) async {
// TODO
return emit( return emit(
state.copyWith( state.copyWith(
roster: state.roster.where( roster: state.roster.where(

View File

@ -1,52 +1,25 @@
/* import "package:moxxyv2/ui/constants.dart";
import "package:moxxyv2/ui/helpers.dart";
import "package:moxxyv2/ui/widgets/topbar.dart"; import "package:moxxyv2/ui/widgets/topbar.dart";
import "package:moxxyv2/ui/widgets/textfield.dart"; import "package:moxxyv2/ui/widgets/textfield.dart";
import "package:moxxyv2/ui/widgets/button.dart"; import "package:moxxyv2/ui/widgets/button.dart";
import "package:moxxyv2/ui/constants.dart"; import "package:moxxyv2/ui/bloc/addcontact_bloc.dart";
import "package:moxxyv2/ui/helpers.dart";
import "package:moxxyv2/ui/redux/state.dart";
import "package:moxxyv2/ui/redux/addcontact/actions.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_redux/flutter_redux.dart"; import "package:flutter_bloc/flutter_bloc.dart";
class _AddContactPageViewModel {
final bool doingWork;
final String? errorText;
final void Function(String jid) addContact;
final void Function() resetErrors;
const _AddContactPageViewModel({ required this.addContact, required this.doingWork, required this.resetErrors, this.errorText });
}
// TODO: Reset the errorText using WillPopScope
class AddContactPage extends StatelessWidget { class AddContactPage extends StatelessWidget {
final TextEditingController _controller; const AddContactPage({ Key? key }) : super(key: key);
AddContactPage({ Key? key }) : _controller = TextEditingController(), super(key: key);
void _addToRoster(BuildContext context, _AddContactPageViewModel viewModel) {
if (_controller.text.isEmpty) return;
viewModel.resetErrors();
viewModel.addContact(_controller.text);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<MoxxyState, _AddContactPageViewModel>( return BlocBuilder<AddContactBloc, AddContactState>(
converter: (store) => _AddContactPageViewModel( builder: (context, state) => Scaffold(
doingWork: store.state.globalState.doingWork,
errorText: store.state.addContactErrorText,
addContact: (jid) => store.dispatch(AddContactAction(jid: jid)),
resetErrors: () => store.dispatch(AddContactSetErrorLogin())
),
builder: (context, viewModel) => Scaffold(
appBar: BorderlessTopbar.simple(title: "Add new contact"), appBar: BorderlessTopbar.simple(title: "Add new contact"),
body: Column( body: Column(
children: [ children: [
Visibility( Visibility(
visible: viewModel.doingWork, visible: state.working,
child: const LinearProgressIndicator(value: null) child: const LinearProgressIndicator(value: null)
), ),
@ -54,11 +27,14 @@ class AddContactPage extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8.0)), padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8.0)),
child: CustomTextField( child: CustomTextField(
maxLines: 1, maxLines: 1,
controller: _controller,
labelText: "XMPP-Address", labelText: "XMPP-Address",
onChanged: (value) => context.read<AddContactBloc>().add(
JidChangedEvent(value)
),
enabled: !state.working,
cornerRadius: textfieldRadiusRegular, cornerRadius: textfieldRadiusRegular,
contentPadding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0), contentPadding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0),
errorText: viewModel.errorText, errorText: state.jidError,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.qr_code), icon: const Icon(Icons.qr_code),
onPressed: () { onPressed: () {
@ -84,7 +60,7 @@ class AddContactPage extends StatelessWidget {
color: Colors.purple, color: Colors.purple,
child: const Text("Add to contacts"), child: const Text("Add to contacts"),
cornerRadius: 32.0, cornerRadius: 32.0,
onTap: () => _addToRoster(context, viewModel) onTap: () => context.read<AddContactBloc>().add(AddedContactEvent())
) )
) )
] ]
@ -96,4 +72,3 @@ class AddContactPage extends StatelessWidget {
); );
} }
} }
*/