From 1d885b1bf12092ecf696f48aa2cff6c2cb90e2e2 Mon Sep 17 00:00:00 2001 From: Reuben Turner Date: Fri, 29 May 2020 17:28:00 -0400 Subject: [PATCH] Data Models (#70) * remove deprecated code * remove more deprecated code * Implement data models * bump version --- README.md | 2 +- lib/data/activity_feed_models.dart | 456 +++++++++++++++++++++ lib/data/following_users.dart | 126 ++++++ lib/data/search_results.dart | 114 ++++++ lib/data/viewer_following.dart | 15 + lib/screens/search_screen.dart | 25 +- lib/services/gh_gql_query_service.dart | 18 +- lib/services/repositories/readmes.dart | 69 ---- lib/widgets/activity_feed.dart | 29 +- lib/widgets/async_markdown_scrollable.dart | 211 ---------- lib/widgets/following_users.dart | 12 +- lib/widgets/github_markdown.dart | 67 --- lib/widgets/issue_card.dart | 23 +- lib/widgets/issue_comment_card.dart | 23 +- lib/widgets/pull_request_card.dart | 27 +- lib/widgets/star_card.dart | 13 +- lib/widgets/user_card.dart | 84 ++-- pubspec.yaml | 2 +- 18 files changed, 860 insertions(+), 456 deletions(-) create mode 100644 lib/data/activity_feed_models.dart create mode 100644 lib/data/following_users.dart create mode 100644 lib/data/search_results.dart create mode 100644 lib/data/viewer_following.dart delete mode 100644 lib/services/repositories/readmes.dart delete mode 100644 lib/widgets/async_markdown_scrollable.dart delete mode 100644 lib/widgets/github_markdown.dart diff --git a/README.md b/README.md index f48fb81..1a0340b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Flutter application for viewing a rich feed of GitHub activity. [![Generic badge](https://badgen.net/badge/support/GitHub%20Spronsors/blue?icon=github())](https://github.com/sponsors/GroovinChip) ## Project status: -**Public Preview**: Version 0.2.2 +**Public Preview**: Version 0.2.3 **Supported platforms**: Android diff --git a/lib/data/activity_feed_models.dart b/lib/data/activity_feed_models.dart new file mode 100644 index 0000000..66a27d4 --- /dev/null +++ b/lib/data/activity_feed_models.dart @@ -0,0 +1,456 @@ +class Following { + List user; + + Following({this.user}); + + Following.fromJson(Map json) { + if (json['user'] != null) { + user = new List(); + json['user'].forEach((v) { + user.add(new UserActivity.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + if (this.user != null) { + data['user'] = this.user.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class UserActivity { + Issues issues; + IssueComments issueComments; + PullRequests pullRequests; + StarredRepositories starredRepositories; + + UserActivity({this.issues, this.issueComments, this.pullRequests, this.starredRepositories}); + + UserActivity.fromJson(Map json) { + issues = json['issues'] != null ? new Issues.fromJson(json['issues']) : null; + issueComments = json['issueComments'] != null ? new IssueComments.fromJson(json['issueComments']) : null; + pullRequests = json['pullRequests'] != null ? new PullRequests.fromJson(json['pullRequests']) : null; + starredRepositories = json['starredRepositories'] != null ? new StarredRepositories.fromJson(json['starredRepositories']) : null; + } + + Map toJson() { + final Map data = new Map(); + if (this.issues != null) { + data['issues'] = this.issues.toJson(); + } + if (this.issueComments != null) { + data['issueComments'] = this.issueComments.toJson(); + } + if (this.pullRequests != null) { + data['pullRequests'] = this.pullRequests.toJson(); + } + if (this.starredRepositories != null) { + data['starredRepositories'] = this.starredRepositories.toJson(); + } + return data; + } +} + +class Issues { + List issues; + + Issues({this.issues}); + + Issues.fromJson(Map json) { + if (json['issue'] != null) { + issues = new List(); + json['issue'].forEach((v) { + issues.add(new Issue.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + if (this.issues != null) { + data['issue'] = this.issues.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Issue { + String sTypename; + int databaseId; + String title; + String url; + int number; + String bodyText; + Author author; + Repository repository; + String createdAt; + + Issue({this.sTypename, this.databaseId, this.title, this.url, this.number, this.bodyText, this.author, this.repository, this.createdAt}); + + Issue.fromJson(Map json) { + sTypename = json['__typename']; + databaseId = json['databaseId']; + title = json['title']; + url = json['url']; + number = json['number']; + bodyText = json['bodyText']; + author = json['author'] != null ? new Author.fromJson(json['author']) : null; + repository = json['repository'] != null ? new Repository.fromJson(json['repository']) : null; + createdAt = json['createdAt']; + } + + Map toJson() { + final Map data = new Map(); + data['__typename'] = this.sTypename; + data['databaseId'] = this.databaseId; + data['title'] = this.title; + data['url'] = this.url; + data['number'] = this.number; + data['bodyText'] = this.bodyText; + if (this.author != null) { + data['author'] = this.author.toJson(); + } + if (this.repository != null) { + data['repository'] = this.repository.toJson(); + } + data['createdAt'] = this.createdAt; + return data; + } +} + +class Author { + String login; + String avatarUrl; + String url; + + Author({this.login, this.avatarUrl, this.url}); + + Author.fromJson(Map json) { + login = json['login']; + avatarUrl = json['avatarUrl']; + url = json['url']; + } + + Map toJson() { + final Map data = new Map(); + data['login'] = this.login; + data['avatarUrl'] = this.avatarUrl; + data['url'] = this.url; + return data; + } +} + +class Repository { + String nameWithOwner; + String description; + String url; + + Repository({this.nameWithOwner, this.description, this.url}); + + Repository.fromJson(Map json) { + nameWithOwner = json['nameWithOwner']; + description = json['description']; + url = json['url']; + } + + Map toJson() { + final Map data = new Map(); + data['nameWithOwner'] = this.nameWithOwner; + data['description'] = this.description; + data['url'] = this.url; + return data; + } +} + +class IssueComments { + List issueComments; + + IssueComments({this.issueComments}); + + IssueComments.fromJson(Map json) { + if (json['issueComment'] != null) { + issueComments = new List(); + json['issueComment'].forEach((v) { + issueComments.add(new IssueComment.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + if (this.issueComments != null) { + data['issueComment'] = this.issueComments.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class IssueComment { + String sTypename; + int databaseId; + String bodyText; + String createdAt; + String url; + Author author; + ParentIssue parentIssue; + + IssueComment({this.sTypename, this.databaseId, this.bodyText, this.createdAt, this.url, this.author, this.parentIssue}); + + IssueComment.fromJson(Map json) { + sTypename = json['__typename']; + databaseId = json['databaseId']; + bodyText = json['bodyText']; + createdAt = json['createdAt']; + url = json['url']; + author = json['author'] != null ? new Author.fromJson(json['author']) : null; + parentIssue = json['parentIssue'] != null ? new ParentIssue.fromJson(json['parentIssue']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['__typename'] = this.sTypename; + data['databaseId'] = this.databaseId; + data['bodyText'] = this.bodyText; + data['createdAt'] = this.createdAt; + data['url'] = this.url; + if (this.author != null) { + data['author'] = this.author.toJson(); + } + if (this.parentIssue != null) { + data['parentIssue'] = this.parentIssue.toJson(); + } + return data; + } +} + +class ParentIssue { + String title; + Author author; + Repository repository; + String id; + int number; + + ParentIssue({this.title, this.author, this.repository, this.id, this.number}); + + ParentIssue.fromJson(Map json) { + title = json['title']; + author = json['author'] != null ? new Author.fromJson(json['author']) : null; + repository = json['repository'] != null ? new Repository.fromJson(json['repository']) : null; + id = json['id']; + number = json['number']; + } + + Map toJson() { + final Map data = new Map(); + data['title'] = this.title; + if (this.author != null) { + data['author'] = this.author.toJson(); + } + if (this.repository != null) { + data['repository'] = this.repository.toJson(); + } + data['id'] = this.id; + data['number'] = this.number; + return data; + } +} + +class PullRequests { + List pullRequests; + + PullRequests({this.pullRequests}); + + PullRequests.fromJson(Map json) { + if (json['pullRequest'] != null) { + pullRequests = new List(); + json['pullRequest'].forEach((v) { + pullRequests.add(new PullRequest.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + if (this.pullRequests != null) { + data['pullRequest'] = this.pullRequests.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PullRequest { + String sTypename; + int databaseId; + String title; + String url; + int number; + String baseRefName; + String headRefName; + String bodyText; + String createdAt; + int changedFiles; + Author author; + Repository repository; + + PullRequest( + {this.sTypename, + this.databaseId, + this.title, + this.url, + this.number, + this.baseRefName, + this.headRefName, + this.bodyText, + this.createdAt, + this.changedFiles, + this.author, + this.repository}); + + PullRequest.fromJson(Map json) { + sTypename = json['__typename']; + databaseId = json['databaseId']; + title = json['title']; + url = json['url']; + number = json['number']; + baseRefName = json['baseRefName']; + headRefName = json['headRefName']; + bodyText = json['bodyText']; + createdAt = json['createdAt']; + changedFiles = json['changedFiles']; + author = json['author'] != null ? new Author.fromJson(json['author']) : null; + repository = json['repository'] != null ? new Repository.fromJson(json['repository']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['__typename'] = this.sTypename; + data['databaseId'] = this.databaseId; + data['title'] = this.title; + data['url'] = this.url; + data['number'] = this.number; + data['baseRefName'] = this.baseRefName; + data['headRefName'] = this.headRefName; + data['bodyText'] = this.bodyText; + data['createdAt'] = this.createdAt; + data['changedFiles'] = this.changedFiles; + if (this.author != null) { + data['author'] = this.author.toJson(); + } + if (this.repository != null) { + data['repository'] = this.repository.toJson(); + } + return data; + } +} + +class StarredRepositories { + List srEdges; + + StarredRepositories({this.srEdges}); + + StarredRepositories.fromJson(Map json) { + if (json['srEdges'] != null) { + srEdges = new List(); + json['srEdges'].forEach((v) { + srEdges.add(new SrEdge.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + if (this.srEdges != null) { + data['srEdges'] = this.srEdges.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class SrEdge { + String createdAt; + String sTypename; + Star star; + + SrEdge({this.createdAt, this.sTypename, this.star}); + + SrEdge.fromJson(Map json) { + createdAt = json['createdAt']; + sTypename = json['__typename']; + star = json['star'] != null ? new Star.fromJson(json['star']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['createdAt'] = this.createdAt; + data['__typename'] = this.sTypename; + if (this.star != null) { + data['star'] = this.star.toJson(); + } + return data; + } +} + +class Star { + String sTypename; + String id; + int databaseId; + String nameWithOwner; + String description; + int forkCount; + bool isFork; + Stargazers stargazers; + String updatedAt; + String url; + + Star( + {this.sTypename, this.id, this.databaseId, this.nameWithOwner, this.description, this.forkCount, this.isFork, this.stargazers, this.updatedAt, this.url}); + + Star.fromJson(Map json) { + sTypename = json['__typename']; + id = json['id']; + databaseId = json['databaseId']; + nameWithOwner = json['nameWithOwner']; + description = json['description']; + forkCount = json['forkCount']; + isFork = json['isFork']; + stargazers = json['stargazers'] != null ? new Stargazers.fromJson(json['stargazers']) : null; + updatedAt = json['updatedAt']; + url = json['url']; + } + + Map toJson() { + final Map data = new Map(); + data['__typename'] = this.sTypename; + data['id'] = this.id; + data['databaseId'] = this.databaseId; + data['nameWithOwner'] = this.nameWithOwner; + data['description'] = this.description; + data['forkCount'] = this.forkCount; + data['isFork'] = this.isFork; + if (this.stargazers != null) { + data['stargazers'] = this.stargazers.toJson(); + } + data['updatedAt'] = this.updatedAt; + data['url'] = this.url; + return data; + } +} + +class Stargazers { + int totalCount; + + Stargazers({this.totalCount}); + + Stargazers.fromJson(Map json) { + totalCount = json['totalCount']; + } + + Map toJson() { + final Map data = new Map(); + data['totalCount'] = this.totalCount; + return data; + } +} diff --git a/lib/data/following_users.dart b/lib/data/following_users.dart new file mode 100644 index 0000000..a7df7ea --- /dev/null +++ b/lib/data/following_users.dart @@ -0,0 +1,126 @@ +class FollowingUsers { + Following following; + + FollowingUsers({this.following}); + + FollowingUsers.fromJson(Map json) { + following = json['following'] != null ? new Following.fromJson(json['following']) : null; + } + + Map toJson() { + final Map data = new Map(); + if (this.following != null) { + data['following'] = this.following.toJson(); + } + return data; + } +} + +class Following { + int totalCount; + List users; + + Following({this.totalCount, this.users}); + + Following.fromJson(Map json) { + totalCount = json['totalCount']; + if (json['users'] != null) { + users = new List(); + json['users'].forEach((v) { + users.add(new FollowingUser.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['totalCount'] = this.totalCount; + if (this.users != null) { + data['users'] = this.users.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class FollowingUser { + String id; + String login; + String url; + String avatarUrl; + String createdAt; + bool viewerIsFollowing; + String bio; + String location; + String name; + String email; + String company; + Status status; + + FollowingUser({ + this.id, + this.login, + this.url, + this.avatarUrl, + this.createdAt, + this.viewerIsFollowing, + this.bio, + this.location, + this.name, + this.email, + this.company, + this.status, + }); + + FollowingUser.fromJson(Map json) { + id = json['id']; + login = json['login']; + url = json['url']; + avatarUrl = json['avatarUrl']; + createdAt = json['createdAt']; + viewerIsFollowing = json['viewerIsFollowing']; + bio = json['bio']; + location = json['location']; + name = json['name']; + email = json['email']; + company = json['company']; + status = json['status'] != null ? new Status.fromJson(json['status']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['login'] = this.login; + data['url'] = this.url; + data['avatarUrl'] = this.avatarUrl; + data['createdAt'] = this.createdAt; + data['viewerIsFollowing'] = this.viewerIsFollowing; + data['bio'] = this.bio; + data['location'] = this.location; + data['name'] = this.name; + data['email'] = this.email; + data['company'] = this.company; + if (this.status != null) { + data['status'] = this.status.toJson(); + } + return data; + } +} + +class Status { + String emoji; + String message; + + Status({this.emoji, this.message}); + + Status.fromJson(Map json) { + emoji = json['emoji']; + message = json['message']; + } + + Map toJson() { + final Map data = new Map(); + data['emoji'] = this.emoji; + data['message'] = this.message; + return data; + } +} diff --git a/lib/data/search_results.dart b/lib/data/search_results.dart new file mode 100644 index 0000000..af07c30 --- /dev/null +++ b/lib/data/search_results.dart @@ -0,0 +1,114 @@ +class SearchResults { + int userCount; + List edges; + + SearchResults({this.userCount, this.edges}); + + SearchResults.fromJson(Map json) { + userCount = json['userCount']; + if (json['edges'] != null) { + edges = new List(); + json['edges'].forEach((v) { + edges.add(new SearchEdge.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['userCount'] = this.userCount; + if (this.edges != null) { + data['edges'] = this.edges.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class SearchEdge { + UserSearchNode node; + + SearchEdge({this.node}); + + SearchEdge.fromJson(Map json) { + node = json['node'] != null ? new UserSearchNode.fromJson(json['node']) : null; + } + + Map toJson() { + final Map data = new Map(); + if (this.node != null) { + data['node'] = this.node.toJson(); + } + return data; + } +} + +class UserSearchNode { + String id; + String login; + String avatarUrl; + bool viewerIsFollowing; + String bio; + String name; + String url; + String company; + Status status; + + UserSearchNode({ + this.id, + this.login, + this.avatarUrl, + this.viewerIsFollowing, + this.bio, + this.name, + this.url, + this.company, + this.status, + }); + + UserSearchNode.fromJson(Map json) { + id = json['id']; + login = json['login']; + avatarUrl = json['avatarUrl']; + viewerIsFollowing = json['viewerIsFollowing']; + bio = json['bio']; + name = json['name']; + url = json['url']; + company = json['company']; + status = json['status'] != null ? new Status.fromJson(json['status']) : null; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['login'] = this.login; + data['avatarUrl'] = this.avatarUrl; + data['viewerIsFollowing'] = this.viewerIsFollowing; + data['bio'] = this.bio; + data['name'] = this.name; + data['url'] = this.url; + data['company'] = this.company; + if (this.status != null) { + data['status'] = this.status.toJson(); + } + return data; + } +} + +class Status { + String emoji; + String message; + + Status({this.emoji, this.message}); + + Status.fromJson(Map json) { + emoji = json['emoji']; + message = json['message']; + } + + Map toJson() { + final Map data = new Map(); + data['emoji'] = this.emoji; + data['message'] = this.message; + return data; + } +} diff --git a/lib/data/viewer_following.dart b/lib/data/viewer_following.dart new file mode 100644 index 0000000..f7627da --- /dev/null +++ b/lib/data/viewer_following.dart @@ -0,0 +1,15 @@ +class ViewerFollowing { + bool viewerIsFollowing; + + ViewerFollowing({this.viewerIsFollowing}); + + ViewerFollowing.fromJson(Map json) { + viewerIsFollowing = json['viewerIsFollowing']; + } + + Map toJson() { + final Map data = new Map(); + data['viewerIsFollowing'] = this.viewerIsFollowing; + return data; + } +} diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index e18f7ea..f49edf7 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:github/github.dart'; +import 'package:github/github.dart' hide SearchResults; +import 'package:github_activity_feed/data/search_results.dart'; import 'package:github_activity_feed/services/gh_gql_query_service.dart'; import 'package:github_activity_feed/widgets/feedback_on_error.dart'; import 'package:github_activity_feed/widgets/user_card.dart'; @@ -11,7 +12,7 @@ class SearchScreen extends SearchDelegate { }); final GitHub gitHub; - List users = []; + SearchResults searchResults; @override String get searchFieldLabel => 'Search users'; @@ -34,7 +35,7 @@ class SearchScreen extends SearchDelegate { icon: Icon(Icons.clear), onPressed: () { query = ''; - users.clear(); + searchResults.edges.clear(); }, ), ]; @@ -48,10 +49,14 @@ class SearchScreen extends SearchDelegate { @override Widget buildResults(BuildContext context) { return ListView.builder( - itemCount: users.length, + itemCount: searchResults.edges.length, itemBuilder: (BuildContext context, int index) { return UserCard( - user: users[index], + avatarUrl: searchResults.edges[index].node.avatarUrl, + id: searchResults.edges[index].node.id, + login: searchResults.edges[index].node.login, + name: searchResults.edges[index].node.name, + url: searchResults.edges[index].node.url, ); }, ); @@ -79,12 +84,16 @@ class SearchScreen extends SearchDelegate { ); } else { /// results - users = snapshot.data['search']['edges']; + searchResults = SearchResults.fromJson(snapshot.data['search']); return ListView.builder( - itemCount: users.length, + itemCount: searchResults.edges.length, itemBuilder: (BuildContext context, int index) { return UserCard( - user: users[index]['node'], + avatarUrl: searchResults.edges[index].node.avatarUrl, + id: searchResults.edges[index].node.id, + login: searchResults.edges[index].node.login, + name: searchResults.edges[index].node.name, + url: searchResults.edges[index].node.url, ); }, ); diff --git a/lib/services/gh_gql_query_service.dart b/lib/services/gh_gql_query_service.dart index eead413..330ad00 100644 --- a/lib/services/gh_gql_query_service.dart +++ b/lib/services/gh_gql_query_service.dart @@ -187,12 +187,12 @@ class GhGraphQLService { Future activityFeed() async { final GQLResponse response = await client.query( query: r''' - query { + { user: viewer { following(last: 10) { - nodes { + user: nodes { issues(last: 10) { - nodes { + issue: nodes { __typename databaseId title @@ -213,7 +213,7 @@ class GhGraphQLService { } } issueComments(last: 10) { - nodes { + issueComment:nodes { __typename databaseId bodyText @@ -224,7 +224,7 @@ class GhGraphQLService { avatarUrl url } - issue { + parentIssue: issue { title author { login @@ -242,7 +242,7 @@ class GhGraphQLService { } } pullRequests(last: 10) { - nodes { + pullRequest: nodes { __typename databaseId title @@ -266,10 +266,10 @@ class GhGraphQLService { } } starredRepositories(last: 10) { - edges { + srEdges: edges { createdAt: starredAt __typename - node { + star: node { __typename id databaseId @@ -310,6 +310,8 @@ class GhGraphQLService { avatarUrl viewerIsFollowing bio + name + url company status { emoji diff --git a/lib/services/repositories/readmes.dart b/lib/services/repositories/readmes.dart deleted file mode 100644 index 36f6041..0000000 --- a/lib/services/repositories/readmes.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:convert'; -import 'dart:math' as math; - -import 'package:crypto/crypto.dart' show md5; -import 'package:flutter/foundation.dart' show compute; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:github/github.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:quiver/collection.dart' show LruMap; - -/// This class will handle all things readme: -/// - fetch, pre-cache, cache -/// - parse markdown -/// - generate unique id's for readmes for referencing through the app -class ReadmeRepository { - /// Readme cache - static final markdownCache = LruMap>(maximumSize: 25); - - /// Parse the markdown for a readme - static List parseMarkdownIsolate(String data) { - return md.Document( - extensionSet: md.ExtensionSet.gitHubFlavored, - inlineSyntaxes: [TaskListSyntax()], - encodeHtml: false, - ).parseLines(LineSplitter.split(data).toList(growable: false)); - } - - /// Generate a unique key for a readme - static String generateHashKey(String data) { - return md5.convert(utf8.encode(data)).toString(); - } - - /// Pre-cache the markdown for a readme - static Future preCacheMarkdown(String markdown) async { - final hash = generateHashKey(markdown); - markdownCache[hash] = await compute(parseMarkdownIsolate, markdown); - } - - /// - static Future preCacheReadmes(GitHub gitHub, List events) async { - final futureFiles = >[]; - for(int i = 0; i < math.min(events.length, 25); i++) { - final event = events[i]; - final slug = RepositorySlug.full(event.repo.name); - futureFiles.add(gitHub.repositories.getReadme(slug)); - } - final files = await Future.wait(futureFiles); - final futureCaches = >[]; - for(final file in files) { - futureCaches.add(preCacheMarkdown(file.content)); - } - await Future.wait(futureCaches); - print('Finished pre-caching readmes'); - } -} - -/// Model class that represents a readme in a GitHub repository -class Readme { - Readme({ - this.key, - this.data, - }); - - /// A unique id key for the readme to easily identify it and retrieve it from the repository - final String key; - - /// The readme data to be parsed - final String data; -} diff --git a/lib/widgets/activity_feed.dart b/lib/widgets/activity_feed.dart index 9583fb6..8019d2b 100644 --- a/lib/widgets/activity_feed.dart +++ b/lib/widgets/activity_feed.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/activity_feed_models.dart'; import 'package:github_activity_feed/services/gh_gql_query_service.dart'; import 'package:github_activity_feed/widgets/feedback_on_error.dart'; import 'package:github_activity_feed/widgets/issue_card.dart'; @@ -22,20 +23,21 @@ class ActivityFeed extends StatelessWidget { return FeedbackOnError(message: snapshot.error.toString()); } else { /// lists of data - final users = snapshot.data['user']['following']['nodes']; - List issues = []; - List issueComments = []; - List pullRequests = []; - List stars = []; + final Following feed = Following.fromJson(snapshot.data['user']['following']); + List issues = []; + List issueComments = []; + List pullRequests = []; + List stars = []; + // todo: turn into Map so that we get the name of the user who starred. Then turn back into list. List activityFeed = []; /// populate lists - for (int uIndex = 0; uIndex < users.length; uIndex++) { - issues += users[uIndex]['issues']['nodes']; - issueComments += users[uIndex]['issueComments']['nodes']; - pullRequests += users[uIndex]['pullRequests']['nodes']; - stars += users[uIndex]['starredRepositories']['edges']; + for (int uIndex = 0; uIndex < feed.user.length; uIndex++) { + issues += feed.user[uIndex].issues.issues; + issueComments += feed.user[uIndex].issueComments.issueComments; + pullRequests += feed.user[uIndex].pullRequests.pullRequests; + stars += feed.user[uIndex].starredRepositories.srEdges; } /// populate master list and sort by date/time @@ -44,7 +46,7 @@ class ActivityFeed extends StatelessWidget { ..addAll(issueComments) ..addAll(pullRequests) ..addAll(stars) - ..sort((e1, e2) => e2['createdAt'].compareTo(e1['createdAt'])); + ..sort((e1, e2) => e2.createdAt.compareTo(e1.createdAt)); /// build activity feed return Scrollbar( @@ -52,7 +54,7 @@ class ActivityFeed extends StatelessWidget { itemCount: activityFeed.length, padding: const EdgeInsets.all(8.0), itemBuilder: (BuildContext context, int index) { - switch (activityFeed[index]['__typename']) { + switch (activityFeed[index].sTypename) { case 'Issue': return IssueCard(issue: activityFeed[index]); case 'IssueComment': @@ -61,7 +63,8 @@ class ActivityFeed extends StatelessWidget { return PullRequestCard(pullRequest: activityFeed[index]); case 'StarredRepositoryEdge': return StarCard( - star: activityFeed[index], + star: activityFeed[index].star, + starredAt: activityFeed[index].createdAt, ); default: return Container(); diff --git a/lib/widgets/async_markdown_scrollable.dart b/lib/widgets/async_markdown_scrollable.dart deleted file mode 100644 index b88dfd0..0000000 --- a/lib/widgets/async_markdown_scrollable.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart' show compute; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:github_activity_feed/services/repositories/readmes.dart'; -import 'package:github_activity_feed/utils/markdown_io.dart'; -import 'package:markdown/markdown.dart' as md; - -/// Not meant to be used directly - use GitHubMarkdown -class AsyncMarkdown extends StatefulWidget { - /// Creates a widget that parses and displays Markdown. - /// - /// The [data] argument must not be null. - const AsyncMarkdown({ - Key key, - @required this.data, - @required this.useScrollable, - this.selectable = false, - this.styleSheet, - this.styleSheetTheme = MarkdownStyleSheetBaseTheme.material, - this.syntaxHighlighter, - this.onTapLink, - this.imageDirectory, - this.extensionSet, - this.imageBuilder, - this.checkboxBuilder, - this.fitContent = false, - this.padding = const EdgeInsets.all(16.0), - this.controller, - this.physics, - this.shrinkWrap = false, - }) : assert(data != null), - assert(selectable != null), - super(key: key); - - final bool useScrollable; - - /// The amount of space by which to inset the children. - final EdgeInsets padding; - - /// An object that can be used to control the position to which this scroll view is scrolled. - /// - /// See also: [ScrollView.controller] - final ScrollController controller; - - /// How the scroll view should respond to user input. - /// - /// See also: [ScrollView.physics] - final ScrollPhysics physics; - - /// Whether the extent of the scroll view in the scroll direction should be - /// determined by the contents being viewed. - /// - /// See also: [ScrollView.shrinkWrap] - final bool shrinkWrap; - - /// The Markdown to display. - final String data; - - /// If true, the text is selectable. - /// - /// Defaults to false. - final bool selectable; - - /// The styles to use when displaying the Markdown. - /// - /// If null, the styles are inferred from the current [Theme]. - final MarkdownStyleSheet styleSheet; - - /// Setting to specify base theme for MarkdownStyleSheet - /// - /// Default to [MarkdownStyleSheetBaseTheme.material] - final MarkdownStyleSheetBaseTheme styleSheetTheme; - - /// The syntax highlighter used to color text in `pre` elements. - /// - /// If null, the [MarkdownStyleSheet.code] style is used for `pre` elements. - final SyntaxHighlighter syntaxHighlighter; - - /// Called when the user taps a link. - final MarkdownTapLinkCallback onTapLink; - - /// The base directory holding images referenced by Img tags with local or network file paths. - final String imageDirectory; - - /// Markdown syntax extension set - /// - /// Defaults to [md.ExtensionSet.gitHubFlavored] - final md.ExtensionSet extensionSet; - - /// Call when build an image widget. - final MarkdownImageBuilder imageBuilder; - - /// Call when build a checkbox widget. - final MarkdownCheckboxBuilder checkboxBuilder; - - /// Whether to allow the widget to fit the child content. - final bool fitContent; - - @override - _AsyncMarkdownWidgetState createState() => _AsyncMarkdownWidgetState(); -} - -class _AsyncMarkdownWidgetState extends State implements MarkdownBuilderDelegate { - List _children; - final List _recognizers = []; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _parseMarkdown().catchError((error, stackTrace) { - // - }); - } - - @override - void didUpdateWidget(AsyncMarkdown oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.data != oldWidget.data || widget.styleSheet != oldWidget.styleSheet) { - _parseMarkdown().catchError((error, stackTrace) { - // - }); - } - } - - @override - void dispose() { - _disposeRecognizers(); - super.dispose(); - } - - Future _parseMarkdown() async { - final MarkdownStyleSheet fallbackStyleSheet = kFallbackStyle(context, widget.styleSheetTheme); - final MarkdownStyleSheet styleSheet = fallbackStyleSheet.merge(widget.styleSheet); - - _disposeRecognizers(); - - List nodes; - - final hash = ReadmeRepository.generateHashKey(widget.data); - nodes = ReadmeRepository.markdownCache[hash]; - if (nodes == null) { - //await Future.delayed(Duration(milliseconds: 250)); - nodes = await compute(ReadmeRepository.parseMarkdownIsolate, widget.data); - ReadmeRepository.markdownCache[hash] = nodes; - } - - final MarkdownBuilder builder = MarkdownBuilder( - delegate: this, - selectable: widget.selectable, - styleSheet: styleSheet, - imageDirectory: widget.imageDirectory, - imageBuilder: widget.imageBuilder, - checkboxBuilder: widget.checkboxBuilder, - fitContent: widget.fitContent, - ); - - setState(() => _children = builder.build(nodes)); - } - - void _disposeRecognizers() { - if (_recognizers.isEmpty) return; - final List localRecognizers = List.from(_recognizers); - _recognizers.clear(); - for (GestureRecognizer recognizer in localRecognizers) recognizer.dispose(); - } - - @override - GestureRecognizer createLink(String href) { - final TapGestureRecognizer recognizer = TapGestureRecognizer() - ..onTap = () { - if (widget.onTapLink != null) widget.onTapLink(href); - }; - _recognizers.add(recognizer); - return recognizer; - } - - @override - TextSpan formatText(MarkdownStyleSheet styleSheet, String code) { - code = code.replaceAll(RegExp(r'\n$'), ''); - if (widget.syntaxHighlighter != null) { - return widget.syntaxHighlighter.format(code); - } - return TextSpan(style: styleSheet.code, text: code); - } - - @override - Widget build(BuildContext context) { - if (widget.useScrollable) { - return ListView.builder( - padding: widget.padding, - controller: widget.controller, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - itemCount: _children?.length ?? 0, - itemBuilder: (BuildContext context, int index) { - return _children[index]; - }, - ); - } else { - if (_children?.length == 1) return _children.single; - return Column( - mainAxisSize: widget.shrinkWrap ? MainAxisSize.min : MainAxisSize.max, - crossAxisAlignment: - widget.fitContent ? CrossAxisAlignment.start : CrossAxisAlignment.stretch, - children: _children ?? [], - ); - } - } -} diff --git a/lib/widgets/following_users.dart b/lib/widgets/following_users.dart index 0f0d18f..38303e1 100644 --- a/lib/widgets/following_users.dart +++ b/lib/widgets/following_users.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/following_users.dart'; import 'package:github_activity_feed/services/gh_gql_query_service.dart'; import 'package:github_activity_feed/widgets/user_card.dart'; import 'package:provider/provider.dart'; @@ -42,14 +43,19 @@ class _ViewerFollowingListState extends State { } else if (snapshot.data.isEmpty && widget.emptyBuilder != null) { return widget.emptyBuilder(context); } else { - List viewerFollowing = snapshot.data['user']['following']['users']; + FollowingUsers viewerFollowing = FollowingUsers.fromJson(snapshot.data['user']); return Scrollbar( child: ListView.builder( - itemCount: viewerFollowing.length, + itemCount: viewerFollowing.following.users.length, padding: const EdgeInsets.all(8.0), itemBuilder: (context, index) { + final FollowingUser user = viewerFollowing.following.users[index]; return UserCard( - user: viewerFollowing[index], + avatarUrl: user.avatarUrl, + id: user.id.toString(), + login: user.login, + name: user.name, + url: user.url, ); }, ), diff --git a/lib/widgets/github_markdown.dart b/lib/widgets/github_markdown.dart deleted file mode 100644 index 2170be4..0000000 --- a/lib/widgets/github_markdown.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:github_activity_feed/widgets/async_markdown_scrollable.dart'; -import 'package:google_fonts/google_fonts.dart'; - -/// Makes use of the custom AsyncMarkdown widget to parse and style markdown -class GitHubMarkdown extends StatelessWidget { - const GitHubMarkdown({ - Key key, - @required this.markdown, - @required this.useScrollable, - }) : super(key: key); - - final String markdown; - final bool useScrollable; - - @override - Widget build(BuildContext context) { - return AsyncMarkdown( - data: markdown, - useScrollable: useScrollable, - styleSheet: MarkdownStyleSheet( - h1: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - h2: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - h3: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - h4: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - fontWeight: FontWeight.bold, - ), - p: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - ), - listBullet: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - ), - codeblockDecoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300] - : Colors.grey[900], - ), - code: GoogleFonts.firaCode( - color: Theme.of(context).colorScheme.onBackground, - ), - blockquoteDecoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? Colors.grey[300] - : Colors.grey[900], - ), - blockquote: TextStyle( - color: Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white, - ), - ), - ); - } -} diff --git a/lib/widgets/issue_card.dart b/lib/widgets/issue_card.dart index c107d73..72540f4 100644 --- a/lib/widgets/issue_card.dart +++ b/lib/widgets/issue_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/activity_feed_models.dart'; import 'package:github_activity_feed/utils/extensions.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; @@ -9,7 +10,7 @@ class IssueCard extends StatelessWidget { @required this.issue, }) : super(key: key); - final dynamic issue; + final Issue issue; @override Widget build(BuildContext context) { @@ -25,24 +26,24 @@ class IssueCard extends StatelessWidget { customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - onTap: () => launch(issue['url']), + onTap: () => launch(issue.url), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( /// user avatar leading: GestureDetector( - onTap: () => launch(issue['author']['url']), + onTap: () => launch(issue.author.url), child: CircleAvatar( backgroundImage: NetworkImage( - issue['author']['avatarUrl'], + issue.author.avatarUrl, ), ), ), /// user with action title: Text( - '${issue['author']['login']} opened issue', + '${issue.author.login} opened issue', style: TextStyle( color: Theme.of(context).colorScheme.onBackground, fontWeight: FontWeight.bold, @@ -55,26 +56,24 @@ class IssueCard extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: '${issue['repository']['nameWithOwner']} ', + text: '${issue.repository.nameWithOwner} ', ), /// this is here for optional styling - TextSpan(text: '#${issue['number']}'), + TextSpan(text: '#${issue.number}'), ], ), ), /// fuzzy timestamp - trailing: Text(timeago - .format(DateTime.parse(issue['createdAt']), locale: 'en_short') - .replaceAll(' ', '')), + trailing: Text(timeago.format(DateTime.parse(issue.createdAt), locale: 'en_short').replaceAll(' ', '')), ), /// issue title Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Text( - issue['title'], + issue.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -88,7 +87,7 @@ class IssueCard extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Text( - issue['bodyText'] ?? 'No description', + issue.bodyText ?? 'No description', maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14), diff --git a/lib/widgets/issue_comment_card.dart b/lib/widgets/issue_comment_card.dart index 93ab4c0..664a841 100644 --- a/lib/widgets/issue_comment_card.dart +++ b/lib/widgets/issue_comment_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/activity_feed_models.dart'; import 'package:github_activity_feed/utils/extensions.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; @@ -9,7 +10,7 @@ class IssueCommentCard extends StatelessWidget { @required this.comment, }) : super(key: key); - final dynamic comment; + final IssueComment comment; @override Widget build(BuildContext context) { @@ -22,24 +23,24 @@ class IssueCommentCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: InkWell( - onTap: () => launch(comment['url']), + onTap: () => launch(comment.url), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( /// user avatar leading: GestureDetector( - onTap: () => launch(comment['author']['url']), + onTap: () => launch(comment.author.url), child: CircleAvatar( backgroundImage: NetworkImage( - comment['author']['avatarUrl'], + comment.author.avatarUrl, ), ), ), /// user with action title: Text( - '${comment['author']['login']} commented on issue', + '${comment.author.login} commented on issue', style: TextStyle( color: Theme.of(context).colorScheme.onBackground, fontWeight: FontWeight.bold, @@ -52,31 +53,29 @@ class IssueCommentCard extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: '${comment['issue']['repository']['nameWithOwner']} ', + text: '${comment.parentIssue.repository.nameWithOwner} ', ), /// this is here for optional styling - TextSpan(text: '#${comment['issue']['number']}'), + TextSpan(text: '#${comment.parentIssue.number}'), ], ), ), /// fuzzy timestamp - trailing: Text(timeago - .format(DateTime.parse(comment['createdAt']), locale: 'en_short') - .replaceAll(' ', '')), + trailing: Text(timeago.format(DateTime.parse(comment.createdAt), locale: 'en_short').replaceAll(' ', '')), ), /// issue body text preview Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Text( - comment['bodyText'], + comment.bodyText, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14), ), - ) + ), ], ), ), diff --git a/lib/widgets/pull_request_card.dart b/lib/widgets/pull_request_card.dart index 28b2fc7..a65f067 100644 --- a/lib/widgets/pull_request_card.dart +++ b/lib/widgets/pull_request_card.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/activity_feed_models.dart'; +import 'package:github_activity_feed/utils/extensions.dart'; import 'package:github_activity_feed/utils/prettyJson.dart'; import 'package:timeago/timeago.dart' as timeago; -import 'package:github_activity_feed/utils/extensions.dart'; import 'package:url_launcher/url_launcher.dart'; class PullRequestCard extends StatelessWidget { @@ -10,7 +11,7 @@ class PullRequestCard extends StatelessWidget { @required this.pullRequest, }) : super(key: key); - final dynamic pullRequest; + final PullRequest pullRequest; @override Widget build(BuildContext context) { @@ -23,7 +24,7 @@ class PullRequestCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: InkWell( - onTap: () => launch(pullRequest['url']), + onTap: () => launch(pullRequest.url), onLongPress: () => printPrettyJson((pullRequest)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -31,17 +32,17 @@ class PullRequestCard extends StatelessWidget { ListTile( /// user avatar leading: GestureDetector( - onTap: () => launch(pullRequest['author']['url']), + onTap: () => launch(pullRequest.author.url), child: CircleAvatar( backgroundImage: NetworkImage( - pullRequest['author']['avatarUrl'], + pullRequest.author.avatarUrl, ), ), ), /// user with action title: Text( - '${pullRequest['author']['login']} opened pull request', + '${pullRequest.author.login} opened pull request', style: TextStyle( color: Theme.of(context).colorScheme.onBackground, fontWeight: FontWeight.bold, @@ -54,26 +55,24 @@ class PullRequestCard extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: '${pullRequest['repository']['nameWithOwner']} ', + text: '${pullRequest.repository.nameWithOwner} ', ), /// this is here for optional styling - TextSpan(text: '#${pullRequest['number']}'), + TextSpan(text: '#${pullRequest.number}'), ], ), ), /// fuzzy timestamp - trailing: Text(timeago - .format(DateTime.parse(pullRequest['createdAt']), locale: 'en_short') - .replaceAll(' ', '')), + trailing: Text(timeago.format(DateTime.parse(pullRequest.createdAt), locale: 'en_short').replaceAll(' ', '')), ), /// issue title Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Text( - pullRequest['title'], + pullRequest.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -87,12 +86,12 @@ class PullRequestCard extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Text( - pullRequest['bodyText'] == '' ? 'No description' : pullRequest['bodyText'], + pullRequest.bodyText == '' ? 'No description' : pullRequest.bodyText, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14), ), - ) + ), ], ), ), diff --git a/lib/widgets/star_card.dart b/lib/widgets/star_card.dart index d143182..5e9538b 100644 --- a/lib/widgets/star_card.dart +++ b/lib/widgets/star_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:github_activity_feed/data/activity_feed_models.dart'; import 'package:github_activity_feed/utils/extensions.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; @@ -8,10 +9,12 @@ class StarCard extends StatelessWidget { Key key, this.user, @required this.star, + @required this.starredAt, }) : super(key: key); final String user; - final dynamic star; + final Star star; + final String starredAt; @override Widget build(BuildContext context) { @@ -27,7 +30,7 @@ class StarCard extends StatelessWidget { customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - onTap: () => launch(star['url']), + onTap: () => launch(star.url), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -45,12 +48,10 @@ class StarCard extends StatelessWidget { ), /// repository with issue number - subtitle: Text(star['node']['nameWithOwner']), + subtitle: Text(star.nameWithOwner), /// fuzzy timestamp - trailing: Text(timeago - .format(DateTime.parse(star['createdAt']), locale: 'en_short') - .replaceAll(' ', '')), + trailing: Text(timeago.format(DateTime.parse(starredAt), locale: 'en_short').replaceAll(' ', '')), ), ], ), diff --git a/lib/widgets/user_card.dart b/lib/widgets/user_card.dart index ed7a0f5..b6f4f3c 100644 --- a/lib/widgets/user_card.dart +++ b/lib/widgets/user_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:github_activity_feed/data/viewer_following.dart'; import 'package:github_activity_feed/services/gh_gql_query_service.dart'; import 'package:github_activity_feed/utils/extensions.dart'; import 'package:github_activity_feed/widgets/report_bug_button.dart'; @@ -10,10 +11,18 @@ import 'package:url_launcher/url_launcher.dart'; class UserCard extends StatelessWidget { const UserCard({ Key key, - @required this.user, + @required this.avatarUrl, + @required this.id, + @required this.login, + @required this.name, + @required this.url, }) : super(key: key); - final dynamic user; + final String avatarUrl; + final String id; + final String login; + final String name; + final String url; @override Widget build(BuildContext context) { @@ -21,13 +30,13 @@ class UserCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ), - onTap: () => launch(user['url']), + onTap: () => launch(url), child: UserInfoRow( - id: user['id'], - avatarUrl: user['avatarUrl'], - login: user['login'], - name: user['name'], - profileUrl: user['url'], + id: id, + avatarUrl: avatarUrl, + login: login, + name: name, + profileUrl: url, ), ); } @@ -119,36 +128,49 @@ class _UserInfoRowState extends State { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ReportBugButton(); + } else if (!snapshot.hasData) { + return SizedBox( + height: 45, + width: 45, + child: Material( + color: Theme.of(context).accentColor, + shape: const CircleBorder(), + child: Center( + child: Icon( + Icons.person_outline, + size: 18, + color: context.colorScheme.onSecondary, + ), + ), + ), + ); } else { + ViewerFollowing viewerFollowing = ViewerFollowing.fromJson(snapshot.data['user']); return SizedBox( height: 45, width: 45, child: Material( color: Theme.of(context).accentColor, shape: const CircleBorder(), - child: !snapshot.hasData - ? Center( - child: CircularProgressIndicator(), - ) - : InkWell( - customBorder: const CircleBorder(), - onTap: () { - if (snapshot.data['user']['viewerIsFollowing'] == true) { - ghGraphQLService.unfollowUser(widget.id); - SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {})); - } else { - ghGraphQLService.followUser(widget.id); - SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {})); - } - }, - child: Center( - child: Icon( - snapshot.data['user']['viewerIsFollowing'] ? MdiIcons.accountMinusOutline : MdiIcons.accountPlusOutline, - size: 18, - color: context.colorScheme.onSecondary, - ), - ), - ), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + if (viewerFollowing.viewerIsFollowing == true) { + ghGraphQLService.unfollowUser(widget.id); + SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {})); + } else { + ghGraphQLService.followUser(widget.id); + SchedulerBinding.instance.addPostFrameCallback((_) => setState(() {})); + } + }, + child: Center( + child: Icon( + viewerFollowing.viewerIsFollowing ? MdiIcons.accountMinusOutline : MdiIcons.accountPlusOutline, + size: 18, + color: context.colorScheme.onSecondary, + ), + ), + ), ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index ef30605..05e26f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: github_activity_feed description: A Flutter application for viewing a rich feed of GitHub activity. publish_to: 'none' -version: 0.2.2 +version: 0.2.3 environment: sdk: ">=2.7.0 <3.0.0"