diff --git a/lib/screens/issue.dart b/lib/screens/issue.dart index dc79ac5..903c078 100644 --- a/lib/screens/issue.dart +++ b/lib/screens/issue.dart @@ -353,26 +353,6 @@ fragment ReactableParts on Reactable { return status; } - _handleReaction(payload) { - return (String emojiKey, bool isRemove) async { - if (emojiKey == null) return; - - var id = payload['id'] as String; - var operation = isRemove ? 'remove' : 'add'; - await Provider.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) { final auth = Provider.of(context); @@ -497,10 +477,7 @@ mutation { CommonStyle.border, ], SizedBox(height: 8), - CommentItem( - p, - onReaction: _handleReaction(p), - ), + CommentItem.gh(p), ], ), ), @@ -508,8 +485,7 @@ mutation { ], ); }, - itemBuilder: (itemPayload) => - TimelineItem(itemPayload, onReaction: _handleReaction(itemPayload)), + itemBuilder: (itemPayload) => TimelineItem(itemPayload), onRefresh: () async { var res = await _queryIssue(); int totalCount = res['timelineItems']['totalCount']; diff --git a/lib/widgets/comment_item.dart b/lib/widgets/comment_item.dart index 14d9a5a..e5bb0c0 100644 --- a/lib/widgets/comment_item.dart +++ b/lib/widgets/comment_item.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:git_touch/models/auth.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/widgets/markdown_view.dart'; import 'package:provider/provider.dart'; @@ -20,44 +21,153 @@ final emojiMap = { 'EYES': '👀' }; -class CommentItem extends StatelessWidget { +class GhEmojiAction extends StatefulWidget { final Map payload; - final Function(String emojiKey, bool isRemove) onReaction; + GhEmojiAction(this.payload); + @override + _GhEmojiActionState createState() => _GhEmojiActionState(); +} - CommentItem(this.payload, {@required this.onReaction}); +class _GhEmojiActionState extends State { + Decoration _getDecorationByKey(String emojiKey) { + return BoxDecoration( + color: _hasReacted(emojiKey) ? PrimerColors.blue000 : Colors.transparent, + ); + } + + get payload => widget.payload; + + onReaction(String emojiKey, bool isRemove) async { + if (emojiKey == null) return; + + var id = payload['id'] as String; + var operation = isRemove ? 'remove' : 'add'; + await Provider.of(context).query(''' +mutation { + ${operation}Reaction(input: {subjectId: "$id", content: $emojiKey}) { + clientMutationId + } +} + '''); + setState(() { + payload[emojiKey]['totalCount'] += isRemove ? -1 : 1; + payload[emojiKey]['viewerHasReacted'] = !isRemove; + }); + } 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) ? PrimerColors.blue000 : Colors.transparent); + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...emojiMap.entries + .where((entry) => payload[entry.key]['totalCount'] as int != 0) + .map((entry) { + var emojiKey = entry.key; + var emoji = entry.value; + var count = payload[entry.key]['totalCount'] as int; + + return Link( + onTap: () { + onReaction(emojiKey, _hasReacted(emojiKey)); + }, + child: Container( + padding: EdgeInsets.all(4), + decoration: _getDecorationByKey(emojiKey), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(emoji, style: TextStyle(fontSize: 18)), + SizedBox(width: 4), + Text(numberFormat.format(count), + style: + TextStyle(color: theme.palette.primary, fontSize: 14)) + ], + ), + ), + ); + }), + Link( + onTap: () async { + final result = await theme.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(4), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text('+', style: TextStyle(color: theme.palette.primary)), + Icon(Octicons.smiley, color: theme.palette.primary, size: 18), + ], + ), + ), + ), + ], + ); } +} + +class CommentItem extends StatelessWidget { + final Avatar avatar; + final String login; + final DateTime createdAt; + final String body; + final List widgets; + + CommentItem.gh(Map payload) + : avatar = Avatar( + url: payload['author']['avatarUrl'], // TODO: deleted user + linkUrl: '/' + payload['author']['login'], + ), + login = payload['author']['login'], + createdAt = DateTime.parse(payload['createdAt']), + body = payload['body'], + widgets = [GhEmojiAction(payload)]; + + CommentItem({ + @required this.avatar, + @required this.login, + @required this.createdAt, + @required this.body, + this.widgets, + }); @override Widget build(BuildContext context) { final theme = Provider.of(context); - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ - Avatar( - url: payload['author']['avatarUrl'], - linkUrl: '/' + payload['author']['login'], - ), + avatar, SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UserName(payload['author']['login']), + UserName(login), SizedBox(height: 2), Text( - timeago.format(DateTime.parse(payload['createdAt'])), + timeago.format(createdAt), style: TextStyle( color: theme.palette.tertiaryText, fontSize: 13), ), @@ -66,69 +176,10 @@ class CommentItem extends StatelessWidget { ), ]), SizedBox(height: 12), - MarkdownView(payload['body'] as String), // TODO: link + MarkdownView(body), // TODO: link SizedBox(height: 12), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ...emojiMap.entries - .where((entry) => payload[entry.key]['totalCount'] as int != 0) - .map((entry) { - var emojiKey = entry.key; - var emoji = entry.value; - var count = payload[entry.key]['totalCount'] as int; - - return Link( - onTap: () { - onReaction(emojiKey, _hasReacted(emojiKey)); - }, - child: Container( - padding: EdgeInsets.all(4), - decoration: _getDecorationByKey(emojiKey), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(emoji, style: TextStyle(fontSize: 18)), - SizedBox(width: 4), - Text(numberFormat.format(count), - style: TextStyle( - color: theme.palette.primary, fontSize: 14)) - ], - ), - ), - ); - }), - Link( - onTap: () async { - var result = - await Provider.of(context).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(4), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text('+', style: TextStyle(color: theme.palette.primary)), - Icon(Octicons.smiley, - color: theme.palette.primary, size: 18), - ], - ), - ), - ), - ], - ), + if (widgets != null) + ...widgets ], ); } diff --git a/lib/widgets/timeline_item.dart b/lib/widgets/timeline_item.dart index 6e4b770..319c3c8 100644 --- a/lib/widgets/timeline_item.dart +++ b/lib/widgets/timeline_item.dart @@ -51,9 +51,7 @@ class TimelineEventItem extends StatelessWidget { class TimelineItem extends StatelessWidget { final Map p; - final Function(String emojiKey, bool isRemove) onReaction; - - TimelineItem(this.p, {@required this.onReaction}); + TimelineItem(this.p); TextSpan _buildReviewText(BuildContext context, item) { switch (item['state']) { @@ -102,7 +100,7 @@ class TimelineItem extends StatelessWidget { p: p, ); case 'IssueComment': - return CommentItem(p, onReaction: onReaction); + return CommentItem.gh(p); case 'CrossReferencedEvent': return TimelineEventItem( actor: p['actor']['login'], @@ -336,9 +334,10 @@ class TimelineItem extends StatelessWidget { Container( padding: CommonStyle.padding.copyWith(left: 50), child: Column( - children: comments.map((v) { - return CommentItem(v, onReaction: (_, __) {}); - }).toList()), + children: [ + for (var v in comments) CommentItem.gh(v), + ], + ), ), ], );