git-touch-android-ios-app/lib/screens/issue.dart

552 lines
13 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
2019-01-25 18:43:09 +01:00
import 'package:flutter/cupertino.dart';
2020-01-07 13:48:50 +01:00
import 'package:git_touch/graphql/gh.dart';
2019-09-27 14:52:38 +02:00
import 'package:git_touch/models/auth.dart';
2020-01-12 08:44:07 +01:00
import 'package:git_touch/models/theme.dart';
2019-12-25 03:41:51 +01:00
import 'package:git_touch/utils/utils.dart';
2019-09-30 10:31:07 +02:00
import 'package:git_touch/widgets/action_button.dart';
2020-01-12 08:44:07 +01:00
import 'package:git_touch/widgets/avatar.dart';
import 'package:git_touch/widgets/link.dart';
2019-05-12 09:14:28 +02:00
import 'package:primer/primer.dart';
2019-09-08 14:07:35 +02:00
import 'package:provider/provider.dart';
import '../scaffolds/long_list.dart';
2019-02-03 07:42:50 +01:00
import '../widgets/timeline_item.dart';
2019-02-06 06:06:11 +01:00
import '../widgets/comment_item.dart';
2019-12-13 06:13:45 +01:00
final issueRouter = RouterScreen(
'/:owner/:name/issues/:number',
(context, params) => IssueScreen(params['owner'].first,
params['name'].first, int.parse(params['number'].first)));
2019-12-14 15:36:02 +01:00
final pullRouter = RouterScreen(
'/:owner/:name/pulls/:number',
(context, params) => IssueScreen(params['owner'].first,
params['name'].first, int.parse(params['number'].first),
isPullRequest: true));
2019-11-30 17:22:19 +01:00
final reactionChunk = emojiMap.entries.map((entry) {
var key = entry.key;
return '''
$key: reactions(content: $key) {
totalCount
viewerHasReacted
}''';
}).join('\n');
/// Screen for issue and pull request
2019-02-03 07:42:50 +01:00
class IssueScreen extends StatefulWidget {
final String owner;
final String name;
2019-11-02 17:02:41 +01:00
final int number;
final bool isPullRequest;
2019-11-02 17:02:41 +01:00
IssueScreen(this.owner, this.name, this.number, {this.isPullRequest = false});
2019-02-03 07:42:50 +01:00
@override
_IssueScreenState createState() => _IssueScreenState();
}
class _IssueScreenState extends State<IssueScreen> {
String get owner => widget.owner;
String get name => widget.name;
int get number => widget.number;
bool get isPullRequest => widget.isPullRequest;
2019-02-03 07:42:50 +01:00
String get resource => isPullRequest ? 'pullRequest' : 'issue';
String get issueChunk {
var base = '''
2020-01-12 08:44:07 +01:00
repository {
owner {
avatarUrl
}
}
2019-11-30 17:22:19 +01:00
title
closed
url
2020-01-07 13:48:50 +01:00
viewerCanReact
viewerCanUpdate
2019-11-30 17:22:19 +01:00
...CommentParts
...ReactableParts
''';
if (isPullRequest) {
base += '''
merged
additions
deletions
2020-01-12 08:44:07 +01:00
changedFiles
commits {
totalCount
}
''';
}
return base;
}
String get timelineChunk {
var base = '''
__typename
... on IssueComment {
2019-11-30 17:22:19 +01:00
...CommentParts
...ReactableParts
}
... on ReferencedEvent {
createdAt
isCrossRepository
actor {
login
}
commit {
oid
url
}
commitRepository {
owner {
login
}
name
}
}
... on RenamedTitleEvent {
createdAt
previousTitle
currentTitle
actor {
login
}
}
... on ClosedEvent {
createdAt
actor {
login
}
}
... on ReopenedEvent {
createdAt
actor {
login
}
}
... on CrossReferencedEvent {
createdAt
actor {
login
}
source {
__typename
... on Issue {
number
repository {
owner {
login
2019-02-07 07:35:19 +01:00
}
name
}
}
... on PullRequest {
number
repository {
owner {
login
2019-02-07 07:35:19 +01:00
}
name
2019-02-07 07:35:19 +01:00
}
}
}
}
... on LabeledEvent {
createdAt
actor {
login
}
label {
name
color
}
}
... on UnlabeledEvent {
createdAt
actor {
login
}
label {
name
color
}
}
... on MilestonedEvent {
createdAt
actor {
login
2019-02-07 07:35:19 +01:00
}
milestoneTitle
}
... on LockedEvent {
createdAt
actor {
login
}
lockReason
}
... on UnlockedEvent {
createdAt
actor {
login
}
}
... on AssignedEvent {
createdAt
actor {
login
}
user {
login
}
}
''';
2019-02-07 07:35:19 +01:00
if (isPullRequest) {
base += '''
2020-01-12 09:03:11 +01:00
... on PullRequestCommit {
prCommit: commit {
committedDate
oid
author {
user {
login
}
}
}
}
2019-11-30 16:49:05 +01:00
... on HeadRefForcePushedEvent {
createdAt
actor {
login
}
pullRequest {
headRef {
name
}
}
beforeCommit {
oid
}
afterCommit {
oid
}
}
... on ReviewRequestedEvent {
createdAt
actor {
login
}
requestedReviewer {
... on User {
login
}
}
}
... on PullRequestReview {
createdAt
state
author {
login
}
2019-11-30 17:22:19 +01:00
comments(first: 10) {
nodes {
...CommentParts
...ReactableParts
}
}
}
... on MergedEvent {
createdAt
mergeRefName
actor {
login
}
commit {
oid
url
}
}
... on HeadRefDeletedEvent {
createdAt
actor {
login
}
headRefName
}
''';
}
return base;
}
Future _queryIssue({String cursor, bool trailing = false}) async {
String timelineParams;
if (trailing) {
timelineParams = 'last: $pageSize';
} else {
timelineParams = 'first: $pageSize';
if (cursor != null) {
2020-01-12 09:07:10 +01:00
timelineParams += ', after: "$cursor"';
}
}
2019-09-27 14:52:38 +02:00
var data = await Provider.of<AuthModel>(context).query('''
2019-11-30 17:22:19 +01:00
fragment CommentParts on Comment {
id
createdAt
body
author {
login
avatarUrl
}
}
fragment ReactableParts on Reactable {
$reactionChunk
}
{
repository(owner: "$owner", name: "$name") {
$resource(number: $number) {
$issueChunk
2020-01-12 09:03:11 +01:00
timelineItems($timelineParams) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
$timelineChunk
}
}
}
}
}
''');
return data['repository'][resource];
}
2019-05-12 09:47:36 +02:00
StateLabelStatus _getLabelStatus(payload) {
StateLabelStatus status;
if (isPullRequest) {
if (payload['merged']) {
2019-05-12 09:47:36 +02:00
status = StateLabelStatus.pullMerged;
} else if (payload['closed']) {
2019-05-12 09:47:36 +02:00
status = StateLabelStatus.pullClosed;
} else {
2019-05-12 09:47:36 +02:00
status = StateLabelStatus.pullOpened;
}
} else {
if (payload['closed']) {
2019-05-12 09:47:36 +02:00
status = StateLabelStatus.issueClosed;
} else {
2019-05-12 09:47:36 +02:00
status = StateLabelStatus.issueOpened;
}
}
2019-05-12 09:47:36 +02:00
return status;
}
2019-04-05 15:19:00 +02:00
_handleReaction(payload) {
return (String emojiKey, bool isRemove) async {
if (emojiKey == null) return;
var id = payload['id'] as String;
var operation = isRemove ? 'remove' : 'add';
2019-09-27 14:52:38 +02:00
await Provider.of<AuthModel>(context).query('''
2019-04-05 15:19:00 +02:00
mutation {
${operation}Reaction(input: {subjectId: "$id", content: $emojiKey}) {
clientMutationId
}
}
''');
setState(() {
payload[emojiKey]['totalCount'] += isRemove ? -1 : 1;
payload[emojiKey]['viewerHasReacted'] = !isRemove;
});
};
}
2019-01-25 18:43:09 +01:00
@override
Widget build(BuildContext context) {
2020-01-07 13:48:50 +01:00
final auth = Provider.of<AuthModel>(context);
2019-10-03 05:00:59 +02:00
return LongListStatefulScaffold(
2020-01-12 08:44:07 +01:00
title: Text(isPullRequest ? 'Pull Request' : 'Issue'),
2020-01-07 13:48:50 +01:00
trailingBuilder: (payload, setState) {
2019-02-20 09:31:22 +01:00
return ActionButton(
title: (isPullRequest ? 'Pull Request' : 'Issue') + ' Actions',
2019-09-30 09:46:06 +02:00
items: [
if (payload != null) ...[
2020-01-07 13:48:50 +01:00
if (!isPullRequest && payload['viewerCanUpdate'])
ActionItem(
text: payload['closed'] ? 'Reopen issue' : 'Close issue',
onTap: (_) async {
final res = await auth.gqlClient.execute(
GhOpenIssueQuery(
variables: GhOpenIssueArguments(
id: payload['id'],
open: payload['closed'],
),
),
);
setState(() {
payload['closed'] = res.data.reopenIssue?.issue?.closed ??
res.data.closeIssue.issue.closed;
});
},
),
2019-09-30 09:46:06 +02:00
ActionItem.share(payload['url']),
ActionItem.launch(payload['url']),
],
2019-02-20 09:31:22 +01:00
],
);
},
2020-01-12 08:44:07 +01:00
headerBuilder: (p) {
final theme = Provider.of<ThemeModel>(context);
2019-09-07 11:48:59 +02:00
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
2019-10-02 10:09:54 +02:00
padding: CommonStyle.padding,
2019-09-07 11:48:59 +02:00
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
2020-01-12 08:44:07 +01:00
Avatar(
url: p['repository']['owner']['avatarUrl'],
size: AvatarSize.extraSmall,
),
SizedBox(width: 4),
Text(
'$owner / $name',
style: TextStyle(
fontSize: 17,
2020-01-14 11:13:07 +01:00
color: theme.paletteOf(context).secondaryText,
2020-01-12 08:44:07 +01:00
),
),
SizedBox(width: 4),
Text(
'#$number',
style: TextStyle(
fontSize: 17,
2020-01-14 11:13:07 +01:00
color: theme.paletteOf(context).tertiaryText,
),
),
2019-09-07 11:48:59 +02:00
],
),
2020-01-12 08:44:07 +01:00
SizedBox(height: 8),
Text(
p['title'],
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8),
StateLabel(_getLabelStatus(p), small: true),
SizedBox(height: 8),
CommonStyle.border,
if (isPullRequest) ...[
Link(
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'${p['changedFiles']} files changed',
style: TextStyle(
2020-01-14 11:13:07 +01:00
color: theme.paletteOf(context).secondaryText,
2020-01-12 08:44:07 +01:00
fontSize: 17,
),
),
Row(
children: <Widget>[
Text(
'+${p['additions']}',
style: TextStyle(
color: Colors.green,
fontSize: 15,
),
),
SizedBox(width: 2),
Text(
'-${p['deletions']}',
style: TextStyle(
color: Colors.red,
fontSize: 15,
),
),
Icon(
Icons.chevron_right,
2020-01-14 11:13:07 +01:00
color: theme.paletteOf(context).border,
2020-01-12 08:44:07 +01:00
),
],
)
],
),
),
url: 'https://github.com/$owner/$name/pull/$number/files',
),
CommonStyle.border,
],
SizedBox(height: 8),
2019-09-07 11:48:59 +02:00
CommentItem(
2020-01-12 08:44:07 +01:00
p,
onReaction: _handleReaction(p),
2019-09-07 11:48:59 +02:00
),
],
),
),
2019-10-02 10:09:54 +02:00
CommonStyle.border,
2019-09-07 11:48:59 +02:00
],
);
},
2019-04-05 15:19:00 +02:00
itemBuilder: (itemPayload) =>
TimelineItem(itemPayload, onReaction: _handleReaction(itemPayload)),
2019-02-03 07:42:50 +01:00
onRefresh: () async {
var res = await _queryIssue();
2020-01-12 09:03:11 +01:00
int totalCount = res['timelineItems']['totalCount'];
String cursor = res['timelineItems']['pageInfo']['endCursor'];
List leadingItems = res['timelineItems']['nodes'];
var payload = LongListPayload(
header: res,
totalCount: totalCount,
cursor: cursor,
leadingItems: leadingItems,
trailingItems: [],
);
if (totalCount > 2 * pageSize) {
var res = await _queryIssue(trailing: true);
2020-01-12 09:03:11 +01:00
payload.trailingItems = res['timelineItems']['nodes'];
2019-02-06 14:35:52 +01:00
}
return payload;
},
onLoadMore: (String _cursor) async {
var res = await _queryIssue(cursor: _cursor);
2020-01-12 09:03:11 +01:00
int totalCount = res['timelineItems']['totalCount'];
String cursor = res['timelineItems']['pageInfo']['endCursor'];
List leadingItems = res['timelineItems']['nodes'];
var payload = LongListPayload(
totalCount: totalCount,
cursor: cursor,
leadingItems: leadingItems,
);
return payload;
2019-02-03 07:42:50 +01:00
},
2019-01-25 18:43:09 +01:00
);
}
}