diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 4c77b9f..bf58000 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -283,6 +283,7 @@
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
"${BUILT_PRODUCTS_DIR}/Just/Just.framework",
"${BUILT_PRODUCTS_DIR}/PercentEncoder/PercentEncoder.framework",
+ "${BUILT_PRODUCTS_DIR}/image_picker_modern/image_picker_modern.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
);
@@ -293,6 +294,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Just.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PercentEncoder.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_modern.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
);
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5d05155..a9600aa 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -46,6 +46,10 @@
UIViewControllerBasedStatusBarAppearance
+ NSPhotoLibraryUsageDescription
+ Profile/Group picture
+ NSCameraUsageDescription
+ Selfie for profile/group picture
NSMicrophoneUsageDescription
$(PRODUCT_NAME) Microphone Usage!
diff --git a/lib/main.dart b/lib/main.dart
index a399de0..e0e19a9 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,8 +1,9 @@
import "package:flutter/services.dart";
import 'routes.dart';
-import "src/blocs/heartbeat_bloc.dart";
+// import 'package:flutter/rendering.dart';
void main() {
+ // debugPaintSizeEnabled = true;
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
Routes();
}
diff --git a/lib/routes.dart b/lib/routes.dart
index 616c012..a8316fa 100644
--- a/lib/routes.dart
+++ b/lib/routes.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
-import 'src/ui/home/home.dart';
+import 'src/ui/home.dart';
import "src/ui/login/welcome.dart";
import "src/services/login_manager.dart";
import 'themer.dart';
diff --git a/lib/src/blocs/conversation_bloc.dart b/lib/src/blocs/conversation_bloc.dart
index befa150..e929e7c 100644
--- a/lib/src/blocs/conversation_bloc.dart
+++ b/lib/src/blocs/conversation_bloc.dart
@@ -13,6 +13,7 @@ class ConversationsBloc {
fetchConversations() async {
List conversationList = await _provider.fetchConversations();
+ print(conversationList);
_conversationsFetcher.sink.add(conversationList);
}
diff --git a/lib/src/blocs/message_bloc.dart b/lib/src/blocs/message_bloc.dart
index 66c1601..c9d52c0 100644
--- a/lib/src/blocs/message_bloc.dart
+++ b/lib/src/blocs/message_bloc.dart
@@ -19,4 +19,4 @@ class MessageBloc {
}
// global instance for access throughout the app
-final messageBloc = MessageBloc();
+final messageChannel = MessageBloc();
diff --git a/lib/src/resources/conversation_api_provider.dart b/lib/src/resources/conversation_api_provider.dart
index 1d95a48..df0c00a 100644
--- a/lib/src/resources/conversation_api_provider.dart
+++ b/lib/src/resources/conversation_api_provider.dart
@@ -65,6 +65,17 @@ class ConversationApiProvider {
}
}
+ Future createConversationMember(
+ String conversationId, String userId) async {
+ final jwt = await loginManager.getToken();
+ await http.post("$baseUrlCore/user/conversation/$conversationId/member",
+ headers: {
+ HttpHeaders.contentTypeHeader: "application/json",
+ HttpHeaders.authorizationHeader: "Bearer $jwt"
+ },
+ body: jsonEncode({"id": userId}));
+ }
+
Future> fetchConversationMembers(String id) async {
final jwt = await loginManager.getToken();
try {
diff --git a/lib/src/ui/bottom_bar/bottom_bar.dart b/lib/src/ui/bottom_bar/bottom_bar.dart
index 3dcf18d..55aaf7e 100644
--- a/lib/src/ui/bottom_bar/bottom_bar.dart
+++ b/lib/src/ui/bottom_bar/bottom_bar.dart
@@ -18,8 +18,8 @@ class BottomBar extends StatelessWidget {
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40.0), topRight: Radius.circular(40.0)),
child: Container(
- padding: EdgeInsets.only(
- top: 20.0, left: 20.0, right: 20.0, bottom: 30.0 + bottomPadding),
+ padding:
+ EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0, bottom: 30.0),
child: (conversationId == "")
? ConversationInactiveView()
: ConversationActiveView(conversationId: conversationId),
diff --git a/lib/src/ui/bottom_bar/widgets/conversation_active_view.dart b/lib/src/ui/bottom_bar/widgets/conversation_active_view.dart
index 403171c..70c416a 100644
--- a/lib/src/ui/bottom_bar/widgets/conversation_active_view.dart
+++ b/lib/src/ui/bottom_bar/widgets/conversation_active_view.dart
@@ -18,7 +18,6 @@ class ConversationActiveView extends StatefulWidget {
}
class _ConversationActiveViewState extends State {
- final bus = messageBloc;
final conversationApiProvider = ConversationApiProvider();
Conversation _conversation;
List _users;
@@ -47,8 +46,8 @@ class _ConversationActiveViewState extends State {
print(users[0].id);
setState(() {
_users = users
- .map((user) =>
- UserAvatar(padding: EdgeInsets.only(right: 5.0), user: user))
+ .map((user) => UserAvatar(
+ radius: 18.0, padding: EdgeInsets.only(right: 5.0), user: user))
.toList();
});
});
@@ -67,8 +66,8 @@ class _ConversationActiveViewState extends State {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
- width: 22.0,
- height: 22.0,
+ width: 15.0,
+ height: 15.0,
decoration: BoxDecoration(
color: Theme.of(context).indicatorColor,
shape: BoxShape.circle)),
@@ -91,7 +90,8 @@ class _ConversationActiveViewState extends State {
icon: Icon(Icons.close),
onPressed: () async {
// Call method to close connection
- await bus.publish({"state": "disconnect"});
+ await messageChannel
+ .publish({"target": "home", "state": "disconnect"});
print("Pressed close");
}),
]),
diff --git a/lib/src/ui/bottom_bar/widgets/conversation_inactive_view.dart b/lib/src/ui/bottom_bar/widgets/conversation_inactive_view.dart
index b57eabd..e0d97f2 100644
--- a/lib/src/ui/bottom_bar/widgets/conversation_inactive_view.dart
+++ b/lib/src/ui/bottom_bar/widgets/conversation_inactive_view.dart
@@ -13,12 +13,15 @@ class ConversationInactiveView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
UserAvatar(
+ radius: 18,
padding: EdgeInsets.only(right: 5.0),
user: User("1", "Isaac", "Tay", "+65 91043593")),
UserAvatar(
+ radius: 18,
padding: EdgeInsets.only(right: 5.0),
user: User("1", "Isaac", "Tay", "+65 91043593")),
UserAvatar(
+ radius: 18,
padding: EdgeInsets.only(right: 5.0),
user: User("1", "Isaac", "Tay", "+65 91043593"))
])
diff --git a/lib/src/ui/contact_tab/contact_tab.dart b/lib/src/ui/contact_tab/contact_tab.dart
new file mode 100644
index 0000000..73c0773
--- /dev/null
+++ b/lib/src/ui/contact_tab/contact_tab.dart
@@ -0,0 +1,42 @@
+import "package:flutter/material.dart";
+
+import "./widgets/home_view.dart";
+import "../../blocs/message_bloc.dart";
+
+class ContactTab extends StatefulWidget {
+ @override
+ State createState() {
+ return _ContactTabState();
+ }
+}
+
+class _ContactTabState extends State {
+ final GlobalKey navigatorKey =
+ new GlobalKey();
+
+ @override
+ initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Navigator(
+ initialRoute: "contact/home",
+ onGenerateRoute: (RouteSettings settings) {
+ WidgetBuilder builder;
+ switch (settings.name) {
+ case "contact/home":
+ builder = (BuildContext _) => HomeView();
+ break;
+ case "contact/new":
+ builder = (BuildContext _) => Center(child: Text("SOON"));
+ break;
+ default:
+ throw Exception("Invalid route: ${settings.name}");
+ }
+ return MaterialPageRoute(builder: builder, settings: settings);
+ },
+ );
+ }
+}
diff --git a/lib/src/ui/contact_tab/widgets/home_view.dart b/lib/src/ui/contact_tab/widgets/home_view.dart
new file mode 100644
index 0000000..e52d0a0
--- /dev/null
+++ b/lib/src/ui/contact_tab/widgets/home_view.dart
@@ -0,0 +1,145 @@
+import "package:flutter/material.dart";
+import 'package:sticky_headers/sticky_headers.dart';
+
+import "../../../models/user_model.dart";
+import "../../../blocs/contact_bloc.dart";
+
+import "../../widgets/contact_item.dart";
+import "../../widgets/top_bar.dart";
+import "../../widgets/search_input.dart";
+import "../../widgets/small_text_button.dart";
+import "../../widgets/list_button.dart";
+
+class HomeView extends StatefulWidget {
+ @override
+ State createState() {
+ return _HomeViewState();
+ }
+}
+
+class _HomeViewState extends State {
+ final searchController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ contactBloc.fetchContacts();
+ }
+
+ @override
+ void dispose() {
+ searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: [
+ TopBar(
+ title: "Contacts",
+ search: SearchInput(
+ controller: searchController, hintText: "Search for people"),
+ children: [
+ SmallTextButton(
+ text: "Edit",
+ onClickCallback: () {
+ print("hello");
+ }),
+ Spacer(),
+ IconButton(
+ icon: Icon(Icons.add),
+ onPressed: () {
+ Navigator.pushNamed(context, "contact/new");
+ }),
+ ]),
+ Expanded(
+ child: StreamBuilder(
+ stream: contactBloc.contacts,
+ builder: (context, AsyncSnapshot> snapshot) {
+ if (snapshot.hasData) {
+ return buildList(snapshot);
+ } else if (snapshot.hasError) {
+ return Text(snapshot.error.toString());
+ }
+ return Center(child: CircularProgressIndicator());
+ }))
+ ]);
+ }
+
+ Widget buildList(AsyncSnapshot> snapshot) {
+ final Map> sortedList = {
+ "A": null,
+ "B": null,
+ "C": null,
+ "D": null,
+ "E": null,
+ "F": null,
+ "G": null,
+ "H": null,
+ "I": null,
+ "J": null,
+ "K": null,
+ "L": null,
+ "M": null,
+ "N": null,
+ "O": null,
+ "P": null,
+ "Q": null,
+ "R": null,
+ "S": null,
+ "T": null,
+ "U": null,
+ "V": null,
+ "W": null,
+ "X": null,
+ "Y": null,
+ "Z": null
+ };
+
+ // Sort the list into alphabets
+ sortedList.forEach((letter, list) {
+ sortedList[letter] = snapshot.data
+ .where((user) => user.firstName.startsWith(letter))
+ .toList();
+ });
+
+ // Create list of children
+ final children = sortedList.entries.map((entry) {
+ if (entry.value.length == 0) {
+ return Container();
+ }
+
+ return Column(mainAxisSize: MainAxisSize.min, children: [
+ StickyHeader(
+ header: Container(
+ height: 21.0,
+ color: Colors.grey[200],
+ padding: EdgeInsets.symmetric(horizontal: 15.0),
+ alignment: Alignment.centerLeft,
+ child: Text(entry.key,
+ style: Theme.of(context)
+ .primaryTextTheme
+ .display1
+ .copyWith(color: Theme.of(context).primaryColorDark)),
+ ),
+ content: ListView.builder(
+ physics: const NeverScrollableScrollPhysics(),
+ padding: EdgeInsets.only(top: 0.0),
+ shrinkWrap: true,
+ itemCount: entry.value.length,
+ itemBuilder: (context, index) {
+ return ContactItem(user: entry.value[index]);
+ }))
+ ]);
+ }).toList();
+
+ children.insertAll(0, [
+ ListButton(
+ icon: Icons.people_outline,
+ text: "Invite Friends",
+ onClickCallback: () {}),
+ ]);
+
+ return ListView(padding: EdgeInsets.only(top: 0.0), children: children);
+ }
+}
diff --git a/lib/src/ui/contact_tab/widgets/new_contact_view.dart b/lib/src/ui/contact_tab/widgets/new_contact_view.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/src/ui/conversation_tab/conversation_tab.dart b/lib/src/ui/conversation_tab/conversation_tab.dart
new file mode 100644
index 0000000..c0b8eb8
--- /dev/null
+++ b/lib/src/ui/conversation_tab/conversation_tab.dart
@@ -0,0 +1,45 @@
+import "package:flutter/material.dart";
+
+import "./widgets/home_view.dart";
+import "./widgets/new_conversation_view.dart";
+import "./widgets/new_group_view.dart";
+import "./widgets/new_group_info_view.dart";
+
+import "../../models/user_model.dart";
+
+class ConversationTab extends StatefulWidget {
+ @override
+ State createState() {
+ return _ConversationTabState();
+ }
+}
+
+class _ConversationTabState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Navigator(
+ initialRoute: "conversation/home",
+ onGenerateRoute: (RouteSettings settings) {
+ WidgetBuilder builder;
+ switch (settings.name) {
+ case "conversation/home":
+ builder = (BuildContext _) => HomeView();
+ break;
+ case "conversation/new":
+ builder = (BuildContext _) => NewConversationView();
+ break;
+ case "conversation/new/group":
+ builder = (BuildContext _) => NewGroupView();
+ break;
+ case "conversation/new/groupinfo":
+ final List users = settings.arguments;
+ builder = (BuildContext _) => NewGroupInfoView(users: users);
+ break;
+ default:
+ throw Exception("Invalid route: ${settings.name}");
+ }
+ return MaterialPageRoute(builder: builder, settings: settings);
+ },
+ );
+ }
+}
diff --git a/lib/src/ui/conversation_tab/widgets/home_view.dart b/lib/src/ui/conversation_tab/widgets/home_view.dart
new file mode 100644
index 0000000..bdc29af
--- /dev/null
+++ b/lib/src/ui/conversation_tab/widgets/home_view.dart
@@ -0,0 +1,80 @@
+import "package:flutter/material.dart";
+
+import "../../../models/conversation_model.dart";
+import "../../../blocs/conversation_bloc.dart";
+
+import "../../widgets/conversation_item.dart";
+import "../../widgets/top_bar.dart";
+import "../../widgets/search_input.dart";
+import "../../widgets/small_text_button.dart";
+
+class HomeView extends StatefulWidget {
+ @override
+ State createState() {
+ return _HomeViewState();
+ }
+}
+
+class _HomeViewState extends State {
+ final searchController = TextEditingController();
+
+ @override
+ initState() {
+ super.initState();
+ conversationsBloc.fetchConversations();
+ }
+
+ @override
+ dispose() {
+ searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: [
+ TopBar(
+ title: "Conversations",
+ search: SearchInput(
+ controller: searchController, hintText: "Search for people"),
+ children: [
+ SmallTextButton(
+ text: "Edit",
+ onClickCallback: () {
+ print("hello");
+ }),
+ Spacer(),
+ IconButton(
+ icon: Icon(Icons.add_comment),
+ onPressed: () {
+ Navigator.pushNamed(context, "conversation/new");
+ }),
+ ]),
+ Expanded(
+ child:
+ ListView(padding: EdgeInsets.only(top: 10.0), children: [
+ StreamBuilder(
+ stream: conversationsBloc.conversations,
+ builder: (context, AsyncSnapshot> snapshot) {
+ if (snapshot.hasData) {
+ return buildList(snapshot.data);
+ } else if (snapshot.hasError) {
+ return Text(snapshot.error.toString());
+ }
+ return Center(child: CircularProgressIndicator());
+ })
+ ]))
+ ]);
+ }
+
+ Widget buildList(List data) {
+ return ListView.builder(
+ padding: EdgeInsets.only(top: 0.0),
+ shrinkWrap: true,
+ itemCount: data.length,
+ itemBuilder: (context, index) {
+ return ConversationItem(conversation: data[index]);
+ },
+ );
+ }
+}
diff --git a/lib/src/ui/conversation_tab/widgets/new_conversation_view.dart b/lib/src/ui/conversation_tab/widgets/new_conversation_view.dart
new file mode 100644
index 0000000..f7abb1f
--- /dev/null
+++ b/lib/src/ui/conversation_tab/widgets/new_conversation_view.dart
@@ -0,0 +1,141 @@
+import "package:flutter/material.dart";
+import 'package:sticky_headers/sticky_headers.dart';
+
+import "../../../models/user_model.dart";
+import "../../../blocs/contact_bloc.dart";
+
+import "../../widgets/contact_item.dart";
+import "../../widgets/top_bar.dart";
+import "../../widgets/search_input.dart";
+import "../../widgets/list_button.dart";
+
+class NewConversationView extends StatefulWidget {
+ @override
+ State createState() {
+ return _NewConversationViewState();
+ }
+}
+
+class _NewConversationViewState extends State {
+ final searchController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ contactBloc.fetchContacts();
+ }
+
+ @override
+ void dispose() {
+ searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: [
+ TopBar(
+ title: "New Conversation",
+ search: SearchInput(
+ controller: searchController, hintText: "Search for people"),
+ children: [
+ IconButton(
+ icon: Icon(Icons.arrow_back),
+ onPressed: () {
+ Navigator.pop(context);
+ }),
+ Spacer(),
+ ]),
+ Expanded(
+ child: StreamBuilder(
+ stream: contactBloc.contacts,
+ builder: (context, AsyncSnapshot> snapshot) {
+ if (snapshot.hasData) {
+ return buildList(snapshot);
+ } else if (snapshot.hasError) {
+ return Text(snapshot.error.toString());
+ }
+ return Center(child: CircularProgressIndicator());
+ }))
+ ]);
+ }
+
+ Widget buildList(AsyncSnapshot> snapshot) {
+ final Map> sortedList = {
+ "A": null,
+ "B": null,
+ "C": null,
+ "D": null,
+ "E": null,
+ "F": null,
+ "G": null,
+ "H": null,
+ "I": null,
+ "J": null,
+ "K": null,
+ "L": null,
+ "M": null,
+ "N": null,
+ "O": null,
+ "P": null,
+ "Q": null,
+ "R": null,
+ "S": null,
+ "T": null,
+ "U": null,
+ "V": null,
+ "W": null,
+ "X": null,
+ "Y": null,
+ "Z": null
+ };
+
+ // Sort the list into alphabets
+ sortedList.forEach((letter, list) {
+ sortedList[letter] = snapshot.data
+ .where((user) => user.firstName.startsWith(letter))
+ .toList();
+ });
+
+ // Create list of children
+ final children = sortedList.entries.map((entry) {
+ if (entry.value.length == 0) {
+ return Container();
+ }
+
+ return Column(mainAxisSize: MainAxisSize.min, children: [
+ StickyHeader(
+ header: Container(
+ height: 21.0,
+ color: Colors.grey[200],
+ padding: EdgeInsets.symmetric(horizontal: 15.0),
+ alignment: Alignment.centerLeft,
+ child: Text(entry.key,
+ style: Theme.of(context)
+ .primaryTextTheme
+ .display1
+ .copyWith(color: Theme.of(context).primaryColorDark)),
+ ),
+ content: ListView.builder(
+ physics: const NeverScrollableScrollPhysics(),
+ padding: EdgeInsets.only(top: 0.0),
+ shrinkWrap: true,
+ itemCount: entry.value.length,
+ itemBuilder: (context, index) {
+ return ContactItem(user: entry.value[index]);
+ }))
+ ]);
+ }).toList();
+
+ children.insertAll(0, [
+ ListButton(
+ icon: Icons.group_add,
+ text: "New Group",
+ onClickCallback: () {
+ Navigator.pushNamed(context, "conversation/new/group");
+ }),
+ ]);
+
+ return ListView(padding: EdgeInsets.only(top: 0.0), children: children);
+ }
+}
diff --git a/lib/src/ui/conversation_tab/widgets/new_group_info_view.dart b/lib/src/ui/conversation_tab/widgets/new_group_info_view.dart
new file mode 100644
index 0000000..bbe99af
--- /dev/null
+++ b/lib/src/ui/conversation_tab/widgets/new_group_info_view.dart
@@ -0,0 +1,139 @@
+import "package:flutter/material.dart";
+import 'package:image_picker_modern/image_picker_modern.dart';
+import "dart:io";
+
+import "../../../models/user_model.dart";
+
+import "../../../resources/conversation_api_provider.dart";
+
+import "../../widgets/contact_item.dart";
+import "../../widgets/top_bar.dart";
+import "../../widgets/small_text_button.dart";
+import "../../widgets/list_button.dart";
+
+class NewGroupInfoView extends StatefulWidget {
+ final List users;
+
+ NewGroupInfoView({@required this.users});
+
+ @override
+ State createState() {
+ return _NewGroupInfoViewState();
+ }
+}
+
+class _NewGroupInfoViewState extends State {
+ final descriptionController = TextEditingController();
+ final nameController = TextEditingController();
+ final conversationApiProvider = ConversationApiProvider();
+
+ File _image;
+
+ @override
+ void dispose() {
+ descriptionController.dispose();
+ nameController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: [
+ TopBar(title: "New Group", children: [
+ IconButton(
+ icon: Icon(Icons.arrow_back),
+ onPressed: () {
+ Navigator.pop(context);
+ }),
+ Spacer(),
+ SmallTextButton(
+ text: "Create",
+ onClickCallback: () async {
+ final conversation = await conversationApiProvider
+ .createConversation(nameController.text);
+
+ widget.users.forEach((user) async => await conversationApiProvider
+ .createConversationMember(conversation.id, user.id));
+ Navigator.pushNamed(context, "conversation/home");
+ })
+ ]),
+ Padding(
+ padding: EdgeInsets.only(left: 15.0, right: 15.0, top: 10.0),
+ child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: <
+ Widget>[
+ (_image != null)
+ ? CircleAvatar(radius: 50, backgroundImage: FileImage(_image))
+ : CircleAvatar(radius: 50, backgroundColor: Colors.grey[300]),
+ Flexible(
+ child:
+ Column(mainAxisSize: MainAxisSize.min, children: [
+ Container(
+ margin: EdgeInsets.only(left: 8.0),
+ padding: EdgeInsets.only(left: 10.0, right: 10.0),
+ color: Colors.grey[100],
+ child: TextField(
+ controller: nameController,
+ autocorrect: false,
+ cursorWidth: 2.0,
+ cursorColor: Colors.grey[500],
+ style: Theme.of(context).textTheme.subtitle.copyWith(
+ color: Colors.grey[500], fontWeight: FontWeight.w300),
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ filled: false,
+ hintText: "Enter group name",
+ hintStyle: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(color: Colors.grey[500])))),
+ Container(
+ margin: EdgeInsets.only(left: 8.0, top: 5.0),
+ padding: EdgeInsets.only(left: 10.0, right: 10.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(5.0),
+ color: Colors.grey[100],
+ ),
+ child: TextField(
+ controller: descriptionController,
+ autocorrect: false,
+ maxLines: 3,
+ cursorWidth: 2.0,
+ cursorColor: Colors.grey[500],
+ style: Theme.of(context).textTheme.subtitle.copyWith(
+ color: Colors.grey[500],
+ fontWeight: FontWeight.w300,
+ ),
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ filled: false,
+ hintText: "Enter group description",
+ hintStyle: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(color: Colors.grey[500])))),
+ ])),
+ ])),
+ Padding(
+ padding: EdgeInsets.only(top: 10.0),
+ child: ListButton(
+ icon: Icons.insert_photo,
+ text: "Add a group photo",
+ onClickCallback: () async {
+ var image =
+ await ImagePicker.pickImage(source: ImageSource.gallery);
+
+ setState(() {
+ _image = image;
+ });
+ })),
+ Expanded(
+ child: Padding(
+ padding: EdgeInsets.only(top: 0.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: widget.users
+ .map((user) => ContactItem(user: user))
+ .toList())))
+ ]);
+ }
+}
diff --git a/lib/src/ui/conversation_tab/widgets/new_group_view.dart b/lib/src/ui/conversation_tab/widgets/new_group_view.dart
new file mode 100644
index 0000000..0c3119e
--- /dev/null
+++ b/lib/src/ui/conversation_tab/widgets/new_group_view.dart
@@ -0,0 +1,167 @@
+import "package:flutter/material.dart";
+import 'package:sticky_headers/sticky_headers.dart';
+
+import "../../../models/user_model.dart";
+import "../../../blocs/contact_bloc.dart";
+
+import "../../widgets/contact_item.dart";
+import "../../widgets/top_bar.dart";
+import "../../widgets/search_input.dart";
+import "../../widgets/small_text_button.dart";
+import "../../widgets/user_chip.dart";
+
+class NewGroupView extends StatefulWidget {
+ @override
+ State createState() {
+ return _NewGroupViewState();
+ }
+}
+
+class _NewGroupViewState extends State {
+ final searchController = TextEditingController();
+
+ List selectedUsers = [];
+
+ @override
+ void initState() {
+ super.initState();
+ contactBloc.fetchContacts();
+ }
+
+ @override
+ void dispose() {
+ searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(children: [
+ TopBar(
+ title: "New Group",
+ children: [
+ IconButton(
+ icon: Icon(Icons.arrow_back),
+ onPressed: () {
+ Navigator.pop(context);
+ }),
+ Spacer(),
+ SmallTextButton(
+ text: "Next",
+ onClickCallback: () {
+ if (selectedUsers.length <= 1) {
+ return;
+ } else {
+ Navigator.pushNamed(context, "conversation/new/groupinfo",
+ arguments: selectedUsers);
+ }
+ })
+ ],
+ search: SearchInput(
+ controller: searchController, hintText: "Search for people"),
+ ),
+ Expanded(
+ child: StreamBuilder(
+ stream: contactBloc.contacts,
+ builder: (context, AsyncSnapshot> snapshot) {
+ if (snapshot.hasData) {
+ return buildList(snapshot);
+ } else if (snapshot.hasError) {
+ return Text(snapshot.error.toString());
+ }
+ return Center(child: CircularProgressIndicator());
+ }))
+ ]);
+ }
+
+ Widget buildList(AsyncSnapshot> snapshot) {
+ final Map> sortedList = {
+ "A": null,
+ "B": null,
+ "C": null,
+ "D": null,
+ "E": null,
+ "F": null,
+ "G": null,
+ "H": null,
+ "I": null,
+ "J": null,
+ "K": null,
+ "L": null,
+ "M": null,
+ "N": null,
+ "O": null,
+ "P": null,
+ "Q": null,
+ "R": null,
+ "S": null,
+ "T": null,
+ "U": null,
+ "V": null,
+ "W": null,
+ "X": null,
+ "Y": null,
+ "Z": null
+ };
+
+ // Sort the list into alphabets
+ sortedList.forEach((letter, list) {
+ sortedList[letter] = snapshot.data
+ .where((user) => user.firstName.startsWith(letter))
+ .toList();
+ });
+
+ // Create list of children
+ final children = sortedList.entries.map((entry) {
+ if (entry.value.length == 0) {
+ return Container();
+ }
+
+ return Column(mainAxisSize: MainAxisSize.min, children: [
+ StickyHeader(
+ header: Container(
+ height: 21.0,
+ color: Colors.grey[200],
+ padding: EdgeInsets.symmetric(horizontal: 15.0),
+ alignment: Alignment.centerLeft,
+ child: Text(entry.key,
+ style: Theme.of(context)
+ .primaryTextTheme
+ .display1
+ .copyWith(color: Theme.of(context).primaryColorDark)),
+ ),
+ content: ListView.builder(
+ physics: const NeverScrollableScrollPhysics(),
+ padding: EdgeInsets.only(top: 0.0),
+ shrinkWrap: true,
+ itemCount: entry.value.length,
+ itemBuilder: (context, index) {
+ return ContactItem(
+ user: entry.value[index],
+ selectable: true,
+ onClickCallback: (selected) {
+ setState(() {
+ if (selected) {
+ selectedUsers.add(entry.value[index]);
+ } else {
+ selectedUsers.remove(entry.value[index]);
+ }
+ });
+ });
+ }))
+ ]);
+ }).toList();
+
+ children.insertAll(0, [
+ Padding(
+ padding: EdgeInsets.only(left: 15.0, right: 15.0),
+ child: Wrap(
+ spacing: 5.0,
+ runSpacing: 0.0,
+ children:
+ selectedUsers.map((user) => UserChip(user: user)).toList(),
+ ))
+ ]);
+ return ListView(padding: EdgeInsets.only(top: 0.0), children: children);
+ }
+}
diff --git a/lib/src/ui/home.dart b/lib/src/ui/home.dart
new file mode 100644
index 0000000..e044170
--- /dev/null
+++ b/lib/src/ui/home.dart
@@ -0,0 +1,119 @@
+import "package:flutter/material.dart";
+import 'dart:ui' as ui;
+
+import "./conversation_tab/conversation_tab.dart";
+import "./contact_tab/contact_tab.dart";
+import "./bottom_bar/bottom_bar.dart";
+import "../services/heartbeat_manager.dart";
+import "../services/conversation_manager.dart";
+import "../blocs/message_bloc.dart";
+
+class Home extends StatefulWidget {
+ @override
+ _HomeState createState() => _HomeState();
+}
+
+class _HomeState extends State with SingleTickerProviderStateMixin {
+ final GlobalKey _scaffoldKey = new GlobalKey();
+ final heartbeatSendManager = HeartbeatSendManager();
+ final conversationManager = ConversationManager();
+
+ // Bottom Bar navigation
+ int _tabNumber = 1;
+ List itemsList = [
+ Icons.contacts,
+ Icons.chat,
+ Icons.settings,
+ ];
+ TabController controller;
+
+ // Current conversaton
+ String currentConversationId = "";
+
+ @override
+ initState() {
+ super.initState();
+ controller = TabController(vsync: this, length: 3);
+ controller.index = 1; // Set default page to conversation page
+
+ messageChannel.bus.listen(
+ (Map data) async => await _processMessage(data));
+ }
+
+ @override
+ dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ _processMessage(Map data) async {
+ if (data["target"] == "home") {
+ if (data["state"] == "disconnect") {
+ // Disconnect and change state
+ await conversationManager.exit();
+ setState(() {
+ currentConversationId = "";
+ });
+ } else if (data["state"] == "connect") {
+ // Connect and change state
+ await conversationManager.join(data["conversationId"]);
+ setState(() {
+ currentConversationId = data["conversationId"];
+ });
+ } else {
+ // show default
+ await conversationManager.exit();
+ setState(() {
+ currentConversationId = "";
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ key: _scaffoldKey,
+ body: TabBarView(
+ physics: NeverScrollableScrollPhysics(),
+ controller: controller,
+ children: [
+ ContactTab(),
+ ConversationTab(),
+ Container(),
+ ]),
+ bottomNavigationBar:
+ Column(mainAxisSize: MainAxisSize.min, children: [
+ BottomBar(conversationId: currentConversationId),
+ BottomNavigationBar(
+ onTap: (int index) {
+ setState(() {
+ _tabNumber = index;
+ controller.index = _tabNumber;
+ });
+ },
+ items: itemsList.map((data) {
+ return BottomNavigationBarItem(
+ icon: itemsList[_tabNumber] == data
+ ? ShaderMask(
+ blendMode: BlendMode.srcIn,
+ shaderCallback: (Rect bounds) {
+ return ui.Gradient.linear(
+ Offset(4.0, 24.0),
+ Offset(24.0, 4.0),
+ [
+ Theme.of(context).primaryColor,
+ Theme.of(context).primaryColorDark,
+ ],
+ );
+ },
+ child: Icon(data, size: 25.0),
+ )
+ : Icon(data, color: Colors.grey, size: 20),
+ title: Container(),
+ );
+ }).toList())
+ ]),
+ );
+ }
+}
diff --git a/lib/src/ui/home/home.dart b/lib/src/ui/home/home.dart
deleted file mode 100644
index b7c5a33..0000000
--- a/lib/src/ui/home/home.dart
+++ /dev/null
@@ -1,95 +0,0 @@
-import "package:flutter/material.dart";
-
-import "./widgets/top_bar.dart";
-import "./widgets/conversation_list.dart";
-import "./widgets/contact_list.dart";
-import "../bottom_bar/bottom_bar.dart";
-import "../../services/heartbeat_manager.dart";
-import "../../services/conversation_manager.dart";
-import "../../blocs/message_bloc.dart";
-import "../../blocs/heartbeat_bloc.dart";
-
-class Home extends StatefulWidget {
- @override
- _HomeState createState() => _HomeState();
-}
-
-class _HomeState extends State {
- final List titleList = ["Conversations", "Contacts", "Settings"];
-
- final GlobalKey _scaffoldKey = new GlobalKey();
- final PageController controller = PageController();
- final heartbeatSendManager = HeartbeatSendManager();
- final conversationManager = ConversationManager();
-
- int _pageNumber = 0;
-
- PersistentBottomSheetController bottomBarController;
-
- @override
- initState() {
- super.initState();
- controller.addListener(_updatePageNumber);
-
- messageBloc.bus.listen(
- (Map data) async => await _processMessage(data));
- _setupBottomState();
- }
-
- @override
- dispose() {
- controller.dispose();
- super.dispose();
- }
-
- _updatePageNumber() {
- setState(() {
- _pageNumber = controller.page.round();
- });
- }
-
- _processMessage(Map data) async {
- if (data["state"] == "disconnect") {
- // Disconnect and change state
- await conversationManager.exit();
- bottomBarController.close();
- bottomBarController = _scaffoldKey.currentState.showBottomSheet(
- (BuildContext context) => BottomBar(conversationId: ""));
- } else if (data["state"] == "connect") {
- // Connect and change state
- await conversationManager.join(data["conversationId"]);
- bottomBarController.close();
- bottomBarController = _scaffoldKey.currentState.showBottomSheet(
- (BuildContext context) =>
- BottomBar(conversationId: data["conversationId"]));
- } else {
- // show default
- await conversationManager.exit();
- bottomBarController.close();
- bottomBarController = _scaffoldKey.currentState.showBottomSheet(
- (BuildContext context) => BottomBar(conversationId: ""));
- }
- }
-
- _setupBottomState() {
- conversationManager.get().then((conversationId) {
- bottomBarController = _scaffoldKey.currentState.showBottomSheet(
- (BuildContext context) => BottomBar(conversationId: conversationId));
- });
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- key: _scaffoldKey,
- body: Column(children: [
- TopBar(title: titleList[_pageNumber], pageNumber: _pageNumber),
- Expanded(
- child: PageView(controller: controller, children: [
- ConversationList(),
- ContactList(),
- ])),
- ]),
- );
- }
-}
diff --git a/lib/src/ui/home/widgets/contact_item.dart b/lib/src/ui/home/widgets/contact_item.dart
deleted file mode 100644
index cb8af16..0000000
--- a/lib/src/ui/home/widgets/contact_item.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import "package:flutter/material.dart";
-
-import "../../../models/user_model.dart";
-import "../../widgets/user_avatar.dart";
-
-class ContactItem extends StatelessWidget {
- final User user;
-
- ContactItem({@required this.user});
-
- @override
- Widget build(BuildContext context) {
- return ListTile(
- contentPadding:
- EdgeInsets.only(top: 0.0, left: 20.0, right: 20.0, bottom: 0.0),
- leading: Row(mainAxisSize: MainAxisSize.min, children: [
- /*Icon(Icons.star, color: Theme.of(context).primaryColorDark),*/
- Text("A",
- style: TextStyle(
- fontSize: 16.0,
- fontWeight: FontWeight.w700,
- color: Theme.of(context).primaryColorDark)),
- UserAvatar(
- user: user, radius: 22.0, padding: EdgeInsets.only(left: 20.0))
- ]),
- title: Text(user.firstName + " " + user.lastName,
- style: Theme.of(context).textTheme.display2,
- overflow: TextOverflow.ellipsis),
- subtitle: Text("Last seen just now",
- style: Theme.of(context).textTheme.subtitle),
- onTap: () => {});
- }
-}
diff --git a/lib/src/ui/home/widgets/contact_list.dart b/lib/src/ui/home/widgets/contact_list.dart
deleted file mode 100644
index 9161465..0000000
--- a/lib/src/ui/home/widgets/contact_list.dart
+++ /dev/null
@@ -1,45 +0,0 @@
-import "package:flutter/material.dart";
-
-import "../../../models/user_model.dart";
-import "../../../blocs/contact_bloc.dart";
-
-import "contact_item.dart";
-
-class ContactList extends StatefulWidget {
- @override
- State createState() {
- return _ContactListState();
- }
-}
-
-class _ContactListState extends State {
- @override
- void initState() {
- super.initState();
- contactBloc.fetchContacts();
- }
-
- @override
- Widget build(BuildContext context) {
- return StreamBuilder(
- stream: contactBloc.contacts,
- builder: (context, AsyncSnapshot> snapshot) {
- if (snapshot.hasData) {
- return buildList(snapshot);
- } else if (snapshot.hasError) {
- return Text(snapshot.error.toString());
- }
- return Center(child: CircularProgressIndicator());
- });
- }
-
- Widget buildList(AsyncSnapshot> snapshot) {
- return ListView.builder(
- padding: EdgeInsets.only(top: 0.0),
- itemCount: snapshot.data.length,
- itemBuilder: (context, index) {
- return ContactItem(user: snapshot.data[index]);
- },
- );
- }
-}
diff --git a/lib/src/ui/home/widgets/conversation_item.dart b/lib/src/ui/home/widgets/conversation_item.dart
deleted file mode 100644
index a76631c..0000000
--- a/lib/src/ui/home/widgets/conversation_item.dart
+++ /dev/null
@@ -1,107 +0,0 @@
-import "package:flutter/material.dart";
-
-import "../../../models/user_model.dart";
-import "../../../models/conversation_model.dart";
-import "../../../blocs/conversation_bloc.dart";
-import "../../../blocs/message_bloc.dart";
-
-import "../../widgets/user_avatar.dart";
-
-class ConversationItem extends StatefulWidget {
- final Conversation conversation;
-
- ConversationItem({@required this.conversation});
- @override
- State createState() {
- return _ConversationItemState(conversation: conversation);
- }
-}
-
-class _ConversationItemState extends State {
- final bloc;
- final Conversation conversation;
- final bus = messageBloc;
-
- _ConversationItemState({@required this.conversation})
- : bloc = ConversationMembersBloc(conversation.id);
-
- @override
- void initState() {
- super.initState();
- bloc.fetchMembers();
- }
-
- @override
- void dispose() {
- bloc.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Material(
- type: MaterialType.transparency,
- elevation: 0,
- child: InkWell(
- onTap: () async {
- await bus.publish(
- {"state": "connect", "conversationId": conversation.id});
- },
- child: Container(
- padding: EdgeInsets.only(
- top: 5.0, left: 20.0, right: 20.0, bottom: 5.0),
- child: Column(children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(conversation.title,
- style: Theme.of(context).textTheme.title),
- Spacer(),
- Text("1m ago",
- style: Theme.of(context)
- .primaryTextTheme
- .body1
- .copyWith(
- fontWeight: FontWeight.w700,
- color: Theme.of(context).primaryColorDark)),
- ]),
- Padding(
- padding: EdgeInsets.only(top: 5.0),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Text(
- "I might have forgotten to close the windows",
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style:
- Theme.of(context).textTheme.subtitle)),
- StreamBuilder(
- stream: bloc.members,
- builder: (context,
- AsyncSnapshot> snapshot) {
- if (snapshot.hasData) {
- return membersBuilder(snapshot.data);
- } else if (snapshot.hasError) {
- return Text(snapshot.error.toString());
- }
- return SizedBox(width: 18.0, height: 18.0);
- }),
- ]))
- ]))));
- }
-
- Widget membersBuilder(List data) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.end,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: data
- .map((user) => UserAvatar(
- radius: 18.0,
- padding: EdgeInsets.only(top: 0.0, left: 5.0),
- user: user))
- .toList());
- }
-}
diff --git a/lib/src/ui/home/widgets/conversation_list.dart b/lib/src/ui/home/widgets/conversation_list.dart
deleted file mode 100644
index 5e7e058..0000000
--- a/lib/src/ui/home/widgets/conversation_list.dart
+++ /dev/null
@@ -1,47 +0,0 @@
-import "package:flutter/material.dart";
-
-import "../../../models/conversation_model.dart";
-import "../../../blocs/conversation_bloc.dart";
-
-import "conversation_item.dart";
-
-class ConversationList extends StatefulWidget {
- @override
- State createState() {
- return _ConversationListState();
- }
-}
-
-class _ConversationListState extends State {
- @override
- initState() {
- super.initState();
- conversationsBloc.fetchConversations();
- }
-
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: EdgeInsets.only(top: 10.0),
- child: StreamBuilder(
- stream: conversationsBloc.conversations,
- builder: (context, AsyncSnapshot> snapshot) {
- if (snapshot.hasData) {
- return buildList(snapshot.data);
- } else if (snapshot.hasError) {
- return Text(snapshot.error.toString());
- }
- return Center(child: CircularProgressIndicator());
- }));
- }
-
- Widget buildList(List data) {
- return ListView.builder(
- padding: EdgeInsets.only(top: 0.0),
- itemCount: data.length,
- itemBuilder: (context, index) {
- return ConversationItem(conversation: data[index]);
- },
- );
- }
-}
diff --git a/lib/src/ui/home/widgets/top_bar.dart b/lib/src/ui/home/widgets/top_bar.dart
deleted file mode 100644
index db07e59..0000000
--- a/lib/src/ui/home/widgets/top_bar.dart
+++ /dev/null
@@ -1,96 +0,0 @@
-import "package:flutter/material.dart";
-
-class TopBar extends StatelessWidget {
- final String title;
- final int pageNumber;
- final double barHeight = 100.0;
- final String logo = "assets/logo.png";
-
- TopBar({@required this.title, @required this.pageNumber});
-
- @override
- Widget build(BuildContext context) {
- final double statusbarHeight = MediaQuery.of(context).padding.top;
-
- // TODO: Fix cropping by moving onto stack, refactor widget into smaller parts
- return Material(
- type: MaterialType.canvas,
- elevation: 10.0,
- child: Container(
- padding: EdgeInsets.only(top: statusbarHeight, bottom: 10.0),
- child: Material(
- type: MaterialType.transparency,
- elevation: 10.0,
- color: Colors.transparent,
- child: Column(children: [
- Stack(alignment: Alignment.center, children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: EdgeInsets.only(left: 10.0),
- child: Image.asset(logo,
- semanticLabel: "Beep Logo",
- width: 24.0,
- height: 24.0)),
- Spacer(),
- IconButton(icon: Icon(Icons.search), onPressed: () {}),
- IconButton(
- icon: Icon(Icons.add_comment), onPressed: () {})
- ],
- ),
- Positioned(
- child: Text(title,
- style: Theme.of(context).accentTextTheme.display1)),
- ]),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Opacity(
- opacity: (pageNumber == 0) ? 1.0 : 0.6,
- child: Padding(
- padding: EdgeInsets.only(left: 5.0),
- child: Container(
- width: 5.0,
- height: 5.0,
- decoration: BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle)))),
- Opacity(
- opacity: (pageNumber == 1) ? 1.0 : 0.6,
- child: Padding(
- padding: EdgeInsets.only(left: 5.0),
- child: Container(
- width: 5.0,
- height: 5.0,
- decoration: BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle)))),
- Opacity(
- opacity: (pageNumber == 2) ? 1.0 : 0.6,
- child: Padding(
- padding: EdgeInsets.only(left: 5.0),
- child: Container(
- width: 5.0,
- height: 5.0,
- decoration: BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle)))),
- ]),
- ])),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- colors: [
- Theme.of(context).primaryColor,
- Theme.of(context).primaryColorDark
- ],
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- stops: [0.0, 1.0],
- tileMode: TileMode.clamp),
- ),
- ));
- }
-}
diff --git a/lib/src/ui/login/widgets/otp_page.dart b/lib/src/ui/login/widgets/otp_page.dart
index ca7e145..9a4ab38 100644
--- a/lib/src/ui/login/widgets/otp_page.dart
+++ b/lib/src/ui/login/widgets/otp_page.dart
@@ -40,12 +40,11 @@ class _OtpPageState extends State {
Text("Almost there.",
textAlign: TextAlign.left,
style: Theme.of(context).accentTextTheme.display3),
- Text(
- "I've sent an authentication code via SMS to your device, enter it below.",
- style: Theme.of(context)
- .accentTextTheme
- .title
- .copyWith(fontWeight: FontWeight.w400)),
+ Padding(
+ padding: EdgeInsets.only(top: 5.0),
+ child: Text(
+ "I've sent an authentication code via SMS to your device, enter it below.",
+ style: Theme.of(context).accentTextTheme.title)),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Center(
diff --git a/lib/src/ui/login/widgets/phone_input.dart b/lib/src/ui/login/widgets/phone_input.dart
index b7b1652..b98732d 100644
--- a/lib/src/ui/login/widgets/phone_input.dart
+++ b/lib/src/ui/login/widgets/phone_input.dart
@@ -15,7 +15,7 @@ class PhoneInput extends StatelessWidget {
width: 45,
child: Center(
child: Text("+65",
- style: Theme.of(context).textTheme.body2.copyWith(
+ style: Theme.of(context).textTheme.title.copyWith(
color: Theme.of(context).primaryColorDark))),
decoration: BoxDecoration(
color: Colors.white,
diff --git a/lib/src/ui/widgets/contact_item.dart b/lib/src/ui/widgets/contact_item.dart
new file mode 100644
index 0000000..c73ac89
--- /dev/null
+++ b/lib/src/ui/widgets/contact_item.dart
@@ -0,0 +1,84 @@
+import "package:flutter/material.dart";
+
+import "../../models/user_model.dart";
+import "../widgets/user_avatar.dart";
+
+typedef void OnClickCallback(bool state);
+
+class ContactItem extends StatefulWidget {
+ final User user;
+ final bool selectable;
+ final OnClickCallback onClickCallback;
+
+ ContactItem(
+ {@required this.user, this.onClickCallback, this.selectable: false});
+
+ @override
+ State createState() {
+ return _ContactItemState();
+ }
+}
+
+class _ContactItemState extends State {
+ bool selected = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ type: MaterialType.transparency,
+ elevation: 1,
+ child: InkWell(
+ onTap: () async {
+ if (widget.selectable == true) {
+ setState(() {
+ selected = !selected;
+ });
+
+ widget.onClickCallback(selected);
+ }
+ },
+ child: Container(
+ padding: EdgeInsets.only(top: 10.0, bottom: 10.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ (widget.selectable)
+ ? Checkbox(
+ value: selected,
+ activeColor: Theme.of(context).primaryColorDark,
+ onChanged: (state) {
+ setState(() {
+ selected = !selected;
+ });
+
+ widget.onClickCallback(selected);
+ })
+ : Container(),
+ UserAvatar(
+ user: widget.user,
+ radius: 18.0,
+ padding: EdgeInsets.only(
+ left: ((widget.selectable) ? 0 : 15.0))),
+ Padding(
+ padding: EdgeInsets.only(left: 15.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.user.firstName +
+ " " +
+ widget.user.lastName,
+ style: Theme.of(context).textTheme.title,
+ overflow: TextOverflow.ellipsis),
+ Padding(
+ padding: EdgeInsets.only(top: 2),
+ child: Text("Last seen x ago",
+ style: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(
+ color: Color(0xFF455A64)))),
+ ]))
+ ]))));
+ }
+}
diff --git a/lib/src/ui/widgets/conversation_item.dart b/lib/src/ui/widgets/conversation_item.dart
new file mode 100644
index 0000000..32807ee
--- /dev/null
+++ b/lib/src/ui/widgets/conversation_item.dart
@@ -0,0 +1,109 @@
+import "package:flutter/material.dart";
+
+import "../../models/user_model.dart";
+import "../../models/conversation_model.dart";
+import "../../blocs/conversation_bloc.dart";
+import "../../blocs/message_bloc.dart";
+
+import "../widgets/user_avatar.dart";
+
+class ConversationItem extends StatefulWidget {
+ final Conversation conversation;
+
+ ConversationItem({@required this.conversation});
+ @override
+ State createState() {
+ return _ConversationItemState(conversation: conversation);
+ }
+}
+
+class _ConversationItemState extends State {
+ final bloc;
+ final Conversation conversation;
+
+ _ConversationItemState({@required this.conversation})
+ : bloc = ConversationMembersBloc(conversation.id);
+
+ @override
+ void initState() {
+ super.initState();
+ bloc.fetchMembers();
+ }
+
+ @override
+ void dispose() {
+ bloc.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ type: MaterialType.transparency,
+ elevation: 1,
+ child: InkWell(
+ onTap: () async {
+ await messageChannel.publish({
+ "target": "home",
+ "state": "connect",
+ "conversationId": conversation.id
+ });
+ },
+ child: Container(
+ padding: EdgeInsets.only(
+ top: 8.0, left: 10.0, right: 10.0, bottom: 8.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ StreamBuilder(
+ stream: bloc.members,
+ builder:
+ (context, AsyncSnapshot> snapshot) {
+ if (snapshot.hasData) {
+ return avatarBuilder(snapshot.data);
+ } else if (snapshot.hasError) {
+ return Text(snapshot.error.toString());
+ }
+ return SizedBox(width: 18.0, height: 18.0);
+ }),
+ Expanded(
+ child: Container(
+ padding: EdgeInsets.only(left: 10.0, right: 5.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(conversation.title,
+ style:
+ Theme.of(context).textTheme.title),
+ Text("yaddaydaadadyasdhbsjdfhsbjdfhsbdug",
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(
+ color: Color(0xFF455A64))),
+ ]))),
+ Column(mainAxisSize: MainAxisSize.min, children: [
+ Text("12:25 PM",
+ style: Theme.of(context)
+ .primaryTextTheme
+ .display2
+ .copyWith(
+ color: Theme.of(context).primaryColorDark)),
+ ])
+ ]))));
+ }
+
+ Widget avatarBuilder(List data) {
+ if (data.length == 1) {
+ return UserAvatar(radius: 25.0, user: data[0]);
+ } else if (data.length > 1) {
+ final groupUser = new User("0", conversation.title, "", "");
+ return UserAvatar(radius: 25.0, user: groupUser);
+ } else {
+ return Container();
+ }
+ }
+}
diff --git a/lib/src/ui/widgets/list_button.dart b/lib/src/ui/widgets/list_button.dart
new file mode 100644
index 0000000..791c4ab
--- /dev/null
+++ b/lib/src/ui/widgets/list_button.dart
@@ -0,0 +1,38 @@
+import "package:flutter/material.dart";
+
+typedef void OnClickCallback();
+
+class ListButton extends StatelessWidget {
+ final IconData icon;
+ final String text;
+ final OnClickCallback onClickCallback;
+
+ ListButton(
+ {@required this.icon,
+ @required this.text,
+ @required this.onClickCallback});
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ type: MaterialType.transparency,
+ elevation: 1,
+ child: InkWell(
+ onTap: onClickCallback,
+ child: Container(
+ padding: EdgeInsets.only(
+ left: 15.0, right: 15.0, top: 12.0, bottom: 12.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(icon,
+ size: 30.0,
+ color: Theme.of(context).primaryColorDark),
+ Padding(
+ padding: EdgeInsets.only(left: 20.0),
+ child: Text(text,
+ style: Theme.of(context).textTheme.title.copyWith(
+ color: Theme.of(context).primaryColorDark))),
+ ]))));
+ }
+}
diff --git a/lib/src/ui/widgets/search_input.dart b/lib/src/ui/widgets/search_input.dart
new file mode 100644
index 0000000..0407174
--- /dev/null
+++ b/lib/src/ui/widgets/search_input.dart
@@ -0,0 +1,42 @@
+import "package:flutter/material.dart";
+
+class SearchInput extends StatelessWidget {
+ final TextEditingController controller;
+ final String hintText;
+
+ SearchInput({@required this.controller, @required this.hintText});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ child: Padding(
+ padding: EdgeInsets.only(left: 10.0, right: 10.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(right: 5.0),
+ child: Icon(Icons.search, color: Colors.white)),
+ Flexible(
+ child: TextField(
+ controller: controller,
+ autocorrect: false,
+ cursorWidth: 3.0,
+ cursorColor: Colors.white,
+ style: Theme.of(context).textTheme.subtitle.copyWith(
+ color: Colors.white, fontWeight: FontWeight.w300),
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ filled: false,
+ hintText: hintText,
+ hintStyle: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(color: Colors.white)))),
+ ])),
+ decoration: BoxDecoration(
+ color: Colors.black.withOpacity(0.05),
+ borderRadius: BorderRadius.all(Radius.circular(10.00)),
+ ));
+ }
+}
diff --git a/lib/src/ui/widgets/small_text_button.dart b/lib/src/ui/widgets/small_text_button.dart
new file mode 100644
index 0000000..48cee69
--- /dev/null
+++ b/lib/src/ui/widgets/small_text_button.dart
@@ -0,0 +1,25 @@
+import "package:flutter/material.dart";
+
+typedef void OnClickCallback();
+
+class SmallTextButton extends StatelessWidget {
+ final String text;
+ final OnClickCallback onClickCallback;
+
+ SmallTextButton({@required this.text, @required this.onClickCallback});
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ type: MaterialType.transparency,
+ elevation: 1,
+ child: Padding(
+ padding: EdgeInsets.all(10.0),
+ child: GestureDetector(
+ onTap: onClickCallback,
+ child: Text(text,
+ style: Theme.of(context)
+ .accentTextTheme
+ .title
+ .copyWith(fontWeight: FontWeight.w300)))));
+ }
+}
diff --git a/lib/src/ui/widgets/top_bar.dart b/lib/src/ui/widgets/top_bar.dart
new file mode 100644
index 0000000..cd42344
--- /dev/null
+++ b/lib/src/ui/widgets/top_bar.dart
@@ -0,0 +1,55 @@
+import "package:flutter/material.dart";
+import "search_input.dart";
+
+class TopBar extends StatelessWidget {
+ final String logo = "assets/logo.png";
+ final SearchInput search;
+ final List children;
+ final String title;
+
+ TopBar({@required this.children, @required this.title, this.search});
+
+ @override
+ Widget build(BuildContext context) {
+ final double statusbarHeight = MediaQuery.of(context).padding.top;
+
+ return Material(
+ type: MaterialType.canvas,
+ elevation: 5.0,
+ child: Container(
+ padding: EdgeInsets.only(top: statusbarHeight, bottom: 0),
+ child: Material(
+ type: MaterialType.transparency,
+ elevation: 0.0,
+ color: Colors.transparent,
+ child: Column(children: [
+ Stack(alignment: Alignment.center, children: [
+ Text(title, style: Theme.of(context).accentTextTheme.display1),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: children,
+ )
+ ]),
+ (search != null)
+ ? Padding(
+ padding: EdgeInsets.only(
+ left: 10.0, right: 10.0, bottom: 10.0),
+ child: search)
+ : Container(),
+ ]),
+ ),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).primaryColor,
+ Theme.of(context).primaryColorDark
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ stops: [0.0, 1.0],
+ tileMode: TileMode.clamp),
+ ),
+ ));
+ }
+}
diff --git a/lib/src/ui/widgets/user_avatar.dart b/lib/src/ui/widgets/user_avatar.dart
index c93a25d..f7f32bb 100644
--- a/lib/src/ui/widgets/user_avatar.dart
+++ b/lib/src/ui/widgets/user_avatar.dart
@@ -42,19 +42,37 @@ class _UserAvatarState extends State {
String lastName =
(widget.user.lastName.isEmpty) ? '' : widget.user.lastName[0];
+ final colors = _stringToColor(widget.user.lastName);
+
return Padding(
padding: widget.padding,
child: Stack(alignment: Alignment.bottomRight, children: [
- CircleAvatar(
- backgroundColor: _stringToColor(widget.user.lastName),
- child: Text(
- firstName.toUpperCase() + lastName.toUpperCase(),
- style: Theme.of(context)
- .accentTextTheme
- .title
- .copyWith(fontSize: widget.radius / 1.2),
- ),
- radius: widget.radius),
+ Container(
+ height: (widget.radius * 2),
+ width: (widget.radius * 2),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topRight,
+ end: Alignment.bottomLeft,
+ stops: [
+ 0,
+ 1
+ ],
+ colors: [
+ colors[0],
+ colors[1],
+ ]),
+ borderRadius:
+ BorderRadius.all(Radius.circular(widget.radius))),
+ child: Center(
+ child: Text(
+ firstName.toUpperCase() + lastName.toUpperCase(),
+ style: Theme.of(context)
+ .accentTextTheme
+ .title
+ .copyWith(fontSize: widget.radius / 1.4),
+ ),
+ )),
StreamBuilder(
stream: heartbeatReceiverBloc.stream,
builder: (context, AsyncSnapshot