ui: Migrate the login to Redux + ConversationRepo -> RosterRepo
This commit is contained in:
parent
f83b465834
commit
f73b2e8558
@ -3,11 +3,11 @@ import 'ui/pages/conversation.dart';
|
||||
import 'ui/pages/conversations.dart';
|
||||
import 'ui/pages/profile.dart';
|
||||
import 'ui/pages/newconversation.dart';
|
||||
import 'ui/pages/login.dart';
|
||||
import 'ui/pages/login/login.dart';
|
||||
import 'ui/pages/register.dart';
|
||||
import 'ui/pages/intro.dart';
|
||||
import 'ui/pages/addcontact.dart';
|
||||
import 'repositories/conversations.dart';
|
||||
import 'repositories/roster.dart';
|
||||
|
||||
import 'package:flutter_redux/flutter_redux.dart';
|
||||
import 'package:redux/redux.dart';
|
||||
@ -17,7 +17,7 @@ import "redux/state.dart";
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
void main() {
|
||||
GetIt.I.registerSingleton<ConversationRepository>(ConversationRepository());
|
||||
GetIt.I.registerSingleton<RosterRepository>(RosterRepository());
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
7
lib/models/roster.dart
Normal file
7
lib/models/roster.dart
Normal file
@ -0,0 +1,7 @@
|
||||
class RosterItem {
|
||||
final String avatarUrl;
|
||||
final String jid;
|
||||
final String title;
|
||||
|
||||
RosterItem({ required this.avatarUrl, required this.jid, required this.title });
|
||||
}
|
7
lib/redux/login/actions.dart
Normal file
7
lib/redux/login/actions.dart
Normal file
@ -0,0 +1,7 @@
|
||||
// TODO: "Send" the login data to perform the actual login
|
||||
class PerformLoginAction {}
|
||||
|
||||
// TODO:
|
||||
class LoginSuccessfulAction {}
|
||||
|
||||
class TogglePasswordVisibilityAction {}
|
12
lib/redux/login/reducers.dart
Normal file
12
lib/redux/login/reducers.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import "package:moxxyv2/redux/login/actions.dart";
|
||||
import "package:moxxyv2/ui/pages/login/state.dart";
|
||||
|
||||
LoginPageState loginReducer(LoginPageState state, dynamic action) {
|
||||
if (action is PerformLoginAction) {
|
||||
return state.copyWith(doingWork: true);
|
||||
} else if (action is TogglePasswordVisibilityAction) {
|
||||
return state.copyWith(showPassword: !state.showPassword);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
@ -1,20 +1,24 @@
|
||||
import "dart:collection";
|
||||
import "package:moxxyv2/redux/conversation/reducers.dart";
|
||||
import "package:moxxyv2/redux/conversations/reducers.dart";
|
||||
import "package:moxxyv2/redux/login/reducers.dart";
|
||||
import "package:moxxyv2/ui/pages/login/state.dart";
|
||||
import "package:moxxyv2/models/message.dart";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
|
||||
MoxxyState moxxyReducer(MoxxyState state, dynamic action) {
|
||||
return MoxxyState(
|
||||
messages: messageReducer(state.messages, action),
|
||||
conversations: conversationReducer(state.conversations, action)
|
||||
conversations: conversationReducer(state.conversations, action),
|
||||
loginPageState: loginReducer(state.loginPageState, action)
|
||||
);
|
||||
}
|
||||
|
||||
class MoxxyState {
|
||||
final HashMap<String, List<Message>> messages;
|
||||
final List<Conversation> conversations;
|
||||
final LoginPageState loginPageState;
|
||||
|
||||
const MoxxyState({ required this.messages, required this.conversations });
|
||||
MoxxyState.initialState() : messages = HashMap(), conversations = List.empty(growable: true);
|
||||
const MoxxyState({ required this.messages, required this.conversations, required this.loginPageState });
|
||||
MoxxyState.initialState() : messages = HashMap(), conversations = List.empty(growable: true), loginPageState = LoginPageState(doingWork: false, showPassword: false);
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
import "dart:collection";
|
||||
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
|
||||
class ConversationRepository {
|
||||
HashMap<String, Conversation> _conversations = HashMap.from({
|
||||
// TODO: Remove
|
||||
"houshou.marine@hololive.tv": Conversation(
|
||||
title: "Houshou Marine",
|
||||
lastMessageBody: "",
|
||||
avatarUrl: "https://vignette.wikia.nocookie.net/virtualyoutuber/images/4/4e/Houshou_Marine_-_Portrait.png/revision/latest?cb=20190821035347",
|
||||
jid: "houshou.marine@hololive.tv",
|
||||
unreadCounter: 0
|
||||
),
|
||||
"nakiri.ayame@hololive.tv": Conversation(
|
||||
title: "Nakiri Ayame",
|
||||
lastMessageBody: "",
|
||||
avatarUrl: "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.pinimg.com%2Foriginals%2F2a%2F77%2F0a%2F2a770a77b0d873331583dfb88b05829f.jpg&f=1&nofb=1",
|
||||
jid: "nakiri.ayame@hololive.tv",
|
||||
unreadCounter: 0
|
||||
)
|
||||
});
|
||||
|
||||
ConversationRepository();
|
||||
|
||||
bool hasJid(String jid) => this._conversations.containsKey(jid);
|
||||
|
||||
Conversation? getConversation(String jid) => this._conversations[jid];
|
||||
void setConversation(Conversation conversation) {
|
||||
this._conversations[conversation.jid] = conversation;
|
||||
}
|
||||
|
||||
List<Conversation> getAllConversations() {
|
||||
return this._conversations.values.toList();
|
||||
}
|
||||
}
|
37
lib/repositories/roster.dart
Normal file
37
lib/repositories/roster.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import "dart:collection";
|
||||
|
||||
import "package:moxxyv2/models/roster.dart";
|
||||
|
||||
class RosterRepository {
|
||||
HashMap<String, RosterItem> _rosterItems = HashMap.from({
|
||||
// TODO: Remove
|
||||
"houshou.marine@hololive.tv": RosterItem(
|
||||
title: "Houshou Marine",
|
||||
avatarUrl: "https://vignette.wikia.nocookie.net/virtualyoutuber/images/4/4e/Houshou_Marine_-_Portrait.png/revision/latest?cb=20190821035347",
|
||||
jid: "houshou.marine@hololive.tv",
|
||||
),
|
||||
"nakiri.ayame@hololive.tv": RosterItem(
|
||||
title: "Nakiri Ayame",
|
||||
avatarUrl: "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.pinimg.com%2Foriginals%2F2a%2F77%2F0a%2F2a770a77b0d873331583dfb88b05829f.jpg&f=1&nofb=1",
|
||||
jid: "nakiri.ayame@hololive.tv",
|
||||
),
|
||||
"momosuzu.nene@hololive.tv": RosterItem(
|
||||
title: "Momosuzu Nene",
|
||||
avatarUrl: "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fstatic.miraheze.org%2Fhololivewiki%2Fthumb%2F3%2F36%2FMomosuzu_Nene_-_Portrait_01-1.png%2F580px-Momosuzu_Nene_-_Portrait_01-1.png&f=1&nofb=1",
|
||||
jid: "momosuzu.nene@hololive.tv",
|
||||
)
|
||||
});
|
||||
|
||||
RosterRepository();
|
||||
|
||||
bool hasJid(String jid) => this._rosterItems.containsKey(jid);
|
||||
|
||||
RosterItem? getRosterItem(String jid) => this._rosterItems[jid];
|
||||
void setRosterItem(RosterItem item) {
|
||||
this._rosterItems[item.jid] = item;
|
||||
}
|
||||
|
||||
List<RosterItem> getAllRosterItems() {
|
||||
return this._rosterItems.values.toList();
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import "package:moxxyv2/models/message.dart";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
import "package:moxxyv2/redux/state.dart";
|
||||
import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
import "package:moxxyv2/repositories/conversations.dart";
|
||||
import "package:moxxyv2/ui/pages/profile.dart";
|
||||
import "package:moxxyv2/ui/constants.dart";
|
||||
|
||||
@ -17,7 +16,6 @@ import 'package:get_it/get_it.dart';
|
||||
typedef SendMessageFunction = void Function(String body);
|
||||
|
||||
// TODO: Maybe use a PageView to combine ConversationsPage and ConversationPage
|
||||
|
||||
// TODO: Move to a separate file
|
||||
class ConversationPageArguments {
|
||||
final String jid;
|
||||
@ -27,9 +25,10 @@ class ConversationPageArguments {
|
||||
|
||||
class _MessageListViewModel {
|
||||
final List<Message> messages;
|
||||
final Conversation conversation;
|
||||
final SendMessageFunction sendMessage;
|
||||
|
||||
_MessageListViewModel({ required this.messages, required this.sendMessage });
|
||||
_MessageListViewModel({ required this.conversation, required this.messages, required this.sendMessage });
|
||||
}
|
||||
|
||||
class _ConversationPageState extends State<ConversationPage> {
|
||||
@ -86,14 +85,12 @@ class _ConversationPageState extends State<ConversationPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var args = ModalRoute.of(context)!.settings.arguments as ConversationPageArguments;
|
||||
|
||||
Conversation conversation = GetIt.I.get<ConversationRepository>().getConversation(args.jid)!;
|
||||
String jid = conversation.jid;
|
||||
String jid = args.jid;
|
||||
|
||||
return StoreConnector<MoxxyState, _MessageListViewModel>(
|
||||
converter: (store) => _MessageListViewModel(
|
||||
// TODO
|
||||
messages: store.state.messages.containsKey(jid) ? store.state.messages[jid]! : [],
|
||||
conversation: store.state.conversations.firstWhere((item) => item.jid == jid),
|
||||
sendMessage: (body) => store.dispatch(
|
||||
// TODO
|
||||
AddMessageAction(
|
||||
@ -104,145 +101,147 @@ class _ConversationPageState extends State<ConversationPage> {
|
||||
)
|
||||
)
|
||||
),
|
||||
builder: (context, viewModel) => Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(60),
|
||||
child: BorderlessTopbar(
|
||||
boxShadow: true,
|
||||
builder: (context, viewModel) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(60),
|
||||
child: BorderlessTopbar(
|
||||
boxShadow: true,
|
||||
children: [
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Icon(Icons.arrow_back)
|
||||
)
|
||||
),
|
||||
Center(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 16.0),
|
||||
child: CircleAvatar(
|
||||
// TODO
|
||||
backgroundImage: NetworkImage(viewModel.conversation.avatarUrl),
|
||||
radius: 25.0
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 2.0),
|
||||
child: Text(
|
||||
viewModel.conversation.title,
|
||||
style: TextStyle(
|
||||
fontSize: 20
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, "/conversation/profile", arguments: ProfilePageArguments(conversation: viewModel.conversation));
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Icon(Icons.arrow_back)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: viewModel.messages.length,
|
||||
itemBuilder: (context, index) => this._renderBubble(viewModel.messages, index)
|
||||
)
|
||||
),
|
||||
Center(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 16.0),
|
||||
child: CircleAvatar(
|
||||
// TODO
|
||||
backgroundImage: NetworkImage(conversation.avatarUrl),
|
||||
radius: 25.0
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 2.0),
|
||||
child: Text(
|
||||
conversation.title,
|
||||
style: TextStyle(
|
||||
fontSize: 20
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: BUBBLE_COLOR_SENT
|
||||
)
|
||||
),
|
||||
// TODO: Fix the TextField being too tall
|
||||
child: TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: this.controller,
|
||||
onChanged: this._onMessageTextChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Send a message...",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(5)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, "/conversation/profile", arguments: ProfilePageArguments(conversation: conversation));
|
||||
}
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8.0),
|
||||
// NOTE: https://stackoverflow.com/a/52786741
|
||||
// Thank you kind sir
|
||||
child: Container(
|
||||
height: 45.0,
|
||||
width: 45.0,
|
||||
child: FittedBox(
|
||||
child: SpeedDial(
|
||||
icon: this._showSendButton ? Icons.send : Icons.add,
|
||||
visible: true,
|
||||
curve: Curves.bounceInOut,
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
openCloseDial: this._isSpeedDialOpen,
|
||||
onPress: () {
|
||||
if (this._showSendButton) {
|
||||
this._onSendButtonPressed(viewModel);
|
||||
} else {
|
||||
this._isSpeedDialOpen.value = true;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.image),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Add Image"
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.photo_camera),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Take photo"
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.attach_file),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Add file"
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: viewModel.messages.length,
|
||||
itemBuilder: (context, index) => this._renderBubble(viewModel.messages, index)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: BUBBLE_COLOR_SENT
|
||||
)
|
||||
),
|
||||
// TODO: Fix the TextField being too tall
|
||||
child: TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: this.controller,
|
||||
onChanged: this._onMessageTextChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Send a message...",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(5)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8.0),
|
||||
// NOTE: https://stackoverflow.com/a/52786741
|
||||
// Thank you kind sir
|
||||
child: Container(
|
||||
height: 45.0,
|
||||
width: 45.0,
|
||||
child: FittedBox(
|
||||
child: SpeedDial(
|
||||
icon: this._showSendButton ? Icons.send : Icons.add,
|
||||
visible: true,
|
||||
curve: Curves.bounceInOut,
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
openCloseDial: this._isSpeedDialOpen,
|
||||
onPress: () {
|
||||
if (this._showSendButton) {
|
||||
this._onSendButtonPressed(viewModel);
|
||||
} else {
|
||||
this._isSpeedDialOpen.value = true;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.image),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Add Image"
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.photo_camera),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Take photo"
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(Icons.attach_file),
|
||||
onTap: () {},
|
||||
backgroundColor: BUBBLE_COLOR_SENT,
|
||||
// TODO: Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
label: "Add file"
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ class IntroPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 64.0).add(EdgeInsets.only(bottom: 64.0)),
|
||||
child: ElevatedButton(
|
||||
child: TextButton(
|
||||
child: Text("Register"),
|
||||
onPressed: () => Navigator.pushNamed(context, "/register")
|
||||
)
|
||||
|
@ -1,153 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
bool _doingWork = false;
|
||||
bool _showPassword = false;
|
||||
|
||||
void _navigateToConversations(BuildContext context) {
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
"/conversations",
|
||||
(route) => false
|
||||
);
|
||||
}
|
||||
|
||||
void _togglePasswordVisibility() {
|
||||
setState(() {
|
||||
this._showPassword = !this._showPassword;
|
||||
});
|
||||
}
|
||||
|
||||
void _performLogin(BuildContext context) {
|
||||
// TODO: Stub
|
||||
setState(() {
|
||||
this._doingWork = true;
|
||||
});
|
||||
|
||||
Future.delayed(Duration(seconds: 3), () => this._navigateToConversations(context));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(60),
|
||||
child: BorderlessTopbar(
|
||||
children: [
|
||||
BackButton(),
|
||||
Text(
|
||||
"Login",
|
||||
style: TextStyle(
|
||||
fontSize: 19
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
// TODO: The TextFields look a bit too smal
|
||||
// TODO: Hide the LinearProgressIndicator if we're not doing anything
|
||||
// TODO: Disable the inputs and the BackButton if we're working on loggin in
|
||||
body: Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: this._doingWork,
|
||||
child: LinearProgressIndicator(value: null)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.purple
|
||||
)
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: 1,
|
||||
enabled: !this._doingWork,
|
||||
decoration: InputDecoration(
|
||||
labelText: "XMPP-Address",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.purple
|
||||
)
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: 1,
|
||||
obscureText: this._showPassword,
|
||||
enabled: !this._doingWork,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Password",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0),
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 8.0),
|
||||
// TODO: Switch this icon depending on the state
|
||||
child: InkWell(
|
||||
onTap: () => this._togglePasswordVisibility(),
|
||||
child: Icon(
|
||||
this._showPassword ? Icons.visibility : Icons.visibility_off
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: ExpansionTile(
|
||||
title: Text("Advanced options"),
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text("Create account on server"),
|
||||
value: false,
|
||||
// TODO
|
||||
onChanged: this._doingWork ? null : (value) {}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: ElevatedButton(
|
||||
child: Text("Login"),
|
||||
onPressed: this._doingWork ? null : () => this._performLogin(context)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({ Key? key }) : super(key: key);
|
||||
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
}
|
156
lib/ui/pages/login/login.dart
Normal file
156
lib/ui/pages/login/login.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import "package:moxxyv2/redux/state.dart";
|
||||
import "package:moxxyv2/redux/login/actions.dart";
|
||||
|
||||
import 'package:flutter_redux/flutter_redux.dart';
|
||||
import 'package:redux/redux.dart';
|
||||
|
||||
class _LoginPageViewModel {
|
||||
final void Function() togglePasswordVisibility;
|
||||
final void Function() performLogin;
|
||||
final bool doingWork;
|
||||
final bool showPassword;
|
||||
|
||||
_LoginPageViewModel({ required this.togglePasswordVisibility, required this.performLogin, required this.doingWork, required this.showPassword });
|
||||
}
|
||||
|
||||
class LoginPage extends StatelessWidget {
|
||||
void _navigateToConversations(BuildContext context) {
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
"/conversations",
|
||||
(route) => false
|
||||
);
|
||||
}
|
||||
|
||||
void _performLogin(BuildContext context, _LoginPageViewModel viewModel) {
|
||||
// TODO: Remove
|
||||
Future.delayed(Duration(seconds: 3), () => this._navigateToConversations(context));
|
||||
|
||||
viewModel.performLogin();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StoreConnector<MoxxyState, _LoginPageViewModel>(
|
||||
converter: (store) => _LoginPageViewModel(
|
||||
togglePasswordVisibility: () => store.dispatch(TogglePasswordVisibilityAction()),
|
||||
performLogin: () => store.dispatch(PerformLoginAction()),
|
||||
doingWork: store.state.loginPageState.doingWork,
|
||||
showPassword: store.state.loginPageState.showPassword
|
||||
),
|
||||
builder: (context, viewModel) => Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(60),
|
||||
child: BorderlessTopbar(
|
||||
children: [
|
||||
BackButton(),
|
||||
Text(
|
||||
"Login",
|
||||
style: TextStyle(
|
||||
fontSize: 19
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
// TODO: The TextFields look a bit too smal
|
||||
// TODO: Hide the LinearProgressIndicator if we're not doing anything
|
||||
// TODO: Disable the inputs and the BackButton if we're working on loggin in
|
||||
body: Column(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: viewModel.doingWork,
|
||||
child: LinearProgressIndicator(value: null)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.purple
|
||||
)
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: 1,
|
||||
enabled: !viewModel.doingWork,
|
||||
decoration: InputDecoration(
|
||||
labelText: "XMPP-Address",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.purple
|
||||
)
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: 1,
|
||||
obscureText: !viewModel.showPassword,
|
||||
enabled: !viewModel.doingWork,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Password",
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(top: 4.0, bottom: 4.0, left: 8.0, right: 8.0),
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 8.0),
|
||||
child: InkWell(
|
||||
onTap: () => viewModel.togglePasswordVisibility(),
|
||||
child: Icon(
|
||||
viewModel.showPassword ? Icons.visibility : Icons.visibility_off
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: ExpansionTile(
|
||||
title: Text("Advanced options"),
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text("Create account on server"),
|
||||
value: false,
|
||||
// TODO
|
||||
onChanged: viewModel.doingWork ? null : (value) {}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: PADDING_VERY_LARGE).add(EdgeInsets.only(top: 8.0)),
|
||||
child: ElevatedButton(
|
||||
child: Text("Login"),
|
||||
onPressed: viewModel.doingWork ? null : () => this._performLogin(context, viewModel)
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
13
lib/ui/pages/login/state.dart
Normal file
13
lib/ui/pages/login/state.dart
Normal file
@ -0,0 +1,13 @@
|
||||
class LoginPageState {
|
||||
final bool doingWork;
|
||||
final bool showPassword;
|
||||
|
||||
LoginPageState({ required this.doingWork, required this.showPassword });
|
||||
|
||||
LoginPageState copyWith({ bool? doingWork, bool? showPassword }) {
|
||||
return LoginPageState(
|
||||
doingWork: doingWork ?? this.doingWork,
|
||||
showPassword: showPassword ?? this.showPassword
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||
import 'package:moxxyv2/ui/widgets/conversation.dart';
|
||||
import 'package:moxxyv2/models/roster.dart';
|
||||
import 'package:moxxyv2/models/conversation.dart';
|
||||
import 'package:moxxyv2/redux/state.dart';
|
||||
import 'package:moxxyv2/redux/conversations/actions.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation.dart';
|
||||
import 'package:moxxyv2/repositories/conversations.dart';
|
||||
import 'package:moxxyv2/repositories/roster.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
|
||||
import 'package:flutter_redux/flutter_redux.dart';
|
||||
@ -17,31 +18,27 @@ typedef AddConversationFunction = void Function(Conversation conversation);
|
||||
class _NewConversationViewModel {
|
||||
final AddConversationFunction addConversation;
|
||||
final List<Conversation> conversations;
|
||||
final List<RosterItem> roster;
|
||||
|
||||
_NewConversationViewModel({ required this.addConversation, required this.conversations });
|
||||
_NewConversationViewModel({ required this.conversations, required this.roster, required this.addConversation });
|
||||
}
|
||||
|
||||
class NewConversationPage extends StatelessWidget {
|
||||
void _addNewContact(_NewConversationViewModel viewModel, BuildContext context, String jid) {
|
||||
bool hasConversation = viewModel.conversations.length > 0 && viewModel.conversations.firstWhere((item) => item.jid == jid, orElse: null) != null;
|
||||
void _addNewContact(_NewConversationViewModel viewModel, BuildContext context, RosterItem rosterItem) {
|
||||
bool hasConversation = viewModel.conversations.length > 0 && viewModel.conversations.firstWhere((item) => item.jid == rosterItem.jid, orElse: null) != null;
|
||||
|
||||
// Prevent adding the same conversation twice to the list of open conversations
|
||||
if (!hasConversation) {
|
||||
Conversation? conversation = GetIt.I.get<ConversationRepository>().getConversation(jid);
|
||||
|
||||
if (conversation == null) {
|
||||
// TODO
|
||||
// TODO: Install a middleware to make sure that the conversation gets added to the
|
||||
// repository. Also handle updates
|
||||
conversation = Conversation(
|
||||
title: jid,
|
||||
jid: jid,
|
||||
lastMessageBody: "",
|
||||
avatarUrl: "",
|
||||
unreadCounter: 0
|
||||
);
|
||||
GetIt.I.get<ConversationRepository>().setConversation(conversation);
|
||||
}
|
||||
// TODO
|
||||
// TODO: Install a middleware to make sure that the conversation gets added to the
|
||||
// repository. Also handle updates
|
||||
Conversation conversation = Conversation(
|
||||
title: rosterItem.title,
|
||||
jid: rosterItem.jid,
|
||||
lastMessageBody: "",
|
||||
avatarUrl: rosterItem.avatarUrl,
|
||||
unreadCounter: 0
|
||||
);
|
||||
viewModel.addConversation(conversation);
|
||||
}
|
||||
|
||||
@ -50,12 +47,12 @@ class NewConversationPage extends StatelessWidget {
|
||||
context,
|
||||
"/conversation",
|
||||
ModalRoute.withName("/conversations"),
|
||||
arguments: ConversationPageArguments(jid: jid));
|
||||
arguments: ConversationPageArguments(jid: rosterItem.jid));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var conversations = GetIt.I.get<ConversationRepository>().getAllConversations();
|
||||
var roster = GetIt.I.get<RosterRepository>().getAllRosterItems();
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(60),
|
||||
@ -81,10 +78,11 @@ class NewConversationPage extends StatelessWidget {
|
||||
jid: c.jid
|
||||
)
|
||||
),
|
||||
conversations: store.state.conversations
|
||||
conversations: store.state.conversations,
|
||||
roster: GetIt.I.get<RosterRepository>().getAllRosterItems()
|
||||
),
|
||||
builder: (context, viewModel) => ListView.builder(
|
||||
itemCount: conversations.length + 2,
|
||||
itemCount: viewModel.roster.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
switch(index) {
|
||||
case 0: {
|
||||
@ -142,9 +140,9 @@ class NewConversationPage extends StatelessWidget {
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
Conversation item = conversations[index - 2];
|
||||
RosterItem item = viewModel.roster[index - 2];
|
||||
return InkWell(
|
||||
onTap: () => this._addNewContact(viewModel, context, item.jid),
|
||||
onTap: () => this._addNewContact(viewModel, context, item),
|
||||
child: ConversationsListRow(item.avatarUrl, item.title, item.jid, 0)
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user