ui: Migrate [AddContactPage] to Bloc
This commit is contained in:
parent
7a999d40d8
commit
489ef364c1
@ -139,6 +139,19 @@ files:
|
||||
removed:
|
||||
type: List<String>
|
||||
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
|
||||
builder_name: "Event"
|
||||
builder_baseclass: "BackgroundEvent"
|
||||
@ -229,6 +242,13 @@ files:
|
||||
preferences:
|
||||
type: PreferencesState
|
||||
deserialise: true
|
||||
- name: AddContactCommand
|
||||
extends:
|
||||
- BackgroundCommand
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
jid: String
|
||||
generate_builder: true
|
||||
# get${builder_Name}FromJson
|
||||
builder_name: "Command"
|
||||
|
@ -4,8 +4,8 @@ import "package:moxxyv2/ui/constants.dart";
|
||||
import "package:moxxyv2/ui/pages/register/register.dart";
|
||||
import "package:moxxyv2/ui/pages/postregister/postregister.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/privacy.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/profile_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/service/service.dart";
|
||||
import "package:moxxyv2/shared/commands.dart";
|
||||
@ -62,6 +63,7 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
||||
GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc());
|
||||
GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc());
|
||||
GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc());
|
||||
GetIt.I.registerSingleton<AddContactBloc>(AddContactBloc());
|
||||
}
|
||||
|
||||
// TODO: Replace all Column(children: [ Padding(), Padding, ...]) with a
|
||||
@ -104,6 +106,9 @@ void main() async {
|
||||
),
|
||||
BlocProvider<PreferencesBloc>(
|
||||
create: (_) => GetIt.I.get<PreferencesBloc>()
|
||||
),
|
||||
BlocProvider<AddContactBloc>(
|
||||
create: (_) => GetIt.I.get<AddContactBloc>()
|
||||
)
|
||||
],
|
||||
child: MyApp(navKey)
|
||||
@ -235,11 +240,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
networkRoute: (context) => const NetworkPage(),
|
||||
privacyRoute: (context) => const PrivacyPage(),
|
||||
debuggingRoute: (context) => DebuggingPage(),
|
||||
addContactRoute: (context) => AddContactPage(),
|
||||
/*
|
||||
registrationRoute: (context) => RegistrationPage(),
|
||||
postRegistrationRoute: (context) => const PostRegistrationPage(),
|
||||
sendFilesRoute: (context) => SendFilesPage(),
|
||||
addContactRoute: (context) => AddContactPage(),
|
||||
*/
|
||||
},
|
||||
home: Splashscreen()
|
||||
|
@ -7,6 +7,7 @@ import "package:moxxyv2/service/preferences.dart";
|
||||
import "package:moxxyv2/service/roster.dart";
|
||||
import "package:moxxyv2/service/database.dart";
|
||||
import "package:moxxyv2/service/blocking.dart";
|
||||
import "package:moxxyv2/service/avatars.dart";
|
||||
import "package:moxxyv2/xmpp/connection.dart";
|
||||
import "package:moxxyv2/xmpp/settings.dart";
|
||||
import "package:moxxyv2/xmpp/jid.dart";
|
||||
@ -207,3 +208,47 @@ Future<void> performSetPreferences(BaseEvent c, { dynamic extra }) async {
|
||||
final command = c as SetPreferencesCommand;
|
||||
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);
|
||||
}
|
||||
|
@ -106,6 +106,20 @@ JidFormatError validateJid(String jid) {
|
||||
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 null if they all are null.
|
||||
T? firstNotNull<T>(List<T?> items) {
|
||||
|
66
lib/ui/bloc/addcontact_bloc.dart
Normal file
66
lib/ui/bloc/addcontact_bloc.dart
Normal 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));
|
||||
}
|
||||
}
|
13
lib/ui/bloc/addcontact_event.dart
Normal file
13
lib/ui/bloc/addcontact_event.dart
Normal 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);
|
||||
}
|
10
lib/ui/bloc/addcontact_state.dart
Normal file
10
lib/ui/bloc/addcontact_state.dart
Normal 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;
|
||||
}
|
@ -11,6 +11,7 @@ import "package:moxxyv2/ui/bloc/conversations_bloc.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
part "conversation_state.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(
|
||||
const NavigationDestination(conversationRoute)
|
||||
)
|
||||
);
|
||||
|
||||
GetIt.I.get<NavigationBloc>().add(navEvent);
|
||||
|
||||
final result = await GetIt.I.get<BackgroundServiceDataSender>().sendData(
|
||||
GetMessagesForJidCommand(
|
||||
|
@ -28,8 +28,16 @@ class RequestedConversationEvent extends ConversationEvent {
|
||||
final String jid;
|
||||
final String title;
|
||||
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
|
||||
|
@ -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:bloc/bloc.dart";
|
||||
|
@ -14,20 +14,6 @@ part "login_state.dart";
|
||||
part "login_event.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> {
|
||||
LoginBloc() : super(LoginState()) {
|
||||
on<LoginJidChangedEvent>(_onJidChanged);
|
||||
@ -49,7 +35,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
|
||||
}
|
||||
|
||||
Future<void> _onSubmitted(LoginSubmittedEvent event, Emitter<LoginState> emit) async {
|
||||
final jidValidity = _validateJid(state.jid);
|
||||
final jidValidity = validateJidString(state.jid);
|
||||
if (jidValidity != null) {
|
||||
return emit(
|
||||
state.copyWith(
|
||||
|
@ -4,14 +4,12 @@ import "package:moxxyv2/shared/helpers.dart";
|
||||
import "package:moxxyv2/shared/models/roster.dart";
|
||||
import "package:moxxyv2/shared/models/conversation.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/navigation_bloc.dart";
|
||||
import "package:moxxyv2/ui/bloc/conversation_bloc.dart";
|
||||
|
||||
import "package:bloc/bloc.dart";
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
part "newconversation_state.dart";
|
||||
part "newconversation_event.dart";
|
||||
@ -38,14 +36,12 @@ class NewConversationBloc extends Bloc<NewConversationEvent, NewConversationStat
|
||||
|
||||
// Guard against an unneccessary roundtrip
|
||||
if (listContains(conversations.state.conversations, (Conversation c) => c.jid == event.jid)) {
|
||||
// TODO: Use the [ConversationBloc]
|
||||
GetIt.I.get<NavigationBloc>().add(
|
||||
PushedNamedAndRemoveUntilEvent(
|
||||
NavigationDestination(
|
||||
conversationRoute,
|
||||
//arguments: ConversationPageArguments(event.jid)
|
||||
),
|
||||
ModalRoute.withName(conversationsRoute)
|
||||
GetIt.I.get<ConversationBloc>().add(
|
||||
RequestedConversationEvent(
|
||||
event.jid,
|
||||
event.title,
|
||||
event.avatarUrl,
|
||||
removeUntilConversations: true
|
||||
)
|
||||
);
|
||||
return;
|
||||
@ -68,20 +64,17 @@ class NewConversationBloc extends Bloc<NewConversationEvent, NewConversationStat
|
||||
conversations.add(ConversationsAddedEvent(result.conversation));
|
||||
}
|
||||
|
||||
// TODO: Use the [ConversationBloc]
|
||||
GetIt.I.get<NavigationBloc>().add(
|
||||
PushedNamedAndRemoveUntilEvent(
|
||||
NavigationDestination(
|
||||
conversationRoute,
|
||||
//arguments: ConversationPageArguments(event.jid)
|
||||
),
|
||||
ModalRoute.withName(conversationsRoute)
|
||||
GetIt.I.get<ConversationBloc>().add(
|
||||
RequestedConversationEvent(
|
||||
event.jid,
|
||||
event.title,
|
||||
event.avatarUrl,
|
||||
removeUntilConversations: true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRosterItemRemoved(NewConversationRosterItemRemovedEvent event, Emitter<NewConversationState> emit) async {
|
||||
// TODO
|
||||
return emit(
|
||||
state.copyWith(
|
||||
roster: state.roster.where(
|
||||
|
@ -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/textfield.dart";
|
||||
import "package:moxxyv2/ui/widgets/button.dart";
|
||||
import "package:moxxyv2/ui/constants.dart";
|
||||
import "package:moxxyv2/ui/helpers.dart";
|
||||
import "package:moxxyv2/ui/redux/state.dart";
|
||||
import "package:moxxyv2/ui/redux/addcontact/actions.dart";
|
||||
import "package:moxxyv2/ui/bloc/addcontact_bloc.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 {
|
||||
final TextEditingController _controller;
|
||||
|
||||
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);
|
||||
}
|
||||
const AddContactPage({ Key? key }) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StoreConnector<MoxxyState, _AddContactPageViewModel>(
|
||||
converter: (store) => _AddContactPageViewModel(
|
||||
doingWork: store.state.globalState.doingWork,
|
||||
errorText: store.state.addContactErrorText,
|
||||
addContact: (jid) => store.dispatch(AddContactAction(jid: jid)),
|
||||
resetErrors: () => store.dispatch(AddContactSetErrorLogin())
|
||||
),
|
||||
builder: (context, viewModel) => Scaffold(
|
||||
return BlocBuilder<AddContactBloc, AddContactState>(
|
||||
builder: (context, state) => Scaffold(
|
||||
appBar: BorderlessTopbar.simple(title: "Add new contact"),
|
||||
body: Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: viewModel.doingWork,
|
||||
visible: state.working,
|
||||
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)),
|
||||
child: CustomTextField(
|
||||
maxLines: 1,
|
||||
controller: _controller,
|
||||
labelText: "XMPP-Address",
|
||||
onChanged: (value) => context.read<AddContactBloc>().add(
|
||||
JidChangedEvent(value)
|
||||
),
|
||||
enabled: !state.working,
|
||||
cornerRadius: textfieldRadiusRegular,
|
||||
contentPadding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0),
|
||||
errorText: viewModel.errorText,
|
||||
errorText: state.jidError,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.qr_code),
|
||||
onPressed: () {
|
||||
@ -84,7 +60,7 @@ class AddContactPage extends StatelessWidget {
|
||||
color: Colors.purple,
|
||||
child: const Text("Add to contacts"),
|
||||
cornerRadius: 32.0,
|
||||
onTap: () => _addToRoster(context, viewModel)
|
||||
onTap: () => context.read<AddContactBloc>().add(AddedContactEvent())
|
||||
)
|
||||
)
|
||||
]
|
||||
@ -96,4 +72,3 @@ class AddContactPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user