Skip to content
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1b057fc
feat(api): fixlets
machour Oct 9, 2017
f0948d0
Merge branch 'master' into multi-client-redux-middleware
machour Oct 9, 2017
0f5746d
fix(browser): Fix openInBrowser url
machour Oct 9, 2017
b710e80
fix(build): add lodash
machour Oct 9, 2017
b52977a
refactor(api): Remove useless files for clarity sake & reducer
machour Oct 10, 2017
ae00beb
refactor: group and host screens under src/screens
machour Oct 10, 2017
de4ab42
refactor: group and host screens under src/screens
machour Oct 10, 2017
83e92b5
refactor: get rid of initialOrg :-O
machour Oct 10, 2017
a24f64e
feat: No more waiting screen, mock entity attributes instead
machour Oct 10, 2017
0ab34f2
refactor: split repository-list into a dumb component
machour Oct 10, 2017
29fd81f
feat(list): add load progress indicator, adjust list margin
machour Oct 11, 2017
a7cd9e1
Merge remote-tracking branch 'upstream/master' into multi-client-redu…
machour Oct 11, 2017
82495a6
refactor: Namespace the API client
machour Oct 11, 2017
455229d
chore: not need to disable no-shadow anymore
machour Oct 11, 2017
cc41686
refactor: enum style for actions
machour Oct 11, 2017
166d6a9
refactor: Use new middleware for paged events
machour Oct 11, 2017
57e2f74
fix: Cleaning up (mostly @lex111 remarks)
machour Oct 11, 2017
440e182
Merge remote-tracking branch 'upstream/master' into multi-client-redu…
machour Oct 11, 2017
9f58286
fix: Use ListFooterComponent instead of pushing a fake element
machour Oct 12, 2017
5e29560
refactor: Remove middleware and use a proxy instead
machour Oct 12, 2017
77e40a2
chore: import lodash entirely
machour Oct 13, 2017
9c07e82
refactor: clean up withReducers()
machour Oct 13, 2017
a54da4b
refactor: Add schema helpers and use milli-seconds timestamps everywhere
machour Oct 13, 2017
2304b4b
fix: pagination/action key should be made of all pureArgs
machour Oct 13, 2017
e7a87f1
feat: add two new decorators: withCounter() and withAuth()
machour Oct 13, 2017
4703a88
Merge remote-tracking branch 'upstream/master' into multi-client-redu…
machour Oct 15, 2017
da4e6b1
refactor: add helpers for decorators
machour Oct 15, 2017
8055862
feat: make withCounter() store its computed data in store.counters
machour Oct 15, 2017
df3515f
refactor: switch notifications to the new API
machour Oct 15, 2017
1d65a0a
fix: Some naming/code clean up using Alexey reviews
machour Oct 15, 2017
8c24f81
refactor: decorators -> proxies
machour Oct 15, 2017
cafa620
Merge remote-tracking branch 'upstream/master' into multi-client-redu…
machour Oct 15, 2017
9cbabee
refactor: Move counting logic in the provider. Add abstract methods
machour Oct 16, 2017
5e576db
refactor: merge createCountProxy into createDispatchProxy
machour Oct 16, 2017
c12429b
Merge remote-tracking branch 'upstream/master' into multi-client-redu…
machour Oct 16, 2017
5c1d82e
fix: Locale & Prettier related changes
machour Oct 16, 2017
10a24ca
refactor: get the proxied client directly from api/rest
machour Oct 16, 2017
e2e9ca3
style: some code clean up from Jouderian
machour Oct 16, 2017
ac036ec
refactor: simplify client.fetch() call
machour Oct 16, 2017
34d6d28
refactor: decorators-helpers => api-helpers. More clean up in proxy
machour Oct 16, 2017
82be98a
fix: Fix several bugs with schemas, remove dumps
machour Oct 16, 2017
40429ca
test: Implement all schemas tests
machour Oct 16, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion App.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ class App extends Component {
{
storage: AsyncStorage,
transforms: [encryptor],
blacklist: ['user'],
blacklist: [
'counters',
'entities',
'pagination',
'errorMessage',
'user',
],
},
() => {
this.setState({ rehydrated: true });
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions root.reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import {
LoginScreen,
WelcomeScreen,
AuthProfileScreen,
EventsScreen,
PrivacyPolicyScreen,
UserOptionsScreen,
EventsScreen,
} from 'auth';

// User
Expand All @@ -31,8 +31,10 @@ import {
FollowingListScreen,
} from 'user';

// Organization
import { OrganizationProfileScreen } from 'organization';
import {
OrganizationRepositoryListScreen,
OrganizationProfileScreen,
} from 'organization';

// Search
import { SearchScreen } from 'search';
Expand Down Expand Up @@ -61,6 +63,12 @@ import {
} from 'issue';

const sharedRoutes = {
OrgRepositoryList: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's better to use the full name here (OrganizationRepositoryList), because the other screens start with the Organization, not the Org (to support consistency)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely

screen: OrganizationRepositoryListScreen,
navigationOptions: ({ navigation }) => ({
title: navigation.state.params.title,
}),
},
RepositoryList: {
screen: RepositoryListScreen,
navigationOptions: ({ navigation }) => ({
Expand Down
15 changes: 15 additions & 0 deletions src/api/rest/actions/activity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createActionSet } from 'utils';

export const ACTIVITY_GET_EVENTS_RECEIVED = createActionSet(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using redux-actions instead of this createActionSet util?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it's done in this PR, actions are barely seeable already :

What would be the benefit from using redux-actions? (I'm not quiet familiar with it)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a PR that uses redux-actions #269

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redux Actions will reduce verbosity introduced by default by Redux. You can see the motivation here https://redux-actions.js.org/docs/introduction/Motivation.html

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the motivation here, but against master, not this PR :/

'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'
);
3 changes: 3 additions & 0 deletions src/api/rest/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './orgs';
export * from './search';
export * from './activity';
5 changes: 5 additions & 0 deletions src/api/rest/actions/orgs.js
Original file line number Diff line number Diff line change
@@ -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');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it used?

And for all these actions: can use a naming convention that was earlier?
ORGS_GET_BY_ID -> GET_ORGS_BY_ID - why ORGS and not ORG?
REPOS_BY_ORG -> GET_ORG_REPOS
MEMBERS_BY_ORG -> GET_ORG_MEMBERS

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I started this PR, I was trying to have the following namespace set up:

client.orgs.* //everything related to orgs operation, like
client.orgs.getById(orgId)
client.orgs.getMembers(orgId)

client.users.get() // gets the authenticated user

The goal is to namespace the API by entities. It seems to me more clear and well dispatched.

But connect() only supports plain functions, so I had to import { getById } from './orgs/';

I was thinking about renaming orgs.getById() to orgsGetById, users.get to usersGet, etc, so that it's more clear what is the real function we're calling

export const ORGS_GET_MEMBERS = createActionSet('ORGS_GET_MEMBERS');
3 changes: 3 additions & 0 deletions src/api/rest/actions/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createActionSet } from 'utils';

export const SEARCH_GET_REPOS = createActionSet('SEARCH_GET_REPOS');
60 changes: 60 additions & 0 deletions src/api/rest/providers/base/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export class Client {
Method = {
GET: 'GET',
HEAD: 'HEAD',
PUT: 'PUT',
DELETE: 'DELETE',
PATCH: 'PATCH',
POST: 'POST',
};

fetch = async (
url,
{
method = this.Method.GET,
schema = null,
normalizrKey = null,
headers = {},
},
params = {}
) => {
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.indexOf('?') !== -1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use here the method includes = finalUrl.includes('?')

? '&'
: '?'}per_page=${params.per_page}`;
}
}

if (finalUrl.indexOf(this.API_ROOT) === -1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same, in order do improve the readability, we can use includes

finalUrl = `${this.API_ROOT}${finalUrl}`;
}

const parameters = {
method,
headers: {
'Cache-Control': 'no-cache',
...this.authHeaders,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, where the this.authHeaders comes from? 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just noted that the child class(github/client.js) have this attribute. Maybe should be more clear include a default implementation here? maybe authHeaders = {}

...headers,
},
};

return fetch(finalUrl, parameters)
.then(response => {
// analyze headers for pagination, rates, etc
return {
response,
schema,
normalizrKey,
};
})
.catch(error => error);
};
}
1 change: 1 addition & 0 deletions src/api/rest/providers/base/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './client';
170 changes: 170 additions & 0 deletions src/api/rest/providers/github/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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 => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposal: I think that if pass the Link header instead of the entire response?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct for Github, but for Gitlab for example, next link information is contained in the JSON response. This is why I decided to pass 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);
};

/**
* The organizations endpoint
*/
orgs = {
/**
* Gets an organization by its id
*
* @param {string} orgId
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no description for params... but I think, but does JSDoc really need it? Usually we do not use it, can remove it, especially when soon will be use Flow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description for params will be the same for all methods. I wanted to avoid duplicating doc comments before finding a good name.

The JSDoc is in Github for now, but I'll move it to the Rest class for later. (We're going to add all the blanks prototypes there along with the JSDocs to be able to generate an API documentation like this: https://octokit.github.io/node-github/#api-activity-markNotificationThreadAsRead)

Does that sound ok to you?

getById: async (orgId, params) => {
return this.fetch(
`orgs/${orgId}`,
{
schema: Schemas.ORG,
},
params
).then(struct => struct);
},
/**
* Gets organization members
*
* @param {string} orgId
*/
getMembers: async (orgId, params) => {
return this.fetch(
`orgs/${orgId}/members`,
{
schema: Schemas.USER_ARRAY,
},
params
).then(struct => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a code style minor thing but you can remove the return key using:

      ).then(struct => ({
          ...struct,
          nextPageUrl: this.getNextPageUrl(struct.response),
      }));

return {
...struct,
nextPageUrl: this.getNextPageUrl(struct.response),
};
});
},
/**
* Gets organization members
*
* @param {string} orgId
*/
getRepos: async (orgId, params) => {
return this.fetch(
`orgs/${orgId}/repos`,
{
schema: Schemas.REPO_ARRAY,
},
params
).then(struct => {
return {
...struct,
nextPageUrl: this.getNextPageUrl(struct.response),
};
});
},
};

/**
* The activity endpoint
*/
activity = {
/**
* Gets received events
*
* @param {string} userId
*/
getEventsReceived: async (userId, params) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename to simply getEvents, if the activity is understood as the main screen?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activity is a reflection of GitHub's endpoint, which contains events, notifications, watching, etc : https://developer.github.com/v3/activity/

I'm sticking with node-github naming mostly for now, as it almost reflects the URL called

return this.fetch(
`users/${userId}/received_events`,
{
schema: Schemas.EVENT_ARRAY,
},
params
).then(struct => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't really understand what's going on here fully 😞

You're passing schema as an object? And why is struct defined as the data type returned?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained this a bit more in my reply to your first review comment.

Today, I'll be expanding this PR to the notifications screens, in order to proof test pure vs reduced api calls:

  • retrieving notifications should be reduced and paginated
  • marking notifications read should not involve the reducer/store at all, but be a pure call

I'll let you know how it plays out!

return {
...struct,
nextPageUrl: this.getNextPageUrl(struct.response),
};
});
},
/**
* 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}`,
{
schema: Schemas.NOTIFICATION_ARRAY,
},
finalParams
).then(struct => {
return {
...struct,
nextPageUrl: this.getNextPageUrl(struct.response),
};
});
},
markNotificationThreadAsRead: async (id, params) => {
return this.fetch(
`notifications/threads/${id}`,
{
method: this.Method.PATCH,
},
params
).then(struct => {
return {
...struct,
};
});
},
};

search = {
/**
* Search repositories
*
* @param {string} query
*/
getRepos: async (query, params) => {
return this.fetch(
`search/repositories?${query}`,
{
schema: Schemas.REPO_ARRAY,
normalizrKey: 'items',
},
params
).then(struct => {
return {
...struct,
nextPageUrl: this.getNextPageUrl(struct.response),
};
});
},
};
}
1 change: 1 addition & 0 deletions src/api/rest/providers/github/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './client';
32 changes: 32 additions & 0 deletions src/api/rest/providers/github/schemas/events.js
Original file line number Diff line number Diff line change
@@ -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 => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that can be refactor in order to avoid processed. repetition

proccessStrategy: entity => ({
    ...initSchema(),
   id: entity.id,
   ....

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your review man! Just went through your remarks and applied the suggestions. I didn't know about String.includes, nice one :)

I just kept EventSchema like that for the moment, cause it still needs some work that may require that processed const. All of this will be polished at the end of the PR.

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;
processed.org = entity.org;
processed.repo = entity.repo;

return processed;
},
}
);
Loading