merge: fixing login_page connflicts
commit
01a07db488
|
@ -283,6 +283,7 @@
|
||||||
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
|
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/Just/Just.framework",
|
"${BUILT_PRODUCTS_DIR}/Just/Just.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/PercentEncoder/PercentEncoder.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}/shared_preferences/shared_preferences.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.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}/WebRTC.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Just.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Just.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PercentEncoder.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}/shared_preferences.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
|
||||||
);
|
);
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Profile/Group picture</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Selfie for profile/group picture</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>$(PRODUCT_NAME) Microphone Usage!</string>
|
<string>$(PRODUCT_NAME) Microphone Usage!</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
import "src/blocs/heartbeat_bloc.dart";
|
// import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// debugPaintSizeEnabled = true;
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
|
||||||
Routes();
|
Routes();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'src/ui/home/home.dart';
|
import 'src/ui/home.dart';
|
||||||
import "src/ui/login/welcome.dart";
|
import "src/ui/login/welcome.dart";
|
||||||
import "src/services/login_manager.dart";
|
import "src/services/login_manager.dart";
|
||||||
import 'themer.dart';
|
import 'themer.dart';
|
||||||
|
|
|
@ -13,6 +13,7 @@ class ConversationsBloc {
|
||||||
|
|
||||||
fetchConversations() async {
|
fetchConversations() async {
|
||||||
List<Conversation> conversationList = await _provider.fetchConversations();
|
List<Conversation> conversationList = await _provider.fetchConversations();
|
||||||
|
print(conversationList);
|
||||||
_conversationsFetcher.sink.add(conversationList);
|
_conversationsFetcher.sink.add(conversationList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,4 +19,4 @@ class MessageBloc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// global instance for access throughout the app
|
// global instance for access throughout the app
|
||||||
final messageBloc = MessageBloc();
|
final messageChannel = MessageBloc();
|
||||||
|
|
|
@ -65,6 +65,17 @@ class ConversationApiProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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<List<User>> fetchConversationMembers(String id) async {
|
Future<List<User>> fetchConversationMembers(String id) async {
|
||||||
final jwt = await loginManager.getToken();
|
final jwt = await loginManager.getToken();
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -18,8 +18,8 @@ class BottomBar extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(40.0), topRight: Radius.circular(40.0)),
|
topLeft: Radius.circular(40.0), topRight: Radius.circular(40.0)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(
|
padding:
|
||||||
top: 20.0, left: 20.0, right: 20.0, bottom: 30.0 + bottomPadding),
|
EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0, bottom: 30.0),
|
||||||
child: (conversationId == "")
|
child: (conversationId == "")
|
||||||
? ConversationInactiveView()
|
? ConversationInactiveView()
|
||||||
: ConversationActiveView(conversationId: conversationId),
|
: ConversationActiveView(conversationId: conversationId),
|
||||||
|
|
|
@ -18,7 +18,6 @@ class ConversationActiveView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConversationActiveViewState extends State<ConversationActiveView> {
|
class _ConversationActiveViewState extends State<ConversationActiveView> {
|
||||||
final bus = messageBloc;
|
|
||||||
final conversationApiProvider = ConversationApiProvider();
|
final conversationApiProvider = ConversationApiProvider();
|
||||||
Conversation _conversation;
|
Conversation _conversation;
|
||||||
List<Widget> _users;
|
List<Widget> _users;
|
||||||
|
@ -47,8 +46,8 @@ class _ConversationActiveViewState extends State<ConversationActiveView> {
|
||||||
print(users[0].id);
|
print(users[0].id);
|
||||||
setState(() {
|
setState(() {
|
||||||
_users = users
|
_users = users
|
||||||
.map((user) =>
|
.map((user) => UserAvatar(
|
||||||
UserAvatar(padding: EdgeInsets.only(right: 5.0), user: user))
|
radius: 18.0, padding: EdgeInsets.only(right: 5.0), user: user))
|
||||||
.toList();
|
.toList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -67,8 +66,8 @@ class _ConversationActiveViewState extends State<ConversationActiveView> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: 22.0,
|
width: 15.0,
|
||||||
height: 22.0,
|
height: 15.0,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).indicatorColor,
|
color: Theme.of(context).indicatorColor,
|
||||||
shape: BoxShape.circle)),
|
shape: BoxShape.circle)),
|
||||||
|
@ -91,7 +90,8 @@ class _ConversationActiveViewState extends State<ConversationActiveView> {
|
||||||
icon: Icon(Icons.close),
|
icon: Icon(Icons.close),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Call method to close connection
|
// Call method to close connection
|
||||||
await bus.publish({"state": "disconnect"});
|
await messageChannel
|
||||||
|
.publish({"target": "home", "state": "disconnect"});
|
||||||
print("Pressed close");
|
print("Pressed close");
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -13,12 +13,15 @@ class ConversationInactiveView extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
|
radius: 18,
|
||||||
padding: EdgeInsets.only(right: 5.0),
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
|
radius: 18,
|
||||||
padding: EdgeInsets.only(right: 5.0),
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
|
radius: 18,
|
||||||
padding: EdgeInsets.only(right: 5.0),
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
user: User("1", "Isaac", "Tay", "+65 91043593"))
|
user: User("1", "Isaac", "Tay", "+65 91043593"))
|
||||||
])
|
])
|
||||||
|
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _ContactTabState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactTabState extends State<ContactTab> {
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey =
|
||||||
|
new GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _HomeViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeViewState extends State<HomeView> {
|
||||||
|
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: <Widget>[
|
||||||
|
TopBar(
|
||||||
|
title: "Contacts",
|
||||||
|
search: SearchInput(
|
||||||
|
controller: searchController, hintText: "Search for people"),
|
||||||
|
children: <Widget>[
|
||||||
|
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<List<User>> snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return buildList(snapshot);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text(snapshot.error.toString());
|
||||||
|
}
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildList(AsyncSnapshot<List<User>> snapshot) {
|
||||||
|
final Map<String, List<User>> 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<Widget>((entry) {
|
||||||
|
if (entry.value.length == 0) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _ConversationTabState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationTabState extends State<ConversationTab> {
|
||||||
|
@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<User> users = settings.arguments;
|
||||||
|
builder = (BuildContext _) => NewGroupInfoView(users: users);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception("Invalid route: ${settings.name}");
|
||||||
|
}
|
||||||
|
return MaterialPageRoute(builder: builder, settings: settings);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _HomeViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeViewState extends State<HomeView> {
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
conversationsBloc.fetchConversations();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(children: <Widget>[
|
||||||
|
TopBar(
|
||||||
|
title: "Conversations",
|
||||||
|
search: SearchInput(
|
||||||
|
controller: searchController, hintText: "Search for people"),
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
StreamBuilder(
|
||||||
|
stream: conversationsBloc.conversations,
|
||||||
|
builder: (context, AsyncSnapshot<List<Conversation>> snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return buildList(snapshot.data);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text(snapshot.error.toString());
|
||||||
|
}
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildList(List<Conversation> data) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.only(top: 0.0),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ConversationItem(conversation: data[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _NewConversationViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewConversationViewState extends State<NewConversationView> {
|
||||||
|
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: <Widget>[
|
||||||
|
TopBar(
|
||||||
|
title: "New Conversation",
|
||||||
|
search: SearchInput(
|
||||||
|
controller: searchController, hintText: "Search for people"),
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
|
Spacer(),
|
||||||
|
]),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder(
|
||||||
|
stream: contactBloc.contacts,
|
||||||
|
builder: (context, AsyncSnapshot<List<User>> snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return buildList(snapshot);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text(snapshot.error.toString());
|
||||||
|
}
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildList(AsyncSnapshot<List<User>> snapshot) {
|
||||||
|
final Map<String, List<User>> 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<Widget>((entry) {
|
||||||
|
if (entry.value.length == 0) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<User> users;
|
||||||
|
|
||||||
|
NewGroupInfoView({@required this.users});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _NewGroupInfoViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewGroupInfoViewState extends State<NewGroupInfoView> {
|
||||||
|
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: <Widget>[
|
||||||
|
TopBar(title: "New Group", children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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())))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _NewGroupViewState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewGroupViewState extends State<NewGroupView> {
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
|
||||||
|
List<User> selectedUsers = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
contactBloc.fetchContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(children: <Widget>[
|
||||||
|
TopBar(
|
||||||
|
title: "New Group",
|
||||||
|
children: <Widget>[
|
||||||
|
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<List<User>> snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return buildList(snapshot);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text(snapshot.error.toString());
|
||||||
|
}
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildList(AsyncSnapshot<List<User>> snapshot) {
|
||||||
|
final Map<String, List<User>> 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<Widget>((entry) {
|
||||||
|
if (entry.value.length == 0) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Home> with SingleTickerProviderStateMixin {
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
final heartbeatSendManager = HeartbeatSendManager();
|
||||||
|
final conversationManager = ConversationManager();
|
||||||
|
|
||||||
|
// Bottom Bar navigation
|
||||||
|
int _tabNumber = 1;
|
||||||
|
List<IconData> 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<String, String> data) async => await _processMessage(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_processMessage(Map<String, String> 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: <Widget>[
|
||||||
|
ContactTab(),
|
||||||
|
ConversationTab(),
|
||||||
|
Container(),
|
||||||
|
]),
|
||||||
|
bottomNavigationBar:
|
||||||
|
Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
|
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())
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Home> {
|
|
||||||
final List<String> titleList = ["Conversations", "Contacts", "Settings"];
|
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
|
||||||
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<String, String> data) async => await _processMessage(data));
|
|
||||||
_setupBottomState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatePageNumber() {
|
|
||||||
setState(() {
|
|
||||||
_pageNumber = controller.page.round();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_processMessage(Map<String, String> data) async {
|
|
||||||
if (data["state"] == "disconnect") {
|
|
||||||
// Disconnect and change state
|
|
||||||
await conversationManager.exit();
|
|
||||||
bottomBarController.close();
|
|
||||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
|
||||||
(BuildContext context) => BottomBar(conversationId: ""));
|
|
||||||
} else if (data["state"] == "connect") {
|
|
||||||
// Connect and change state
|
|
||||||
await conversationManager.join(data["conversationId"]);
|
|
||||||
bottomBarController.close();
|
|
||||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
|
||||||
(BuildContext context) =>
|
|
||||||
BottomBar(conversationId: data["conversationId"]));
|
|
||||||
} else {
|
|
||||||
// show default
|
|
||||||
await conversationManager.exit();
|
|
||||||
bottomBarController.close();
|
|
||||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
|
||||||
(BuildContext context) => BottomBar(conversationId: ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupBottomState() {
|
|
||||||
conversationManager.get().then((conversationId) {
|
|
||||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
|
||||||
(BuildContext context) => BottomBar(conversationId: conversationId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
body: Column(children: <Widget>[
|
|
||||||
TopBar(title: titleList[_pageNumber], pageNumber: _pageNumber),
|
|
||||||
Expanded(
|
|
||||||
child: PageView(controller: controller, children: <Widget>[
|
|
||||||
ConversationList(),
|
|
||||||
ContactList(),
|
|
||||||
])),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: <Widget>[
|
|
||||||
/*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: () => {});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StatefulWidget> createState() {
|
|
||||||
return _ContactListState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ContactListState extends State<ContactList> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
contactBloc.fetchContacts();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StreamBuilder(
|
|
||||||
stream: contactBloc.contacts,
|
|
||||||
builder: (context, AsyncSnapshot<List<User>> snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return buildList(snapshot);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return Text(snapshot.error.toString());
|
|
||||||
}
|
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildList(AsyncSnapshot<List<User>> snapshot) {
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.only(top: 0.0),
|
|
||||||
itemCount: snapshot.data.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return ContactItem(user: snapshot.data[index]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StatefulWidget> createState() {
|
|
||||||
return _ConversationItemState(conversation: conversation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConversationItemState extends State<ConversationItem> {
|
|
||||||
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: <Widget>[
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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<List<User>> 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<User> 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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StatefulWidget> createState() {
|
|
||||||
return _ConversationListState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConversationListState extends State<ConversationList> {
|
|
||||||
@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<List<Conversation>> snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return buildList(snapshot.data);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return Text(snapshot.error.toString());
|
|
||||||
}
|
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildList(List<Conversation> data) {
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.only(top: 0.0),
|
|
||||||
itemCount: data.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return ConversationItem(conversation: data[index]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: <Widget>[
|
|
||||||
Stack(alignment: Alignment.center, children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,12 +40,11 @@ class _OtpPageState extends State<OtpPage> {
|
||||||
Text("Almost there.",
|
Text("Almost there.",
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: Theme.of(context).accentTextTheme.display3),
|
style: Theme.of(context).accentTextTheme.display3),
|
||||||
Text(
|
Padding(
|
||||||
"I've sent an authentication code via SMS to your device, enter it below.",
|
padding: EdgeInsets.only(top: 5.0),
|
||||||
style: Theme.of(context)
|
child: Text(
|
||||||
.accentTextTheme
|
"I've sent an authentication code via SMS to your device, enter it below.",
|
||||||
.title
|
style: Theme.of(context).accentTextTheme.title)),
|
||||||
.copyWith(fontWeight: FontWeight.w400)),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 20.0),
|
padding: EdgeInsets.only(top: 20.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|
|
@ -15,7 +15,7 @@ class PhoneInput extends StatelessWidget {
|
||||||
width: 45,
|
width: 45,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text("+65",
|
child: Text("+65",
|
||||||
style: Theme.of(context).textTheme.body2.copyWith(
|
style: Theme.of(context).textTheme.title.copyWith(
|
||||||
color: Theme.of(context).primaryColorDark))),
|
color: Theme.of(context).primaryColorDark))),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _ContactItemState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactItemState extends State<ContactItem> {
|
||||||
|
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>[
|
||||||
|
(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: <Widget>[
|
||||||
|
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)))),
|
||||||
|
]))
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _ConversationItemState(conversation: conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationItemState extends State<ConversationItem> {
|
||||||
|
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: <Widget>[
|
||||||
|
StreamBuilder(
|
||||||
|
stream: bloc.members,
|
||||||
|
builder:
|
||||||
|
(context, AsyncSnapshot<List<User>> 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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
Text("12:25 PM",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.primaryTextTheme
|
||||||
|
.display2
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context).primaryColorDark)),
|
||||||
|
])
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget avatarBuilder(List<User> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: <Widget>[
|
||||||
|
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))),
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: <Widget>[
|
||||||
|
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)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Widget> 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: <Widget>[
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,19 +42,37 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||||
String lastName =
|
String lastName =
|
||||||
(widget.user.lastName.isEmpty) ? '' : widget.user.lastName[0];
|
(widget.user.lastName.isEmpty) ? '' : widget.user.lastName[0];
|
||||||
|
|
||||||
|
final colors = _stringToColor(widget.user.lastName);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: Stack(alignment: Alignment.bottomRight, children: <Widget>[
|
child: Stack(alignment: Alignment.bottomRight, children: <Widget>[
|
||||||
CircleAvatar(
|
Container(
|
||||||
backgroundColor: _stringToColor(widget.user.lastName),
|
height: (widget.radius * 2),
|
||||||
child: Text(
|
width: (widget.radius * 2),
|
||||||
firstName.toUpperCase() + lastName.toUpperCase(),
|
decoration: BoxDecoration(
|
||||||
style: Theme.of(context)
|
gradient: LinearGradient(
|
||||||
.accentTextTheme
|
begin: Alignment.topRight,
|
||||||
.title
|
end: Alignment.bottomLeft,
|
||||||
.copyWith(fontSize: widget.radius / 1.2),
|
stops: [
|
||||||
),
|
0,
|
||||||
radius: widget.radius),
|
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(
|
StreamBuilder(
|
||||||
stream: heartbeatReceiverBloc.stream,
|
stream: heartbeatReceiverBloc.stream,
|
||||||
builder: (context, AsyncSnapshot<Map<String, String>> snapshot) {
|
builder: (context, AsyncSnapshot<Map<String, String>> snapshot) {
|
||||||
|
@ -84,7 +102,7 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashing username into a pastel color
|
// Hashing username into a pastel color
|
||||||
Color _stringToColor(String str) {
|
List<Color> _stringToColor(String str) {
|
||||||
int hash = 0;
|
int hash = 0;
|
||||||
|
|
||||||
str.runes.forEach((int rune) {
|
str.runes.forEach((int rune) {
|
||||||
|
@ -93,6 +111,9 @@ class _UserAvatarState extends State<UserAvatar> {
|
||||||
|
|
||||||
hash = hash % 360;
|
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()
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,16 +31,17 @@ TextTheme buildTextTheme(TextTheme base) {
|
||||||
.copyWith(fontSize: 40.0, fontWeight: FontWeight.w600),
|
.copyWith(fontSize: 40.0, fontWeight: FontWeight.w600),
|
||||||
display3: base.display3
|
display3: base.display3
|
||||||
.copyWith(fontSize: 30.0, fontWeight: FontWeight.w700),
|
.copyWith(fontSize: 30.0, fontWeight: FontWeight.w700),
|
||||||
display2: base.display2
|
display2: base.display2.copyWith(
|
||||||
.copyWith(fontSize: 18.0, fontWeight: FontWeight.w500),
|
fontSize: 12.0, fontWeight: FontWeight.w500), // Used for time
|
||||||
display1: base.display1
|
display1: base.display1.copyWith(
|
||||||
.copyWith(fontSize: 19.0, fontWeight: FontWeight.w600),
|
fontSize: 16.0,
|
||||||
|
fontWeight: FontWeight.w600), // Used for overall title
|
||||||
title:
|
title:
|
||||||
base.title.copyWith(fontSize: 18.0, fontWeight: FontWeight.w500),
|
base.title.copyWith(fontSize: 16.0, fontWeight: FontWeight.w500),
|
||||||
subtitle: base.subtitle
|
subtitle: base.subtitle
|
||||||
.copyWith(fontSize: 12.0, fontWeight: FontWeight.w300),
|
.copyWith(fontSize: 13.0, fontWeight: FontWeight.w300),
|
||||||
body2:
|
body2: base.body2.copyWith(
|
||||||
base.body2.copyWith(fontSize: 16.0, fontWeight: FontWeight.w600),
|
fontSize: 12.0, fontWeight: FontWeight.w600), // Bold normal
|
||||||
body1:
|
body1:
|
||||||
base.body1.copyWith(fontSize: 12.0, fontWeight: FontWeight.w400))
|
base.body1.copyWith(fontSize: 12.0, fontWeight: FontWeight.w400))
|
||||||
.apply(
|
.apply(
|
||||||
|
|
|
@ -16,6 +16,8 @@ dependencies:
|
||||||
shared_preferences: ^0.5.1
|
shared_preferences: ^0.5.1
|
||||||
sqflite: ^1.1.0
|
sqflite: ^1.1.0
|
||||||
eventsource: ^0.2.1
|
eventsource: ^0.2.1
|
||||||
|
sticky_headers: ^0.1.7
|
||||||
|
image_picker_modern: ^0.4.12+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue