feat: notification mark as read

This commit is contained in:
Rongjian Zhang 2019-02-06 13:06:11 +08:00
parent 8e678a37d4
commit c0629c5739
15 changed files with 450 additions and 202 deletions

View File

@ -52,22 +52,18 @@ class _HomeState extends State<Home> {
List<BottomNavigationBarItem> _buildNavigationItems() {
return [
BottomNavigationBarItem(
icon: Icon(Icons.inbox),
title: Text('Inbox'),
),
BottomNavigationBarItem(
icon: Icon(Icons.rss_feed),
title: Text('News'),
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'),
),
BottomNavigationBarItem(
icon: _buildNotificationIcon(context),
title: Text('Notification'),
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
title: Text('Me'),
@ -78,14 +74,12 @@ class _HomeState extends State<Home> {
_buildScreen(int index) {
switch (index) {
case 0:
return InboxScreen();
case 1:
return NewsScreen();
case 1:
return NotificationScreen();
case 2:
return SearchScreen();
case 3:
return NotificationScreen();
case 4:
return ProfileScreen();
}
}

View File

@ -30,7 +30,7 @@ class _SettingsProviderState extends State<SettingsProvider> {
if (Platform.isIOS) {
layout = LayoutMap.cupertino;
}
layout = LayoutMap.material;
// layout = LayoutMap.material;
}
@override

View File

@ -1,8 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/list_scaffold.dart';
import '../widgets/notification_item.dart';
import '../utils/utils.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import '../widgets/link.dart';
class NotificationPayload {
String type;
String owner;
String name;
int number;
String title;
String updateAt;
bool unread;
NotificationPayload.fromJson(input) {
type = input['subject']['type'];
name = input['repository']['name'];
owner = input['repository']['owner']['login'];
String url = input['subject']['url'];
String numberStr = url.split('/').lastWhere((_) => true);
number = int.parse(numberStr);
title = input['subject']['title'];
updateAt = TimeAgo.formatFromString(input['updated_at']);
unread = input['unread'];
}
}
class NotificationItem extends StatelessWidget {
const NotificationItem({
Key key,
@required this.payload,
}) : super(key: key);
final NotificationPayload payload;
Widget _buildRoute() {
switch (payload.type) {
case 'Issue':
return IssueScreen(payload.number, payload.owner, payload.name);
case 'PullRequest':
return PullRequestScreen(payload.number, payload.owner, payload.name);
default:
// throw new Exception('Unhandled notification type: $type');
return Text('test');
}
}
IconData _buildIconData() {
switch (payload.type) {
case 'Issue':
return Octicons.issue_opened;
// color: Color.fromRGBO(0x28, 0xa7, 0x45, 1),
case 'PullRequest':
return Octicons.git_pull_request;
// color: Color.fromRGBO(0x6f, 0x42, 0xc1, 1),
default:
return Octicons.person;
}
}
@override
Widget build(BuildContext context) {
return Link(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => _buildRoute()),
);
},
child: Container(
padding: EdgeInsets.all(8),
// color: payload.unread ? Colors.white : Colors.black12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.only(right: 8, top: 20),
child: Icon(_buildIconData(), color: Colors.black45),
),
Expanded(
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
payload.owner +
'/' +
payload.name +
' #' +
payload.number.toString(),
style: TextStyle(fontSize: 13, color: Colors.black54),
),
Padding(padding: EdgeInsets.only(top: 4)),
Text(
payload.title,
style: TextStyle(fontSize: 15),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Padding(padding: EdgeInsets.only(top: 6)),
Text(
payload.updateAt,
style: TextStyle(
fontSize: 12,
// fontWeight: FontWeight.w300,
color: Colors.black54,
),
)
],
),
),
),
Column(
children: <Widget>[
Icon(Octicons.check, color: Colors.black45),
Icon(Octicons.unmute, color: Colors.black45)
],
),
],
),
],
),
),
);
}
}
Future<List<NotificationPayload>> fetchNotifications(int page) async {
List items =
@ -10,6 +138,7 @@ Future<List<NotificationPayload>> fetchNotifications(int page) async {
return items.map((item) => NotificationPayload.fromJson(item)).toList();
}
/// [@deprecated]
class InboxScreen extends StatefulWidget {
@override
_InboxScreenState createState() => _InboxScreenState();
@ -27,17 +156,27 @@ class _InboxScreenState extends State<InboxScreen> {
2: 'All',
};
Future<void> _refresh() async {
page = 1;
var items = await fetchNotifications(page);
setState(() {
_items = items;
});
}
@override
Widget build(BuildContext context) {
return ListScaffold(
title: Text('Inbox'),
onRefresh: () async {
page = 1;
var items = await fetchNotifications(page);
setState(() {
_items = items;
});
trailingIconData: Octicons.check,
trailingOnTap: () async {
bool answer = await showConfim(context, 'Mark all as read?');
if (answer == true) {
await putWithCredentials('/notifications');
_refresh();
}
},
onRefresh: _refresh,
onLoadMore: () async {
page = page + 1;
var items = await fetchNotifications(page);

View File

@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import '../utils/utils.dart';
import '../widgets/list_scaffold.dart';
import '../widgets/timeline_item.dart';
import '../widgets/comment_item.dart';
Future queryIssue(int id, String owner, String name) async {
var data = await query('''
@ -41,7 +42,25 @@ class _IssueScreenState extends State<IssueScreen> {
Map<String, dynamic> payload;
Widget _buildHeader() {
return Text('issue');
return Column(children: <Widget>[
Container(
// padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
payload['title'],
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.2,
),
),
CommentItem(payload),
],
),
)
]);
}
get _fullName => widget.owner + '/' + widget.name;

View File

@ -1,17 +1,22 @@
import 'package:flutter/material.dart' hide Notification;
import 'package:flutter/cupertino.dart' hide Notification;
import '../providers/settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../widgets/refresh_scaffold.dart';
import '../providers/notification.dart';
import '../widgets/notification_item.dart';
import '../widgets/loading.dart';
import '../widgets/list_group.dart';
import '../widgets/link.dart';
import '../utils/utils.dart';
class NotificationGroup {
String fullName;
List<Notification> items = [];
Future<List<NotificationPayload>> fetchNotifications([int page = 1]) async {
List items = await getWithCredentials('/notifications?page=$page&all=true');
return items.map((item) => NotificationPayload.fromJson(item)).toList();
}
NotificationGroup(this.fullName);
class NotificationGroup {
String repo;
List<NotificationPayload> items = [];
NotificationGroup(this.repo);
}
class NotificationScreen extends StatefulWidget {
@ -22,108 +27,98 @@ class NotificationScreen extends StatefulWidget {
class NotificationScreenState extends State<NotificationScreen> {
int active = 0;
bool loading = false;
List<NotificationGroup> groups = [];
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 0)).then((_) {
_onSwitchTab(context, 0);
});
}
Widget _buildGroupItem(BuildContext context, int index) {
if (loading) {
return Loading(more: false);
}
var group = groups[index];
Map<String, NotificationGroup> groupMap = {};
Widget _buildGroupItem(String key, NotificationGroup group) {
var repo = group.repo;
return ListGroup(
title: Text(
group.fullName,
style: TextStyle(color: Colors.black, fontSize: 15),
),
items: group.items,
itemBuilder: (item) => NotificationItem(payload: item),
);
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
repo,
style: TextStyle(color: Colors.black, fontSize: 15),
),
Link(
onTap: () async {
await putWithCredentials('/repos/$repo/notifications');
await _refresh();
},
child: Icon(
Octicons.check,
color: Colors.black45,
size: 20,
),
),
],
),
items: group.items,
itemBuilder: (item, index) {
return NotificationItem(
payload: item,
markAsRead: () {
setState(() {
groupMap[key].items[index].unread = false;
});
},
);
});
}
void _onSwitchTab(BuildContext context, int index) async {
setState(() {
active = index;
loading = true;
});
Future<void> _onSwitchTab(BuildContext context, int index) async {
// setState(() {
// active = index;
// loading = true;
// });
var ns = await ghClient.activity
.listNotifications(all: index == 2, participating: index == 1)
.toList();
var ns = await fetchNotifications();
NotificationProvider.of(context).setCount(ns.length);
// NotificationProvider.of(context).setCount(ns.length);
Map<String, NotificationGroup> groupMap = {};
Map<String, NotificationGroup> _groupMap = {};
ns.forEach((item) {
String repo = item.repository.fullName;
if (groupMap[repo] == null) {
groupMap[repo] = NotificationGroup(repo);
String repo = item.owner + '/' + item.name;
if (_groupMap[repo] == null) {
_groupMap[repo] = NotificationGroup(repo);
}
groupMap[repo].items.add(item);
_groupMap[repo].items.add(item);
});
setState(() {
groups = groupMap.values.toList();
loading = false;
groupMap = _groupMap;
// loading = false;
});
}
// TODO: filter
// CupertinoSegmentedControl(
// groupValue: active,
// onValueChanged: (index) => _onSwitchTab(context, index),
// children: {
// 0: Text('Unread'),
// 1: Text('Paticipating'),
// 2: Text('All')
// },
// )
Future<void> _refresh() async {
print('onrefresh');
await _onSwitchTab(context, active);
}
@override
Widget build(context) {
// NotificationBloc bloc = NotificationProvider.of(context);
TextStyle _textStyle = DefaultTextStyle.of(context).style;
return RefreshScaffold(
title: Text('Notifications'),
onRefresh: _refresh,
bodyBuilder: () {
var children = groupMap.entries
.map((entry) => _buildGroupItem(entry.key, entry.value))
.toList();
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: SizedBox.expand(
child: DefaultTextStyle(
style: _textStyle,
child: CupertinoSegmentedControl(
groupValue: active,
onValueChanged: (index) => _onSwitchTab(context, index),
children: {
0: Text('Unread'),
1: Text('Paticipating'),
2: Text('All')
},
),
),
),
),
child: SafeArea(
child: CustomScrollView(slivers: [
CupertinoSliverRefreshControl(),
SliverSafeArea(
top: false,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
_buildGroupItem,
childCount: groups.length,
),
),
),
]),
),
);
default:
return Scaffold(
appBar: AppBar(title: Text('Notification')),
body: ListView.builder(
itemCount: groups.length,
itemBuilder: _buildGroupItem,
),
);
}
return Column(children: children);
},
);
}
}

View File

@ -98,11 +98,13 @@ class _PullRequestScreenState extends State<PullRequestScreen> {
child: Row(
children: <Widget>[
Icon(iconData, color: Colors.white, size: 15),
Text(text,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
)),
Text(
text,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
);

View File

@ -91,7 +91,7 @@ class _UserScreenState extends State<UserScreen> {
return ListGroup(
title: Text(title),
items: items,
itemBuilder: (item) => RepoItem(item),
itemBuilder: (item, _) => RepoItem(item),
);
}

View File

@ -36,6 +36,23 @@ Future<dynamic> postWithCredentials(String url, String body,
return data;
}
Future<dynamic> putWithCredentials(String url,
{String contentType, String body}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
if (contentType != null) {
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.put(prefix + url, headers: headers, body: body ?? {});
final data = json.decode(res.body);
return data;
}
Future<dynamic> patchWithCredentials(String url) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
await http.patch(prefix + url, headers: headers);
return true;
}
Future<dynamic> query(String query) async {
final res =
await postWithCredentials('/graphql', json.encode({'query': query}));

View File

@ -20,6 +20,61 @@ class Option<T> {
Option({this.value, this.widget});
}
Future<bool> showConfim(BuildContext context, String text) {
switch (SettingsProvider.of(context).layout) {
case LayoutMap.cupertino:
return showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: Text(text),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, false);
},
),
CupertinoDialogAction(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
},
);
default:
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(
text,
// style: dialogTextStyle
),
actions: <Widget>[
FlatButton(
child: const Text('CANCEL'),
onPressed: () {
Navigator.pop(context, false);
},
),
FlatButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context, true);
},
)
],
);
},
);
}
}
Future<T> showOptions<T>(BuildContext context, List<Option<T>> options) {
var builder = (BuildContext context) {
return CupertinoAlertDialog(

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import '../screens/screens.dart';
import '../utils/utils.dart';
import '../widgets/widgets.dart';
import '../widgets/link.dart';
/// Events types:
///

View File

@ -4,14 +4,15 @@ import '../providers/settings.dart';
class Link extends StatelessWidget {
final Widget child;
final GestureTapCallback onTap;
final Color bgColor;
Link({@required this.child, @required this.onTap});
Link({@required this.child, @required this.onTap, this.bgColor});
@override
Widget build(BuildContext context) {
return Material(
child: Ink(
color: Colors.white,
color: bgColor ?? Colors.white,
child: InkWell(
splashColor:
SettingsProvider.of(context).layout == LayoutMap.cupertino

View File

@ -3,16 +3,16 @@ import 'package:flutter/material.dart';
class ListGroup<T> extends StatelessWidget {
final Widget title;
final List<T> items;
final Widget Function(T item) itemBuilder;
final Widget Function(T item, int index) itemBuilder;
ListGroup({this.title, this.items, this.itemBuilder});
Widget _buildItem(T item) {
Widget _buildItem(MapEntry<int, T> entry) {
return Container(
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.black12)),
),
child: itemBuilder(item),
child: itemBuilder(entry.value, entry.key),
);
}
@ -30,7 +30,7 @@ class ListGroup<T> extends StatelessWidget {
color: Color(0x10000000),
child: title,
),
Column(children: items.map(_buildItem).toList())
Column(children: items.asMap().entries.map(_buildItem).toList())
],
),
),

View File

@ -2,12 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
import '../widgets/link.dart';
import 'loading.dart';
typedef RefreshCallback = Future<void> Function();
class ListScaffold extends StatefulWidget {
final Widget title;
final IconData trailingIconData;
final Function trailingOnTap;
final Widget header;
final int itemCount;
final IndexedWidgetBuilder itemBuilder;
@ -16,6 +19,8 @@ class ListScaffold extends StatefulWidget {
ListScaffold({
@required this.title,
this.trailingIconData,
this.trailingOnTap,
this.header,
@required this.itemCount,
@required this.itemBuilder,
@ -47,7 +52,7 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Future<void> _refresh() async {
print('refresh');
print('list scaffold refresh');
setState(() {
loading = true;
});
@ -63,7 +68,7 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Future<void> _loadMore() async {
print('more');
print('list scaffold load more');
setState(() {
loadingMore = true;
});
@ -130,7 +135,18 @@ class _ListScaffoldState extends State<ListScaffold> {
slivers.add(_buildSliver(context));
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(middle: widget.title),
navigationBar: CupertinoNavigationBar(
middle: widget.title,
trailing: Link(
child: Icon(
widget.trailingIconData,
size: 24,
color: Colors.blueAccent,
),
onTap: widget.trailingOnTap,
bgColor: Colors.transparent,
),
),
child: SafeArea(
child: CustomScrollView(
controller: _controller,
@ -140,7 +156,17 @@ class _ListScaffoldState extends State<ListScaffold> {
);
default:
return Scaffold(
appBar: AppBar(title: widget.title),
appBar: AppBar(
title: widget.title,
actions: widget.trailingIconData == null
? []
: <Widget>[
IconButton(
icon: Icon(widget.trailingIconData),
onPressed: widget.trailingOnTap,
)
],
),
body: RefreshIndicator(
onRefresh: widget.onRefresh,
child: _buildBody(context),

View File

@ -1,12 +1,12 @@
import 'dart:core';
import 'package:flutter/material.dart' hide Notification;
import 'package:flutter/cupertino.dart' hide Notification;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../utils/utils.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import 'link.dart';
class NotificationPayload {
String id;
String type;
String owner;
String name;
@ -16,6 +16,7 @@ class NotificationPayload {
bool unread;
NotificationPayload.fromJson(input) {
id = input['id'];
type = input['subject']['type'];
name = input['repository']['name'];
owner = input['repository']['owner']['login'];
@ -30,13 +31,23 @@ class NotificationPayload {
}
}
class NotificationItem extends StatelessWidget {
const NotificationItem({
class NotificationItem extends StatefulWidget {
final NotificationPayload payload;
final Function markAsRead;
NotificationItem({
Key key,
@required this.payload,
@required this.markAsRead,
}) : super(key: key);
final NotificationPayload payload;
@override
_NotificationItemState createState() => _NotificationItemState();
}
class _NotificationItemState extends State<NotificationItem> {
NotificationPayload get payload => widget.payload;
bool loading = false;
Widget _buildRoute() {
switch (payload.type) {
@ -63,6 +74,14 @@ class NotificationItem extends StatelessWidget {
}
}
Widget _buildCheckIcon() {
return Icon(
payload.unread ? Octicons.check : Octicons.primitive_dot,
color: loading ? Colors.black12 : Colors.black45,
size: 20,
);
}
@override
Widget build(BuildContext context) {
return Link(
@ -71,59 +90,43 @@ class NotificationItem extends StatelessWidget {
CupertinoPageRoute(builder: (context) => _buildRoute()),
);
},
child: Container(
padding: EdgeInsets.all(8),
color: payload.unread ? Colors.white : Colors.black12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.only(right: 8, top: 20),
child: Icon(_buildIconData(), color: Colors.black45),
child: Opacity(
opacity: payload.unread ? 1 : 0.5,
child: Container(
padding: EdgeInsets.all(8),
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.only(right: 8),
child: Icon(_buildIconData(), color: Colors.black45, size: 20),
),
Expanded(
child: Text(
payload.title,
overflow: TextOverflow.ellipsis,
),
Expanded(
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
payload.owner +
'/' +
payload.name +
' #' +
payload.number.toString(),
style: TextStyle(fontSize: 13, color: Colors.black54),
),
Padding(padding: EdgeInsets.only(top: 4)),
Text(
payload.title,
style: TextStyle(fontSize: 15),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Padding(padding: EdgeInsets.only(top: 6)),
Text(
payload.updateAt,
style: TextStyle(
fontSize: 12,
// fontWeight: FontWeight.w300,
color: Colors.black54,
),
)
],
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Octicons.check, color: Colors.black45),
),
],
),
],
),
Link(
child: _buildCheckIcon(),
onTap: () async {
if (payload.unread && !loading) {
setState(() {
loading = true;
});
try {
await patchWithCredentials(
'/notifications/threads/' + payload.id);
widget.markAsRead();
} finally {
setState(() {
loading = false;
});
}
}
},
),
],
),
),
),
);

View File

@ -36,15 +36,15 @@ class _RefreshScaffoldState extends State<RefreshScaffold> {
setState(() {
loading = true;
});
try {
await widget.onRefresh();
} catch (err) {
print(err);
} finally {
setState(() {
loading = false;
});
}
// try {
await widget.onRefresh();
// } catch (err) {
// print(err);
// } finally {
setState(() {
loading = false;
});
// }
}
Widget _buildBody(BuildContext context) {
@ -64,7 +64,7 @@ class _RefreshScaffoldState extends State<RefreshScaffold> {
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverRefreshControl(onRefresh: widget.onRefresh),
CupertinoSliverRefreshControl(onRefresh: _refresh),
SliverToBoxAdapter(child: _buildBody(context))
],
),
@ -74,8 +74,8 @@ class _RefreshScaffoldState extends State<RefreshScaffold> {
return Scaffold(
appBar: AppBar(title: widget.title),
body: RefreshIndicator(
onRefresh: widget.onRefresh,
child: _buildBody(context),
onRefresh: _refresh,
child: SingleChildScrollView(child: _buildBody(context)),
),
);
}