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 && - -