feat: add and remove reactions

This commit is contained in:
Rongjian Zhang 2019-04-05 21:19:00 +08:00
parent af79d33a7f
commit 3bc1e7edcd
6 changed files with 155 additions and 44 deletions

View File

@ -346,6 +346,17 @@ class SettingsProviderState extends State<SettingsProvider> {
return true;
}
Future<dynamic> postWithCredentials(String url,
{String contentType, String body}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
final res = await http
.post(prefix + url, headers: headers, body: body ?? {})
.timeout(_timeoutDuration);
// print(res.body);
return true;
}
Future<dynamic> deleteWithCredentials(String url) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
final res = await http

View File

@ -53,6 +53,7 @@ class _IssueScreenState extends State<IssueScreen> {
String get issueChunk {
var base = '''
id
title
createdAt
body
@ -82,6 +83,7 @@ commits {
var base = '''
__typename
... on IssueComment {
id
createdAt
body
author {
@ -344,6 +346,26 @@ __typename
);
}
_handleReaction(payload) {
return (String emojiKey, bool isRemove) async {
if (emojiKey == null) return;
var id = payload['id'] as String;
var operation = isRemove ? 'remove' : 'add';
await SettingsProvider.of(context).query('''
mutation {
${operation}Reaction(input: {subjectId: "$id", content: $emojiKey}) {
clientMutationId
}
}
''');
setState(() {
payload[emojiKey]['totalCount'] += isRemove ? -1 : 1;
payload[emojiKey]['viewerHasReacted'] = !isRemove;
});
};
}
@override
Widget build(BuildContext context) {
return LongListScaffold(
@ -394,13 +416,17 @@ __typename
],
),
Padding(padding: EdgeInsets.only(bottom: 16)),
CommentItem(payload),
CommentItem(
payload,
onReaction: _handleReaction(payload),
),
],
),
)
]);
},
itemBuilder: (itemPayload) => TimelineItem(itemPayload),
itemBuilder: (itemPayload) =>
TimelineItem(itemPayload, onReaction: _handleReaction(itemPayload)),
onRefresh: () async {
var res = await _queryIssue();
int totalCount = res['timeline']['totalCount'];

View File

@ -91,37 +91,58 @@ class DialogOption<T> {
DialogOption({this.value, this.widget});
}
Future<T> showOptions<T>(BuildContext context, List<DialogOption<T>> options) {
var builder = (BuildContext context) {
return CupertinoAlertDialog(
actions: options.map((option) {
return CupertinoDialogAction(
child: option.widget,
onPressed: () {
Navigator.pop(context, option.value);
},
);
}).toList(),
);
};
Future<T> showDialogOptions<T>(
BuildContext context, List<DialogOption<T>> options) {
var title = Text('Pick your reaction');
var cancelWidget = Text('Cancel');
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return showCupertinoDialog<T>(
context: context,
builder: builder,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: title,
actions: options.map((option) {
return CupertinoDialogAction(
child: option.widget,
onPressed: () {
Navigator.pop(context, option.value);
},
);
}).toList()
..add(
CupertinoDialogAction(
child: cancelWidget,
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context, null);
},
),
),
);
},
);
default:
return showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Column(
children: <Widget>[
PopupMenuItem(child: Text('a')),
PopupMenuItem(child: Text('b')),
],
),
return SimpleDialog(
title: title,
children: options.map<Widget>((option) {
return SimpleDialogOption(
child: option.widget,
onPressed: () {
Navigator.pop(context, option.value);
},
);
}).toList()
..add(SimpleDialogOption(
child: cancelWidget,
onPressed: () {
Navigator.pop(context, null);
},
)),
);
},
);
@ -162,6 +183,7 @@ class Palette {
static const link = Color(0xff0366d6);
static const branchName = Palette.link;
static const branchBackground = Color(0xffeaf5ff);
static const emojiBackground = Color(0xfff1f8ff);
}
// final pageSize = 5;

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../utils/utils.dart';
import 'avatar.dart';
import 'link.dart';
import 'user_name.dart';
final emojiMap = {
@ -17,8 +18,21 @@ final emojiMap = {
class CommentItem extends StatelessWidget {
final Map<String, dynamic> payload;
final Function(String emojiKey, bool isRemove) onReaction;
CommentItem(this.payload);
CommentItem(this.payload, {@required this.onReaction});
bool _hasReacted(String emojiKey) {
if (payload[emojiKey] == null) return false;
return payload[emojiKey]['viewerHasReacted'] as bool;
}
Decoration _getDecorationByKey(String emojiKey) {
return BoxDecoration(
color: _hasReacted(emojiKey)
? Palette.emojiBackground
: Colors.transparent);
}
@override
Widget build(BuildContext context) {
@ -55,28 +69,63 @@ class CommentItem extends StatelessWidget {
),
Wrap(
children: emojiMap.entries
.where((entry) => payload[entry.key]['totalCount'] != 0)
.map((entry) {
.where((entry) => payload[entry.key]['totalCount'] as int != 0)
.map<Widget>((entry) {
var emojiKey = entry.key;
var emoji = entry.value;
int count = payload[entry.key]['totalCount'];
var count = payload[entry.key]['totalCount'] as int;
return Container(
padding: EdgeInsets.all(6),
child: RichText(
text: TextSpan(
style: TextStyle(fontSize: 16),
children: [
TextSpan(text: emoji),
TextSpan(text: ' '),
TextSpan(
text: count.toString(),
style: TextStyle(color: Palette.link),
),
],
return Link(
onTap: () {
onReaction(emojiKey, _hasReacted(emojiKey));
},
child: Container(
padding: EdgeInsets.all(6),
decoration: _getDecorationByKey(emojiKey),
child: RichText(
text: TextSpan(
style: TextStyle(fontSize: 16),
children: [
TextSpan(text: emoji),
TextSpan(text: ' '),
TextSpan(
text: count.toString(),
style: TextStyle(color: Palette.link),
),
],
),
),
),
);
}).toList(),
}).toList()
..add(
Link(
onTap: () async {
var result = await showDialogOptions(
context,
emojiMap.entries.map((entry) {
var emojiKey = entry.key;
return DialogOption(
value: emojiKey,
widget: Container(
decoration: _getDecorationByKey(emojiKey),
child: Text(emojiKey + ' ' + entry.value),
),
);
}).toList(),
);
onReaction(result, _hasReacted(result));
},
child: Container(
padding: EdgeInsets.all(12),
child: Icon(
Octicons.smiley,
color: Palette.link,
size: 16,
),
),
),
),
),
],
);

View File

@ -8,6 +8,8 @@ class Loading extends StatelessWidget {
Loading({this.more = false});
Widget _buildIndicator(BuildContext context) {
// return Image.asset('images/loading.webp');
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoActivityIndicator(radius: 12);

View File

@ -7,8 +7,9 @@ import 'user_name.dart';
class TimelineItem extends StatelessWidget {
final Map<String, dynamic> payload;
final Function(String emojiKey, bool isRemove) onReaction;
TimelineItem(this.payload);
TimelineItem(this.payload, {@required this.onReaction});
TextSpan _buildReviewText(BuildContext context, item) {
switch (item['state']) {
@ -95,7 +96,7 @@ class TimelineItem extends StatelessWidget {
item: payload,
);
case 'IssueComment':
return CommentItem(payload);
return CommentItem(payload, onReaction: onReaction);
case 'CrossReferencedEvent':
return _buildItem(
actor: payload['actor']['login'],
@ -295,7 +296,7 @@ class TimelineItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10),
padding: EdgeInsets.all(12),
child: _buildByType(context),
);
}