diff --git a/lib/models/github.dart b/lib/models/github.dart index 231ceef..e36f53c 100644 --- a/lib/models/github.dart +++ b/lib/models/github.dart @@ -62,7 +62,7 @@ class GithubEventPayload { String action; String ref; String before; - String after; + String head; List commits; Map forkee; @@ -79,6 +79,11 @@ class GithubEventIssue { int number; String body; dynamic pullRequest; + String state; + int comments; + bool merged; + + bool get isPullRequestComment => pullRequest != null; GithubEventIssue(); diff --git a/lib/models/github.g.dart b/lib/models/github.g.dart index 5425d13..4109b5a 100644 --- a/lib/models/github.g.dart +++ b/lib/models/github.g.dart @@ -68,7 +68,7 @@ GithubEventPayload _$GithubEventPayloadFromJson(Map json) { ..action = json['action'] as String ..ref = json['ref'] as String ..before = json['before'] as String - ..after = json['after'] as String + ..head = json['head'] as String ..commits = (json['commits'] as List) ?.map((e) => e == null ? null @@ -85,7 +85,7 @@ Map _$GithubEventPayloadToJson(GithubEventPayload instance) => 'action': instance.action, 'ref': instance.ref, 'before': instance.before, - 'after': instance.after, + 'head': instance.head, 'commits': instance.commits, 'forkee': instance.forkee, }; @@ -98,7 +98,10 @@ GithubEventIssue _$GithubEventIssueFromJson(Map json) { : GithubEventUser.fromJson(json['user'] as Map) ..number = json['number'] as int ..body = json['body'] as String - ..pullRequest = json['pull_request']; + ..pullRequest = json['pull_request'] + ..state = json['state'] as String + ..comments = json['comments'] as int + ..merged = json['merged'] as bool; } Map _$GithubEventIssueToJson(GithubEventIssue instance) => @@ -108,6 +111,9 @@ Map _$GithubEventIssueToJson(GithubEventIssue instance) => 'number': instance.number, 'body': instance.body, 'pull_request': instance.pullRequest, + 'state': instance.state, + 'comments': instance.comments, + 'merged': instance.merged, }; GithubEventComment _$GithubEventCommentFromJson(Map json) { diff --git a/lib/models/theme.dart b/lib/models/theme.dart index be07bd4..a3a0ede 100644 --- a/lib/models/theme.dart +++ b/lib/models/theme.dart @@ -71,6 +71,7 @@ class Palette { Color secondaryText; Color tertiaryText; Color background; + Color grayBackground; Color border; Palette({ @@ -79,6 +80,7 @@ class Palette { this.secondaryText, this.tertiaryText, this.background, + this.grayBackground, this.border, }); } @@ -110,6 +112,7 @@ class ThemeModel with ChangeNotifier { secondaryText: PrimerColors.gray700, tertiaryText: PrimerColors.gray500, background: PrimerColors.white, + grayBackground: PrimerColors.gray100, border: PrimerColors.gray100, ); case Brightness.dark: @@ -119,6 +122,7 @@ class ThemeModel with ChangeNotifier { secondaryText: PrimerColors.gray400, tertiaryText: PrimerColors.gray500, background: PrimerColors.black, + grayBackground: PrimerColors.gray800, border: PrimerColors.gray900, ); default: diff --git a/lib/widgets/event_item.dart b/lib/widgets/event_item.dart index 21c3c74..5285e90 100644 --- a/lib/widgets/event_item.dart +++ b/lib/widgets/event_item.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:git_touch/models/github.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/widgets/action_button.dart'; -import 'package:primer/primer.dart'; +import 'package:git_touch/widgets/issue_icon.dart'; import 'package:provider/provider.dart'; import 'package:timeago/timeago.dart' as timeago; import 'avatar.dart'; @@ -11,9 +11,9 @@ import '../widgets/link.dart'; import '../utils/utils.dart'; class EventItem extends StatelessWidget { - final GithubEvent event; + final GithubEvent e; - EventItem(this.event); + EventItem(this.e); TextSpan _buildLinkSpan(ThemeModel theme, String text) { return TextSpan( @@ -24,8 +24,7 @@ class EventItem extends StatelessWidget { ); } - TextSpan _buildRepo(ThemeModel theme) => - _buildLinkSpan(theme, event.repo.name); + TextSpan _buildRepo(ThemeModel theme) => _buildLinkSpan(theme, e.repo.name); Iterable _getUserActions(List users) { // Remove duplicates @@ -38,16 +37,15 @@ class EventItem extends StatelessWidget { @required BuildContext context, @required List spans, String detail, - Widget detailWidget, + Widget card, IconData iconData = Octicons.octoface, String url, List actionItems, }) { final theme = Provider.of(context); - if (detailWidget == null && detail != null) { - detailWidget = - Text(detail.trim(), overflow: TextOverflow.ellipsis, maxLines: 5); + if (card == null && detail != null) { + card = Text(detail.trim(), overflow: TextOverflow.ellipsis, maxLines: 5); } return Container( @@ -59,8 +57,8 @@ class EventItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Link( - url: '/' + event.actor.login, - child: Avatar.small(url: event.actor.avatarUrl), + url: '/' + e.actor.login, + child: Avatar.small(url: e.actor.avatarUrl), ), SizedBox(width: 10), Expanded( @@ -74,7 +72,7 @@ class EventItem extends StatelessWidget { color: theme.palette.text, ), children: [ - _buildLinkSpan(theme, event.actor.login), + _buildLinkSpan(theme, e.actor.login), ...spans, ], ), @@ -85,7 +83,7 @@ class EventItem extends StatelessWidget { Icon(iconData, color: theme.palette.tertiaryText, size: 14), SizedBox(width: 4), - Text(timeago.format(event.createdAt), + Text(timeago.format(e.createdAt), style: TextStyle( fontSize: 13, color: theme.palette.tertiaryText, @@ -99,18 +97,7 @@ class EventItem extends StatelessWidget { // ), ], ), - if (detailWidget != null) - Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: PrimerColors.gray100, - borderRadius: BorderRadius.all(Radius.circular(4))), - child: DefaultTextStyle( - style: TextStyle( - color: theme.palette.text, fontSize: 14), - child: detailWidget, - ), - ), + if (card != null) card ]), ), ), @@ -127,28 +114,133 @@ class EventItem extends StatelessWidget { context: context, spans: [ TextSpan( - text: ' ' + event.type, + text: ' ' + e.type, style: TextStyle(color: theme.palette.primary), ) ], iconData: Octicons.octoface, - detail: 'Woops, ${event.type} not implemented yet', + detail: 'Woops, ${e.type} not implemented yet', ); } - Widget _buildIssueCard(GithubEventIssue issue, String body) { - return Column( - children: [ - Row( - children: [ - Icon(Octicons.issue_opened), - Text('#' + issue.number.toString()), - Text(issue.title), + Widget _buildCommitsCard(BuildContext context) { + final theme = Provider.of(context); + return Link( + url: + 'https://github.com/${e.repoOwner}/${e.repoName}/compare/${e.payload.before}...${e.payload.head}', + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.palette.grayBackground, + borderRadius: BorderRadius.all(Radius.circular(4))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: TextStyle(color: theme.palette.text), + children: [ + TextSpan( + text: + e.payload.commits.length.toString() + ' commits to '), + WidgetSpan( + child: PrimerBranchName( + e.payload.ref.replaceFirst('refs/heads/', '')), + ), + ], + ), + ), + SizedBox(height: 4), + ...e.payload.commits.map((commit) { + return Row( + children: [ + Text( + commit.sha.substring(0, 7), + style: TextStyle( + color: theme.palette.primary, + fontSize: 13, + fontFamily: CommonStyle.monospace, + ), + ), + SizedBox(width: 6), + Expanded( + child: Text( + commit.message, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(color: theme.palette.text), + ), + ) + ], + ); + }).toList() ], ), - SizedBox(height: 4), - if (body != null) Text(body), - ], + ), + ); + } + + Widget _buildIssueCard( + BuildContext context, GithubEventIssue issue, String body, + {isPullRequest = false}) { + final theme = Provider.of(context); + IssueIconState state; + if (isPullRequest) { + if (issue.merged == true) { + state = IssueIconState.prMerged; + } else if (issue.state == 'open') { + state = IssueIconState.prOpen; + } else { + state = IssueIconState.prClosed; + } + } else { + if (issue.state == 'open') { + state = IssueIconState.open; + } else { + state = IssueIconState.closed; + } + } + + return Link( + url: + '/${e.repoOwner}/${e.repoName}/${isPullRequest ? 'pulls' : 'issues'}/${issue.number}', + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.palette.grayBackground, + borderRadius: BorderRadius.all(Radius.circular(4))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IssueIcon(state, size: 20), + SizedBox(width: 4), + Expanded( + child: Text( + '#' + issue.number.toString() + ' ' + issue.title, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: theme.palette.text, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + if (body != null) ...[ + SizedBox(height: 6), + Text( + body, + overflow: TextOverflow.ellipsis, + maxLines: 3, + style: TextStyle(color: theme.palette.text), + ), + ] + ], + ), + ), ); } @@ -158,7 +250,7 @@ class EventItem extends StatelessWidget { // all events types here: // https://developer.github.com/v3/activity/events/types/#event-types--payloads - switch (event.type) { + switch (e.type) { case 'CheckRunEvent': case 'CheckSuiteEvent': case 'CommitCommentEvent': @@ -172,8 +264,8 @@ class EventItem extends StatelessWidget { // TODO: return _buildDefaultItem(context); case 'ForkEvent': - final forkeeOwner = event.payload.forkee['owner']['login'] as String; - final forkeeName = event.payload.forkee['name'] as String; + final forkeeOwner = e.payload.forkee['owner']['login'] as String; + final forkeeName = e.payload.forkee['name'] as String; return _buildItem( context: context, spans: [ @@ -185,9 +277,9 @@ class EventItem extends StatelessWidget { iconData: Octicons.repo_forked, url: '/$forkeeOwner/$forkeeName', actionItems: [ - ..._getUserActions([event.actor.login, forkeeOwner]), + ..._getUserActions([e.actor.login, forkeeOwner]), ActionItem.repository(forkeeOwner, forkeeName), - ActionItem.repository(event.repoOwner, event.repoName), + ActionItem.repository(e.repoOwner, e.repoName), ], ); case 'ForkApplyEvent': @@ -199,46 +291,46 @@ class EventItem extends StatelessWidget { // TODO: return _buildDefaultItem(context); case 'IssueCommentEvent': - final isPullRequest = event.payload.issue.pullRequest != null; - return _buildItem( context: context, spans: [ TextSpan( text: - ' commented on ${isPullRequest ? 'pull request' : 'issue'} '), - _buildLinkSpan(theme, '#${event.payload.issue.number}'), + ' commented on ${e.payload.issue.isPullRequestComment ? 'pull request' : 'issue'} '), + _buildLinkSpan(theme, '#${e.payload.issue.number}'), TextSpan(text: ' at '), _buildRepo(theme), ], - detailWidget: - _buildIssueCard(event.payload.issue, event.payload.comment.body), + card: _buildIssueCard( + context, + e.payload.issue, + e.payload.comment.body, + isPullRequest: e.payload.issue.isPullRequestComment, + ), iconData: Octicons.comment_discussion, - url: - '/${event.repoOwner}/${event.repoName}/${isPullRequest ? 'pulls' : 'issues'}/${event.payload.issue.number}', actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), + ..._getUserActions([e.actor.login, e.repoOwner]), ActionItem.pullRequest( - event.repoOwner, event.repoName, event.payload.issue.number), + e.repoOwner, e.repoName, e.payload.issue.number), ], ); case 'IssuesEvent': - final issue = event.payload.issue; + final issue = e.payload.issue; return _buildItem( context: context, spans: [ - TextSpan(text: ' ${event.payload.action} issue '), + TextSpan(text: ' ${e.payload.action} issue '), _buildLinkSpan(theme, '#${issue.number}'), TextSpan(text: ' at '), _buildRepo(theme), ], iconData: Octicons.issue_opened, - detailWidget: _buildIssueCard(issue, issue.body), - url: '/${event.repoOwner}/${event.repoName}/issues/${issue.number}', + card: _buildIssueCard(context, issue, issue.body), + url: '/${e.repoOwner}/${e.repoName}/issues/${issue.number}', actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), - ActionItem.repository(event.repoOwner, event.repoName), - ActionItem.issue(event.repoOwner, event.repoName, issue.number), + ..._getUserActions([e.actor.login, e.repoOwner]), + ActionItem.repository(e.repoOwner, e.repoName), + ActionItem.issue(e.repoOwner, e.repoName, issue.number), ], ); case 'LabelEvent': @@ -256,29 +348,29 @@ class EventItem extends StatelessWidget { // TODO: return _buildDefaultItem(context); case 'PullRequestEvent': - final pr = event.payload.pullRequest; + final pr = e.payload.pullRequest; return _buildItem( context: context, spans: [ - TextSpan(text: ' ${event.payload.action} pull request '), + TextSpan(text: ' ${e.payload.action} pull request '), _buildLinkSpan(theme, '#${pr.number}'), TextSpan(text: ' at '), _buildRepo(theme), ], iconData: Octicons.git_pull_request, - detailWidget: _buildIssueCard(pr, pr.body), - url: '/${event.repoOwner}/${event.repoName}/pulls/${pr.number}', + card: _buildIssueCard(context, pr, pr.body, isPullRequest: true), + url: '/${e.repoOwner}/${e.repoName}/pulls/${pr.number}', actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), - ActionItem.repository(event.repoOwner, event.repoName), - ActionItem.pullRequest(event.repoOwner, event.repoName, pr.number), + ..._getUserActions([e.actor.login, e.repoOwner]), + ActionItem.repository(e.repoOwner, e.repoName), + ActionItem.pullRequest(e.repoOwner, e.repoName, pr.number), ], ); case 'PullRequestReviewEvent': // TODO: return _buildDefaultItem(context); case 'PullRequestReviewCommentEvent': - final pr = event.payload.pullRequest; + final pr = e.payload.pullRequest; return _buildItem( context: context, spans: [ @@ -287,12 +379,12 @@ class EventItem extends StatelessWidget { TextSpan(text: ' at '), _buildRepo(theme), ], - detailWidget: _buildIssueCard(pr, pr.body), - url: '/${event.repoOwner}/${event.repoName}/pulls/${pr.number}', + card: _buildIssueCard(context, pr, pr.body), + url: '/${e.repoOwner}/${e.repoName}/pulls/${pr.number}', actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), - ActionItem.repository(event.repoOwner, event.repoName), - ActionItem.pullRequest(event.repoOwner, event.repoName, pr.number), + ..._getUserActions([e.actor.login, e.repoOwner]), + ActionItem.repository(e.repoOwner, e.repoName), + ActionItem.pullRequest(e.repoOwner, e.repoName, pr.number), ], ); case 'PushEvent': @@ -300,52 +392,10 @@ class EventItem extends StatelessWidget { context: context, spans: [TextSpan(text: ' pushed to '), _buildRepo(theme)], iconData: Octicons.repo_push, - detailWidget: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - style: TextStyle(color: theme.palette.primary), - children: [ - TextSpan( - text: event.payload.commits.length.toString() + - ' commits to '), - WidgetSpan( - child: PrimerBranchName( - event.payload.ref.replaceFirst('refs/heads/', '')), - ), - ], - ), - ), - ...event.payload.commits.map((commit) { - return Row( - children: [ - Text( - commit.sha.substring(0, 7), - style: TextStyle( - color: theme.palette.primary, - fontSize: 13, - fontFamily: CommonStyle.monospace, - ), - ), - SizedBox(width: 6), - Expanded( - child: Text( - commit.message, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ) - ], - ); - }).toList() - ], - ), - url: - 'https://github.com/${event.repoOwner}/${event.repoName}/compare/${event.payload.before}...${event.payload.after}', + card: _buildCommitsCard(context), actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), - ActionItem.repository(event.repoOwner, event.repoName), + ..._getUserActions([e.actor.login, e.repoOwner]), + ActionItem.repository(e.repoOwner, e.repoName), ], ); case 'ReleaseEvent': @@ -363,10 +413,10 @@ class EventItem extends StatelessWidget { context: context, spans: [TextSpan(text: ' starred '), _buildRepo(theme)], iconData: Octicons.star, - url: '/${event.repoOwner}/${event.repoName}', + url: '/${e.repoOwner}/${e.repoName}', actionItems: [ - ..._getUserActions([event.actor.login, event.repoOwner]), - ActionItem.repository(event.repoOwner, event.repoName), + ..._getUserActions([e.actor.login, e.repoOwner]), + ActionItem.repository(e.repoOwner, e.repoName), ], ); default: diff --git a/lib/widgets/issue_icon.dart b/lib/widgets/issue_icon.dart new file mode 100644 index 0000000..ab39145 --- /dev/null +++ b/lib/widgets/issue_icon.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/utils/utils.dart'; + +enum IssueIconState { + open, + closed, + prOpen, + prClosed, + prMerged, +} + +class IssueIcon extends StatelessWidget { + final IssueIconState state; + final double size; + + IssueIcon(this.state, {this.size}); + + @override + Widget build(BuildContext context) { + switch (state) { + case IssueIconState.open: + return Icon(Octicons.issue_opened, + color: GithubPalette.open, size: size); + case IssueIconState.closed: + return Icon(Octicons.issue_closed, + color: GithubPalette.closed, size: size); + case IssueIconState.prOpen: + return Icon(Octicons.git_pull_request, + color: GithubPalette.open, size: size); + case IssueIconState.prClosed: + return Icon(Octicons.git_pull_request, + color: GithubPalette.closed, size: size); + case IssueIconState.prMerged: + return Icon(Octicons.git_merge, + color: GithubPalette.merged, size: size); + default: + return null; + } + } +} diff --git a/lib/widgets/notification_item.dart b/lib/widgets/notification_item.dart index 016c430..9f6e107 100644 --- a/lib/widgets/notification_item.dart +++ b/lib/widgets/notification_item.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:git_touch/models/notification.dart'; import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/widgets/issue_icon.dart'; import '../utils/utils.dart'; -import '../screens/issue.dart'; import 'package:git_touch/models/auth.dart'; import 'package:provider/provider.dart'; import 'link.dart'; @@ -36,9 +36,9 @@ class _NotificationItemState extends State { case 'Issue': switch (payload.state) { case 'OPEN': - return _buildIcon(Octicons.issue_opened, GithubPalette.open); + return IssueIcon(IssueIconState.open, size: 20); case 'CLOSED': - return _buildIcon(Octicons.issue_closed, GithubPalette.closed); + return IssueIcon(IssueIconState.closed, size: 20); default: return _buildIcon(Octicons.person); } @@ -46,11 +46,11 @@ class _NotificationItemState extends State { case 'PullRequest': switch (payload.state) { case 'OPEN': - return _buildIcon(Octicons.git_pull_request, GithubPalette.open); + return IssueIcon(IssueIconState.prOpen, size: 20); case 'CLOSED': - return _buildIcon(Octicons.git_pull_request, GithubPalette.closed); + return IssueIcon(IssueIconState.prClosed, size: 20); case 'MERGED': - return _buildIcon(Octicons.git_merge, GithubPalette.merged); + return IssueIcon(IssueIconState.prMerged, size: 20); default: return _buildIcon(Octicons.person); }