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> snapshot) { @@ -84,7 +102,7 @@ class _UserAvatarState extends State { } // Hashing username into a pastel color - Color _stringToColor(String str) { + List _stringToColor(String str) { int hash = 0; str.runes.forEach((int rune) { @@ -93,6 +111,9 @@ class _UserAvatarState extends State { hash = hash % 360; - return HSLColor.fromAHSL(1.0, hash.toDouble(), 0.8, 0.4).toColor(); + return [ + HSLColor.fromAHSL(1.0, hash.toDouble(), 0.8, 0.4).toColor(), + HSLColor.fromAHSL(1.0, hash.toDouble(), 0.8, 0.5).toColor() + ]; } } diff --git a/lib/src/ui/widgets/user_chip.dart b/lib/src/ui/widgets/user_chip.dart new file mode 100644 index 0000000..4b5d371 --- /dev/null +++ b/lib/src/ui/widgets/user_chip.dart @@ -0,0 +1,18 @@ +import "package:flutter/material.dart"; + +import "../../models/user_model.dart"; +import "./user_avatar.dart"; + +class UserChip extends StatelessWidget { + final User user; + + UserChip({@required this.user}); + + @override + Widget build(BuildContext context) { + return Chip( + avatar: UserAvatar(user: user, radius: 12.0), + elevation: 1.5, + label: Text(user.firstName + " " + user.lastName)); + } +} diff --git a/lib/themer.dart b/lib/themer.dart index 220bc01..0af5d81 100644 --- a/lib/themer.dart +++ b/lib/themer.dart @@ -31,16 +31,17 @@ TextTheme buildTextTheme(TextTheme base) { .copyWith(fontSize: 40.0, fontWeight: FontWeight.w600), display3: base.display3 .copyWith(fontSize: 30.0, fontWeight: FontWeight.w700), - display2: base.display2 - .copyWith(fontSize: 18.0, fontWeight: FontWeight.w500), - display1: base.display1 - .copyWith(fontSize: 19.0, fontWeight: FontWeight.w600), + display2: base.display2.copyWith( + fontSize: 12.0, fontWeight: FontWeight.w500), // Used for time + display1: base.display1.copyWith( + fontSize: 16.0, + fontWeight: FontWeight.w600), // Used for overall title title: - base.title.copyWith(fontSize: 18.0, fontWeight: FontWeight.w500), + base.title.copyWith(fontSize: 16.0, fontWeight: FontWeight.w500), subtitle: base.subtitle - .copyWith(fontSize: 12.0, fontWeight: FontWeight.w300), - body2: - base.body2.copyWith(fontSize: 16.0, fontWeight: FontWeight.w600), + .copyWith(fontSize: 13.0, fontWeight: FontWeight.w300), + body2: base.body2.copyWith( + fontSize: 12.0, fontWeight: FontWeight.w600), // Bold normal body1: base.body1.copyWith(fontSize: 12.0, fontWeight: FontWeight.w400)) .apply( diff --git a/pubspec.yaml b/pubspec.yaml index 09a91e1..e6b8ebd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: shared_preferences: ^0.5.1 sqflite: ^1.1.0 eventsource: ^0.2.1 + sticky_headers: ^0.1.7 + image_picker_modern: ^0.4.12+2 dev_dependencies: flutter_test: