diff --git a/App.js b/App.js
index 83c539699..ca43089fe 100644
--- a/App.js
+++ b/App.js
@@ -61,7 +61,13 @@ class App extends Component {
{
storage: AsyncStorage,
transforms: [encryptor],
- blacklist: ['user'],
+ blacklist: [
+ 'counters',
+ 'entities',
+ 'pagination',
+ 'errorMessage',
+ 'user',
+ ],
},
() => {
this.setState({ rehydrated: true });
diff --git a/__tests__/api/rest/github/__snapshots__/schemas.test.js.snap b/__tests__/api/rest/github/__snapshots__/schemas.test.js.snap
new file mode 100644
index 000000000..ea13cd5b6
--- /dev/null
+++ b/__tests__/api/rest/github/__snapshots__/schemas.test.js.snap
@@ -0,0 +1,373 @@
+exports[`test normalizes events correctly 1`] = `
+Object {
+ "entities": Object {
+ "events": Object {
+ "6723241817": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "actor": "machour2",
+ "createdAt": 1508176374000,
+ "id": "6723241817",
+ "org": null,
+ "payload": Object {
+ "action": "created",
+ "comment": Object {
+ "author_association": "NONE",
+ "body": "bla",
+ "created_at": "2017-10-16T17:52:54Z",
+ "html_url": "https://github.com/machour/git-point-playground/issues/15#issuecomment-336970090",
+ "id": 336970090,
+ "issue_url": "https://api.github.com/repos/machour/git-point-playground/issues/15",
+ "updated_at": "2017-10-16T17:52:54Z",
+ "url": "https://api.github.com/repos/machour/git-point-playground/issues/comments/336970090",
+ "user": Object {
+ "avatar_url": "https://avatars0.githubusercontent.com/u/32770098?v=4",
+ "events_url": "https://api.github.com/users/machour2/events{/privacy}",
+ "followers_url": "https://api.github.com/users/machour2/followers",
+ "following_url": "https://api.github.com/users/machour2/following{/other_user}",
+ "gists_url": "https://api.github.com/users/machour2/gists{/gist_id}",
+ "gravatar_id": "",
+ "html_url": "https://github.com/machour2",
+ "id": 32770098,
+ "login": "machour2",
+ "organizations_url": "https://api.github.com/users/machour2/orgs",
+ "received_events_url": "https://api.github.com/users/machour2/received_events",
+ "repos_url": "https://api.github.com/users/machour2/repos",
+ "site_admin": false,
+ "starred_url": "https://api.github.com/users/machour2/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/machour2/subscriptions",
+ "type": "User",
+ "url": "https://api.github.com/users/machour2",
+ },
+ },
+ "issue": Object {
+ "assignee": null,
+ "assignees": Array [],
+ "author_association": "NONE",
+ "body": "",
+ "closed_at": null,
+ "comments": 0,
+ "comments_url": "https://api.github.com/repos/machour/git-point-playground/issues/15/comments",
+ "created_at": "2017-10-16T17:31:03Z",
+ "events_url": "https://api.github.com/repos/machour/git-point-playground/issues/15/events",
+ "html_url": "https://github.com/machour/git-point-playground/issues/15",
+ "id": 265851173,
+ "labels": Array [],
+ "labels_url": "https://api.github.com/repos/machour/git-point-playground/issues/15/labels{/name}",
+ "locked": false,
+ "milestone": null,
+ "number": 15,
+ "repository_url": "https://api.github.com/repos/machour/git-point-playground",
+ "state": "open",
+ "title": "notif",
+ "updated_at": "2017-10-16T17:52:54Z",
+ "url": "https://api.github.com/repos/machour/git-point-playground/issues/15",
+ "user": Object {
+ "avatar_url": "https://avatars0.githubusercontent.com/u/32770098?v=4",
+ "events_url": "https://api.github.com/users/machour2/events{/privacy}",
+ "followers_url": "https://api.github.com/users/machour2/followers",
+ "following_url": "https://api.github.com/users/machour2/following{/other_user}",
+ "gists_url": "https://api.github.com/users/machour2/gists{/gist_id}",
+ "gravatar_id": "",
+ "html_url": "https://github.com/machour2",
+ "id": 32770098,
+ "login": "machour2",
+ "organizations_url": "https://api.github.com/users/machour2/orgs",
+ "received_events_url": "https://api.github.com/users/machour2/received_events",
+ "repos_url": "https://api.github.com/users/machour2/repos",
+ "site_admin": false,
+ "starred_url": "https://api.github.com/users/machour2/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/machour2/subscriptions",
+ "type": "User",
+ "url": "https://api.github.com/users/machour2",
+ },
+ },
+ },
+ "repo": "machour/git-point-playground",
+ "type": "IssueCommentEvent",
+ },
+ "6723243870": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "actor": "machour2",
+ "createdAt": 1508176399000,
+ "id": "6723243870",
+ "org": null,
+ "payload": Object {
+ "action": "created",
+ "comment": Object {
+ "author_association": "NONE",
+ "body": "plop",
+ "created_at": "2017-10-16T17:53:19Z",
+ "html_url": "https://github.com/machour/git-point-playground/issues/14#issuecomment-336970278",
+ "id": 336970278,
+ "issue_url": "https://api.github.com/repos/machour/git-point-playground/issues/14",
+ "updated_at": "2017-10-16T17:53:19Z",
+ "url": "https://api.github.com/repos/machour/git-point-playground/issues/comments/336970278",
+ "user": Object {
+ "avatar_url": "https://avatars0.githubusercontent.com/u/32770098?v=4",
+ "events_url": "https://api.github.com/users/machour2/events{/privacy}",
+ "followers_url": "https://api.github.com/users/machour2/followers",
+ "following_url": "https://api.github.com/users/machour2/following{/other_user}",
+ "gists_url": "https://api.github.com/users/machour2/gists{/gist_id}",
+ "gravatar_id": "",
+ "html_url": "https://github.com/machour2",
+ "id": 32770098,
+ "login": "machour2",
+ "organizations_url": "https://api.github.com/users/machour2/orgs",
+ "received_events_url": "https://api.github.com/users/machour2/received_events",
+ "repos_url": "https://api.github.com/users/machour2/repos",
+ "site_admin": false,
+ "starred_url": "https://api.github.com/users/machour2/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/machour2/subscriptions",
+ "type": "User",
+ "url": "https://api.github.com/users/machour2",
+ },
+ },
+ "issue": Object {
+ "assignee": null,
+ "assignees": Array [],
+ "author_association": "NONE",
+ "body": "",
+ "closed_at": null,
+ "comments": 1,
+ "comments_url": "https://api.github.com/repos/machour/git-point-playground/issues/14/comments",
+ "created_at": "2017-10-15T14:48:14Z",
+ "events_url": "https://api.github.com/repos/machour/git-point-playground/issues/14/events",
+ "html_url": "https://github.com/machour/git-point-playground/issues/14",
+ "id": 265577462,
+ "labels": Array [],
+ "labels_url": "https://api.github.com/repos/machour/git-point-playground/issues/14/labels{/name}",
+ "locked": false,
+ "milestone": null,
+ "number": 14,
+ "repository_url": "https://api.github.com/repos/machour/git-point-playground",
+ "state": "open",
+ "title": "more test",
+ "updated_at": "2017-10-16T17:53:19Z",
+ "url": "https://api.github.com/repos/machour/git-point-playground/issues/14",
+ "user": Object {
+ "avatar_url": "https://avatars0.githubusercontent.com/u/32770098?v=4",
+ "events_url": "https://api.github.com/users/machour2/events{/privacy}",
+ "followers_url": "https://api.github.com/users/machour2/followers",
+ "following_url": "https://api.github.com/users/machour2/following{/other_user}",
+ "gists_url": "https://api.github.com/users/machour2/gists{/gist_id}",
+ "gravatar_id": "",
+ "html_url": "https://github.com/machour2",
+ "id": 32770098,
+ "login": "machour2",
+ "organizations_url": "https://api.github.com/users/machour2/orgs",
+ "received_events_url": "https://api.github.com/users/machour2/received_events",
+ "repos_url": "https://api.github.com/users/machour2/repos",
+ "site_admin": false,
+ "starred_url": "https://api.github.com/users/machour2/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/machour2/subscriptions",
+ "type": "User",
+ "url": "https://api.github.com/users/machour2",
+ },
+ },
+ },
+ "repo": "machour/git-point-playground",
+ "type": "IssueCommentEvent",
+ },
+ },
+ "repos": Object {
+ "machour/git-point-playground": Object {
+ "_entityUrl": "https://github.com/machour/git-point-playground",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "fullName": "machour/git-point-playground",
+ "id": "machour/git-point-playground",
+ "shortName": "git-point-playground",
+ },
+ },
+ "users": Object {
+ "machour2": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "avatarUrl": "https://avatars.githubusercontent.com/u/32770098?",
+ "id": "machour2",
+ "login": "machour2",
+ },
+ },
+ },
+ "result": Array [
+ "6723243870",
+ "6723241817",
+ ],
+}
+`;
+
+exports[`test normalizes notifications correctly 1`] = `
+Object {
+ "entities": Object {
+ "notifications": Object {
+ "266802681": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "id": "266802681",
+ "link": "https://api.github.com/repos/machour/git-point-playground/issues/14",
+ "reason": "subscribed",
+ "repo": "machour/git-point-playground",
+ "title": "more test",
+ "type": "Issue",
+ "unread": true,
+ "updatedAt": 1508176400000,
+ },
+ "267118003": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "id": "267118003",
+ "link": "https://api.github.com/repos/machour/git-point-playground/issues/15",
+ "reason": "subscribed",
+ "repo": "machour/git-point-playground",
+ "title": "notif",
+ "type": "Issue",
+ "unread": true,
+ "updatedAt": 1508176376000,
+ },
+ },
+ "repos": Object {
+ "machour/git-point-playground": Object {
+ "_entityUrl": "https://github.com/machour/git-point-playground",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "description": null,
+ "fullName": "machour/git-point-playground",
+ "id": "machour/git-point-playground",
+ "orgOwner": false,
+ "private": false,
+ "shortName": "git-point-playground",
+ "userOwner": "machour",
+ },
+ },
+ "users": Object {
+ "machour": Object {
+ "_entityUrl": false,
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "avatarUrl": "https://avatars2.githubusercontent.com/u/304450?v=4",
+ "id": "machour",
+ "login": "machour",
+ },
+ },
+ },
+ "result": Array [
+ "266802681",
+ "267118003",
+ ],
+}
+`;
+
+exports[`test normalizes org correctly 1`] = `
+Object {
+ "entities": Object {
+ "orgs": Object {
+ "gitpoint": Object {
+ "_entityUrl": "https://github.com/gitpoint",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": true,
+ "avatarUrl": "https://avatars0.githubusercontent.com/u/30082377?v=4&updatedAt=1501807857000",
+ "countPrivateRepos": 0,
+ "countPublicRepos": 2,
+ "countRepos": 2,
+ "createdAt": 1499788147000,
+ "description": "An open source GitHub client for iOS and Android. Built with React Native :iphone:",
+ "id": "gitpoint",
+ "location": "Toronto",
+ "login": "gitpoint",
+ "name": "GitPoint",
+ "updatedAt": 1501807857000,
+ "webSite": "https://gitpoint.co",
+ },
+ },
+ },
+ "result": "gitpoint",
+}
+`;
+
+exports[`test normalizes repo correctly 1`] = `
+Object {
+ "entities": Object {
+ "orgs": Object {
+ "gitpoint": Object {
+ "_entityUrl": "https://github.com/gitpoint",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "avatarUrl": "https://avatars0.githubusercontent.com/u/30082377?v=4",
+ "id": "gitpoint",
+ "login": "gitpoint",
+ },
+ },
+ "repos": Object {
+ "gitpoint/git-point": Object {
+ "_entityUrl": "https://github.com/gitpoint/git-point",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": false,
+ "countForks": 224,
+ "countOpenIssues": 93,
+ "countStargazzers": 2501,
+ "countWatchers": 2501,
+ "defaultBranch": "master",
+ "description": "GitHub in your pocket :iphone:",
+ "fullName": "gitpoint/git-point",
+ "hasIssues": true,
+ "id": "gitpoint/git-point",
+ "language": "JavaScript",
+ "orgOwner": "gitpoint",
+ "private": false,
+ "shortName": "git-point",
+ "userOwner": false,
+ },
+ },
+ },
+ "result": "gitpoint/git-point",
+}
+`;
+
+exports[`test normalizes user correctly 1`] = `
+Object {
+ "entities": Object {
+ "users": Object {
+ "machour": Object {
+ "_entityUrl": "https://github.com/machour",
+ "_fetchedAt": 1508189841664,
+ "_isAuth": false,
+ "_isComplete": true,
+ "avatarUrl": "https://avatars2.githubusercontent.com/u/304450?v=4&updatedAt=1507576478000",
+ "bio": null,
+ "company": "IDK",
+ "countFollowers": 65,
+ "countFollowing": 43,
+ "countPrivateRepos": 0,
+ "countPublicRepos": 56,
+ "countRepos": 56,
+ "createdAt": 1276477765000,
+ "fullName": "Mehdi Achour",
+ "id": "machour",
+ "location": "Tunis",
+ "login": "machour",
+ "updatedAt": 1507576478000,
+ "webSite": "https://machour.idk.tn/",
+ },
+ },
+ },
+ "result": "machour",
+}
+`;
diff --git a/__tests__/api/rest/github/mocks/events.json.js b/__tests__/api/rest/github/mocks/events.json.js
new file mode 100644
index 000000000..f7c1222e9
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/events.json.js
@@ -0,0 +1,220 @@
+export default [
+ {
+ id: '6723243870',
+ type: 'IssueCommentEvent',
+ actor: {
+ id: 32770098,
+ login: 'machour2',
+ display_login: 'machour2',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ avatar_url: 'https://avatars.githubusercontent.com/u/32770098?',
+ },
+ repo: {
+ id: 103052187,
+ name: 'machour/git-point-playground',
+ url: 'https://api.github.com/repos/machour/git-point-playground',
+ },
+ payload: {
+ action: 'created',
+ issue: {
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14',
+ repository_url:
+ 'https://api.github.com/repos/machour/git-point-playground',
+ labels_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14/labels{/name}',
+ comments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14/comments',
+ events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14/events',
+ html_url: 'https://github.com/machour/git-point-playground/issues/14',
+ id: 265577462,
+ number: 14,
+ title: 'more test',
+ user: {
+ login: 'machour2',
+ id: 32770098,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/32770098?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ html_url: 'https://github.com/machour2',
+ followers_url: 'https://api.github.com/users/machour2/followers',
+ following_url:
+ 'https://api.github.com/users/machour2/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour2/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour2/starred{/owner}{/repo}',
+ subscriptions_url:
+ 'https://api.github.com/users/machour2/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour2/orgs',
+ repos_url: 'https://api.github.com/users/machour2/repos',
+ events_url: 'https://api.github.com/users/machour2/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour2/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ labels: [],
+ state: 'open',
+ locked: false,
+ assignee: null,
+ assignees: [],
+ milestone: null,
+ comments: 1,
+ created_at: '2017-10-15T14:48:14Z',
+ updated_at: '2017-10-16T17:53:19Z',
+ closed_at: null,
+ author_association: 'NONE',
+ body: '',
+ },
+ comment: {
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments/336970278',
+ html_url:
+ 'https://github.com/machour/git-point-playground/issues/14#issuecomment-336970278',
+ issue_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14',
+ id: 336970278,
+ user: {
+ login: 'machour2',
+ id: 32770098,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/32770098?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ html_url: 'https://github.com/machour2',
+ followers_url: 'https://api.github.com/users/machour2/followers',
+ following_url:
+ 'https://api.github.com/users/machour2/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour2/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour2/starred{/owner}{/repo}',
+ subscriptions_url:
+ 'https://api.github.com/users/machour2/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour2/orgs',
+ repos_url: 'https://api.github.com/users/machour2/repos',
+ events_url: 'https://api.github.com/users/machour2/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour2/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ created_at: '2017-10-16T17:53:19Z',
+ updated_at: '2017-10-16T17:53:19Z',
+ author_association: 'NONE',
+ body: 'plop',
+ },
+ },
+ public: true,
+ created_at: '2017-10-16T17:53:19Z',
+ },
+ {
+ id: '6723241817',
+ type: 'IssueCommentEvent',
+ actor: {
+ id: 32770098,
+ login: 'machour2',
+ display_login: 'machour2',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ avatar_url: 'https://avatars.githubusercontent.com/u/32770098?',
+ },
+ repo: {
+ id: 103052187,
+ name: 'machour/git-point-playground',
+ url: 'https://api.github.com/repos/machour/git-point-playground',
+ },
+ payload: {
+ action: 'created',
+ issue: {
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15',
+ repository_url:
+ 'https://api.github.com/repos/machour/git-point-playground',
+ labels_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15/labels{/name}',
+ comments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15/comments',
+ events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15/events',
+ html_url: 'https://github.com/machour/git-point-playground/issues/15',
+ id: 265851173,
+ number: 15,
+ title: 'notif',
+ user: {
+ login: 'machour2',
+ id: 32770098,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/32770098?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ html_url: 'https://github.com/machour2',
+ followers_url: 'https://api.github.com/users/machour2/followers',
+ following_url:
+ 'https://api.github.com/users/machour2/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour2/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour2/starred{/owner}{/repo}',
+ subscriptions_url:
+ 'https://api.github.com/users/machour2/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour2/orgs',
+ repos_url: 'https://api.github.com/users/machour2/repos',
+ events_url: 'https://api.github.com/users/machour2/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour2/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ labels: [],
+ state: 'open',
+ locked: false,
+ assignee: null,
+ assignees: [],
+ milestone: null,
+ comments: 0,
+ created_at: '2017-10-16T17:31:03Z',
+ updated_at: '2017-10-16T17:52:54Z',
+ closed_at: null,
+ author_association: 'NONE',
+ body: '',
+ },
+ comment: {
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments/336970090',
+ html_url:
+ 'https://github.com/machour/git-point-playground/issues/15#issuecomment-336970090',
+ issue_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15',
+ id: 336970090,
+ user: {
+ login: 'machour2',
+ id: 32770098,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/32770098?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour2',
+ html_url: 'https://github.com/machour2',
+ followers_url: 'https://api.github.com/users/machour2/followers',
+ following_url:
+ 'https://api.github.com/users/machour2/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour2/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour2/starred{/owner}{/repo}',
+ subscriptions_url:
+ 'https://api.github.com/users/machour2/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour2/orgs',
+ repos_url: 'https://api.github.com/users/machour2/repos',
+ events_url: 'https://api.github.com/users/machour2/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour2/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ created_at: '2017-10-16T17:52:54Z',
+ updated_at: '2017-10-16T17:52:54Z',
+ author_association: 'NONE',
+ body: 'bla',
+ },
+ },
+ public: true,
+ created_at: '2017-10-16T17:52:54Z',
+ },
+];
diff --git a/__tests__/api/rest/github/mocks/index.js b/__tests__/api/rest/github/mocks/index.js
new file mode 100644
index 000000000..a6f8aa1fa
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/index.js
@@ -0,0 +1,13 @@
+import userJson from './user.json';
+import repoJson from './repo.json';
+import orgJson from './org.json';
+import eventsJson from './events.json';
+import notificationsJson from './notifications.json';
+
+export default {
+ userJson,
+ repoJson,
+ orgJson,
+ eventsJson,
+ notificationsJson,
+};
diff --git a/__tests__/api/rest/github/mocks/notifications.json.js b/__tests__/api/rest/github/mocks/notifications.json.js
new file mode 100644
index 000000000..639ced6cc
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/notifications.json.js
@@ -0,0 +1,246 @@
+export default [
+ {
+ id: '266802681',
+ unread: true,
+ reason: 'subscribed',
+ updated_at: '2017-10-16T17:53:20Z',
+ last_read_at: '2017-10-15T15:09:29Z',
+ subject: {
+ title: 'more test',
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/14',
+ latest_comment_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments/336970278',
+ type: 'Issue',
+ },
+ repository: {
+ id: 103052187,
+ name: 'git-point-playground',
+ full_name: 'machour/git-point-playground',
+ owner: {
+ login: 'machour',
+ id: 304450,
+ avatar_url: 'https://avatars2.githubusercontent.com/u/304450?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour',
+ html_url: 'https://github.com/machour',
+ followers_url: 'https://api.github.com/users/machour/followers',
+ following_url:
+ 'https://api.github.com/users/machour/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour/starred{/owner}{/repo}',
+ subscriptions_url: 'https://api.github.com/users/machour/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour/orgs',
+ repos_url: 'https://api.github.com/users/machour/repos',
+ events_url: 'https://api.github.com/users/machour/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ private: false,
+ html_url: 'https://github.com/machour/git-point-playground',
+ description: null,
+ fork: false,
+ url: 'https://api.github.com/repos/machour/git-point-playground',
+ forks_url:
+ 'https://api.github.com/repos/machour/git-point-playground/forks',
+ keys_url:
+ 'https://api.github.com/repos/machour/git-point-playground/keys{/key_id}',
+ collaborators_url:
+ 'https://api.github.com/repos/machour/git-point-playground/collaborators{/collaborator}',
+ teams_url:
+ 'https://api.github.com/repos/machour/git-point-playground/teams',
+ hooks_url:
+ 'https://api.github.com/repos/machour/git-point-playground/hooks',
+ issue_events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/events{/number}',
+ events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/events',
+ assignees_url:
+ 'https://api.github.com/repos/machour/git-point-playground/assignees{/user}',
+ branches_url:
+ 'https://api.github.com/repos/machour/git-point-playground/branches{/branch}',
+ tags_url:
+ 'https://api.github.com/repos/machour/git-point-playground/tags',
+ blobs_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/blobs{/sha}',
+ git_tags_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/tags{/sha}',
+ git_refs_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/refs{/sha}',
+ trees_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/trees{/sha}',
+ statuses_url:
+ 'https://api.github.com/repos/machour/git-point-playground/statuses/{sha}',
+ languages_url:
+ 'https://api.github.com/repos/machour/git-point-playground/languages',
+ stargazers_url:
+ 'https://api.github.com/repos/machour/git-point-playground/stargazers',
+ contributors_url:
+ 'https://api.github.com/repos/machour/git-point-playground/contributors',
+ subscribers_url:
+ 'https://api.github.com/repos/machour/git-point-playground/subscribers',
+ subscription_url:
+ 'https://api.github.com/repos/machour/git-point-playground/subscription',
+ commits_url:
+ 'https://api.github.com/repos/machour/git-point-playground/commits{/sha}',
+ git_commits_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/commits{/sha}',
+ comments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/comments{/number}',
+ issue_comment_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments{/number}',
+ contents_url:
+ 'https://api.github.com/repos/machour/git-point-playground/contents/{+path}',
+ compare_url:
+ 'https://api.github.com/repos/machour/git-point-playground/compare/{base}...{head}',
+ merges_url:
+ 'https://api.github.com/repos/machour/git-point-playground/merges',
+ archive_url:
+ 'https://api.github.com/repos/machour/git-point-playground/{archive_format}{/ref}',
+ downloads_url:
+ 'https://api.github.com/repos/machour/git-point-playground/downloads',
+ issues_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues{/number}',
+ pulls_url:
+ 'https://api.github.com/repos/machour/git-point-playground/pulls{/number}',
+ milestones_url:
+ 'https://api.github.com/repos/machour/git-point-playground/milestones{/number}',
+ notifications_url:
+ 'https://api.github.com/repos/machour/git-point-playground/notifications{?since,all,participating}',
+ labels_url:
+ 'https://api.github.com/repos/machour/git-point-playground/labels{/name}',
+ releases_url:
+ 'https://api.github.com/repos/machour/git-point-playground/releases{/id}',
+ deployments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/deployments',
+ },
+ url: 'https://api.github.com/notifications/threads/266802681',
+ subscription_url:
+ 'https://api.github.com/notifications/threads/266802681/subscription',
+ },
+ {
+ id: '267118003',
+ unread: true,
+ reason: 'subscribed',
+ updated_at: '2017-10-16T17:52:56Z',
+ last_read_at: '2017-10-16T17:51:33Z',
+ subject: {
+ title: 'notif',
+ url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/15',
+ latest_comment_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments/336970090',
+ type: 'Issue',
+ },
+ repository: {
+ id: 103052187,
+ name: 'git-point-playground',
+ full_name: 'machour/git-point-playground',
+ owner: {
+ login: 'machour',
+ id: 304450,
+ avatar_url: 'https://avatars2.githubusercontent.com/u/304450?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour',
+ html_url: 'https://github.com/machour',
+ followers_url: 'https://api.github.com/users/machour/followers',
+ following_url:
+ 'https://api.github.com/users/machour/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour/gists{/gist_id}',
+ starred_url:
+ 'https://api.github.com/users/machour/starred{/owner}{/repo}',
+ subscriptions_url: 'https://api.github.com/users/machour/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour/orgs',
+ repos_url: 'https://api.github.com/users/machour/repos',
+ events_url: 'https://api.github.com/users/machour/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/machour/received_events',
+ type: 'User',
+ site_admin: false,
+ },
+ private: false,
+ html_url: 'https://github.com/machour/git-point-playground',
+ description: null,
+ fork: false,
+ url: 'https://api.github.com/repos/machour/git-point-playground',
+ forks_url:
+ 'https://api.github.com/repos/machour/git-point-playground/forks',
+ keys_url:
+ 'https://api.github.com/repos/machour/git-point-playground/keys{/key_id}',
+ collaborators_url:
+ 'https://api.github.com/repos/machour/git-point-playground/collaborators{/collaborator}',
+ teams_url:
+ 'https://api.github.com/repos/machour/git-point-playground/teams',
+ hooks_url:
+ 'https://api.github.com/repos/machour/git-point-playground/hooks',
+ issue_events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/events{/number}',
+ events_url:
+ 'https://api.github.com/repos/machour/git-point-playground/events',
+ assignees_url:
+ 'https://api.github.com/repos/machour/git-point-playground/assignees{/user}',
+ branches_url:
+ 'https://api.github.com/repos/machour/git-point-playground/branches{/branch}',
+ tags_url:
+ 'https://api.github.com/repos/machour/git-point-playground/tags',
+ blobs_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/blobs{/sha}',
+ git_tags_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/tags{/sha}',
+ git_refs_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/refs{/sha}',
+ trees_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/trees{/sha}',
+ statuses_url:
+ 'https://api.github.com/repos/machour/git-point-playground/statuses/{sha}',
+ languages_url:
+ 'https://api.github.com/repos/machour/git-point-playground/languages',
+ stargazers_url:
+ 'https://api.github.com/repos/machour/git-point-playground/stargazers',
+ contributors_url:
+ 'https://api.github.com/repos/machour/git-point-playground/contributors',
+ subscribers_url:
+ 'https://api.github.com/repos/machour/git-point-playground/subscribers',
+ subscription_url:
+ 'https://api.github.com/repos/machour/git-point-playground/subscription',
+ commits_url:
+ 'https://api.github.com/repos/machour/git-point-playground/commits{/sha}',
+ git_commits_url:
+ 'https://api.github.com/repos/machour/git-point-playground/git/commits{/sha}',
+ comments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/comments{/number}',
+ issue_comment_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues/comments{/number}',
+ contents_url:
+ 'https://api.github.com/repos/machour/git-point-playground/contents/{+path}',
+ compare_url:
+ 'https://api.github.com/repos/machour/git-point-playground/compare/{base}...{head}',
+ merges_url:
+ 'https://api.github.com/repos/machour/git-point-playground/merges',
+ archive_url:
+ 'https://api.github.com/repos/machour/git-point-playground/{archive_format}{/ref}',
+ downloads_url:
+ 'https://api.github.com/repos/machour/git-point-playground/downloads',
+ issues_url:
+ 'https://api.github.com/repos/machour/git-point-playground/issues{/number}',
+ pulls_url:
+ 'https://api.github.com/repos/machour/git-point-playground/pulls{/number}',
+ milestones_url:
+ 'https://api.github.com/repos/machour/git-point-playground/milestones{/number}',
+ notifications_url:
+ 'https://api.github.com/repos/machour/git-point-playground/notifications{?since,all,participating}',
+ labels_url:
+ 'https://api.github.com/repos/machour/git-point-playground/labels{/name}',
+ releases_url:
+ 'https://api.github.com/repos/machour/git-point-playground/releases{/id}',
+ deployments_url:
+ 'https://api.github.com/repos/machour/git-point-playground/deployments',
+ },
+ url: 'https://api.github.com/notifications/threads/267118003',
+ subscription_url:
+ 'https://api.github.com/notifications/threads/267118003/subscription',
+ },
+];
diff --git a/__tests__/api/rest/github/mocks/org.json.js b/__tests__/api/rest/github/mocks/org.json.js
new file mode 100644
index 000000000..2ce6a9fbc
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/org.json.js
@@ -0,0 +1,30 @@
+export default {
+ login: 'gitpoint',
+ id: 30082377,
+ url: 'https://api.github.com/orgs/gitpoint',
+ repos_url: 'https://api.github.com/orgs/gitpoint/repos',
+ events_url: 'https://api.github.com/orgs/gitpoint/events',
+ hooks_url: 'https://api.github.com/orgs/gitpoint/hooks',
+ issues_url: 'https://api.github.com/orgs/gitpoint/issues',
+ members_url: 'https://api.github.com/orgs/gitpoint/members{/member}',
+ public_members_url:
+ 'https://api.github.com/orgs/gitpoint/public_members{/member}',
+ avatar_url: 'https://avatars0.githubusercontent.com/u/30082377?v=4',
+ description:
+ 'An open source GitHub client for iOS and Android. Built with React Native :iphone:',
+ name: 'GitPoint',
+ company: null,
+ blog: 'https://gitpoint.co',
+ location: 'Toronto',
+ email: '',
+ has_organization_projects: true,
+ has_repository_projects: true,
+ public_repos: 2,
+ public_gists: 0,
+ followers: 0,
+ following: 0,
+ html_url: 'https://github.com/gitpoint',
+ created_at: '2017-07-11T15:49:07Z',
+ updated_at: '2017-08-04T00:50:57Z',
+ type: 'Organization',
+};
diff --git a/__tests__/api/rest/github/mocks/repo.json.js b/__tests__/api/rest/github/mocks/repo.json.js
new file mode 100644
index 000000000..098b85080
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/repo.json.js
@@ -0,0 +1,133 @@
+export default {
+ id: 86202845,
+ name: 'git-point',
+ full_name: 'gitpoint/git-point',
+ owner: {
+ login: 'gitpoint',
+ id: 30082377,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/30082377?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/gitpoint',
+ html_url: 'https://github.com/gitpoint',
+ followers_url: 'https://api.github.com/users/gitpoint/followers',
+ following_url:
+ 'https://api.github.com/users/gitpoint/following{/other_user}',
+ gists_url: 'https://api.github.com/users/gitpoint/gists{/gist_id}',
+ starred_url: 'https://api.github.com/users/gitpoint/starred{/owner}{/repo}',
+ subscriptions_url: 'https://api.github.com/users/gitpoint/subscriptions',
+ organizations_url: 'https://api.github.com/users/gitpoint/orgs',
+ repos_url: 'https://api.github.com/users/gitpoint/repos',
+ events_url: 'https://api.github.com/users/gitpoint/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/gitpoint/received_events',
+ type: 'Organization',
+ site_admin: false,
+ },
+ private: false,
+ html_url: 'https://github.com/gitpoint/git-point',
+ description: 'GitHub in your pocket :iphone:',
+ fork: false,
+ url: 'https://api.github.com/repos/gitpoint/git-point',
+ forks_url: 'https://api.github.com/repos/gitpoint/git-point/forks',
+ keys_url: 'https://api.github.com/repos/gitpoint/git-point/keys{/key_id}',
+ collaborators_url:
+ 'https://api.github.com/repos/gitpoint/git-point/collaborators{/collaborator}',
+ teams_url: 'https://api.github.com/repos/gitpoint/git-point/teams',
+ hooks_url: 'https://api.github.com/repos/gitpoint/git-point/hooks',
+ issue_events_url:
+ 'https://api.github.com/repos/gitpoint/git-point/issues/events{/number}',
+ events_url: 'https://api.github.com/repos/gitpoint/git-point/events',
+ assignees_url:
+ 'https://api.github.com/repos/gitpoint/git-point/assignees{/user}',
+ branches_url:
+ 'https://api.github.com/repos/gitpoint/git-point/branches{/branch}',
+ tags_url: 'https://api.github.com/repos/gitpoint/git-point/tags',
+ blobs_url: 'https://api.github.com/repos/gitpoint/git-point/git/blobs{/sha}',
+ git_tags_url:
+ 'https://api.github.com/repos/gitpoint/git-point/git/tags{/sha}',
+ git_refs_url:
+ 'https://api.github.com/repos/gitpoint/git-point/git/refs{/sha}',
+ trees_url: 'https://api.github.com/repos/gitpoint/git-point/git/trees{/sha}',
+ statuses_url:
+ 'https://api.github.com/repos/gitpoint/git-point/statuses/{sha}',
+ languages_url: 'https://api.github.com/repos/gitpoint/git-point/languages',
+ stargazers_url: 'https://api.github.com/repos/gitpoint/git-point/stargazers',
+ contributors_url:
+ 'https://api.github.com/repos/gitpoint/git-point/contributors',
+ subscribers_url:
+ 'https://api.github.com/repos/gitpoint/git-point/subscribers',
+ subscription_url:
+ 'https://api.github.com/repos/gitpoint/git-point/subscription',
+ commits_url: 'https://api.github.com/repos/gitpoint/git-point/commits{/sha}',
+ git_commits_url:
+ 'https://api.github.com/repos/gitpoint/git-point/git/commits{/sha}',
+ comments_url:
+ 'https://api.github.com/repos/gitpoint/git-point/comments{/number}',
+ issue_comment_url:
+ 'https://api.github.com/repos/gitpoint/git-point/issues/comments{/number}',
+ contents_url:
+ 'https://api.github.com/repos/gitpoint/git-point/contents/{+path}',
+ compare_url:
+ 'https://api.github.com/repos/gitpoint/git-point/compare/{base}...{head}',
+ merges_url: 'https://api.github.com/repos/gitpoint/git-point/merges',
+ archive_url:
+ 'https://api.github.com/repos/gitpoint/git-point/{archive_format}{/ref}',
+ downloads_url: 'https://api.github.com/repos/gitpoint/git-point/downloads',
+ issues_url: 'https://api.github.com/repos/gitpoint/git-point/issues{/number}',
+ pulls_url: 'https://api.github.com/repos/gitpoint/git-point/pulls{/number}',
+ milestones_url:
+ 'https://api.github.com/repos/gitpoint/git-point/milestones{/number}',
+ notifications_url:
+ 'https://api.github.com/repos/gitpoint/git-point/notifications{?since,all,participating}',
+ labels_url: 'https://api.github.com/repos/gitpoint/git-point/labels{/name}',
+ releases_url: 'https://api.github.com/repos/gitpoint/git-point/releases{/id}',
+ deployments_url:
+ 'https://api.github.com/repos/gitpoint/git-point/deployments',
+ created_at: '2017-03-26T02:45:46Z',
+ updated_at: '2017-10-16T17:15:40Z',
+ pushed_at: '2017-10-16T20:44:40Z',
+ git_url: 'git://github.com/gitpoint/git-point.git',
+ ssh_url: 'git@github.com:gitpoint/git-point.git',
+ clone_url: 'https://github.com/gitpoint/git-point.git',
+ svn_url: 'https://github.com/gitpoint/git-point',
+ homepage: 'https://gitpoint.co/',
+ size: 3798,
+ stargazers_count: 2501,
+ watchers_count: 2501,
+ language: 'JavaScript',
+ has_issues: true,
+ has_projects: true,
+ has_downloads: true,
+ has_wiki: true,
+ has_pages: false,
+ forks_count: 224,
+ mirror_url: null,
+ open_issues_count: 93,
+ forks: 224,
+ open_issues: 93,
+ watchers: 2501,
+ default_branch: 'master',
+ organization: {
+ login: 'gitpoint',
+ id: 30082377,
+ avatar_url: 'https://avatars0.githubusercontent.com/u/30082377?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/gitpoint',
+ html_url: 'https://github.com/gitpoint',
+ followers_url: 'https://api.github.com/users/gitpoint/followers',
+ following_url:
+ 'https://api.github.com/users/gitpoint/following{/other_user}',
+ gists_url: 'https://api.github.com/users/gitpoint/gists{/gist_id}',
+ starred_url: 'https://api.github.com/users/gitpoint/starred{/owner}{/repo}',
+ subscriptions_url: 'https://api.github.com/users/gitpoint/subscriptions',
+ organizations_url: 'https://api.github.com/users/gitpoint/orgs',
+ repos_url: 'https://api.github.com/users/gitpoint/repos',
+ events_url: 'https://api.github.com/users/gitpoint/events{/privacy}',
+ received_events_url:
+ 'https://api.github.com/users/gitpoint/received_events',
+ type: 'Organization',
+ site_admin: false,
+ },
+ network_count: 224,
+ subscribers_count: 50,
+};
diff --git a/__tests__/api/rest/github/mocks/user.json.js b/__tests__/api/rest/github/mocks/user.json.js
new file mode 100644
index 000000000..c236701de
--- /dev/null
+++ b/__tests__/api/rest/github/mocks/user.json.js
@@ -0,0 +1,32 @@
+export default {
+ login: 'machour',
+ id: 304450,
+ avatar_url: 'https://avatars2.githubusercontent.com/u/304450?v=4',
+ gravatar_id: '',
+ url: 'https://api.github.com/users/machour',
+ html_url: 'https://github.com/machour',
+ followers_url: 'https://api.github.com/users/machour/followers',
+ following_url: 'https://api.github.com/users/machour/following{/other_user}',
+ gists_url: 'https://api.github.com/users/machour/gists{/gist_id}',
+ starred_url: 'https://api.github.com/users/machour/starred{/owner}{/repo}',
+ subscriptions_url: 'https://api.github.com/users/machour/subscriptions',
+ organizations_url: 'https://api.github.com/users/machour/orgs',
+ repos_url: 'https://api.github.com/users/machour/repos',
+ events_url: 'https://api.github.com/users/machour/events{/privacy}',
+ received_events_url: 'https://api.github.com/users/machour/received_events',
+ type: 'User',
+ site_admin: false,
+ name: 'Mehdi Achour',
+ company: 'IDK',
+ blog: 'https://machour.idk.tn/',
+ location: 'Tunis',
+ email: null,
+ hireable: true,
+ bio: null,
+ public_repos: 56,
+ public_gists: 4,
+ followers: 65,
+ following: 43,
+ created_at: '2010-06-14T01:09:25Z',
+ updated_at: '2017-10-09T19:14:38Z',
+};
diff --git a/__tests__/api/rest/github/schemas.test.js b/__tests__/api/rest/github/schemas.test.js
new file mode 100644
index 000000000..09eeeacab
--- /dev/null
+++ b/__tests__/api/rest/github/schemas.test.js
@@ -0,0 +1,32 @@
+import 'react-native';
+import { normalize } from 'normalizr';
+import schemas from 'api/rest/providers/github/schemas';
+
+import mocks from './mocks';
+
+jest.mock('react-native-i18n', () => {
+ return {};
+});
+Date.now = jest.fn(() => 1508189841664);
+
+it('normalizes user correctly', () => {
+ expect(normalize(mocks.userJson, schemas.USER)).toMatchSnapshot();
+});
+
+it('normalizes repo correctly', () => {
+ expect(normalize(mocks.repoJson, schemas.REPO)).toMatchSnapshot();
+});
+
+it('normalizes org correctly', () => {
+ expect(normalize(mocks.orgJson, schemas.ORG)).toMatchSnapshot();
+});
+
+it('normalizes events correctly', () => {
+ expect(normalize(mocks.eventsJson, schemas.EVENT_ARRAY)).toMatchSnapshot();
+});
+
+it('normalizes notifications correctly', () => {
+ expect(
+ normalize(mocks.notificationsJson, schemas.NOTIFICATION_ARRAY)
+ ).toMatchSnapshot();
+});
diff --git a/__tests__/index.android.js b/__tests__/index.android.js
deleted file mode 100644
index 84e6b670d..000000000
--- a/__tests__/index.android.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import 'react-native';
-import React from 'react';
-import renderer from 'react-test-renderer';
-import Index from '../index.android';
-
-// Note: test renderer must be required after react-native.
-
-it('renders correctly', () => {
- const tree = renderer.create();
-});
diff --git a/__tests__/index.ios.js b/__tests__/index.ios.js
deleted file mode 100644
index 3b7b2e010..000000000
--- a/__tests__/index.ios.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import 'react-native';
-import React from 'react';
-import renderer from 'react-test-renderer';
-import Index from '../index.ios';
-
-// Note: test renderer must be required after react-native.
-
-it('renders correctly', () => {
- const tree = renderer.create();
-});
diff --git a/package.json b/package.json
index de991565f..d0e4bca92 100644
--- a/package.json
+++ b/package.json
@@ -49,11 +49,12 @@
"dependencies": {
"entities": "^1.1.1",
"fuzzysort": "^1.0.1",
- "lodash.uniqby": "^4.7.0",
+ "lodash": "^4.17.4",
"lowlight": "^1.5.0",
"md5": "^2.2.1",
"moment": "^2.17.1",
"node-emoji": "^1.7.0",
+ "normalizr": "^3.2.3",
"opencollective": "^1.0.3",
"parse-diff": "^0.4.0",
"query-string": "^4.3.1",
@@ -117,12 +118,17 @@
"minicat": "^1.0.0",
"prettier": "^1.7.4",
"react-native-cli": "^2.0.1",
- "react-test-renderer": "16.0.0-alpha.6",
+ "react-test-renderer": "16.0.0-alpha.12",
"reactotron-react-native": "^1.12.2",
"reactotron-redux": "^1.12.2"
},
"jest": {
- "preset": "react-native"
+ "preset": "react-native",
+ "testPathIgnorePatterns": [
+ "/node_modules/",
+ "/node_modules/",
+ "/mocks/"
+ ]
},
"rnpm": {
"assets": [
diff --git a/root.reducer.js b/root.reducer.js
index 07ca2eabf..e752bbb92 100644
--- a/root.reducer.js
+++ b/root.reducer.js
@@ -2,16 +2,25 @@ import { combineReducers } from 'redux';
import { authReducer } from 'auth';
import { userReducer } from 'user';
import { repositoryReducer } from 'repository';
-import { organizationReducer } from 'organization';
import { issueReducer } from 'issue';
import { searchReducer } from 'search';
import { notificationsReducer } from 'notifications';
+import {
+ entities,
+ counters,
+ pagination,
+ errorMessage,
+} from 'api/rest/reducers';
+
export const rootReducer = combineReducers({
+ entities,
+ counters,
+ pagination,
+ errorMessage,
auth: authReducer,
user: userReducer,
repository: repositoryReducer,
- organization: organizationReducer,
issue: issueReducer,
search: searchReducer,
notifications: notificationsReducer,
diff --git a/routes.js b/routes.js
index 017f41065..33edc86da 100644
--- a/routes.js
+++ b/routes.js
@@ -18,9 +18,9 @@ import {
LoginScreen,
WelcomeScreen,
AuthProfileScreen,
- EventsScreen,
PrivacyPolicyScreen,
UserOptionsScreen,
+ EventsScreen,
} from 'auth';
// User
@@ -31,8 +31,10 @@ import {
FollowingListScreen,
} from 'user';
-// Organization
-import { OrganizationProfileScreen } from 'organization';
+import {
+ OrganizationRepositoryListScreen,
+ OrganizationProfileScreen,
+} from 'organization';
// Search
import { SearchScreen } from 'search';
@@ -61,6 +63,12 @@ import {
} from 'issue';
const sharedRoutes = {
+ OrgRepositoryList: {
+ screen: OrganizationRepositoryListScreen,
+ navigationOptions: ({ navigation }) => ({
+ title: navigation.state.params.title,
+ }),
+ },
RepositoryList: {
screen: RepositoryListScreen,
navigationOptions: ({ navigation }) => ({
@@ -136,10 +144,7 @@ const sharedRoutes = {
const { issue, issueURL, isPR, locale } = navigation.state.params;
const number = issue ? issue.number : issueURL.match(issueNumberRegex)[1];
const langKey = isPR ? 'pullRequest' : 'issue';
- const langTitle = translate(
- `issue.main.screenTitles.${langKey}`,
- locale
- );
+ const langTitle = translate(`issue.main.screenTitles.${langKey}`, locale);
return {
title: `${langTitle} #${number}`,
diff --git a/src/api/rest/actions/activity.js b/src/api/rest/actions/activity.js
new file mode 100644
index 000000000..4a4f1b699
--- /dev/null
+++ b/src/api/rest/actions/activity.js
@@ -0,0 +1,15 @@
+import { createActionSet } from 'utils';
+
+export const ACTIVITY_GET_EVENTS_RECEIVED = createActionSet(
+ 'ACTIVITY_GET_EVENTS_RECEIVED'
+);
+export const ACTIVITY_GET_NOTIFICATIONS = createActionSet(
+ 'ACTIVITY_GET_NOTIFICATIONS'
+);
+
+export const COUNT_ACTIVITY_GET_NOTIFICATIONS = createActionSet(
+ 'COUNT_ACTIVITY_GET_NOTIFICATIONS'
+);
+export const ACTIVITY_MARK_NOTIFICATION_THREAD_AS_READ = createActionSet(
+ 'ACTIVITY_MARK_NOTIFICATION_THREAD_AS_READ'
+);
diff --git a/src/api/rest/actions/index.js b/src/api/rest/actions/index.js
new file mode 100644
index 000000000..06745f8c5
--- /dev/null
+++ b/src/api/rest/actions/index.js
@@ -0,0 +1,3 @@
+export * from './orgs';
+export * from './search';
+export * from './activity';
diff --git a/src/api/rest/actions/orgs.js b/src/api/rest/actions/orgs.js
new file mode 100644
index 000000000..0d3fc0b1f
--- /dev/null
+++ b/src/api/rest/actions/orgs.js
@@ -0,0 +1,5 @@
+import { createActionSet } from 'utils';
+
+export const ORGS_GET_BY_ID = createActionSet('ORGS_GET_BY_ID');
+export const ORGS_GET_REPOS = createActionSet('ORGS_GET_REPOS');
+export const ORGS_GET_MEMBERS = createActionSet('ORGS_GET_MEMBERS');
diff --git a/src/api/rest/actions/search.js b/src/api/rest/actions/search.js
new file mode 100644
index 000000000..48187e1de
--- /dev/null
+++ b/src/api/rest/actions/search.js
@@ -0,0 +1,3 @@
+import { createActionSet } from 'utils';
+
+export const SEARCH_GET_REPOS = createActionSet('SEARCH_GET_REPOS');
diff --git a/src/api/rest/index.js b/src/api/rest/index.js
new file mode 100644
index 000000000..3ff1ab7ba
--- /dev/null
+++ b/src/api/rest/index.js
@@ -0,0 +1,4 @@
+import { GitHub } from './providers/github';
+import { createDispatchProxy } from './proxies';
+
+export const client = createDispatchProxy(GitHub);
diff --git a/src/api/rest/providers/base/client.js b/src/api/rest/providers/base/client.js
new file mode 100644
index 000000000..5385be5a0
--- /dev/null
+++ b/src/api/rest/providers/base/client.js
@@ -0,0 +1,78 @@
+export class Client {
+ /**
+ * Enum for HTTP methods.
+ *
+ * @enum {string}
+ */
+ Method = {
+ GET: 'GET',
+ HEAD: 'HEAD',
+ PUT: 'PUT',
+ DELETE: 'DELETE',
+ PATCH: 'PATCH',
+ POST: 'POST',
+ };
+
+ authHeaders = {};
+
+ fetch = async (
+ url,
+ params = {},
+ { method = this.Method.GET, headers = {} } = {}
+ ) => {
+ let finalUrl;
+
+ if (params.url) {
+ // a different url was provided, use it instead (paginated)
+ finalUrl = params.url;
+ } else {
+ finalUrl = url;
+ // add explicitely specified parameters
+ if (params.per_page) {
+ finalUrl = `${finalUrl}${finalUrl.includes('?')
+ ? '&'
+ : '?'}per_page=${params.per_page}`;
+ }
+ }
+
+ if (!finalUrl.includes(this.API_ROOT)) {
+ finalUrl = `${this.API_ROOT}${finalUrl}`;
+ }
+
+ const parameters = {
+ method,
+ headers: {
+ 'Cache-Control': 'no-cache',
+ ...this.authHeaders,
+ ...headers,
+ },
+ };
+
+ return fetch(finalUrl, parameters)
+ .then(response => response)
+ .catch(error => error);
+ };
+
+ /* eslint-disable no-unused-vars */
+
+ /**
+ * Sets the authorization headers given an access token.
+ *
+ * @abstract
+ * @param {string} token The oAuth access token
+ */
+ setAuthHeaders = token => {
+ throw new Error('Not implemented');
+ };
+
+ /**
+ * Counts the entities available by analysing the Response object
+ *
+ * @abstract
+ * @async
+ * @param {Response} response
+ */
+ getCount = async response => {
+ throw new Error('Not implemented');
+ };
+}
diff --git a/src/api/rest/providers/base/index.js b/src/api/rest/providers/base/index.js
new file mode 100644
index 000000000..4f1cce44f
--- /dev/null
+++ b/src/api/rest/providers/base/index.js
@@ -0,0 +1 @@
+export * from './client';
diff --git a/src/api/rest/providers/github/client.js b/src/api/rest/providers/github/client.js
new file mode 100644
index 000000000..26fff155a
--- /dev/null
+++ b/src/api/rest/providers/github/client.js
@@ -0,0 +1,159 @@
+import { Client } from '../base';
+import Schemas from './schemas';
+
+export class GitHub extends Client {
+ API_ROOT = 'https://api.github.com/';
+
+ setAuthHeaders = token => {
+ this.authHeaders = { Authorization: `token ${token}` };
+ };
+
+ getNextPageUrl = response => {
+ const link = response.headers.get('link');
+
+ if (!link) {
+ return null;
+ }
+
+ const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1);
+
+ if (!nextLink) {
+ return null;
+ }
+
+ return nextLink.split(';')[0].slice(1, -1);
+ };
+
+ /**
+ * Counts the entities available by analysing the Response object
+ *
+ * @async
+ * @param {Response} response
+ */
+ getCount = async response => {
+ if (!response.ok) {
+ return 0;
+ }
+
+ let linkHeader = response.headers.get('Link');
+
+ if (linkHeader !== null) {
+ linkHeader = linkHeader.match(/page=(\d)+/g).pop();
+
+ return linkHeader.split('=').pop();
+ }
+
+ return response
+ .json()
+ .then(data => data.length)
+ .catch(() => 0);
+ };
+
+ /**
+ * The organizations endpoint
+ */
+ orgs = {
+ /**
+ * Gets an organization by its id
+ *
+ * @param {string} orgId
+ */
+ getById: async (orgId, params) => {
+ return this.fetch(`orgs/${orgId}`, params).then(response => ({
+ response,
+ schema: Schemas.ORG,
+ }));
+ },
+ /**
+ * Gets organization members
+ *
+ * @param {string} orgId
+ */
+ getMembers: async (orgId, params) => {
+ return this.fetch(`orgs/${orgId}/members`, params).then(response => ({
+ response,
+ nextPageUrl: this.getNextPageUrl(response),
+ schema: Schemas.USER_ARRAY,
+ }));
+ },
+ /**
+ * Gets organization members
+ *
+ * @param {string} orgId
+ */
+ getRepos: async (orgId, params) => {
+ return this.fetch(`orgs/${orgId}/repos`, params).then(response => ({
+ response,
+ nextPageUrl: this.getNextPageUrl(response),
+ schema: Schemas.REPO_ARRAY,
+ }));
+ },
+ };
+
+ /**
+ * The activity endpoint
+ */
+ activity = {
+ /**
+ * Gets received events
+ *
+ * @param {string} userId
+ */
+ getEventsReceived: async (userId, params) => {
+ return this.fetch(
+ `users/${userId}/received_events`,
+ params
+ ).then(response => ({
+ response,
+ nextPageUrl: this.getNextPageUrl(response),
+ schema: Schemas.EVENT_ARRAY,
+ }));
+ },
+ /**
+ * Get all notifications for the current user
+ *
+ * @param {boolean} all If true, show notifications marked as read.
+ * @param {boolean} participating If true, in which the user is directly participating or mentioned.
+ */
+ getNotifications: async (all, participating, params) => {
+ const finalParams = {
+ per_page: 100,
+ ...params,
+ };
+
+ return this.fetch(
+ `notifications?all=${all}&participating=${participating}`,
+ finalParams,
+ {}
+ ).then(response => ({
+ response,
+ nextPageUrl: this.getNextPageUrl(response),
+ schema: Schemas.NOTIFICATION_ARRAY,
+ }));
+ },
+ markNotificationThreadAsRead: async (id, params) => {
+ return this.fetch(`notifications/threads/${id}`, params, {
+ method: this.Method.PATCH,
+ }).then(response => ({ response }));
+ },
+ };
+
+ search = {
+ /**
+ * Search repositories
+ *
+ * @param {string} query
+ */
+ getRepos: async (query, params) => {
+ return this.fetch(
+ `search/repositories?${query}`,
+ params
+ ).then(response => ({
+ response,
+ nextPageUrl: this.getNextPageUrl(response),
+ schema: Schemas.REPO_ARRAY,
+ normalizrKey: 'items',
+ }));
+ },
+ };
+}
diff --git a/src/api/rest/providers/github/index.js b/src/api/rest/providers/github/index.js
new file mode 100644
index 000000000..4f1cce44f
--- /dev/null
+++ b/src/api/rest/providers/github/index.js
@@ -0,0 +1 @@
+export * from './client';
diff --git a/src/api/rest/providers/github/schemas/events.js b/src/api/rest/providers/github/schemas/events.js
new file mode 100644
index 000000000..86f78a2e7
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/events.js
@@ -0,0 +1,32 @@
+import { schema } from 'normalizr';
+import { initSchema, toTimestamp } from 'utils';
+
+import { userSchema } from './users';
+import { orgSchema } from './orgs';
+import { repoSchema } from './repos';
+
+export const eventSchema = new schema.Entity(
+ 'events',
+ {
+ actor: userSchema,
+ org: orgSchema,
+ repo: repoSchema,
+ },
+ {
+ idAttribute: event => event.id,
+ processStrategy: entity => {
+ const processed = initSchema();
+
+ processed.id = entity.id;
+ processed.type = entity.type; // TODO: needs to be normalized in an Enum
+ processed.payload = entity.payload; // TODO: needs to be inspected for more nested entities (forkee)
+ processed.createdAt = toTimestamp(entity.created_at);
+
+ processed.actor = entity.actor ? entity.actor : null;
+ processed.org = entity.org ? entity.org : null;
+ processed.repo = entity.repo;
+
+ return processed;
+ },
+ }
+);
diff --git a/src/api/rest/providers/github/schemas/index.js b/src/api/rest/providers/github/schemas/index.js
new file mode 100644
index 000000000..61c78a5d0
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/index.js
@@ -0,0 +1,18 @@
+import { orgSchema } from './orgs';
+import { userSchema } from './users';
+import { repoSchema } from './repos';
+import { eventSchema } from './events';
+import { notificationSchema } from './notifications';
+
+export default {
+ USER: userSchema,
+ USER_ARRAY: [userSchema],
+ ORG: orgSchema,
+ ORG_ARRAY: [orgSchema],
+ REPO: repoSchema,
+ REPO_ARRAY: [repoSchema],
+ EVENT: eventSchema,
+ EVENT_ARRAY: [eventSchema],
+ NOTIFICATION: notificationSchema,
+ NOTIFICATION_ARRAY: [notificationSchema],
+};
diff --git a/src/api/rest/providers/github/schemas/notifications.js b/src/api/rest/providers/github/schemas/notifications.js
new file mode 100644
index 000000000..65de77429
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/notifications.js
@@ -0,0 +1,25 @@
+import { schema } from 'normalizr';
+import { initSchema, toTimestamp } from 'utils';
+
+import { repoSchema } from './repos';
+
+export const notificationSchema = new schema.Entity(
+ 'notifications',
+ {
+ repo: repoSchema,
+ },
+ {
+ idAttribute: notification => notification.id,
+ processStrategy: entity => ({
+ ...initSchema(),
+ id: entity.id,
+ updatedAt: toTimestamp(entity.updated_at),
+ unread: entity.unread,
+ reason: entity.reason, // TODO: normalize it
+ type: entity.subject.type, // TODO: normalize it
+ link: entity.subject.url,
+ title: entity.subject.title,
+ repo: entity.repository,
+ }),
+ }
+);
diff --git a/src/api/rest/providers/github/schemas/orgs.js b/src/api/rest/providers/github/schemas/orgs.js
new file mode 100644
index 000000000..d5b2dea69
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/orgs.js
@@ -0,0 +1,53 @@
+import { schema } from 'normalizr';
+import { initSchema, toTimestamp } from 'utils';
+
+export const orgSchema = new schema.Entity(
+ 'orgs',
+ {},
+ {
+ idAttribute: org => org.login.toLowerCase(),
+ processStrategy: entity => {
+ const processed = initSchema();
+
+ // These are provided in both mini & full modes
+ processed.id = entity.login.toLowerCase(); // id should be always used for navigation
+ processed.login = entity.login;
+ processed.avatarUrl = entity.avatar_url;
+
+ // name is only present in full mode, we base our full parsing on its presence
+ if (typeof entity.name !== 'undefined') {
+ processed.name = entity.name;
+ processed.webSite = entity.blog;
+ processed.location = entity.location;
+ processed.description = entity.description;
+
+ processed.countPublicRepos = entity.public_repos;
+ processed.countPrivateRepos = 0;
+ processed.countRepos = entity.public_repos;
+
+ processed._entityUrl = entity.html_url;
+
+ processed.createdAt = toTimestamp(entity.created_at);
+ processed.updatedAt = toTimestamp(entity.updated_at);
+
+ // Clear avatar cached URL to make sure picture is refetched on profile change
+ processed.avatarUrl += `&updatedAt=${processed.updatedAt}`;
+
+ // The entity is to be considered complete.
+ processed._isComplete = true;
+
+ if (typeof entity.total_private_repos !== 'undefined') {
+ // This org belongs to the authenticated user, update some props
+ processed._isAuth = true;
+ processed.countPrivateRepos = entity.total_private_repos;
+ processed.countRepos += entity.total_private_repos;
+ }
+ } else {
+ // We can try our best to fill in some props on our own:
+ processed._entityUrl = `https://github.com/${processed.id}`;
+ }
+
+ return processed;
+ },
+ }
+);
diff --git a/src/api/rest/providers/github/schemas/repos.js b/src/api/rest/providers/github/schemas/repos.js
new file mode 100644
index 000000000..f75749a3f
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/repos.js
@@ -0,0 +1,64 @@
+import { schema } from 'normalizr';
+import { initSchema } from 'utils';
+
+import { userSchema } from './users';
+import { orgSchema } from './orgs';
+
+const isInMinimalisticForm = entity => typeof entity.full_name === 'undefined';
+
+export const repoSchema = new schema.Entity(
+ 'repos',
+ {
+ userOwner: userSchema,
+ orgOwner: orgSchema,
+ },
+ {
+ idAttribute: repo =>
+ (isInMinimalisticForm(repo) ? repo.name : repo.full_name).toLowerCase(),
+ processStrategy: entity => {
+ const processed = initSchema();
+
+ // Repo received from events
+ if (isInMinimalisticForm(entity)) {
+ processed.id = entity.name.toLowerCase();
+ processed.fullName = entity.name;
+ processed.shortName = entity.name.substring(
+ entity.name.indexOf('/') + 1
+ );
+ processed._entityUrl = `https://github.com/${entity.name}`;
+
+ return processed;
+ }
+
+ processed.id = entity.full_name.toLowerCase();
+ processed.fullName = entity.full_name;
+ processed.shortName = entity.name;
+ processed.description = entity.description;
+ processed.private = entity.private;
+
+ if (entity.owner.type === 'User') {
+ processed.userOwner = entity.owner;
+ processed.orgOwner = false;
+ } else {
+ processed.userOwner = false;
+ processed.orgOwner = entity.owner;
+ }
+
+ if (typeof entity.default_branch !== 'undefined') {
+ processed.defaultBranch = entity.default_branch;
+ processed.language = entity.language; // needs to be normalized
+
+ processed.countStargazzers = entity.stargazers_count;
+ processed.countForks = entity.forks_count;
+ processed.countWatchers = entity.watchers_count;
+ processed.countOpenIssues = entity.open_issues_count;
+
+ processed.hasIssues = entity.has_issues;
+ }
+
+ processed._entityUrl = entity.html_url;
+
+ return processed;
+ },
+ }
+);
diff --git a/src/api/rest/providers/github/schemas/users.js b/src/api/rest/providers/github/schemas/users.js
new file mode 100644
index 000000000..c6b0944f9
--- /dev/null
+++ b/src/api/rest/providers/github/schemas/users.js
@@ -0,0 +1,46 @@
+import { schema } from 'normalizr';
+import { initSchema, toTimestamp } from 'utils';
+
+export const userSchema = new schema.Entity(
+ 'users',
+ {},
+ {
+ idAttribute: user => user.login.toLowerCase(),
+ processStrategy: entity => {
+ const processed = initSchema();
+
+ // These are provided in both mini & full modes
+ processed.id = entity.login.toLowerCase(); // id should be always used for navigation
+ processed.login = entity.login;
+ processed.avatarUrl = entity.avatar_url;
+
+ // name is only present in full mode, we base our full parsing on its presence
+ if (typeof entity.name !== 'undefined') {
+ processed.fullName = entity.name;
+ processed.company = entity.company;
+ processed.webSite = entity.blog;
+ processed.location = entity.location;
+ processed.bio = entity.bio;
+
+ processed.countPublicRepos = entity.public_repos;
+ processed.countPrivateRepos = 0;
+ processed.countRepos = entity.public_repos;
+ processed.countFollowers = entity.followers;
+ processed.countFollowing = entity.following;
+
+ processed.createdAt = toTimestamp(entity.created_at); // as unix timestamp
+ processed.updatedAt = toTimestamp(entity.updated_at); // as unix timestamp
+
+ // Clear avatar cached URL to make sure picture is refetched on profile change
+ processed.avatarUrl += `&updatedAt=${processed.updatedAt}`;
+
+ processed._entityUrl = `https://github.com/${entity.login}`;
+
+ // The entity is to be considered complete.
+ processed._isComplete = true;
+ }
+
+ return processed;
+ },
+ }
+);
diff --git a/src/api/rest/proxies/create-dispatch-proxy.js b/src/api/rest/proxies/create-dispatch-proxy.js
new file mode 100644
index 000000000..9fa6c32f4
--- /dev/null
+++ b/src/api/rest/proxies/create-dispatch-proxy.js
@@ -0,0 +1,212 @@
+import * as Actions from 'api/rest/actions';
+import { normalize } from 'normalizr';
+
+import {
+ getActionKeyFromArgs,
+ splitArgs,
+ displayError,
+ actionNameForCall,
+} from 'utils/api-helpers';
+
+const handleCountOperation = (
+ client,
+ namespace,
+ method,
+ args,
+ dispatch,
+ getState
+) => {
+ // Used as a key for state.pagination
+ const actionName = actionNameForCall(namespace, method, 'COUNT_');
+ const action = Actions[actionName];
+
+ if (!action) {
+ return displayError(
+ `Unknown action. Did you forget to define Actions.${actionName}?`
+ );
+ }
+
+ const { pureArgs, extraArg } = splitArgs(client[namespace][method], args);
+
+ extraArg.per_page = 1;
+
+ const finalArgs = [...pureArgs, extraArg];
+ const actionKey = getActionKeyFromArgs(pureArgs);
+
+ dispatch({
+ key: actionKey,
+ type: action.PENDING,
+ });
+
+ client.setAuthHeaders(getState().auth.accessToken);
+
+ /* eslint-disable no-unexpected-multiline */
+ return client[namespace][method](...finalArgs).then(struct => {
+ client
+ .getCount(struct.response)
+ .then(count => {
+ dispatch({
+ counters: count,
+ key: actionKey,
+ name: actionName,
+ type: action.SUCCESS,
+ });
+ })
+ .catch(error => {
+ displayError(error.toString());
+ dispatch({
+ key: actionKey,
+ type: action.ERROR,
+ });
+
+ return error;
+ });
+ });
+};
+
+export const createDispatchProxy = Provider => {
+ const client = new Provider();
+
+ return new Proxy(createDispatchProxy, {
+ get: (c, namespace) => {
+ return new Proxy(client[namespace], {
+ get: (endpoint, method) => (...args) => (dispatch, getState) => {
+ if (!endpoint[method]) {
+ // Non existant method.. is the user asking for a count?
+ if (method.match(/Count$/)) {
+ const actualMethod = method.slice(0, -5);
+
+ if (endpoint[actualMethod]) {
+ return handleCountOperation(
+ client,
+ namespace,
+ actualMethod,
+ args,
+ dispatch,
+ getState
+ );
+ }
+ }
+
+ return displayError(
+ `Unknown API method. Did you implement client.${namespace}.${method}()?`
+ );
+ }
+
+ // Used as a key for state.pagination
+ const actionName = actionNameForCall(namespace, method);
+ const action = Actions[actionName];
+
+ if (!action) {
+ return displayError(
+ `Unknown action. Did you forget to define Actions.${actionName}?`
+ );
+ }
+
+ const { pureArgs, extraArg } = splitArgs(endpoint[method], args);
+ const paginator = getState().pagination[actionName];
+ const actionKey = getActionKeyFromArgs(pureArgs);
+
+ let finalArgs = args;
+
+ if (paginator) {
+ const { loadMore = false, forceRefresh = false } = extraArg;
+ const { pageCount = 0, isFetching = false, nextPageUrl } =
+ paginator[actionKey] || {};
+
+ if (
+ !forceRefresh &&
+ (isFetching || // Already fetching, don't retrigger a call
+ (pageCount > 0 && !loadMore) || // We already have the first page of data
+ (loadMore && !nextPageUrl)) // We've already fetched the last page
+ ) {
+ return Promise.resolve();
+ }
+
+ if (loadMore) {
+ // next page explicitely requested
+ extraArg.url = nextPageUrl;
+ } else if (forceRefresh) {
+ // TODO: reset pagination state properly via an action
+ // console.log('TODO: reset pagination');
+ }
+
+ finalArgs = [...pureArgs, extraArg];
+ }
+
+ // Get accessToken from state
+ client.setAuthHeaders(getState().auth.accessToken);
+
+ dispatch({
+ id: actionKey,
+ type: action.PENDING,
+ });
+
+ return endpoint[method](...finalArgs)
+ .then(struct => {
+ if (!struct.response.ok) {
+ return struct.response.json().then(error => {
+ return Promise.reject(
+ [
+ `Call: client.${namespace}.${method}()`,
+ `Url: ${struct.response.url}`,
+ `Error: [${struct.response.status}] ${error.message}`,
+ ].join('\n')
+ );
+ });
+ }
+
+ // Successful PUT or PATCH request, there will be no JSON, simply dispatch success
+ // TODO: We need a better test here
+ if (struct.response.status === 205) {
+ dispatch({
+ id: actionKey,
+ type: action.SUCCESS,
+ });
+
+ return Promise.resolve();
+ }
+
+ return struct.response.json().then(json => {
+ // Treat the JSON & normalize it
+ const normalizedJson = normalize(
+ struct.normalizrKey ? json[struct.normalizrKey] : json,
+ struct.schema
+ );
+
+ if (paginator) {
+ normalizedJson.pagination = {
+ name: actionName,
+ key: actionKey,
+ ids: normalizedJson.result,
+ nextPageUrl: struct.nextPageUrl,
+ };
+
+ delete normalizedJson.result;
+ }
+
+ // Success, let's dispatch it
+ dispatch({
+ ...normalizedJson,
+ id: actionKey,
+ type: action.SUCCESS,
+ });
+
+ return Promise.resolve();
+ });
+ })
+ .catch(error => {
+ displayError(error.toString());
+
+ dispatch({
+ id: actionKey,
+ type: action.ERROR,
+ });
+
+ return error;
+ });
+ },
+ });
+ },
+ });
+};
diff --git a/src/api/rest/proxies/index.js b/src/api/rest/proxies/index.js
new file mode 100644
index 000000000..cb889065d
--- /dev/null
+++ b/src/api/rest/proxies/index.js
@@ -0,0 +1 @@
+export * from './create-dispatch-proxy';
diff --git a/src/api/rest/reducers/counters.js b/src/api/rest/reducers/counters.js
new file mode 100644
index 000000000..3b72920d9
--- /dev/null
+++ b/src/api/rest/reducers/counters.js
@@ -0,0 +1,15 @@
+import { merge } from 'lodash';
+
+// Updates an entity cache in response to any action with response.counters.
+export const counters = (state = {}, action) => {
+ if (action && action.counters) {
+ const pushed = {};
+
+ pushed[action.name] = {};
+ pushed[action.name][action.key] = action.counters;
+
+ return merge({}, state, pushed);
+ }
+
+ return state;
+};
diff --git a/src/api/rest/reducers/entities.js b/src/api/rest/reducers/entities.js
new file mode 100644
index 000000000..dbdcd5a17
--- /dev/null
+++ b/src/api/rest/reducers/entities.js
@@ -0,0 +1,18 @@
+import { merge } from 'lodash';
+
+// Updates an entity cache in response to any action with response.entities.
+export const entities = (
+ state = {
+ users: {},
+ orgs: {},
+ repos: {},
+ events: {},
+ },
+ action
+) => {
+ if (action && action.entities) {
+ return merge({}, state, action.entities);
+ }
+
+ return state;
+};
diff --git a/src/api/rest/reducers/errorMessage.js b/src/api/rest/reducers/errorMessage.js
new file mode 100644
index 000000000..e9859a803
--- /dev/null
+++ b/src/api/rest/reducers/errorMessage.js
@@ -0,0 +1,14 @@
+const RESET_ERROR_MESSAGE = 'RESET_ERROR';
+
+// Updates error message to notify about the failed fetches.
+export const errorMessage = (state = null, action) => {
+ const { type, error } = action;
+
+ if (type === RESET_ERROR_MESSAGE) {
+ return null;
+ } else if (error) {
+ return error;
+ }
+
+ return state;
+};
diff --git a/src/api/rest/reducers/index.js b/src/api/rest/reducers/index.js
new file mode 100644
index 000000000..61ce5c113
--- /dev/null
+++ b/src/api/rest/reducers/index.js
@@ -0,0 +1,4 @@
+export * from './entities';
+export * from './counters';
+export * from './pagination';
+export * from './errorMessage';
diff --git a/src/api/rest/reducers/pagination.js b/src/api/rest/reducers/pagination.js
new file mode 100644
index 000000000..8432b414c
--- /dev/null
+++ b/src/api/rest/reducers/pagination.js
@@ -0,0 +1,77 @@
+import { combineReducers } from 'redux';
+import { union } from 'lodash';
+
+import * as Actions from '../actions';
+
+// Creates a reducer managing pagination, given the action types to handle,
+// and a function telling how to extract the key from an action.
+const paginate = types => {
+ if (typeof types !== 'object' || Object.keys(types).length !== 3) {
+ throw new Error('Expected types to be an object of three props.');
+ }
+
+ const updatePagination = (
+ state = {
+ isFetching: false,
+ nextPageUrl: undefined,
+ pageCount: 0,
+ ids: [],
+ },
+ action
+ ) => {
+ switch (action.type) {
+ case types.PENDING:
+ return {
+ ...state,
+ isFetching: true,
+ };
+ case types.SUCCESS:
+ return {
+ ...state,
+ isFetching: false,
+ ids: union(state.ids, action.pagination.ids),
+ nextPageUrl: action.pagination.nextPageUrl,
+ pageCount: state.pageCount + 1,
+ };
+ case types.ERROR:
+ return {
+ ...state,
+ isFetching: false,
+ };
+ default:
+ return state;
+ }
+ };
+
+ /* eslint-disable no-case-declarations */
+ return (state = {}, action) => {
+ // Update pagination by key
+ switch (action.type) {
+ case types.PENDING:
+ case types.SUCCESS:
+ case types.ERROR:
+ const key = action.id;
+
+ // console.log("PAGINATE", action);
+ if (typeof key !== 'string') {
+ throw new Error('Expected key to be a string.');
+ }
+
+ return {
+ ...state,
+ [key]: updatePagination(state[key], action),
+ };
+ default:
+ return state;
+ }
+ };
+};
+
+// Updates the pagination data for different actions.
+export const pagination = combineReducers({
+ ACTIVITY_GET_EVENTS_RECEIVED: paginate(Actions.ACTIVITY_GET_EVENTS_RECEIVED),
+ ORGS_GET_REPOS: paginate(Actions.ORGS_GET_REPOS),
+ ORGS_GET_MEMBERS: paginate(Actions.ORGS_GET_MEMBERS),
+ SEARCH_GET_REPOS: paginate(Actions.SEARCH_GET_REPOS),
+ ACTIVITY_GET_NOTIFICATIONS: paginate(Actions.ACTIVITY_GET_NOTIFICATIONS),
+});
diff --git a/src/auth/auth.action.js b/src/auth/auth.action.js
index 76680b84c..fe798f5da 100644
--- a/src/auth/auth.action.js
+++ b/src/auth/auth.action.js
@@ -1,6 +1,5 @@
import { AsyncStorage } from 'react-native';
-
-import uniqby from 'lodash.uniqby';
+import { uniqby } from 'lodash';
import { delay, resetNavigationTo, configureLocale, saveLocale } from 'utils';
import {
@@ -8,7 +7,6 @@ import {
fetchAuthUser,
fetchAuthUserOrgs,
fetchUserOrgs,
- fetchUserEvents,
fetchStarCount,
} from 'api';
import {
@@ -16,7 +14,6 @@ import {
LOGOUT,
GET_AUTH_USER,
GET_AUTH_ORGS,
- GET_EVENTS,
CHANGE_LOCALE,
GET_AUTH_STAR_COUNT,
} from './auth.type';
@@ -137,28 +134,6 @@ export const getOrgs = () => {
};
};
-export const getUserEvents = user => {
- return (dispatch, getState) => {
- const accessToken = getState().auth.accessToken;
-
- dispatch({ type: GET_EVENTS.PENDING });
-
- fetchUserEvents(user, accessToken)
- .then(data => {
- dispatch({
- type: GET_EVENTS.SUCCESS,
- payload: data,
- });
- })
- .catch(error => {
- dispatch({
- type: GET_EVENTS.ERROR,
- payload: error,
- });
- });
- };
-};
-
export const changeLocale = locale => {
return dispatch => {
dispatch({ type: CHANGE_LOCALE.SUCCESS, payload: locale });
diff --git a/src/auth/auth.reducer.js b/src/auth/auth.reducer.js
index 88e3c6c3e..28271cef0 100644
--- a/src/auth/auth.reducer.js
+++ b/src/auth/auth.reducer.js
@@ -4,7 +4,6 @@ import {
LOGOUT,
GET_AUTH_USER,
GET_AUTH_ORGS,
- GET_EVENTS,
CHANGE_LOCALE,
GET_AUTH_STAR_COUNT,
} from './auth.type';
@@ -115,23 +114,6 @@ export const authReducer = (state = initialState, action = {}) => {
error: action.payload,
isPendingOrgs: false,
};
- case GET_EVENTS.PENDING:
- return {
- ...state,
- isPendingEvents: true,
- };
- case GET_EVENTS.SUCCESS:
- return {
- ...state,
- events: action.payload,
- isPendingEvents: false,
- };
- case GET_EVENTS.ERROR:
- return {
- ...state,
- error: action.payload,
- isPendingEvents: false,
- };
case CHANGE_LOCALE.SUCCESS:
return {
...state,
diff --git a/src/auth/auth.type.js b/src/auth/auth.type.js
index 00e5dd515..e82380776 100644
--- a/src/auth/auth.type.js
+++ b/src/auth/auth.type.js
@@ -4,6 +4,5 @@ export const LOGIN = createActionSet('LOGIN');
export const LOGOUT = createActionSet('LOGOUT');
export const GET_AUTH_USER = createActionSet('GET_AUTH_USER');
export const GET_AUTH_ORGS = createActionSet('GET_AUTH_ORGS');
-export const GET_EVENTS = createActionSet('GET_EVENTS');
export const CHANGE_LOCALE = createActionSet('CHANGE_LOCALE');
export const GET_AUTH_STAR_COUNT = createActionSet('GET_AUTH_STAR_COUNT');
diff --git a/src/auth/screens/auth-profile.screen.js b/src/auth/screens/auth-profile.screen.js
index febf2082b..59d1e437d 100644
--- a/src/auth/screens/auth-profile.screen.js
+++ b/src/auth/screens/auth-profile.screen.js
@@ -163,7 +163,12 @@ class AuthProfile extends Component {
)}
{!isPending && (
-
+
)}
{!isPending && (
diff --git a/src/auth/screens/events.screen.js b/src/auth/screens/events.screen.js
index 93a3441e5..bfb1bc791 100644
--- a/src/auth/screens/events.screen.js
+++ b/src/auth/screens/events.screen.js
@@ -2,33 +2,42 @@
/* eslint-disable no-shadow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { StyleSheet, Text, FlatList, View } from 'react-native';
+import {
+ StyleSheet,
+ Text,
+ FlatList,
+ View,
+ ActivityIndicator,
+} from 'react-native';
import moment from 'moment/min/moment-with-locales.min';
-
import { LoadingUserListItem, UserListItem, ViewContainer } from 'components';
import { colors, fonts, normalize } from 'config';
import { emojifyText, translate } from 'utils';
-import { getUserEvents, getUser } from 'auth';
-import { getNotificationsCount } from 'notifications';
-
-const mapStateToProps = state => ({
- user: state.auth.user,
- userEvents: state.auth.events,
- locale: state.auth.locale,
- isPendingEvents: state.auth.isPendingEvents,
- accessToken: state.auth.accessToken,
-});
+import { client } from 'api/rest';
-const mapDispatchToProps = dispatch =>
- bindActionCreators(
- {
- getUserEvents,
- getNotificationsCount,
- getUser,
- },
- dispatch
- );
+const mapStateToProps = state => {
+ const {
+ auth: { user },
+ pagination: { ACTIVITY_GET_EVENTS_RECEIVED },
+ entities: { repos, users, events },
+ } = state;
+
+ const userEventsPagination = ACTIVITY_GET_EVENTS_RECEIVED[user.login] || {
+ ids: [],
+ };
+ const userEvents = userEventsPagination.ids.map(id => events[id]);
+
+ return {
+ repos,
+ users,
+ userEvents,
+ userEventsPagination,
+ user: state.auth.user,
+ locale: state.auth.locale,
+ isPendingEvents: state.auth.isPendingEvents,
+ accessToken: state.auth.accessToken,
+ };
+};
const styles = StyleSheet.create({
descriptionContainer: {
@@ -79,25 +88,22 @@ const styles = StyleSheet.create({
class Events extends Component {
componentDidMount() {
- const { user: { login }, getUser } = this.props;
+ const { getEvents, getNotificationsCount, user: { login } } = this.props;
- if (login) {
- this.getUserEvents();
- } else {
- getUser();
- }
+ getEvents(login);
+ getNotificationsCount(false, false);
}
componentWillReceiveProps(nextProps) {
+ // TODO: not sure if needed
if (nextProps.user.login && !this.props.user.login) {
- this.getUserEvents(nextProps);
+ this.nextProps.getEvents(nextProps.user.login);
}
}
- getUserEvents = ({ user, accessToken } = this.props) => {
- this.props.getUserEvents(user.login);
- this.props.getNotificationsCount(accessToken);
- };
+ /*
+ // TODO: getUser() should be called and waited for in the auth process
+ */
getAction = userEvent => {
const { locale } = this.props;
@@ -173,13 +179,9 @@ class Events extends Component {
}
);
} else if (action === 'edited') {
- return translate(
- 'auth.events.pullRequestReviewEditedEvent',
- locale,
- {
- action: translate(`auth.events.actions.${action}`, locale),
- }
- );
+ return translate('auth.events.pullRequestReviewEditedEvent', locale, {
+ action: translate(`auth.events.actions.${action}`, locale),
+ });
} else if (action === 'deleted') {
return translate(
'auth.events.pullRequestReviewDeletedEvent',
@@ -227,7 +229,7 @@ class Events extends Component {
style={styles.linkDescription}
onPress={() => this.navigateToRepository(userEvent)}
>
- {userEvent.repo.name}
+ {userEvent.repo}
);
}
@@ -248,7 +250,7 @@ class Events extends Component {
style={styles.linkDescription}
onPress={() => this.navigateToRepository(userEvent)}
>
- {userEvent.repo.name}
+ {userEvent.repo}
);
case 'GollumEvent':
@@ -259,7 +261,7 @@ class Events extends Component {
style={styles.linkDescription}
onPress={() => this.navigateToRepository(userEvent)}
>
- {userEvent.repo.name}
+ {userEvent.repo}
{' '}
wiki
@@ -312,7 +314,7 @@ class Events extends Component {
}
}}
>
- {userEvent.repo.name}
+ {userEvent.repo}
);
default:
@@ -385,7 +387,7 @@ class Events extends Component {
style={styles.linkDescription}
onPress={() => this.navigateToRepository(userEvent)}
>
- {userEvent.repo.name}
+ {userEvent.repo}
);
case 'ForkEvent':
@@ -461,15 +463,10 @@ class Events extends Component {
});
navigateToRepository = (userEvent, isForkEvent) => {
+ const repo = this.props.repos[userEvent.repo];
+
this.props.navigation.navigate('Repository', {
- repository: !isForkEvent
- ? {
- ...userEvent.repo,
- name: userEvent.repo.name.substring(
- userEvent.repo.name.indexOf('/') + 1
- ),
- }
- : userEvent.payload.forkee,
+ repository: !isForkEvent ? repo : userEvent.payload.forkee,
});
};
@@ -500,7 +497,7 @@ class Events extends Component {
style={styles.linkDescription}
onPress={() => this.navigateToProfile(userEvent, true)}
>
- {userEvent.actor.login}{' '}
+ {userEvent.actor}{' '}
{this.getAction(userEvent)}
{this.getItem(userEvent)}
@@ -509,15 +506,35 @@ class Events extends Component {
{this.getItem(userEvent) && ' '}
{this.getSecondItem(userEvent)}
{this.getItem(userEvent) && this.getConnector(userEvent) && ' '}
-
- {moment(userEvent.created_at).fromNow()}
-
+ {moment(userEvent.createdAt).fromNow()}
);
}
+ renderFooter = () => {
+ if (this.props.userEventsPagination.nextPageUrl === null) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
render() {
- const { isPendingEvents, userEvents, locale, navigation } = this.props;
+ const {
+ users,
+ isPendingEvents,
+ userEvents,
+ locale,
+ navigation,
+ } = this.props;
const linebreaksPattern = /(\r\n|\n|\r)/gm;
let content;
@@ -542,10 +559,14 @@ class Events extends Component {
onRefresh={this.getUserEvents}
refreshing={isPendingEvents}
keyExtractor={this.keyExtractor}
+ onEndReached={() =>
+ this.props.getEvents(this.props.user.login, { loadMore: true })}
+ onEndReachedThreshold={0.5}
+ ListFooterComponent={this.renderFooter}
renderItem={({ item }) => (
{emojifyText(item.emojiCode)}
-
- {item.name}
-
+ {item.name}
}
titleStyle={styles.listTitle}
@@ -227,9 +225,7 @@ class UserOptions extends Component {
-
- GitPoint v{version}
-
+ GitPoint v{version}
{this.state.updateText}
diff --git a/src/components/entity-info.component.js b/src/components/entity-info.component.js
index 8b0dc06a6..b4dcd1ec3 100644
--- a/src/components/entity-info.component.js
+++ b/src/components/entity-info.component.js
@@ -147,6 +147,23 @@ export const EntityInfo = ({ entity, orgs, locale, navigation }: Props) => {
underlayColor={colors.greyLight}
/>
)}
+
+ {!!entity.webSite &&
+ (entity.webSite !== '' && (
+ Communications.web(getBlogLink(entity.webSite))}
+ underlayColor={colors.greyLight}
+ />
+ ))}
);
};
diff --git a/src/components/index.js b/src/components/index.js
index e991d6c54..3339fc919 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -12,9 +12,12 @@ export * from './label-list-item.component';
export * from './github-htmlview.component';
export * from './markdown-webview.component';
export * from './members.component';
+export * from './users-avatar-list.component';
export * from './mention-area.component';
export * from './notification-list-item.component';
export * from './parallax-scroll.component';
+export * from './repo-list-item.component';
+export * from './repository-list.component';
export * from './repository-list-item.component';
export * from './repository-profile.component';
export * from './repository-section-title.component';
@@ -23,6 +26,7 @@ export * from './section-list.component';
export * from './state-badge.component';
export * from './user-list-item.component';
export * from './user-profile.component';
+export * from './org-profile.component';
export * from './view-container.component';
export * from './image-zoom.component';
export * from './button.component';
diff --git a/src/components/issue-description.component.js b/src/components/issue-description.component.js
index 7ba0dc7b4..72468e9dc 100644
--- a/src/components/issue-description.component.js
+++ b/src/components/issue-description.component.js
@@ -115,7 +115,7 @@ export class IssueDescription extends Component {
return (
- {issue.repository_url &&
+ {issue.repository_url && (
onRepositoryPress(issue.repository_url)}
hideChevron
- />}
+ />
+ )}
)}
+ !isPendingCheckMerge && (
+
+ ))}
- {issue.pull_request &&
+ {issue.pull_request && (
- {isPendingDiff &&
- }
+ {isPendingDiff && (
+
+ )}
{!isPendingDiff &&
- (lineAdditions !== 0 || lineDeletions !== 0) &&
-
- navigation.navigate('PullDiff', {
- title: translate('repository.pullDiff.title', locale),
- locale,
- diff,
- })}
- />}
- }
+ (lineAdditions !== 0 || lineDeletions !== 0) && (
+
+ navigation.navigate('PullDiff', {
+ title: translate('repository.pullDiff.title', locale),
+ locale,
+ diff,
+ })}
+ />
+ )}
+
+ )}
{issue.labels &&
- issue.labels.length > 0 &&
-
- {this.renderLabelButtons(issue.labels)}
- }
+ issue.labels.length > 0 && (
+
+ {this.renderLabelButtons(issue.labels)}
+
+ )}
{issue.assignees &&
- issue.assignees.length > 0 &&
-
-
- }
+ issue.assignees.length > 0 && (
+
+
+
+ )}
{issue.pull_request &&
!isMerged &&
issue.state === 'open' &&
- userHasPushPermission &&
-
- }
+ userHasPushPermission && (
+
+
+ )}
);
}
diff --git a/src/components/notification-icon.component.js b/src/components/notification-icon.component.js
index 57493775c..60957c90b 100644
--- a/src/components/notification-icon.component.js
+++ b/src/components/notification-icon.component.js
@@ -5,8 +5,8 @@ import { View, StyleSheet } from 'react-native';
import { Icon } from 'react-native-elements';
import { Badge } from 'components';
-
import { colors } from 'config';
+import { getCountFromState } from 'utils';
const styles = StyleSheet.create({
badgeContainer: {
@@ -17,7 +17,11 @@ const styles = StyleSheet.create({
});
const mapStateToProps = state => ({
- notificationsCount: state.notifications.notificationsCount,
+ notificationsCount: getCountFromState(
+ state,
+ 'COUNT_ACTIVITY_GET_NOTIFICATIONS',
+ 'false-false'
+ ),
});
class NotificationIconComponent extends Component {
diff --git a/src/components/notification-list-item.component.js b/src/components/notification-list-item.component.js
index 5a1dc87b4..e677fd8ea 100644
--- a/src/components/notification-list-item.component.js
+++ b/src/components/notification-list-item.component.js
@@ -54,7 +54,7 @@ export const NotificationListItem = ({
navigationAction,
}: Props) => {
const TitleComponent =
- notification.subject.type === 'Commit' ? View : TouchableOpacity;
+ notification.type === 'Commit' ? View : TouchableOpacity;
return (
@@ -67,9 +67,9 @@ export const NotificationListItem = ({
color={colors.grey}
size={22}
name={
- notification.subject.type === 'Commit'
+ notification.type === 'Commit'
? 'git-commit'
- : notification.subject.type === 'PullRequest'
+ : notification.type === 'PullRequest'
? 'git-pull-request'
: 'issue-opened'
}
@@ -77,7 +77,7 @@ export const NotificationListItem = ({
/>
- {notification.subject.title}
+ {notification.title}
diff --git a/src/components/org-profile.component.js b/src/components/org-profile.component.js
new file mode 100644
index 000000000..bf0494ddf
--- /dev/null
+++ b/src/components/org-profile.component.js
@@ -0,0 +1,147 @@
+/* eslint-disable no-prototype-builtins */
+import React from 'react';
+import {
+ StyleSheet,
+ Text,
+ View,
+ TouchableOpacity,
+ ActivityIndicator,
+} from 'react-native';
+import { colors, fonts, normalize } from 'config';
+import { translate } from 'utils';
+import { ImageZoom } from 'components';
+
+type Props = {
+ org: Object,
+ locale: string,
+ navigation: Object,
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ wrapperContainer: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ profile: {
+ flex: 3,
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ },
+ avatar: {
+ width: 75,
+ height: 75,
+ marginBottom: 20,
+ borderRadius: 37.5,
+ },
+ userAvatar: {
+ borderColor: colors.white,
+ borderWidth: 2,
+ },
+ title: {
+ color: colors.white,
+ ...fonts.fontPrimaryBold,
+ fontSize: normalize(16),
+ marginBottom: 2,
+ },
+ subtitle: {
+ color: colors.white,
+ ...fonts.fontPrimary,
+ fontSize: normalize(12),
+ marginBottom: 50,
+ paddingLeft: 15,
+ paddingRight: 15,
+ textAlign: 'center',
+ },
+ details: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ unit: {
+ flex: 1,
+ },
+ unitNumber: {
+ textAlign: 'center',
+ color: colors.white,
+ ...fonts.fontPrimaryBold,
+ fontSize: normalize(16),
+ },
+ unitText: {
+ textAlign: 'center',
+ color: colors.white,
+ fontSize: normalize(10),
+ ...fonts.fontPrimary,
+ },
+ unitStatus: {
+ textAlign: 'center',
+ color: colors.lighterBoldGreen,
+ fontSize: normalize(8),
+ ...fonts.fontPrimary,
+ },
+ badge: {
+ paddingTop: 3,
+ paddingBottom: 3,
+ marginTop: 5,
+ marginLeft: 17,
+ marginRight: 17,
+ borderWidth: 0.5,
+ borderRadius: 5,
+ borderColor: colors.lighterBoldGreen,
+ justifyContent: 'center',
+ },
+ green: {
+ color: colors.lightGreen,
+ },
+});
+
+const mockAttribute = (entity, attribute, replacement) => {
+ return entity && entity[attribute] ? entity[attribute] : replacement;
+};
+
+export const OrgProfile = ({ org, locale, navigation }: Props) => {
+ const countRepos = mockAttribute(org, 'countRepos', 0);
+
+ return (
+
+
+
+ {org &&
+ org.avatarUrl && (
+
+ )}
+ {(!org || !org.avatarUrl) && (
+
+ )}
+ {mockAttribute(org, 'name', ' ')}
+
+ {mockAttribute(org, 'login', ' ')}
+
+
+
+
+ navigation.navigate('OrgRepositoryList', {
+ title: translate('user.repositoryList.title', locale),
+ orgId: org.id,
+ repoCount: countRepos > 15 ? 15 : countRepos,
+ })}
+ >
+ {countRepos}
+
+ {translate('common.repositories', locale)}
+
+
+
+
+
+ );
+};
diff --git a/src/components/repo-list-item.component.js b/src/components/repo-list-item.component.js
new file mode 100644
index 000000000..c2b1c65be
--- /dev/null
+++ b/src/components/repo-list-item.component.js
@@ -0,0 +1,138 @@
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { ListItem, Icon } from 'react-native-elements';
+
+import { emojifyText, abbreviateNumber } from 'utils';
+import { colors, languageColors, fonts, normalize } from 'config';
+
+type Props = {
+ repository: Object,
+ showFullName: boolean,
+ navigation: Object,
+};
+
+const styles = StyleSheet.create({
+ wrapper: {
+ marginTop: 5,
+ marginBottom: 5,
+ marginLeft: 5,
+ },
+ titleWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ title: {
+ color: colors.primaryDark,
+ ...fonts.fontPrimarySemiBold,
+ },
+ privateIconContainer: {
+ marginLeft: 6,
+ },
+ description: {
+ color: colors.primaryDark,
+ ...fonts.fontPrimaryLight,
+ },
+ extraInfo: {
+ flexDirection: 'row',
+ flex: 1,
+ paddingTop: 5,
+ },
+ extraInfoSubject: {
+ color: colors.greyDark,
+ paddingLeft: 3,
+ paddingTop: 2,
+ marginRight: 15,
+ fontSize: normalize(10),
+ ...fonts.fontPrimary,
+ },
+ repositoryContainer: {
+ justifyContent: 'center',
+ flex: 1,
+ },
+});
+
+const renderTitle = (repository, showFullName) => (
+
+
+
+
+ {showFullName ? repository.fullName : repository.shortName}
+
+ {repository.private && (
+
+
+
+ )}
+
+
+ {emojifyText(repository.description)}
+
+
+
+
+
+
+ {abbreviateNumber(repository.countStargazzers)}
+
+
+
+
+
+ {abbreviateNumber(repository.countForks)}
+
+
+ {repository.language !== null && (
+
+ )}
+
+ {repository.language}
+
+
+);
+
+export const RepoListItem = ({
+ repository,
+ showFullName,
+ navigation,
+}: Props) => (
+ navigation.navigate('Repository', { repository })}
+ />
+);
+
+RepoListItem.defaultProps = {
+ showFullName: true,
+};
diff --git a/src/components/repository-list.component.js b/src/components/repository-list.component.js
new file mode 100644
index 000000000..16c566769
--- /dev/null
+++ b/src/components/repository-list.component.js
@@ -0,0 +1,184 @@
+/* eslint-disable no-shadow */
+import React, { Component } from 'react';
+import { View, Dimensions, StyleSheet, FlatList } from 'react-native';
+
+import {
+ ViewContainer,
+ RepoListItem,
+ LoadingRepositoryListItem,
+ SearchBar,
+} from 'components';
+import { colors } from 'config';
+
+const styles = StyleSheet.create({
+ header: {
+ borderBottomColor: colors.greyLight,
+ borderBottomWidth: 1,
+ },
+ searchBarWrapper: {
+ flexDirection: 'row',
+ },
+ searchContainer: {
+ width: Dimensions.get('window').width,
+ backgroundColor: colors.white,
+ flex: 1,
+ },
+ searchCancelButton: {
+ color: colors.black,
+ },
+ listContainer: {
+ marginBottom: 57,
+ },
+});
+
+export class RepositoryList extends Component {
+ props: {
+ // The actions
+ loadRepositories: Function,
+ loadSearchResults: Function,
+
+ // Entity repositories
+ repositories: Array,
+ repositoriesPagination: Object,
+
+ // Search repositories
+ searchResults: Array,
+ searchResultsPagination: Object,
+
+ authUser: Object,
+ navigation: Object,
+ };
+
+ state: {
+ searchMode: boolean,
+ searchFocus: boolean,
+ };
+
+ constructor() {
+ super();
+
+ this.state = {
+ searchMode: false,
+ searchFocus: false,
+ };
+
+ this.search = this.search.bind(this);
+ this.getList = this.getList.bind(this);
+ }
+
+ componentWillMount() {
+ this.props.loadRepositories();
+ }
+
+ getList = () => {
+ const { searchResults, repositories } = this.props;
+ const { searchMode } = this.state;
+
+ return searchMode ? searchResults : repositories;
+ };
+
+ getPagination = () => {
+ const { searchResultsPagination, repositoriesPagination } = this.props;
+ const { searchMode } = this.state;
+
+ return searchMode ? searchResultsPagination : repositoriesPagination;
+ };
+
+ loadMore = () => {
+ const { loadRepositories, loadSearchResults, navigation } = this.props;
+
+ if (navigation.state.params.searchedKeyword) {
+ loadSearchResults(navigation.state.params.searchedKeyword, true);
+ } else {
+ loadRepositories(true);
+ }
+ };
+
+ search(keyword) {
+ if (keyword !== '') {
+ this.setState({
+ searchMode: true,
+ });
+
+ const { loadSearchResults } = this.props;
+
+ loadSearchResults(keyword);
+ }
+ }
+
+ keyExtractor = item => {
+ return item.id;
+ };
+
+ renderFooter = () => {
+ if (this.getPagination().nextPageUrl === null) {
+ return null;
+ }
+
+ return ;
+ };
+
+ render() {
+ const {
+ authUser,
+ navigation,
+ searchResultsPagination,
+ repositoriesPagination,
+ } = this.props;
+ const repoCount = navigation.state.params.repoCount;
+ const { searchMode, searchFocus } = this.state;
+ const loading =
+ (searchResultsPagination.isFetching &&
+ !searchResultsPagination.pageCount) ||
+ (repositoriesPagination.isFetching && !repositoriesPagination.pageCount);
+
+ return (
+
+
+
+
+
+ this.setState({ searchFocus: true })}
+ onCancelButtonPress={() =>
+ this.setState({ searchMode: false })}
+ onSearchButtonPress={query => {
+ this.search(query);
+ }}
+ hideBackground
+ />
+
+
+
+
+ {loading &&
+ [...Array(searchMode ? repoCount : 10)].map(
+ (item, index) => // eslint-disable-line react/no-array-index-key
+ )}
+
+ {!loading && (
+ {
+ return (
+
+ );
+ }}
+ />
+ )}
+
+
+ );
+ }
+}
diff --git a/src/components/user-list-item.component.js b/src/components/user-list-item.component.js
index ea9146aab..2a263a2ae 100644
--- a/src/components/user-list-item.component.js
+++ b/src/components/user-list-item.component.js
@@ -131,7 +131,7 @@ class UserListItemComponent extends Component {
diff --git a/src/components/users-avatar-list.component.js b/src/components/users-avatar-list.component.js
new file mode 100644
index 000000000..d4e6fbbd9
--- /dev/null
+++ b/src/components/users-avatar-list.component.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import {
+ StyleSheet,
+ View,
+ Text,
+ FlatList,
+ TouchableHighlight,
+ Image,
+} from 'react-native';
+import { List, ListItem } from 'react-native-elements';
+
+import { colors, fonts } from 'config';
+
+const mapStateToProps = state => ({
+ authUser: state.auth.user,
+});
+
+type Props = {
+ title: string,
+ members: Array,
+ noMembersMessage: string,
+ containerStyle: Object,
+ smallTitle: string,
+ navigation: Object,
+ authUser: Object,
+ loadMore: Function,
+};
+
+const size = 30;
+
+const styles = StyleSheet.create({
+ container: {
+ marginTop: 30,
+ },
+ avatarContainer: {
+ backgroundColor: colors.greyLight,
+ borderRadius: size / 2,
+ width: size,
+ height: size,
+ marginRight: 5,
+ },
+ avatar: {
+ borderRadius: size / 2,
+ height: size,
+ width: size,
+ },
+ list: {
+ marginTop: 0,
+ },
+ sectionTitle: {
+ color: colors.black,
+ ...fonts.fontPrimaryBold,
+ marginBottom: 10,
+ paddingLeft: 15,
+ },
+ sectionTitleSmall: {
+ color: colors.primaryDark,
+ ...fonts.fontPrimarySemiBold,
+ marginBottom: 10,
+ paddingLeft: 15,
+ },
+ flatList: {
+ marginLeft: 15,
+ marginRight: 15,
+ },
+});
+
+const UsersAvatarListComponent = ({
+ title,
+ members,
+ noMembersMessage,
+ containerStyle,
+ smallTitle,
+ navigation,
+ loadMore,
+ authUser,
+}: Props) => (
+
+
+ {title}
+
+
+ {noMembersMessage &&
+ !members.length && (
+
+
+
+ )}
+
+ loadMore()}
+ onEndReachedThreshold={0.4}
+ renderItem={({ item }) => (
+ {
+ navigation.navigate(
+ authUser.login === item.login ? 'AuthProfile' : 'Profile',
+ {
+ user: item,
+ }
+ );
+ }}
+ underlayColor="transparent"
+ style={styles.avatarContainer}
+ >
+
+
+ )}
+ keyExtractor={item => item.id}
+ horizontal
+ />
+
+);
+
+export const UsersAvatarList = connect(mapStateToProps)(
+ UsersAvatarListComponent
+);
diff --git a/src/issue/screens/issue-settings.screen.js b/src/issue/screens/issue-settings.screen.js
index b26d13b71..235cb1f3d 100644
--- a/src/issue/screens/issue-settings.screen.js
+++ b/src/issue/screens/issue-settings.screen.js
@@ -172,7 +172,7 @@ class IssueSettings extends Component {
noItemsMessage={translate('issue.settings.noneMessage', locale)}
title={translate('issue.settings.labelsTitle', locale)}
>
- {issue.labels.map(item =>
+ {issue.labels.map(item => (
- )}
+ ))}
- {issue.assignees.map(item =>
+ {issue.assignees.map(item => (
- )}
+ ))}
@@ -263,7 +263,7 @@ class IssueSettings extends Component {
onPress={this.showLockIssueActionSheet}
/>
- {!isMerged &&
+ {!isMerged && (
}
+ />
+ )}
diff --git a/src/issue/screens/issue.screen.js b/src/issue/screens/issue.screen.js
index b86113473..95be1a1c2 100644
--- a/src/issue/screens/issue.screen.js
+++ b/src/issue/screens/issue.screen.js
@@ -333,44 +333,46 @@ class Issue extends Component {
return (
- {isShowLoadingContainer &&
- }
+ {isShowLoadingContainer && (
+
+ )}
{!isPendingComments &&
!isPendingIssue &&
- issue &&
-
- {
- this.commentsList = ref;
- }}
- refreshing={isLoadingData}
- onRefresh={this.getIssueInformation}
- contentContainerStyle={{ flexGrow: 1 }}
- ListHeaderComponent={this.renderHeader}
- removeClippedSubviews={false}
- data={fullComments}
- keyExtractor={this.keyExtractor}
- renderItem={this.renderItem}
- />
-
-
- }
+ issue && (
+
+ {
+ this.commentsList = ref;
+ }}
+ refreshing={isLoadingData}
+ onRefresh={this.getIssueInformation}
+ contentContainerStyle={{ flexGrow: 1 }}
+ ListHeaderComponent={this.renderHeader}
+ removeClippedSubviews={false}
+ data={fullComments}
+ keyExtractor={this.keyExtractor}
+ renderItem={this.renderItem}
+ />
+
+
+
+ )}
{
diff --git a/src/issue/screens/new-issue.screen.js b/src/issue/screens/new-issue.screen.js
index 00af1f90e..5a4a501da 100644
--- a/src/issue/screens/new-issue.screen.js
+++ b/src/issue/screens/new-issue.screen.js
@@ -78,11 +78,9 @@ class NewIssue extends Component {
const owner = repository.owner.login;
if (issueTitle === '') {
- Alert.alert(
- translate('issue.newIssue.missingTitleAlert', locale),
- null,
- [{ text: translate('common.ok', locale) }]
- );
+ Alert.alert(translate('issue.newIssue.missingTitleAlert', locale), null, [
+ { text: translate('common.ok', locale) },
+ ]);
} else {
submitNewIssue(owner, repoName, issueTitle, issueComment).then(issue => {
navigation.navigate('Issue', {
@@ -135,9 +133,7 @@ class NewIssue extends Component {
/>
-
+
({
- unread: state.notifications.unread,
- participating: state.notifications.participating,
- all: state.notifications.all,
- issue: state.issue.issue,
- locale: state.auth.locale,
- isPendingUnread: state.notifications.isPendingUnread,
- isPendingParticipating: state.notifications.isPendingParticipating,
- isPendingAll: state.notifications.isPendingAll,
- isPendingMarkAllNotificationsAsRead:
- state.notifications.isPendingMarkAllNotificationsAsRead,
-});
+} from '../index'; */
+
+const mapStateToProps = state => {
+ const { entities: { orgs, repos, users, notifications } } = state;
-const mapDispatchToProps = dispatch =>
- bindActionCreators(
+ const unreadPagination = getPaginationFromState(
+ state,
+ 'ACTIVITY_GET_NOTIFICATIONS',
+ 'false-false',
{
- getUnreadNotifications,
- getParticipatingNotifications,
- getAllNotifications,
- markAsRead,
- markRepoAsRead,
- getNotificationsCount,
- markAllNotificationsAsRead,
- },
- dispatch
+ ids: [],
+ isFetching: true,
+ }
+ );
+ const unread = unreadPagination.ids.map(id => notifications[id]);
+
+ const participatingPagination = getPaginationFromState(
+ state,
+ 'ACTIVITY_GET_NOTIFICATIONS',
+ 'true-false',
+ {
+ ids: [],
+ isFetching: true,
+ }
+ );
+ const participating = participatingPagination.ids.map(
+ id => notifications[id]
);
+ const allPagination = getPaginationFromState(
+ state,
+ 'ACTIVITY_GET_NOTIFICATIONS',
+ 'false-true',
+ {
+ ids: [],
+ isFetching: true,
+ }
+ );
+ const all = allPagination.ids.map(id => notifications[id]);
+
+ return {
+ unread,
+ unreadPagination,
+ participating,
+ participatingPagination,
+ all,
+ allPagination,
+ issue: state.issue.issue, // ?
+ locale: state.auth.locale,
+ repos,
+ users,
+ orgs,
+ // TODO: Remove me
+ isPendingMarkAllNotificationsAsRead:
+ state.notifications.isPendingMarkAllNotificationsAsRead,
+ };
+};
+
const styles = StyleSheet.create({
buttonGroupWrapper: {
backgroundColor: colors.greyLight,
@@ -134,24 +158,36 @@ const styles = StyleSheet.create({
},
});
+const NotificationsType = {
+ UNREAD: 0,
+ PARTICIPATING: 1,
+ ALL: 2,
+};
+
class Notifications extends Component {
props: {
- getUnreadNotifications: Function,
- getParticipatingNotifications: Function,
- getAllNotifications: Function,
- markAsRead: Function,
- markRepoAsRead: Function,
+ getNotifications: Function,
getNotificationsCount: Function,
+ markThreadAsRead: Function,
+ markRepoAsRead: Function,
markAllNotificationsAsRead: Function,
+ // Arrays holding notifications
unread: Array,
participating: Array,
all: Array,
+ // Their paginations
+ unreadPagination: Array,
+ participatingPagination: Array,
+ allPagination: Array,
+
locale: string,
- isPendingUnread: boolean,
- isPendingParticipating: boolean,
- isPendingAll: boolean,
- isPendingMarkAllNotificationsAsRead: boolean,
navigation: Object,
+ repos: Array,
+ users: Array,
+ orgs: Array,
+
+ // TODO: get rid of those
+ isPendingMarkAllNotificationsAsRead: boolean,
};
constructor() {
@@ -171,8 +207,9 @@ class Notifications extends Component {
this.getNotifications();
}
+ // TODO: clean me
componentWillReceiveProps(nextProps) {
- const pendingType = this.getPendingType();
+ const paginationName = this.getPaginationName();
if (
!nextProps.isPendingMarkAllNotificationsAsRead &&
@@ -182,53 +219,65 @@ class Notifications extends Component {
this.getNotificationsForCurrentType()();
}
- if (!nextProps[pendingType] && this.props[pendingType]) {
- this.props.getNotificationsCount();
+ if (
+ !nextProps[paginationName].isFetching &&
+ this.props[paginationName].isFetching
+ ) {
+ this.props.getNotificationsCount(false, false);
}
}
+ // OK
getImage(repoName) {
+ const { repos, users, orgs } = this.props;
const notificationForRepo = this.notifications().find(
- notification => notification.repository.full_name === repoName
+ notification => notification.repo === repoName
);
- return notificationForRepo.repository.owner.avatar_url;
+ const repository = repos[notificationForRepo.repo];
+
+ return repository.userOwner
+ ? users[repository.userOwner].avatarUrl
+ : orgs[repository.orgOwner].avatarUrl;
}
+ // OK
getNotifications() {
- this.props.getUnreadNotifications();
- this.props.getParticipatingNotifications();
- this.props.getAllNotifications();
- this.props.getNotificationsCount();
+ this.props.getNotifications(false, false); // unread
+ this.props.getNotifications(true, false); // participating
+ this.props.getNotifications(false, true); // all
}
+ // OK
getNotificationsForCurrentType() {
- const {
- getUnreadNotifications,
- getParticipatingNotifications,
- getAllNotifications,
- } = this.props;
const { type } = this.state;
+ const { getNotifications } = this.props;
switch (type) {
- case 0:
- return getUnreadNotifications;
- case 1:
- return getParticipatingNotifications;
- case 2:
- return getAllNotifications;
+ case NotificationsType.UNREAD:
+ return () =>
+ getNotifications(false, false, {
+ forceRefresh: true,
+ });
+ case NotificationsType.PARTICIPATING:
+ return () =>
+ getNotifications(true, false, {
+ forceRefresh: true,
+ });
+ case NotificationsType.ALL:
+ return () =>
+ getNotifications(false, true, {
+ forceRefresh: true,
+ });
default:
return null;
}
}
+ // OK
getSortedRepos = () => {
const repositories = [
- ...new Set(
- this.notifications().map(
- notification => notification.repository.full_name
- )
- ),
+ ...new Set(this.notifications().map(notification => notification.repo)),
];
return repositories.sort((a, b) => {
@@ -236,21 +285,23 @@ class Notifications extends Component {
});
};
- getPendingType = () => {
+ // OK
+ getPaginationName = () => {
const { type } = this.state;
switch (type) {
- case 0:
- return 'isPendingUnread';
- case 1:
- return 'isPendingParticipating';
- case 2:
- return 'isPendingAll';
+ case NotificationsType.UNREAD:
+ return 'unreadPagination';
+ case NotificationsType.PARTICIPATING:
+ return 'participatingPagination';
+ case NotificationsType.ALL:
+ return 'allPagination';
default:
return null;
}
};
+ // OK
navigateToRepo = fullName => {
const { navigation } = this.props;
@@ -259,57 +310,62 @@ class Notifications extends Component {
});
};
+ // OK
saveContentBlockHeight = e => {
const { height } = e.nativeEvent.layout;
this.setState({ contentBlockHeight: height });
};
+ // OK
keyExtractor = (item, index) => {
return index;
};
+ // TODO: clean me
isLoading() {
const {
unread,
+ unreadPagination,
participating,
+ participatingPagination,
all,
- isPendingUnread,
- isPendingParticipating,
- isPendingAll,
+ allPagination,
} = this.props;
const { type } = this.state;
switch (type) {
- case 0:
- return unread && isPendingUnread;
- case 1:
- return participating && isPendingParticipating;
- case 2:
- return all && isPendingAll;
+ case NotificationsType.UNREAD:
+ return unread && unreadPagination.isFetching;
+ case NotificationsType.PARTICIPATING:
+ return participating && participatingPagination.isFetching;
+ case NotificationsType.ALL:
+ return all && allPagination.isFetching;
default:
return false;
}
}
+ // OK
notifications() {
const { unread, participating, all } = this.props;
const { type } = this.state;
switch (type) {
- case 0:
+ case NotificationsType.UNREAD:
return unread;
- case 1:
+ case NotificationsType.PARTICIPATING:
return participating;
- case 2:
+ case NotificationsType.ALL:
return all;
default:
return [];
}
}
+ // OK
switchType(selectedType) {
- const { unread, participating, all } = this.props;
+ const { unread, participating, all, getNotifications } = this.props;
if (this.state.type !== selectedType) {
this.setState({
@@ -317,12 +373,15 @@ class Notifications extends Component {
});
}
- if (selectedType === 0 && unread.length === 0) {
- this.props.getUnreadNotifications();
- } else if (selectedType === 1 && participating.length === 0) {
- this.props.getParticipatingNotifications();
- } else if (selectedType === 2 && all.length === 0) {
- this.props.getAllNotifications();
+ if (selectedType === NotificationsType.UNREAD && unread.length === 0) {
+ getNotifications(false, false);
+ } else if (
+ selectedType === NotificationsType.PARTICIPATING &&
+ participating.length === 0
+ ) {
+ getNotifications(true, false);
+ } else if (selectedType === NotificationsType.ALL && all.length === 0) {
+ getNotifications(false, true);
}
if (this.notifications().length > 0) {
@@ -334,17 +393,19 @@ class Notifications extends Component {
}
}
+ // OK
navigateToThread(notification) {
- const { markAsRead, navigation } = this.props;
+ const { markThreadAsRead, navigation } = this.props;
- markAsRead(notification.id);
+ markThreadAsRead(notification.id);
navigation.navigate('Issue', {
- issueURL: notification.subject.url.replace(/pulls\/(\d+)$/, 'issues/$1'),
- isPR: notification.subject.type === 'PullRequest',
+ issueURL: notification.link.replace(/pulls\/(\d+)$/, 'issues/$1'),
+ isPR: notification.type === 'PullRequest',
locale: this.props.locale,
});
}
+ // OK
navigateToRepo = fullName => {
const { navigation } = this.props;
@@ -353,15 +414,16 @@ class Notifications extends Component {
});
};
+ // TODO: Implement & use new api calls
renderItem = ({ item }) => {
const {
- markAsRead,
+ markThreadAsRead,
markRepoAsRead,
markAllNotificationsAsRead,
} = this.props;
const { type } = this.state;
const notifications = this.notifications().filter(
- notification => notification.repository.full_name === item
+ notification => notification.repo === item
);
const isFirstItem = this.getSortedRepos().indexOf(item) === 0;
const isFirstTab = type === 0;
@@ -413,7 +475,7 @@ class Notifications extends Component {
markAsRead(notificationID)}
+ iconAction={notificationID => markThreadAsRead(notificationID)}
navigationAction={notify => this.navigateToThread(notify)}
navigation={this.props.navigation}
/>
@@ -506,6 +568,8 @@ class Notifications extends Component {
}
}
-export const NotificationsScreen = connect(mapStateToProps, mapDispatchToProps)(
- Notifications
-);
+export const NotificationsScreen = connect(mapStateToProps, {
+ getNotifications: client.activity.getNotifications,
+ markThreadAsRead: client.activity.markNotificationThreadAsRead,
+ getNotificationsCount: client.activity.getNotificationsCount,
+})(Notifications);
diff --git a/src/organization/index.js b/src/organization/index.js
index ec9c14b2a..c9de5c34d 100644
--- a/src/organization/index.js
+++ b/src/organization/index.js
@@ -1,6 +1 @@
-export * from './organization.action';
-export * from './organization.reducer';
-export * from './organization.constants';
-export * from './organization.selectors';
-
export * from './screens';
diff --git a/src/organization/organization.action.js b/src/organization/organization.action.js
deleted file mode 100644
index 19bd0eaae..000000000
--- a/src/organization/organization.action.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { createAction } from 'redux-actions';
-
-import { fetchOrg, fetchOrgMembers, v3 } from 'api';
-import {
- GET_ORG,
- GET_ORG_LOADING,
- GET_ORG_ERROR,
- GET_ORG_REPOS,
- GET_ORG_REPOS_LOADING,
- GET_ORG_REPOS_ERROR,
- GET_ORG_MEMBERS,
- GET_ORG_MEMBERS_LOADING,
- GET_ORG_MEMBERS_ERROR,
-} from './organization.constants';
-
-export const getOrg = createAction(GET_ORG);
-export const getOrgLoading = createAction(GET_ORG_LOADING);
-export const getOrgError = createAction(GET_ORG_ERROR);
-export const getOrgRepos = createAction(GET_ORG_REPOS);
-export const getOrgReposLoading = createAction(GET_ORG_REPOS_LOADING);
-export const getOrgReposError = createAction(GET_ORG_REPOS_ERROR);
-export const getOrgMembers = createAction(GET_ORG_MEMBERS);
-export const getOrgMembersLoading = createAction(GET_ORG_MEMBERS_LOADING);
-export const getOrgMembersError = createAction(GET_ORG_MEMBERS_ERROR);
-
-export const fetchOrganizations = orgName => (dispatch, getState) => {
- // use a selector here
- const accessToken = getState().auth.accessToken;
-
- dispatch(getOrgLoading(true));
- dispatch(getOrgError(''));
-
- return fetchOrg(orgName, accessToken)
- .then(data => {
- dispatch(getOrgLoading(false));
- dispatch(getOrg(data));
- })
- .catch(error => {
- dispatch(getOrgLoading(false));
- dispatch(getOrgError(error));
- });
-};
-
-export const fetchOrganizationRepos = url => (dispatch, getState) => {
- const accessToken = getState().auth.accessToken;
-
- dispatch(getOrgReposLoading(true));
- dispatch(getOrgReposError(''));
-
- v3
- .getJson(url, accessToken)
- .then(data => {
- dispatch(getOrgReposLoading(false));
- dispatch(getOrgRepos(data));
- })
- .catch(error => {
- dispatch(getOrgReposLoading(false));
- dispatch(getOrgReposError(error));
- });
-};
-
-export const fetchOrganizationMembers = orgName => (dispatch, getState) => {
- const accessToken = getState().auth.accessToken;
-
- dispatch(getOrgMembersLoading(true));
- dispatch(getOrgMembersError(''));
-
- return fetchOrgMembers(orgName, accessToken)
- .then(data => {
- dispatch(getOrgMembersLoading(false));
- dispatch(getOrgMembers(data));
- })
- .catch(error => {
- dispatch(getOrgMembersLoading(false));
- dispatch(getOrgMembersError(error));
- });
-};
diff --git a/src/organization/organization.constants.js b/src/organization/organization.constants.js
deleted file mode 100644
index 76e3d0e2c..000000000
--- a/src/organization/organization.constants.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export const GET_ORG = 'GET_ORG';
-export const GET_ORG_LOADING = 'GET_ORG_LOADING';
-export const GET_ORG_ERROR = 'GET_ORG_ERROR';
-export const GET_ORG_REPOS = 'GET_ORG_REPOS';
-export const GET_ORG_REPOS_LOADING = 'GET_ORG_REPOS_LOADING';
-export const GET_ORG_REPOS_ERROR = 'GET_ORG_REPOS_ERROR';
-export const GET_ORG_MEMBERS = 'GET_ORG_MEMBERS';
-export const GET_ORG_MEMBERS_LOADING = 'GET_ORG_MEMBERS_LOADING';
-export const GET_ORG_MEMBERS_ERROR = 'GET_ORG_MEMBERS_ERROR';
diff --git a/src/organization/organization.reducer.js b/src/organization/organization.reducer.js
deleted file mode 100644
index 42e21db6c..000000000
--- a/src/organization/organization.reducer.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { handleActions } from 'redux-actions';
-
-import {
- GET_ORG,
- GET_ORG_LOADING,
- GET_ORG_ERROR,
- GET_ORG_REPOS,
- GET_ORG_REPOS_LOADING,
- GET_ORG_REPOS_ERROR,
- GET_ORG_MEMBERS,
- GET_ORG_MEMBERS_LOADING,
- GET_ORG_MEMBERS_ERROR,
-} from './organization.constants';
-
-const initialState = {
- organization: {},
- repositories: [],
- members: [],
- isPendingOrg: false,
- isPendingRepos: false,
- isPendingMembers: false,
- organizationError: '',
- organizationRepositoriesError: '',
- organizationMembersError: '',
-};
-
-export const organizationReducer = handleActions(
- {
- [GET_ORG]: (state, { payload }) => {
- return {
- ...state,
- organization: payload,
- };
- },
- [GET_ORG_LOADING]: (state, { payload }) => {
- return {
- ...state,
- isPendingOrg: payload,
- };
- },
- [GET_ORG_ERROR]: (state, { payload }) => {
- return {
- ...state,
- organizationError: payload,
- };
- },
- [GET_ORG_REPOS]: (state, { payload }) => {
- return {
- ...state,
- repositories: payload,
- };
- },
- [GET_ORG_REPOS_LOADING]: (state, { payload }) => {
- return {
- ...state,
- isPendingRepos: payload,
- };
- },
- [GET_ORG_REPOS_ERROR]: (state, { payload }) => {
- return {
- ...state,
- organizationRepositoriesError: payload,
- };
- },
- [GET_ORG_MEMBERS]: (state, { payload }) => {
- return {
- ...state,
- members: payload,
- };
- },
- [GET_ORG_MEMBERS_LOADING]: (state, { payload }) => {
- return {
- ...state,
- isPendingMembers: payload,
- };
- },
- [GET_ORG_MEMBERS_ERROR]: (state, { payload }) => {
- return {
- ...state,
- organizationMembersError: payload,
- };
- },
- },
- initialState
-);
diff --git a/src/organization/organization.selectors.js b/src/organization/organization.selectors.js
deleted file mode 100644
index a8f170a29..000000000
--- a/src/organization/organization.selectors.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { createSelector } from 'reselect';
-
-const getOrganizationFromStore = state => state.organization;
-
-export const getOrganization = createSelector(
- getOrganizationFromStore,
- organization => organization.organization || {}
-);
-
-export const getOrganizationRepositories = createSelector(
- getOrganizationFromStore,
- organization => organization.repositories || []
-);
-
-export const getOrganizationMembers = createSelector(
- getOrganizationFromStore,
- organization => organization.members || []
-);
-
-export const getOrganizationIsPendingOrg = createSelector(
- getOrganizationFromStore,
- organization => organization.isPendingOrg || false
-);
-
-export const getOrganizationIsPendingRepos = createSelector(
- getOrganizationFromStore,
- organization => organization.isPendingRepos || false
-);
-
-export const getOrganizationIsPendingMembers = createSelector(
- getOrganizationFromStore,
- organization => organization.isPendingMembers || false
-);
-
-export const getOrganizationError = createSelector(
- getOrganizationFromStore,
- organization => organization.organizationError || ''
-);
-
-export const getOrganizationRepositoriesError = createSelector(
- getOrganizationFromStore,
- organization => organization.organizationRepositoriesError || ''
-);
-
-export const getOrganizationMembersError = createSelector(
- getOrganizationFromStore,
- organization => organization.organizationMembersError || ''
-);
diff --git a/src/organization/screens/index.js b/src/organization/screens/index.js
index 544587740..15fe341fc 100644
--- a/src/organization/screens/index.js
+++ b/src/organization/screens/index.js
@@ -1 +1,2 @@
export * from './organization-profile.screen';
+export * from './organization-repository-list.screen';
diff --git a/src/organization/screens/organization-profile.screen.js b/src/organization/screens/organization-profile.screen.js
index f82931cd0..76db32352 100644
--- a/src/organization/screens/organization-profile.screen.js
+++ b/src/organization/screens/organization-profile.screen.js
@@ -1,51 +1,20 @@
import React, { Component } from 'react';
-import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { StyleSheet, RefreshControl } from 'react-native';
import { ListItem } from 'react-native-elements';
-import { createStructuredSelector } from 'reselect';
import ActionSheet from 'react-native-actionsheet';
-import { getAuthLocale } from 'auth';
import {
ViewContainer,
- UserProfile,
- LoadingMembersList,
- MembersList,
+ OrgProfile,
SectionList,
ParallaxScroll,
+ LoadingMembersList,
EntityInfo,
+ UsersAvatarList,
} from 'components';
import { emojifyText, translate, openURLInView } from 'utils';
import { colors, fonts } from 'config';
-import {
- // actions
- fetchOrganizations,
- fetchOrganizationMembers,
- // Selectors
- getOrganization,
- getOrganizationRepositories,
- getOrganizationMembers,
- getOrganizationIsPendingOrg,
- getOrganizationIsPendingRepos,
- getOrganizationIsPendingMembers,
-} from '../index';
-
-const selectors = createStructuredSelector({
- organization: getOrganization,
- repositories: getOrganizationRepositories,
- members: getOrganizationMembers,
- isPendingOrg: getOrganizationIsPendingOrg,
- isPendingRepos: getOrganizationIsPendingRepos,
- isPendingMembers: getOrganizationIsPendingMembers,
- locale: getAuthLocale,
-});
-
-const actionCreators = {
- fetchOrganizations,
- fetchOrganizationMembers,
-};
-
-const actions = dispatch => bindActionCreators(actionCreators, dispatch);
+import { client } from 'api/rest';
const styles = StyleSheet.create({
listTitle: {
@@ -58,19 +27,52 @@ const styles = StyleSheet.create({
},
});
+const loadData = ({ orgId, getOrgById, getOrgMembers }) => {
+ getOrgById(orgId, { requiredFields: ['name'] });
+ getOrgMembers(orgId);
+};
+
+const mapStateToProps = (state, ownProps) => {
+ // TODO: This should be normalized to params.id
+ const orgId = ownProps.navigation.state.params.organization.login.toLowerCase();
+
+ const {
+ auth: { locale },
+ pagination: { ORGS_GET_MEMBERS },
+ entities: { orgs, users },
+ } = state;
+
+ const membersPagination = ORGS_GET_MEMBERS[orgId] || {
+ ids: [],
+ isFetching: true,
+ };
+ const members = membersPagination.ids.map(id => users[id]);
+
+ if (ownProps.navigation.state.params.refreshing) {
+ // We were asked to refresh and we're here, so we're done.
+ ownProps.navigation.setParams({ refreshing: false });
+ }
+
+ return {
+ orgId,
+ members,
+ membersPagination,
+ // normalized attribute
+ entity: orgs[orgId],
+ locale,
+ };
+};
+
class OrganizationProfile extends Component {
props: {
- fetchOrganizations: Function,
- // getOrgReposByDispatch: Function,
- fetchOrganizationMembers: Function,
- organization: Object,
- // repositories: Array,
+ orgId: String,
+ entity: Object,
members: Array,
- isPendingOrg: boolean,
- // isPendingRepos: boolean,
- isPendingMembers: boolean,
+ membersPagination: Object,
navigation: Object,
locale: string,
+ getOrgMembers: Function,
+ getOrgById: Function,
};
state: {
@@ -84,23 +86,29 @@ class OrganizationProfile extends Component {
};
}
- componentDidMount() {
- const organization = this.props.navigation.state.params.organization;
+ componentWillMount() {
+ loadData(this.props);
+ }
- this.props.fetchOrganizations(organization.login);
- this.props.fetchOrganizationMembers(organization.login);
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.orgId !== this.props.orgId) {
+ loadData(nextProps);
+ }
}
- getOrgData = () => {
- const organization = this.props.navigation.state.params.organization;
+ refreshData = () => {
+ const { navigation, getOrgMembers, getOrgById } = this.props;
+ const orgId = navigation.state.params.organization.login;
+
+ navigation.setParams({ refreshing: true });
+ getOrgById(orgId, { forceRefresh: true });
+ getOrgMembers(orgId, { forceRefresh: true });
+ };
+
+ loadMoreMembers = () => {
+ const { orgId, getOrgMembers } = this.props;
- this.setState({ refreshing: true });
- Promise.all([
- this.props.fetchOrganizations(organization.login),
- this.props.fetchOrganizationMembers(organization.login),
- ]).then(() => {
- this.setState({ refreshing: false });
- });
+ getOrgMembers(orgId, { loadMore: true });
};
showMenuActionSheet = () => {
@@ -109,79 +117,77 @@ class OrganizationProfile extends Component {
handleActionSheetPress = index => {
if (index === 0) {
- openURLInView(this.props.organization.html_url);
+ openURLInView(this.props.entity._entityUrl);
}
};
render() {
const {
- organization,
+ orgId,
+ entity,
members,
- isPendingOrg,
- isPendingMembers,
+ membersPagination,
navigation,
locale,
} = this.props;
const { refreshing } = this.state;
- const initialOrganization = this.props.navigation.state.params.organization;
const organizationActions = [translate('common.openInBrowser', locale)];
return (
- }
+ renderContent={() => (
+
+ )}
refreshControl={
this.refreshData()}
refreshing={refreshing}
/>
}
- stickyTitle={organization.name}
+ stickyTitle={orgId}
navigateBack
navigation={navigation}
showMenu
menuAction={() => this.showMenuActionSheet()}
>
- {isPendingMembers &&
- }
+ {membersPagination.isFetching &&
+ !membersPagination.pageCount && (
+
+ )}
- {!isPendingMembers &&
- 0) && (
+ }
-
- {!!organization.description &&
- organization.description !== '' &&
-
-
- }
-
- {!isPendingOrg &&
+ />
+ )}
+
+ {entity &&
+ !!entity.description &&
+ entity.description !== '' && (
+
+
+
+ )}
+ {entity && (
}
+ />
+ )}
+ `q=${keyword}+user:${orgId}+fork:true&per_page=8`;
+
+class OrganizationRepositoryList extends Component {
+ props: {
+ searchRepos: Function,
+ getOrgRepos: Function,
+ orgId: String,
+
+ searchedResults: Array,
+ repositories: Array,
+
+ repositoriesPagination: Object,
+ searchedResultsPagination: Object,
+
+ authUser: Object,
+
+ navigation: Object,
+ };
+
+ render() {
+ const {
+ orgId,
+ authUser,
+ navigation,
+ getOrgRepos,
+ searchRepos,
+ repositories,
+ repositoriesPagination,
+ searchedResults,
+ searchedResultsPagination,
+ } = this.props;
+
+ return (
+
+ getOrgRepos(orgId, { loadMore })}
+ loadSearchResults={(keyword, loadMore = false) => {
+ navigation.setParams({ searchedKeyword: keyword });
+
+ return searchRepos(getQueryString(keyword, orgId), { loadMore });
+ }}
+ repositories={repositories}
+ repositoriesPagination={repositoriesPagination}
+ searchResults={searchedResults}
+ searchResultsPagination={searchedResultsPagination}
+ />
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ const orgId = ownProps.navigation.state.params.orgId;
+
+ const {
+ auth: { user },
+ pagination: { ORGS_GET_REPOS, SEARCH_GET_REPOS },
+ entities: { orgs, repos },
+ } = state;
+
+ const repositoriesPagination = ORGS_GET_REPOS[orgId] || { ids: [] };
+ const repositories = repositoriesPagination.ids.map(id => repos[id]);
+
+ const searchedKeyword = ownProps.navigation.state.params.searchedKeyword;
+ const queryString = getQueryString(searchedKeyword, orgId);
+
+ const searchedResultsPagination =
+ searchedKeyword && SEARCH_GET_REPOS[queryString]
+ ? SEARCH_GET_REPOS[queryString]
+ : { ids: [] };
+ const searchedResults = searchedResultsPagination.ids.map(id => repos[id]);
+
+ return {
+ orgId,
+ authUser: user,
+ repositories,
+ repositoriesPagination,
+ searchedResults,
+ searchedResultsPagination,
+ org: orgs[orgId],
+ };
+};
+
+export const OrganizationRepositoryListScreen = connect(mapStateToProps, {
+ getOrgRepos: client.orgs.getRepos,
+ searchRepos: client.search.getRepos,
+})(OrganizationRepositoryList);
diff --git a/src/repository/screens/issue-list.screen.js b/src/repository/screens/issue-list.screen.js
index 268d10770..5df7bee6c 100644
--- a/src/repository/screens/issue-list.screen.js
+++ b/src/repository/screens/issue-list.screen.js
@@ -269,13 +269,9 @@ class IssueList extends Component {
searchType === 0 && (
)}
@@ -284,13 +280,9 @@ class IssueList extends Component {
searchType === 1 && (
)}
diff --git a/src/repository/screens/pull-list.screen.js b/src/repository/screens/pull-list.screen.js
index c07d654b1..4fffae8d0 100644
--- a/src/repository/screens/pull-list.screen.js
+++ b/src/repository/screens/pull-list.screen.js
@@ -244,13 +244,9 @@ class PullList extends Component {
searchType === 0 && (
)}
@@ -259,13 +255,9 @@ class PullList extends Component {
searchType === 1 && (
)}
diff --git a/src/repository/screens/read-me.screen.js b/src/repository/screens/read-me.screen.js
index 708f74573..d8b584d20 100644
--- a/src/repository/screens/read-me.screen.js
+++ b/src/repository/screens/read-me.screen.js
@@ -100,22 +100,25 @@ class ReadMe extends Component {
return (
- {isPendingReadMe &&
- }
+ {isPendingReadMe && (
+
+ )}
{!isPendingReadMe &&
- !noReadMe &&
- }
+ !noReadMe && (
+
+ )}
{!isPendingReadMe &&
- noReadMe &&
-
-
- {translate('repository.readMe.noReadMeFound', locale)}
-
- }
+ noReadMe && (
+
+
+ {translate('repository.readMe.noReadMeFound', locale)}
+
+
+ )}
{
diff --git a/src/repository/screens/repository.screen.js b/src/repository/screens/repository.screen.js
index e703b49b5..59919957d 100644
--- a/src/repository/screens/repository.screen.js
+++ b/src/repository/screens/repository.screen.js
@@ -280,32 +280,35 @@ class Repository extends Component {
>
{initalRepository &&
!initalRepository.owner &&
- isPendingRepository &&
-
-
- }
+ isPendingRepository && (
+
+
+
+ )}
{!(initalRepository && initalRepository.owner) &&
(repository && repository.owner) &&
- !isPendingRepository &&
-
-
- }
+ !isPendingRepository && (
+
+
+
+ )}
{initalRepository &&
- initalRepository.owner &&
-
-
- }
+ initalRepository.owner && (
+
+
+
+ )}
{(isPendingRepository || isPendingContributors) && (
)}
- {!isPendingContributors &&
+ {!isPendingContributors && (
}
+ />
+ )}
- {showReadMe &&
+ {showReadMe && (
}
+ />
+ )}
{!repository.fork &&
- repository.has_issues &&
- 0
- ? translate('repository.main.viewAllButton', locale)
- : translate('repository.main.newIssueButton', locale)
- }
- buttonAction={() => {
- if (pureIssues.length > 0) {
- navigation.navigate('IssueList', {
- title: translate('repository.issueList.title', locale),
- type: 'issue',
- issues: pureIssues,
- });
- } else {
- navigation.navigate('NewIssue', {
- title: translate('issue.newIssue.title', locale),
- });
+ repository.has_issues && (
+
- {openIssues
- .slice(0, 3)
- .map(item =>
-
- )}
- }
+ showButton
+ buttonTitle={
+ pureIssues.length > 0
+ ? translate('repository.main.viewAllButton', locale)
+ : translate('repository.main.newIssueButton', locale)
+ }
+ buttonAction={() => {
+ if (pureIssues.length > 0) {
+ navigation.navigate('IssueList', {
+ title: translate('repository.issueList.title', locale),
+ type: 'issue',
+ issues: pureIssues,
+ });
+ } else {
+ navigation.navigate('NewIssue', {
+ title: translate('issue.newIssue.title', locale),
+ });
+ }
+ }}
+ >
+ {openIssues
+ .slice(0, 3)
+ .map(item => (
+
+ ))}
+
+ )}
+ renderContent={() => (
}
+ />
+ )}
refreshControl={
- {isPending &&
+ {isPending && (
}
+ />
+ )}
{!isPending &&
- initialUser.login === user.login &&
-
- {!!user.bio &&
- user.bio !== '' &&
-
-
- }
+ initialUser.login === user.login && (
+
+ {!!user.bio &&
+ user.bio !== '' && (
+
+
+
+ )}
-
+
-
- {orgs.map(item =>
-
- )}
-
- }
+
+ {orgs.map(item => (
+
+ ))}
+
+
+ )}
args.join('-');
+
+export const actionNameForCall = (namespace, method, prefix = '') => {
+ const upperCased = `${namespace}_${method}`
+ .replace(/([A-Z])/g, '_$1')
+ .toUpperCase();
+
+ return `${prefix}${upperCased}`;
+};
+
+export const splitArgs = (fn, args) => {
+ const declaredArgsCount = fn.length;
+ const isExtraArgAvailable = args.length === declaredArgsCount;
+
+ return {
+ pureArgs: isExtraArgAvailable ? args.slice(0, -1) : args,
+ extraArg: isExtraArgAvailable ? args[args.length - 1] : {},
+ };
+};
+
+// TODO: used mainly for developping this PR, but may come handy for #430
+export const displayError = error => {
+ Alert.alert('API Error', error);
+};
+
+export const getCountFromState = (state, name, key) =>
+ state.counters[name] ? state.counters[name][key] : 0;
+
+export const getPaginationFromState = (
+ state,
+ name,
+ key,
+ initialValue = { ids: [] }
+) =>
+ state.pagination[name] && state.pagination[name][key]
+ ? state.pagination[name][key]
+ : initialValue;
diff --git a/src/utils/index.js b/src/utils/index.js
index 3bdc96801..d5aca5b61 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -1,5 +1,7 @@
export * from './action-helper';
+export * from './schema-helper';
export * from './loading-animation';
export * from './text-helper';
export * from './method-helpers';
+export * from './api-helpers';
export * from './localization-helper';
diff --git a/src/utils/schema-helper.js b/src/utils/schema-helper.js
new file mode 100644
index 000000000..ede4c5d51
--- /dev/null
+++ b/src/utils/schema-helper.js
@@ -0,0 +1,10 @@
+import moment from 'moment/min/moment.min';
+
+export const toTimestamp = value => parseInt(moment(value).format('x'), 10);
+
+export const initSchema = () => ({
+ _isComplete: false, // entity not fully fetched yet
+ _isAuth: false, // entity doesn't belong to the auth user
+ _entityUrl: false, // The github url for the entity. To be used in openInBrowser()
+ _fetchedAt: toTimestamp(),
+});
diff --git a/yarn.lock b/yarn.lock
index 376e85c22..d54700b10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4236,10 +4236,6 @@ lodash.toarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
-lodash.uniqby@^4.7.0:
- version "4.7.0"
- resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
-
lodash@^3.5.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -4679,6 +4675,10 @@ normalize-path@^2.0.0, normalize-path@^2.0.1:
dependencies:
remove-trailing-separator "^1.0.1"
+normalizr@^3.2.3:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/normalizr/-/normalizr-3.2.4.tgz#16aafc540ca99dc1060ceaa1933556322eac4429"
+
npm-path@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe"
@@ -5541,9 +5541,9 @@ react-syntax-highlighter@^5.6.2:
highlight.js "~9.12.0"
lowlight "~1.9.1"
-react-test-renderer@16.0.0-alpha.6:
- version "16.0.0-alpha.6"
- resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0-alpha.6.tgz#c032def0dc8319cee39caa4e4373a60019cb3786"
+react-test-renderer@16.0.0-alpha.12:
+ version "16.0.0-alpha.12"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0-alpha.12.tgz#9e4cc5d8ce8bfca72778340de3e1454b9d6c0cc5"
dependencies:
fbjs "^0.8.9"
object-assign "^4.1.0"