...
 
Commits (2)
......@@ -60,4 +60,7 @@ buck-out/
# Jest cache
.jest/
coverage/
\ No newline at end of file
coverage/
# Sentry secrets
sentry.properties
\ No newline at end of file
......@@ -8,7 +8,6 @@
import './global';
import './shim'
import crypto from "crypto"; // DO NOT REMOVE!
import codePush from "react-native-code-push"; // For auto updates
import React, {
Component
......@@ -64,6 +63,11 @@ import connectivityService from './src/common/services/connectivity.service';
import sqliteStorageProviderService from './src/common/services/sqlite-storage-provider.service';
import commentStorageService from './src/comments/CommentStorageService';
import { Sentry } from 'react-native-sentry';
Sentry.config('https://d650fc58f2da4dc8ae9d95847bce152d@sentry.io/1538735').install();
let deepLinkUrl = '';
// init push service
......@@ -77,6 +81,12 @@ CookieManager.clearAll();
// On app login (runs if the user login or if it is already logged in)
sessionService.onLogin(async () => {
const user = sessionService.getUser();
Sentry.setUserContext({
userID: user.guid
});
logService.info('[App] Getting minds settings and onboarding progress');
// load minds settings and onboarding progresss on login
const results = await Promise.all([mindsService.getSettings(), stores.onboarding.getProgress()]);
......@@ -163,7 +173,6 @@ type Props = {
/**
* App
*/
@codePush
export default class App extends Component<Props, State> {
state = {
......@@ -217,8 +226,6 @@ export default class App extends Component<Props, State> {
logService.info('[App] session initialized');
}
}
await this.checkForUpdates();
} catch(err) {
logService.exception('[App] Error initializing the app', err);
Alert.alert(
......@@ -283,21 +290,6 @@ export default class App extends Component<Props, State> {
}
}
async checkForUpdates() {
try {
const params = {
updateDialog: Platform.OS !== 'ios',
installMode: codePush.InstallMode.ON_APP_RESUME,
};
if (CODE_PUSH_TOKEN) params.deploymentKey = CODE_PUSH_TOKEN;
let response = await codePush.sync(params);
} catch (err) {
logService.exception('[App] Error checking for code push updated', err);
}
}
/**
* Render
*/
......
......@@ -9,6 +9,7 @@ import {
import { onError } from "mobx-react";
import logService from './src/common/services/log.service';
import Sentry from 'react-native-sentry';
onError(error => {
console.log(error);
......@@ -48,7 +49,9 @@ if (!__DEV__) {
* Native Errors
*/
setNativeExceptionHandler((exceptionString) => {
Sentry.captureException(new Error(exceptionString), {
logger: 'NativeExceptionHandler',
});
console.log(exceptionString);
logService.exception(exceptionString);
});
}
......@@ -3,7 +3,7 @@ import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// select first image
const firstImage = await driver.waitForElementByAccessibilityId('Gallery Image 0', wd.asserters.isDisplayed, 5000);
const firstImage = await driver.waitForElementByAccessibilityId('Gallery image/jpeg', wd.asserters.isDisplayed, 5000);
await firstImage.click();
await sleep(3000);
......
......@@ -42,7 +42,7 @@ describe('Discovery post edit flow', () => {
it('should search for the post', async () => {
// select all list
const all = await driver.waitForElementByAccessibilityId('Discovery All', wd.asserters.isDisplayed, 1000);
const all = await driver.waitForElementByAccessibilityId('Discovery All', wd.asserters.isDisplayed, 5000);
await all.click();
await sleep(500);
......
......@@ -66,7 +66,7 @@ describe('Post flow tests', () => {
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[1]');
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
......@@ -138,7 +138,7 @@ describe('Post flow tests', () => {
// get the Image touchable
const imageButton = await driver.waitForElementByAccessibilityId('Posted Image', wd.asserters.isDisplayed, 10000);
await imageButton.click();
const image = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.widget.ImageView');
const image = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.widget.ImageView');
const goBack = await driver.waitForElementByAccessibilityId('Go back button', wd.asserters.isDisplayed, 10000);
await goBack.click();
......
......@@ -27,9 +27,10 @@ describe('Top-bar tests', () => {
});
it('should open the boost console', async () => {
const button = await driver.waitForElementByAccessibilityId('boost-console button', wd.asserters.isDisplayed, 5000);
const button = await driver.waitForElementByAccessibilityId('boost-console button', wd.asserters.isDisplayed, 7000);
await button.click();
const textElement = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView');
const textElement = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView');
expect(await textElement.text()).toBe('Boost Console');
const back = await driver.waitForElementByXPath('//android.widget.Button[@content-desc="Go back"]/android.view.ViewGroup/android.widget.ImageView');
......@@ -39,10 +40,9 @@ describe('Top-bar tests', () => {
it('should open the users profile on clicking the profile avatar', async () => {
const button = await driver.waitForElementByAccessibilityId('topbar avatar button', wd.asserters.isDisplayed, 5000);
await button.click();
await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[1]/android.widget.ImageView');
await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[3]/android.widget.ImageView');
const back = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView')
back.click();
const back = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView'); back.click();
});
it('should open the menu when clicking the hamburger menu', async () => {
......
......@@ -16,6 +16,7 @@ export default function(guid, owner=userFakeFactory(100)) {
"featured_id":false,
"ownerObj": owner,
"category":"education",
"impressions": 100,
"thumbs:up:count": 2,
"thumbs:up:user_guids":["1","2"],
"thumbs:down:count":1,
......
......@@ -14,6 +14,9 @@ export default function(guid, name='Some User', username='someusermane') {
"access_id":"2",
"language":"en",
"icontime":"1523515420",
"impressions":100,
"subscribers_count": 50,
"blocked": false,
"legacy_guid":false,
"featured_id":"725165605782560768",
"banned":"no",
......
module.exports = {
getVersion: jest.fn().mockImplementation(() => '3.8.0'),
getBuildNumber: jest.fn(),
// add more methods as needed
};
\ No newline at end of file
......@@ -3,7 +3,7 @@ import React from 'react';
import App from '../App';
import videochat from '../src/common/services/videochat.service';
import sqliteStorageProviderService from '../src/common/services/sqlite-storage-provider.service';
import logservice from '../src/common/services/log.service';
import logService from '../src/common/services/log.service';
import {
BackHandler,
} from 'react-native';
......@@ -16,6 +16,7 @@ import renderer from 'react-test-renderer';
BackHandler.addEventListener = jest.fn();
jest.mock('../src/common/services/sqlite-storage-provider.service')
jest.mock('../src/common/services/log.service', () => {});
jest.mock('../src/common/services/push.service');
jest.mock('react-native-gesture-handler', () => {});
jest.mock('react-navigation-stack', () => { Header: {} });
jest.mock('react-navigation', () => {
......
......@@ -16,12 +16,15 @@ import { activitiesServiceFaker } from '../../../__mocks__/fake/ActivitiesFaker'
import CommentList from '../../../src/comments/CommentList';
jest.mock('../../../src/newsfeed/NewsfeedService');
import { getSingle } from '../../../src/newsfeed/NewsfeedService';
import entitiesService from '../../../src/common/services/entities.service';
jest.mock('../../../src/newsfeed/activity/Activity', () => 'Activity');
jest.mock('../../../src/comments/CommentList', () => 'CommentList');
jest.mock('../../../src/common/components/CenteredLoading', () => 'CenteredLoading');
jest.mock('../../../src/comments/CommentsStore');
jest.mock('../../../src/comments/CommentsStoreProvider');
jest.mock('../../../src/common/services/entities.service');
describe('Activity screen component', () => {
......@@ -50,39 +53,27 @@ describe('Activity screen component', () => {
params: {entity: activitiesServiceFaker().load(1).activities[0]}
}
};
screen = shallow(
<ActivityScreen navigation={navigation}/>
);
expect(screen).toMatchSnapshot();
// should have a comment list component
expect(screen.find(CommentList)).toHaveLength(1);
});
it('should show loader until it loads the activity', async (done) => {
navigation = {
push: jest.fn(),
state: {
routeName: 'some',
params: {guid: '1'}
}
};
getSingle.mockResolvedValue(activitiesServiceFaker().load(1).activities[0]);
entitiesService.single.mockResolvedValue(navigation.state.params.entity);
screen = shallow(
<ActivityScreen navigation={navigation}/>
);
// shoul show loading
expect(screen).toMatchSnapshot();
screen.update();
// unmount
await screen.instance().componentDidMount();
// workaround to run after the async didmount
setImmediate(() => {
// should show the activity
expect(screen).toMatchSnapshot();
done();
});
jest.runAllTicks();
// await is important here!
await screen.update();
// should show the activity
expect(screen).toMatchSnapshot();
// should have a comment list component
expect(screen.find(CommentList)).toHaveLength(1);
});
});
......@@ -18,6 +18,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -74,6 +75,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -146,6 +148,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -246,6 +249,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -313,6 +317,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -376,6 +381,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -445,6 +451,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -507,6 +514,7 @@ exports[`Activity component renders correctly 1`] = `
entity={
ActivityModel {
"__list": null,
"allow_comments": true,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Activity screen component renders correctly with an entity as param 1`] = `
exports[`Activity screen component renders correctly with an entity as param 1`] = `<CenteredLoading />`;
exports[`Activity screen component renders correctly with an entity as param 2`] = `
<View
style={
Array [
......@@ -15,11 +17,9 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
>
<CommentList
entity={
ActivityModel {
"__list": null,
Object {
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
......@@ -27,12 +27,9 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
"edited": "",
"getThumbSource": [Function],
"guid": "activityguid0",
"is_visible": true,
"mature": false,
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"ownerObj": Object {
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -41,16 +38,10 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"paywall": undefined,
"perma_url": false,
"pinned": undefined,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"thumbs:down:count": undefined,
"thumbs:down:user_guids": undefined,
"thumbs:up:count": undefined,
"thumbs:up:user_guids": undefined,
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
......@@ -62,11 +53,9 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
<Activity
autoHeight={false}
entity={
ActivityModel {
"__list": null,
Object {
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
......@@ -74,12 +63,9 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
"edited": "",
"getThumbSource": [Function],
"guid": "activityguid0",
"is_visible": true,
"mature": false,
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"ownerObj": Object {
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -88,16 +74,10 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"paywall": undefined,
"perma_url": false,
"pinned": undefined,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"thumbs:down:count": undefined,
"thumbs:down:user_guids": undefined,
"thumbs:up:count": undefined,
"thumbs:up:user_guids": undefined,
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
......@@ -321,213 +301,3 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
/>
</View>
`;
exports[`Activity screen component should show loader until it loads the activity 1`] = `<CenteredLoading />`;
exports[`Activity screen component should show loader until it loads the activity 2`] = `
<View
style={
Array [
Object {
"flex": 1,
},
Object {
"backgroundColor": "white",
},
]
}
>
<CommentList
entity={
ActivityModel {
"__list": null,
"comments:count": undefined,
"edited": undefined,
"is_visible": true,
"mature": undefined,
"mature_visibility": false,
"message": undefined,
"paywall": undefined,
"pinned": undefined,
"thumbs:down:count": undefined,
"thumbs:down:user_guids": undefined,
"thumbs:up:count": undefined,
"thumbs:up:user_guids": undefined,
}
}
header={
<Activity
autoHeight={false}
entity={
ActivityModel {
"__list": null,
"comments:count": undefined,
"edited": undefined,
"is_visible": true,
"mature": undefined,
"mature_visibility": false,
"message": undefined,
"paywall": undefined,
"pinned": undefined,
"thumbs:down:count": undefined,
"thumbs:down:user_guids": undefined,
"thumbs:up:count": undefined,
"thumbs:up:user_guids": undefined,
}
}
navigation={
Object {
"push": [MockFunction],
"state": Object {
"params": Object {
"guid": "1",
},
"routeName": "some",
},
}
}
/>
}
navigation={
Object {
"push": [MockFunction],
"state": Object {
"params": Object {
"guid": "1",
},
"routeName": "some",
},
}
}
onInputFocus={[Function]}
store={
Object {
"attachment": Object {},
"comments": Array [
Object {
"attachment_guid": false,
"blurb": false,
"container_guid": "guid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "guid0",
"mature": false,
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"thumbnail_src": false,
"type": "comment",
},
Object {
"attachment_guid": false,
"blurb": false,
"container_guid": "guid1",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "guid1",
"mature": false,
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"thumbnail_src": false,
"type": "comment",
},
Object {
"attachment_guid": false,
"blurb": false,
"container_guid": "guid2",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "guid2",
"mature": false,
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"thumbnail_src": false,
"type": "comment",
},
Object {
"attachment_guid": false,
"blurb": false,
"container_guid": "guid3",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "guid3",
"mature": false,
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"thumbnail_src": false,
"type": "comment",
},
Object {
"attachment_guid": false,
"blurb": false,
"container_guid": "guid4",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "guid4",
"mature": false,
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"thumbnail_src": false,
"type": "comment",
},
],
"embed": RichEmbedStore {
"_richEmbedFetchTimer": null,
"clearRichEmbedAction": [Function],
"richEmbedUrl": "",
"setRichEmbedPromise": null,
},
"loadComments": [Function],
"loadNext": "aaaaaa",
"loadPrevious": "aaaaaa",
"post": [Function],
"setText": [Function],
}
}
/>
</View>
`;
......@@ -17,7 +17,7 @@ exports[`Comment action component renders correctly 1`] = `
>
<Icon
color="rgb(96, 125, 139)"
name="chat-bubble"
name="speaker-notes-off"
raised={false}
reverse={false}
reverseColor="white"
......
......@@ -5,6 +5,7 @@ import { shallow } from 'enzyme';
import LoginForm from '../../src/auth/LoginForm';
import authService from '../../src/auth/AuthService';
import TextInput from '../../src/common/components/TextInput';
jest.mock('../../src/auth/AuthService');
......@@ -35,7 +36,7 @@ describe('LoginForm component', () => {
// simulate user input
const render = wrapper.dive();
render.find('TextInput').forEach(child => {
render.find(TextInput).forEach(child => {
child.simulate('changeText', 'data');
});
......
......@@ -271,7 +271,7 @@ exports[`ForgotPassword component should renders correctly 1`] = `
]
}
>
CONTIUE
CONTINUE
</Text>
</View>
</View>
......
......@@ -335,7 +335,7 @@ exports[`ForgotScreen component should renders correctly 1`] = `
]
}
>
CONTIUE
CONTINUE
</Text>
</View>
</View>
......
......@@ -12,6 +12,7 @@ exports[`LoginForm component should renders correctly 1`] = `
<TextInput
allowFontScaling={true}
autoCapitalize="none"
editable={false}
onChangeText={[Function]}
placeholder="Username"
placeholderTextColor="#444"
......@@ -42,6 +43,7 @@ exports[`LoginForm component should renders correctly 1`] = `
<TextInput
allowFontScaling={true}
autoCapitalize="none"
editable={false}
onChangeText={[Function]}
placeholder="Password"
placeholderTextColor="#444"
......
......@@ -39,7 +39,7 @@ exports[`blog card component should renders correctly 1`] = `
source={
Object {
"headers": Object {
"App-Version": "3.8.0-rc1",
"App-Version": "3.8.0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
......
......@@ -90,6 +90,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "1",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -98,6 +99,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -128,6 +130,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -156,6 +159,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -216,6 +220,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "2",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -224,6 +229,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -254,6 +260,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -282,6 +289,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -342,6 +350,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "3",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -350,6 +359,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -380,6 +390,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -408,6 +419,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -642,6 +654,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "1",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -650,6 +663,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -680,6 +694,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -708,6 +723,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -785,6 +801,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "2",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -793,6 +810,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -823,6 +841,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -851,6 +870,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -928,6 +948,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "3",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -936,6 +957,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -966,6 +988,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -994,6 +1017,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......
import 'react-native';
import React from 'react';
import { Platform, CameraRoll, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import CaptureGallery from '../../src/capture/CaptureGallery';
import { shallow, render } from 'enzyme';
import androidPermission from '../../src/common/services/android-permissions.service';
import { getPhotosFaker } from '../../__mocks__/fake/CameraRollFaker';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
jest.mock('../../src/common/services/android-permissions.service');
//jest.mock('TouchableOpacity', () => 'TouchableOpacity' )
jest.mock('CameraRoll');
CameraRoll.getPhotos = jest.fn();
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos.mockResolvedValue(response);
import CaptureGallery from '../../src/capture/CaptureGallery';
/**
* Tests
*/
describe('cature gallery component', () => {
beforeEach(() => {
CameraRoll.getPhotos.mockClear();
androidPermission.checkReadExternalStorage.mockClear();
androidPermission.readExternalStorage.mockClear();
});
it('should renders correctly', () => {
it('should renders correctly', async() => {
const galley = renderer.create(
<CaptureGallery />
<CaptureGallery />
).toJSON();
expect(galley).toMatchSnapshot();
});
it('should load photos on mount', () => {
const spyWillMount = jest.spyOn(CaptureGallery.prototype, '_loadPhotos');
const spyWillMount = jest.spyOn(CaptureGallery.prototype, 'loadPhotos');
Platform.OS = 'ios';
......@@ -35,7 +42,6 @@ describe('cature gallery component', () => {
<CaptureGallery />
);
// the call is dalayed (setTimeout) so we fast-forward timers
jest.runAllTimers();
......@@ -78,30 +84,20 @@ describe('cature gallery component', () => {
it('should calls onSelected when the user select an image', async(done) => {
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos = jest.fn();
CameraRoll.getPhotos.mockResolvedValue(response);
const mockFn = jest.fn();
try {
const wrapper = shallow(
<CaptureGallery onSelected={mockFn}/>
);
const wrapper = renderer.create(<CaptureGallery onSelected={mockFn}/>);
// load phoyos
await wrapper.instance()._loadPhotos();
await wrapper.getInstance()._loadPhotos();
// update component
wrapper.update();
expect( CameraRoll.getPhotos).toBeCalled();
// find TouchableOpacity (rendered images in lists)
const images = wrapper.find(TouchableOpacity);
const images = wrapper.root.findAllByType(TouchableOpacity);
// simulate press on image
images.at(1).simulate('press');
images[0].props.onPress();
// expect fn to be called once
expect(mockFn).toBeCalled();
......@@ -112,27 +108,16 @@ describe('cature gallery component', () => {
});
it('should show loaded images', async(done) => {
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos = jest.fn();
CameraRoll.getPhotos.mockResolvedValue(response);
const mockFn = jest.fn();
try {
const wrapper = shallow(
<CaptureGallery onSelected={mockFn}/>
);
const wrapper = renderer.create(<CaptureGallery onSelected={mockFn}/>);
// load phoyos
await wrapper.instance()._loadPhotos();
// update component
wrapper.update();
await wrapper.getInstance()._loadPhotos();
// find TouchableOpacity (rendered images in lists)
const images = wrapper.find(TouchableOpacity);
const images = wrapper.root.findAllByType(TouchableOpacity);
// expect 5 images rendered
expect(images.length).toEqual(5);
......
import 'react-native';
import React from 'react';
import { Alert } from 'react-native';
import { Alert, CameraRoll } from 'react-native';
import { shallow } from 'enzyme';
import { Icon } from 'react-native-elements'
import CapturePoster from '../../src/capture/CapturePoster';
import CapturePreview from '../../src/capture/CapturePreview';
import CaptureGallery from '../../src/capture/CaptureGallery';
import UserStore from '../../src/auth/UserStore';
import CaptureStore from '../../src/capture/CaptureStore';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import { getPhotosFaker } from '../../__mocks__/fake/CameraRollFaker';
jest.mock('../../src/auth/UserStore');
jest.mock('../../src/capture/CaptureStore');
jest.mock('../../src/capture/CapturePostButton', () => 'CapturePostButton');
jest.mock('../../src/capture/CapturePosterFlags', () => 'CapturePosterFlags');
jest.mock('../../src/capture/CapturePreview', () => 'CapturePreview');
jest.mock('../../src/capture/CapturePosterFlags', () => 'CapturePosterFlags');
jest.mock('../../src/common/services/translation.service');
Alert.alert = jest.fn();
jest.mock('CameraRoll');
CameraRoll.getPhotos = jest.fn();
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos.mockResolvedValue(response);
/**
* Tests
......@@ -43,14 +50,15 @@ describe('cature poster component', () => {
});
it('should renders correctly', () => {
const galley = renderer.create(
const screen = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
navigation={navigation}
/>
).toJSON();
expect(galley).toMatchSnapshot();
expect(screen).toMatchSnapshot();
});
it('should receive text parameters on did mount', () => {
......@@ -118,8 +126,12 @@ describe('cature poster component', () => {
it('should show the preview when an image is attached', async (done) => {
try {
// emulate image attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsImage.uri;
capture.attachment.type = paramsImage.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -127,18 +139,14 @@ describe('cature poster component', () => {
/>
);
// emulate image attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsImage.uri;
capture.attachment.type = paramsImage.type;
const gallery = wrapper.root.findByType(CaptureGallery);
// update component
wrapper.update();
await gallery.instance._loadPhotos();
// find Capture Preview
const preview = wrapper.find(CapturePreview);
const preview = wrapper.root.findByType(CapturePreview);
expect(preview.length).toBe(1);
expect(preview).toBeDefined();
done();
} catch (e) {
......@@ -148,8 +156,12 @@ describe('cature poster component', () => {
it('should show the preview when a video is attached', async (done) => {
try {
// emulate video attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -157,18 +169,14 @@ describe('cature poster component', () => {
/>
);
// emulate video attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const gallery = wrapper.root.findByType(CaptureGallery);
// update component
wrapper.update();
await gallery.instance._loadPhotos();
// find Capture Preview
const preview = wrapper.find(CapturePreview);
const preview = wrapper.root.findByType(CapturePreview);
expect(preview.length).toBe(1);
expect(preview).toBeDefined();
done();
} catch (e) {
......@@ -184,7 +192,7 @@ describe('cature poster component', () => {
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -192,13 +200,17 @@ describe('cature poster component', () => {
/>
);
const gallery = wrapper.root.findByType(CaptureGallery);
await gallery.instance._loadPhotos();
// find delete icon
const icon = wrapper.find(Icon);
const icon = wrapper.root.findByType(Icon);
expect(icon.length).toBe(1);
expect(icon).toBeDefined();
// simulate press on image
icon.at(0).simulate('press');
icon.props.onPress();
// should be called
expect(capture.attachment.delete).toHaveBeenCalled();
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`cature gallery component should renders correctly 1`] = `
<View>
<View
style={
Object {
"flex": 1,
}
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "white",
"flex": 1,
"justifyContent": "center",
"minHeight": 100,
}
>
<View
style={
Object {
"height": 125,
}
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 1,
"paddingRight": 1,
}
}
>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Gallery
</Text>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Photo
</Text>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Video
</Text>
</View>
</View>
</View>
</View>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "white",
"flex": 1,
"justifyContent": "center",
"minHeight": 100,
}
}
>
<ActivityIndicator
animating={true}
color="#999999"
hidesWhenStopped={true}
size="large"
/>
</View>
}
>
<ActivityIndicator
animating={true}
color="#999999"
hidesWhenStopped={true}
size="large"
/>
</View>
`;
......@@ -8,316 +8,23 @@ exports[`cature poster component should renders correctly 1`] = `
}
}
>
<RCTScrollView
keyboardShouldPersistTaps="always"
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "white",
"flex": 1,
"justifyContent": "center",
"minHeight": 100,
}
}
>
<View>
<View
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#FFF",
"flexDirection": "row",
"minHeight": 100,
}
}
>
<TextInput
allowFontScaling={true}
editable={true}
multiline={true}
onChangeText={[Function]}
onSelectionChange={[Function]}
placeholder="Speak your mind..."
placeholderTextColor="#ccc"
rejectResponderTermination={true}
selectTextOnFocus={false}
style={
Object {
"flex": 1,
"padding": 12,
"paddingTop": 24,
}
}
testID="PostInput"
underlineColorAndroid="transparent"
value=""
/>
</View>
<CapturePosterFlags
containerStyle={
Array [
Object {
"flexDirection": "row",
"justifyContent": "flex-end",
},
]
}
hideShare={true}
lockValue={null}
matureValue={false}
nsfwValue={Array []}
onLocking={[Function]}
onMature={[Function]}
onNsfw={[Function]}
onShare={[Function]}
shareValue={Object {}}
/>
<View>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"height": 125,
}
}
>
<View
style={
Object {
"flex": 1,
"flexDirection": "row",
"paddingLeft": 1,
"paddingRight": 1,
}
}
>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Gallery
</Text>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Photo
</Text>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignContent": "center",
"alignItems": "center",
"backgroundColor": "#FFF",
"borderColor": "#ECECEC",
"borderWidth": 1,
"flex": 1,
"flexDirection": "column",
"justifyContent": "center",
"margin": 1,
}
}
>
<View
style={
Object {
"alignContent": "center",
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": undefined,
"fontSize": 36,
},
Object {
"color": "#444",
},
Object {
"fontFamily": "Ionicons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Object {
"color": "#444",
"letterSpacing": 1.25,
}
}
>
Video
</Text>
</View>
</View>
</View>
</View>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "white",
"flex": 1,
"justifyContent": "center",
"minHeight": 100,
}
}
>
<ActivityIndicator
animating={true}
color="#999999"
hidesWhenStopped={true}
size="large"
/>
</View>
</View>
</View>
</RCTScrollView>
<ActivityIndicator
animating={true}
color="#999999"
hidesWhenStopped={true}
size="large"
/>
</View>
</View>
`;
......@@ -13,6 +13,7 @@ jest.mock('../../src/channel/ChannelStore');
jest.mock('../../src/channel/UserModel');
jest.mock('../../src/common/services/features.service');
jest.mock('../../AppStores');
jest.mock('../../src/common/services/boosted-content.service');
/**
* Tests
......
......@@ -24,6 +24,7 @@ import UserModel from '../../src/channel/UserModel';
import Touchable from '../../src/common/components/Touchable';
import session from '../../src/common/services/session.service';
import ChannelStore from '../../src/channel/ChannelStore';
import featuresService from '../../src/common/services/features.service';
jest.mock('../../src/common/helpers/abortableFetch');
jest.mock('../../src/channel/UserModel');
......@@ -37,6 +38,7 @@ jest.mock('../../src/common/components/FeedList', () => 'FeedList');
jest.mock('../../src/capture/CaptureFab', () => 'CaptureFab');
jest.mock('../../src/blogs/BlogCard', () => 'BlogCard');
jest.mock('../../src/common/components/Touchable', () => 'Touchable');
jest.mock('../../src/common/services/boosted-content.service');
/**
* Tests
......
......@@ -14,7 +14,7 @@ import appStores from '../../../AppStores';
jest.mock('../../../src/auth/UserStore');
jest.mock('../../../AppStores');
jest.mock('../../../src/channel/ChannelStore');
jest.mock('../../../src/common/services/boosted-content.service');
jest.mock('TouchableHighlight', () => 'TouchableHighlight');
/**
......
......@@ -32,7 +32,7 @@ exports[`channel header component owner should render correctly 1`] = `
source={
Object {
"headers": Object {
"App-Version": "3.8.0-rc1",
"App-Version": "3.8.0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
......@@ -144,14 +144,18 @@ exports[`channel header component owner should render correctly 1`] = `
<Text>
SUBSCRIBERS
</Text>
<Text />
<Text>
50
</Text>
</View>
</TouchableHighlight>
<View>
<Text>
VIEWS
</Text>
<Text />
<Text>
100
</Text>
</View>
</View>
<View>
......
......@@ -75,10 +75,9 @@ exports[`Media view component renders correctly 1`] = `
}
imageStyle={
Object {
"backgroundColor": "transparent",
"backgroundColor": "black",
}
}
loadingIndicator="placeholder"
onError={[Function]}
source={
Object {
......
import api from '../../../src/common/services/api.service';
import api, { ApiError } from '../../../src/common/services/api.service';
import session from '../../../src/common/services/session.service';
import abortableFetch, {abort} from '../../../src/common/helpers/abortableFetch';
import { MINDS_API_URI } from '../../../src/config/Config';
......@@ -46,14 +46,14 @@ describe('api service POST', () => {
const res = await api.post('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(response);
expect(err).toBeInstanceOf(ApiError);
}
});
it('should return api error', async () => {
const response = { json: jest.fn(), ok: true };
const respBody = { status: 'error', error: 'some error' };
const respBody = { status: 500, error: 'some error' };
response.json.mockResolvedValue(respBody);
const params = {p1: 1, p2: 2};
......@@ -63,7 +63,7 @@ describe('api service POST', () => {
const res = await api.post('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(respBody);
expect(err).toBeInstanceOf(ApiError);
}
});
});
......@@ -105,7 +105,7 @@ describe('api service GET', () => {
const res = await api.get('api/path', params, null);
} catch (err){
// assert on the error
expect(err).toEqual(response);
expect(err).toBeInstanceOf(ApiError);
}
});
......@@ -122,7 +122,7 @@ describe('api service GET', () => {
const res = await api.get('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(respBody);
expect(err).toBeInstanceOf(ApiError);
}
});
});
......@@ -167,7 +167,7 @@ describe('api service DELETE', () => {
const res = await api.delete('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(response);
expect(err).toBeInstanceOf(ApiError);
}
});
......@@ -184,7 +184,7 @@ describe('api service DELETE', () => {
const res = await api.delete('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(respBody);
expect(err).toBeInstanceOf(ApiError);
}
});
});
......@@ -229,7 +229,7 @@ describe('api service PUT', () => {
const res = await api.put('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(response);
expect(err).toBeInstanceOf(ApiError);
}
});
......@@ -245,7 +245,7 @@ describe('api service PUT', () => {
const res = await api.put('api/path', params);
} catch (err) {
// assert on the error
expect(err).toEqual(respBody);
expect(err).toBeInstanceOf(ApiError);
}
});
});
......
import boostedContentService from "../../../src/common/services/boosted-content.service";
import FeedsService from "../../../src/common/services/feeds.service";
jest.mock('../../../src/common/services/feeds.service');
jest.mock('../../../src/common/services/session.service');
/**
* Tests
*/
describe('Boosted content service', () => {
it('should fetch the boosts from the server', async () => {
const fakeBoosts = [{guid: 1}, {guid: 2}, {guid: 3}];
boostedContentService.feedsService.getEntities.mockResolvedValue(fakeBoosts);
// load the boosts
await boostedContentService.load();
// should fetch the feed
expect(boostedContentService.feedsService.setEndpoint).toBeCalledWith('api/v2/boost/feed');
expect(boostedContentService.feedsService.setOffset).toBeCalledWith(0);
expect(boostedContentService.feedsService.setLimit).toBeCalledWith(12);
expect(boostedContentService.feedsService.fetchRemoteOrLocal).toBeCalled();
// should fetch the boosts entities
expect(boostedContentService.feedsService.getEntities).toBeCalled();
// the boosts should be stored in the boosts property
expect(boostedContentService.boosts).toBe(fakeBoosts);
});
it('should return next boost and start again when the end is reached', () => {
const fakeBoosts = [{guid: 1}, {guid: 2}, {guid: 3}];
boostedContentService.boosts = fakeBoosts;
// next
expect(boostedContentService.fetch()).toBe(fakeBoosts[0]);
// next
expect(boostedContentService.fetch()).toBe(fakeBoosts[1]);
// next
expect(boostedContentService.fetch()).toBe(fakeBoosts[2]);
// start again
expect(boostedContentService.fetch()).toBe(fakeBoosts[0]);
});
});
\ No newline at end of file
......@@ -79,7 +79,7 @@ project.ext.react = [
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
/**
* Set this to true to create two separate APKs instead of one:
......@@ -163,6 +163,9 @@ android {
}
dependencies {
implementation project(':react-native-device-info')
implementation project(':react-native-sentry')
implementation project(':react-native-notifications')
implementation project(':@react-native-community_netinfo')
implementation project(':react-native-screens')
implementation project(':rn-apk')
......@@ -173,7 +176,8 @@ dependencies {
implementation project(':react-native-cookies')
compile project(':react-native-exit-app')
compile project(':react-native-background-timer')
implementation 'com.google.firebase:firebase-core:16.0.7'
implementation 'com.google.firebase:firebase-messaging:17.3.3' // IMPORTANT: fix the crash when the app receives a push notification
implementation(project(':react-native-jitsi-meet')) {
// solve issues with the packages shared with the jitsi bundle
exclude group: 'com.facebook.react', module:'react-native-vector-icons'
......@@ -182,16 +186,13 @@ dependencies {
exclude group: 'com.facebook.react',module:'react-native-fast-image'
exclude group: 'com.facebook.react',module:'react-native-background-timer'
// exclude group: 'com.facebook.react',module:'react-native-google-signin'
exclude group: 'com.google.android.gms', module: 'play-services-base'
exclude group: 'com.google.firebase'
}
implementation project(':react-native-sqlite-storage')
implementation project(':react-native-code-push')
implementation project(':react-native-background-timer')
implementation project(':react-native-android-sms-listener')
implementation project(':react-native-fs')
implementation project(':react-native-media-meta')
implementation project(':react-native-share-menu')
implementation project(':react-native-notifications')
implementation project(':react-native-keep-awake')
implementation project(':react-native-share')
// implementation project(':react-native-udp')
......@@ -208,17 +209,13 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:0.59.10" // From node_modules
implementation 'com.google.android.gms:play-services-auth:16.0.1'
// implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.facebook.fresco:imagepipeline-okhttp3:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
implementation 'com.facebook.fresco:fresco:1.10.0'
// fix 0.58 update
// implementation 'com.facebook.fresco:imagepipeline-okhttp:0.8.1'
implementation 'com.facebook.soloader:soloader:0.6.0'
// Glide
implementation("com.github.bumptech.glide:glide:${glideVersion}") {
exclude group: "com.android.support", module: "glide"
......@@ -236,3 +233,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
}
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
apply plugin: 'com.google.gms.google-services'
{
"project_info": {
"project_number": "216497927604",
"firebase_url": "https://testproyect-b002e.firebaseio.com",
"project_id": "testproyect-b002e",
"storage_bucket": "testproyect-b002e.appspot.com"
"project_number": "81109256529",
"firebase_url": "https://minds-com-api-project-81109256529.firebaseio.com",
"project_id": "minds.com:api-project-81109256529"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:216497927604:android:e9dc243d6758e397",
"mobilesdk_app_id": "1:81109256529:android:471681f082570885",
"android_client_info": {
"package_name": "com.minds.mobiletest"
"package_name": "com.minds.mobile"
}
},
"oauth_client": [
{
"client_id": "216497927604-dslu0dgqa34p7upomu9e0aqnlrkgq7ap.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "216497927604-dslu0dgqa34p7upomu9e0aqnlrkgq7ap.apps.googleusercontent.com",
"client_id": "81109256529-7bnt4565mfv7nelmd1suvb2mkakt3d6o.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAXqcOREGAzA2BFVfl8j9bnswBzau5mReU"
"current_key": "AIzaSyDt4mTYivmWtg1Sb4TUbnObO8YQ-5aZGA0"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
"other_platform_oauth_client": [
{
"client_id": "81109256529-7bnt4565mfv7nelmd1suvb2mkakt3d6o.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
......
......@@ -20,8 +20,6 @@
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-sdk
android:minSdkVersion="16"
......@@ -79,7 +77,6 @@
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<activity android:name="com.reactnativejitsimeet.JitsiMeetNavigatorActivity" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="@string/gcm_sender_id"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_stat_name" />
<provider
android:name="android.support.v4.content.FileProvider"
......
......@@ -3,6 +3,10 @@ package com.minds.mobile;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import io.sentry.RNSentryPackage;
import com.reactnativejitsimeet.JitsiMeetPackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.reactnativecommunity.netinfo.NetInfoPackage;
import com.swmansion.rnscreens.RNScreensPackage;
import be.skyzohlabs.rnapk.ReactNativeAPKPackage;
......@@ -27,14 +31,11 @@ import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.reactnative.photoview.PhotoViewPackage;
import com.corbt.keepawake.KCKeepAwakePackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import cl.json.RNSharePackage;
import cl.json.ShareApplication;
import com.meedan.ShareMenuPackage;
import com.mybigday.rnmediameta.RNMediaMetaPackage;
import com.rnfs.RNFSPackage;
import com.centaurwarchief.smslistener.SmsListenerPackage;
import com.microsoft.codepush.react.CodePush;
import com.reactnativejitsimeet.JitsiMeetPackage;
import com.ocetnik.timer.BackgroundTimerPackage;
import org.pgsqlite.SQLitePluginPackage;
......@@ -46,10 +47,7 @@ public class MainApplication extends Application implements ShareApplication, Re
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected String getJSBundleFile() {
return CodePush.getBundleUrl("app.bundle");
}
@Override
public boolean getUseDeveloperSupport() {
......@@ -60,6 +58,9 @@ public class MainApplication extends Application implements ShareApplication, Re
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNDeviceInfo(),
new RNSentryPackage(),
new JitsiMeetPackage(),
new NetInfoPackage(),
new RNScreensPackage(),
new ReactNativeAPKPackage(),
......@@ -85,9 +86,6 @@ public class MainApplication extends Application implements ShareApplication, Re
new ShareMenuPackage(),
new RNMediaMetaPackage(),
new RNFSPackage(),
new SmsListenerPackage(),
new CodePush("_C083_CqL7CmKwASrv6Xrj1wqH7erJMhIBnRQ", MainApplication.this, BuildConfig.DEBUG),
new JitsiMeetPackage(),
new SQLitePluginPackage(),
new BackgroundTimerPackage()
);
......
<resources>
<string moduleConfig="true" name="reactNativeCodePush_androidDeploymentKey">_C083_CqL7CmKwASrv6Xrj1wqH7erJMhIBnRQ</string>
<string name="app_name">Minds</string>
<string name="gcm_sender_id">81109256529</string>
</resources>
......@@ -14,7 +14,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
classpath 'com.google.gms:google-services:4.3.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
......
......@@ -25,6 +25,7 @@ platform :android do
lane :assemble_build do
sh("echo $ANDROID_KEYSTORE | base64 --decode > ../app/minds.keystore")
sh("echo $ANDROID_KEYSTORE | base64 --decode > ../minds.keystore")
sh("echo $SENTRY_ANDROID_PROPERTIES | base64 --decode > ../sentry.properties")
gradle(
task: "assemble",
build_type: "Release",
......@@ -42,7 +43,7 @@ platform :android do
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
......
......@@ -22,10 +22,10 @@ org.gradle.jvmargs=-Xmx2048m
systemProp.org.gradle.internal.http.connectionTimeout=180000
systemProp.org.gradle.internal.http.socketTimeout=180000
versionName=3.8.0
versionName=3.9.1
# CUSTOM
versionCode=1050000009
versionCode=1050000013
# PLAY STORE
#versionCode=310026
# versionCode=310031
rootProject.name = 'Minds'
include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-jitsi-meet'
project(':react-native-jitsi-meet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-jitsi-meet/android')
include ':react-native-notifications'
project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
include ':@react-native-community_netinfo'
project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
include ':react-native-screens'
......@@ -22,20 +30,12 @@ include ':react-native-exit-app'
project(':react-native-exit-app').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exit-app/android')
include ':react-native-background-timer'
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
include ':react-native-jitsi-meet'
project(':react-native-jitsi-meet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-jitsi-meet/android')
include ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
include ':react-native-android-sms-listener'
project(':react-native-android-sms-listener').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-sms-listener/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-media-meta'
project(':react-native-media-meta').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-media-meta/android')
include ':react-native-share-menu'
project(':react-native-share-menu').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share-menu/android')
include ':react-native-notifications'
project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':react-native-tcp'
......
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.8.0</string>
<string>3.9.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
......
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>3.8.0</string>
<string>3.9.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
......
This diff is collapsed.
......@@ -10,17 +10,22 @@
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#if __has_include(<React/RNSentry.h>)
#import <React/RNSentry.h> // This is used for versions of react >= 0.40
#else
#import "RNSentry.h" // This is used for versions of react < 0.40
#endif
#import <React/RCTLinkingManager.h>
#import <CodePush/CodePush.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"Minds"
initialProperties:nil];
[RNSentry installWithRootView:rootView];
rootView.backgroundColor = [UIColor whiteColor];
......@@ -31,6 +36,7 @@
rootViewController.view = rootView;
self.window.rootViewController = navigationController;
[self.window makeKeyAndVisible];
[RNNotifications startMonitorNotifications];
return YES;
}
......@@ -39,7 +45,7 @@
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [CodePush bundleURL];
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
// return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
......@@ -61,13 +67,7 @@
// Required to register for notifications
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
[RNNotifications didRegisterUserNotificationSettings:notificationSettings];
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
......@@ -75,15 +75,4 @@
[RNNotifications didFailToRegisterForRemoteNotificationsWithError:error];
}
// Required for the notification event.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification {
[RNNotifications didReceiveRemoteNotification:notification];
}
// Required for the localNotification event.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
[RNNotifications didReceiveLocalNotification:notification];
}
@end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Minds</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleGetInfoString</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.minds.mobile</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.8.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>minds</string>
<key>CFBundleURLSchemes</key>
<array>
<string>minds</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>5</string>
<key>CodePushDeploymentKey</key>
<string>_C083_CqL7CmKwASrv6Xrj1wqH7erJMhIBnRQ</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>dev.minds.io</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>$(PRODUCT_NAME) need access to the calendar to schedule your gatherings</string>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to use your camera in order to upload images or videos</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your microphone in order to record videos </string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your Photo Library in order to upload images</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) would like access to your photo gallery so that you can upload images</string>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>
<string>EvilIcons.ttf</string>
<string>Feather.ttf</string>
<string>FontAwesome.ttf</string>
<string>Foundation.ttf</string>
<string>Ionicons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>Octicons.ttf</string>
<string>SimpleLineIcons.ttf</string>
<string>Zocial.ttf</string>
<string>Roboto-Black.ttf</string>
<string>Roboto-BlackItalic.ttf</string>
<string>Roboto-Bold.ttf</string>
<string>Roboto-BoldItalic.ttf</string>
<string>Roboto-Italic.ttf</string>
<string>Roboto-Light.ttf</string>
<string>Roboto-LightItalic.ttf</string>
<string>Roboto-Medium.ttf</string>
<string>Roboto-MediumItalic.ttf</string>
<string>Roboto-Regular.ttf</string>
<string>Roboto-Thin.ttf</string>
<string>Roboto-ThinItalic.ttf</string>
<string>AntDesign.ttf</string>
<string>FontAwesome5_Brands.ttf</string>
<string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome5_Solid.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Minds</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleGetInfoString</key>
<string/>
<key>CFBundleIdentifier</key>
<string>com.minds.mobile</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.9.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>minds</string>
<key>CFBundleURLSchemes</key>
<array>
<string>minds</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>5</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>dev.minds.io</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>$(PRODUCT_NAME) need access to the calendar to schedule your gatherings</string>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to use your camera in order to upload images or videos</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string/>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your microphone in order to record videos </string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your Photo Library in order to upload images</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) would like access to your photo gallery so that you can upload images</string>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>
<string>EvilIcons.ttf</string>
<string>Feather.ttf</string>
<string>FontAwesome.ttf</string>
<string>Foundation.ttf</string>
<string>Ionicons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>Octicons.ttf</string>
<string>SimpleLineIcons.ttf</string>
<string>Zocial.ttf</string>
<string>Roboto-Black.ttf</string>
<string>Roboto-BlackItalic.ttf</string>
<string>Roboto-Bold.ttf</string>
<string>Roboto-BoldItalic.ttf</string>
<string>Roboto-Italic.ttf</string>
<string>Roboto-Light.ttf</string>
<string>Roboto-LightItalic.ttf</string>
<string>Roboto-Medium.ttf</string>
<string>Roboto-MediumItalic.ttf</string>
<string>Roboto-Regular.ttf</string>
<string>Roboto-Thin.ttf</string>
<string>Roboto-ThinItalic.ttf</string>
<string>AntDesign.ttf</string>
<string>FontAwesome5_Brands.ttf</string>
<string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome5_Solid.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>3.8.0</string>
<string>3.9.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
......
......@@ -27,6 +27,7 @@ platform :ios do
desc "Build release"
lane :buildrelease do
sh("echo $SENTRY_IOS_PROPERTIES | base64 --decode > ../sentry.properties")
match(type: "appstore")
increment_build_number(xcodeproj: "Minds.xcodeproj", build_number: latest_testflight_build_number + 1)
build_app(scheme: "Minds")
......
......@@ -234,7 +234,9 @@
"removeOwner":"Remove as Owner",
"makeModerator":"Make Moderator",
"removeModerator":"Remove as Moderator",
"listMembersCount":"Members {{count}}"
"listMembersCount":"Members {{count}}",
"disableConversations":"Disable conversations",
"enableConversations":"Enable conversations"
},
"keychain": {
"unlockMessage": "Unlock {{keychain}} keychain",
......@@ -318,7 +320,10 @@
"postedIn":"posted in",
"channel":"channel",
"deleted":"This post was deleted",
"dontHave":"You don't have any notifications"
"dontHave":"You don't have any notifications",
"rewardsStateIncrease": "Congratulations! You just became a {{state}} user.\nWe've increased your daily token reward multiplier to {{multiplier}}x",
"rewardsStateDecrease": "We miss you! You just dropped down to a {{state}} user.\nYour daily token reward multiplier has been reduced to {{multiplier}}x",
"rewardsSummary": "You earned {{amount}} tokens yesterday."
},
"notificationSettings":{
"enableDisable":"Enable and disable push notifications",
......@@ -515,6 +520,7 @@
"estimatedReward":"Estimated Reward",
"yourShare":"Your Share",
"yourScore":"Your Score",
"yourRewardFactor": "Your Reward Multiplier",
"networkScore":"Network Score",
"transactionsTitle":"Transactions",
"transactionsDescription":"A list of transactions you have made with your addresses",
......@@ -630,7 +636,7 @@
"readMore":"Read More",
"showLess":"Show less",
"featureUnavailablePlatform":"This feature is currently unavailable on your platform",
"continue":"Contiue",
"continue":"Continue",
"errorDisplaying":"Error displaying the content",
"tapCopyError":"Tap to copy the error",
"save":"Save",
......@@ -725,6 +731,8 @@
"wantToUpdate":"Do you want to update the app?",
"updateAvailable":"Update available",
"rememberTomorrow":"Remind me later",
"enableComments": "Enable comments",
"disableComments": "Disable comments",
"noInternet":"No Internet Connection",
"offline":"Offline",
"cantReachServer":"Can't reach the server",
......
......@@ -217,7 +217,9 @@
"banConfirm": "Estás seguro? Quieres suspender a este usuario?",
"errorLoading": "Error cargando grupos",
"confirmKick": "Estás seguro? Quieres expulsar a este usuario?",
"listMembersCount": "Miembros {{count}}"
"listMembersCount": "Miembros",
"disableConversations":"Inhabilitar conversaciones",
"enableConversations":"Habilitar conversaciones"
},
"keychain": {
"unlockMessage": "Desbloquear {{keychain}} keychain",
......@@ -746,6 +748,8 @@
"wantToUpdate": "Quieres actualizar la aplicación? ",
"updateAvailable": "Actualización disponible",
"rememberTomorrow": "Recuerdame más tarde",
"enableComments": "Habilitar comentarios",
"disableComments": "Inhabilitar comentarios",
"noInternet": "Sin conexión a internet",
"offline": "Fuera de línea",
"cantReachServer": "No puedo conectarme al servidor",
......
......@@ -5,6 +5,7 @@
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"validate-locales": "node tasks/validate-locales.js",
"update-settings": "ts-node tasks/update-settings.js",
"test": "jest",
"locale": "ts-node tasks/poeditor.js",
"e2e": "jest -c jest.e2e.config.js",
......@@ -49,16 +50,15 @@
"react": "16.8.6",
"react-native": "0.59.10",
"react-native-actionsheet": "^2.3.0",
"react-native-android-sms-listener": "^0.7.0",
"react-native-animatable": "^1.3.0",
"react-native-background-timer": "^2.1.1",
"react-native-code-push": "^5.5.2",
"react-native-collapsible-header-views": "^1.0.2",
"react-native-cookies": "^3.3.0",
"react-native-crypto": "^2.1.0",
"react-native-device-info": "^2.3.2",
"react-native-device-log": "Minds/react-native-device-log#74f06b09c6656aa228a9a3a474c714d82abf509e",
"react-native-elements": "^0.19.1",
"react-native-exception-handler": "^2.10.7",
"react-native-exception-handler": "^2.10.8",
"react-native-exit-app": "^1.0.0",
"react-native-fast-image": "^5.4.2",
"react-native-file-type": "^0.0.8",
......@@ -77,13 +77,14 @@
"react-native-minds-encryption": "https://github.com/Minds/react-native-minds-encryption",
"react-native-modal": "^9.0.0",
"react-native-modal-datetime-picker": "^6.0.0",
"react-native-notifications": "Minds/react-native-notifications#357fed9aad65b90e3ad02448fe450ce629f8473e",
"react-native-notifications": "Minds/react-native-notifications#333e376f866a805f250cc68706d97ace503b948a",
"react-native-os": "^1.2.1",
"react-native-phone-input": "thegamenicorus/react-native-phone-input#6ab1a91a09ddd4272fe85e30ecedf092f0fcb1a7",
"react-native-photo-view": "alwx/react-native-photo-view#e28f5416574cbfa07b2d4fa862c0048df56f7b02",
"react-native-progress": "^3.4.0",
"react-native-qrcode": "^0.2.6",
"react-native-randombytes": "^3.4.0",
"react-native-sentry": "^0.43.2",
"react-native-share": "1.1.2",
"react-native-share-menu": "Minds/react-native-share-menu",
"react-native-simple-radio-button": "^2.7.1",
......
......@@ -15,6 +15,7 @@ configure({ adapter: new Adapter() });
jest.mock('react-native-localize');
jest.mock('mobx-react/native', () => require('mobx-react/custom'));
jest.mock('./AppStores');
jest.useFakeTimers();
......
......@@ -7,7 +7,7 @@ import * as Animatable from 'react-native-animatable';
import {
View,
Text,
TextInput,
// TextInput,
StyleSheet,
KeyboardAvoidingView,
} from 'react-native';
......@@ -25,6 +25,9 @@ import testID from '../common/helpers/testID';
import logService from '../common/services/log.service';
import ModalPicker from '../common/components/ModalPicker';
// workaround for android copy/paste issue
import TextInput from '../common/components/TextInput';
/**
* Login Form
*/
......
......@@ -21,6 +21,8 @@ import {
Alert,
} from 'react-native';
import ActionSheet from 'react-native-actionsheet';
import { Header } from 'react-navigation';
import { inject, observer } from 'mobx-react/native';
......@@ -44,6 +46,7 @@ import CommentList from '../comments/CommentList';
import CenteredLoading from '../common/components/CenteredLoading';
import logService from '../common/services/log.service';
import i18n from '../common/services/i18n.service';
import featuresService from '../common/services/features.service';
/**
* Blog View Screen
......@@ -142,10 +145,18 @@ export default class BlogsViewScreen extends Component {
</View>
)
const image = blog.getBannerSource();
const actionSheet = this.getActionSheet();
const optMenu = featuresService.has('allow-comments-toggle') ?
(<View style={styles.rightToolbar}>
<Icon name="more-vert" onPress={() => this.showActionSheet()} size={26} style={styles.icon}/>
{actionSheet}
</View>) : (null);
return (
<View style={styles.screen}>
<FastImage source={image} resizeMode={FastImage.resizeMode.cover} style={styles.image} />
<Text style={styles.title}>{blog.title}</Text>
{optMenu}
<View style={styles.ownerBlockContainer}>
<OwnerBlock entity={blog} navigation={this.props.navigation} rightToolbar={actions}>
<Text style={styles.timestamp}>{formatDate(blog.time_created)}</Text>
......@@ -170,6 +181,51 @@ export default class BlogsViewScreen extends Component {
)
}
getActionSheet() {
let options = [ i18n.t('cancel') ];
options.push(this.props.blogsView.blog.allow_comments ? i18n.t('disableComments') : i18n.t('enableComments'));
return (
<ActionSheet
ref={o => this.ActionSheet = o}
options={options}
onPress={ (i) => { this.handleActionSheetSelection(options[i]) }}
cancelButtonIndex={0}
/>
)
}
async showActionSheet() {
this.ActionSheet.show();
}
async handleActionSheetSelection(option) {
switch(option) {
case i18n.t('disableComments'):
case i18n.t('enableComments'):
try {
await this.props.blogsView.blog.toggleAllowComments();
} catch (err) {
console.error(err);
this.showError();
}
}
}
/**
* Show an error message
*/
showError() {
Alert.alert(
i18n.t('sorry'),
i18n.t('errorMessage') + '\n' + i18n.t('activity.tryAgain'),
[
{text: i18n.t('ok'), onPress: () => {}},
],
{ cancelable: false }
);
}
/**
* Render
*/
......@@ -314,4 +370,12 @@ const styles = StyleSheet.create({
color: '#888',
fontSize: 10,
},
rightToolbar: {
alignSelf: 'flex-end',
bottom: 35,
right: 10
},
icon: {
color: '#888',
},
});
......@@ -47,6 +47,10 @@ export default class Boost extends Component {
renderEntity() {
const entity = this.props.boost.entity;
if (!entity) {
return null;
}
switch (entity.type) {
case 'activity':
return <Activity entity={ActivityModel.create(entity)} hideTabs={true} navigation={this.props.navigation} />;
......
import _ from 'lodash';
import React, {
PureComponent
} from 'react';
import {
Text,
StyleSheet,
CameraRoll,
ActivityIndicator,
TouchableOpacity,
InteractionManager,
Image,
View,
FlatList,
......@@ -21,7 +16,6 @@ import Icon from 'react-native-vector-icons/Ionicons';
import { Button } from 'react-native-elements';
import CenteredLoading from '../common/components/CenteredLoading'
import CaptureTabs from './CaptureTabs';
import androidPermissionsService from '../common/services/android-permissions.service';
import testID from '../common/helpers/testID';
import logService from '../common/services/log.service';
......@@ -31,12 +25,14 @@ import logService from '../common/services/log.service';
*/
export default class CaptureGallery extends PureComponent {
listRef = null;
state = {
header: null,
photos: [],
imageUri: '',
isPosting: false,
imagesLoaded: false,
offset: '',
hasMore: true,
loading: false
}
static navigationOptions = {
......@@ -64,26 +60,36 @@ export default class CaptureGallery extends PureComponent {
/**
* Load photos
*/
_loadPhotos() {
_loadPhotos = async() => {
if (this.state.loading || !this.state.hasMore) {
return;
}
const params = {
first: 30,
assetType: 'All',
}
this.setState({loading: true});
if (Platform.OS === 'ios') params.groupTypes = 'All';
if (this.state.offset) params.after = this.state.offset;
try {
const result = await CameraRoll.getPhotos(params);
CameraRoll.getPhotos(params)
.then(r => {
this.setState({
imagesLoaded: true,
photos: r.edges,
navigation: r.page_info,
});
})
.catch((err) => {
logService.exception('[CaptureGallery] loadPhotos', err)
//Error Loading Images
this.setState({
imagesLoaded: true,
photos: this.state.photos.concat(result.edges),
offset: result.page_info.end_cursor,
hasMore: result.page_info.has_next_page,
loading: false
});
} catch (err) {
logService.exception('[CaptureGallery] loadPhotos', err)
this.setState({loading: false});
}
}
/**
......@@ -100,28 +106,37 @@ export default class CaptureGallery extends PureComponent {
*/
render() {
const body = this.state.imagesLoaded ?
_.chunk(this.state.photos.map((p, i) => this.renderTile(p, i)), 3)
.map((c, i) => <View style={styles.row} key={i}>{c}</View>)
: <CenteredLoading />
if (!this.state.imagesLoaded) {
return <CenteredLoading />
}
return (
<View>
<CaptureTabs onSelectedMedia={this.onSelected} />
{body}
</View>
<FlatList
ref={this.setListRef}
ListHeaderComponent={this.props.header}
data={this.state.photos}
renderItem={this.renderTile}
onEndReached={this._loadPhotos}
ListFooterComponent={this.state.loading ? <CenteredLoading /> : null}
numColumns={3}
/>
)
}
/**
* Sets List reference
*/
setListRef = ref => this.listRef = ref;
/**
* render list tile
*/
renderTile = (item, index) => {
const node = item.node;
renderTile = (item) => {
const node = item.item.node;
return (
<TouchableOpacity
style={styles.tileImage}
key={index}
key={item.index}
onPress={() => {
this.onSelected({
uri: node.image.uri,
......@@ -132,7 +147,7 @@ export default class CaptureGallery extends PureComponent {
})
}
}
{...testID(`Gallery Image ${index}`)}
{...testID(`Gallery ${node.type}`)}
>
<Image
source={{ uri : node.image.uri }}
......@@ -142,7 +157,13 @@ export default class CaptureGallery extends PureComponent {
);
}
/**
* On media selected
*/
onSelected = (response) => {
// scroll to top on selection
this.listRef.scrollToOffset({x: 0, y: 0, animated: true});
this.props.onSelected(response);
}
......
......@@ -3,13 +3,8 @@ import {
StyleSheet,
View,
ScrollView,
TextInput,
Text,
Alert,
Button,
TouchableHighlight,
TouchableOpacity,
ActivityIndicator
Alert
} from 'react-native';
import { observer, inject } from 'mobx-react/native';
......@@ -38,6 +33,10 @@ import testID from '../common/helpers/testID';
import logService from '../common/services/log.service';
import i18n from '../common/services/i18n.service';
import settingsStore from '../settings/SettingsStore';
import CaptureTabs from './CaptureTabs';
// workaround for android copy/paste
import TextInput from '../common/components/TextInput';
@inject('user', 'capture')
@observer
......@@ -103,6 +102,9 @@ export default class CapturePoster extends Component {
this.loadNsfwFromPersistentStorage();
}
/**
* Load last saved nsfw values
*/
async loadNsfwFromPersistentStorage() {
this.setState({
nsfw: settingsStore.creatorNsfw,
......@@ -162,10 +164,71 @@ export default class CapturePoster extends Component {
this.setState({selection: event.nativeEvent.selection});
}
/**
* Get header
*
* @param {boolean} showAttachmentFeatures
*/
getHeader(showAttachmentFeatures = false) {
return (
<React.Fragment>
{this.showContext()}
<View style={styles.posterWrapper}>
<TextInput
style={styles.poster}
editable={true}
placeholder={i18n.t('capture.placeholder')}
placeholderTextColor='#ccc'
underlineColorAndroid='transparent'
onChangeText={this.setText}
value={this.state.text}
multiline={true}
selectTextOnFocus={false}
onSelectionChange={this.onSelectionChanges}
{...testID('PostInput')}
/>
</View>
{showAttachmentFeatures && this.getAttachFeature()}
</React.Fragment>
)
}
/**
* Render
*/
render() {
const params = this.props.navigation.state.params || {};
return params.isRemind ? this.renderRemind() : this.renderNormal();
}
/**
* Screen content for poster
*/
renderNormal() {
const navigation = this.props.navigation;
const params = navigation.state.params || {};
return (
<View style={CS.flexContainer}>
<CaptureGallery
onSelected={this.onAttachedMedia}
header={this.getHeader(true)}
/>
<UserAutocomplete
text={this.props.capture.text}
selection={this.state.selection}
onSelect={this.onSelectTag}
/>
</View>
);
}
/**
* Screen content for remind
*/
renderRemind() {
const text = this.props.capture.text;
const navigation = this.props.navigation;
......@@ -173,24 +236,9 @@ export default class CapturePoster extends Component {
return (
<View style={CS.flexContainer}>
<ScrollView style={styles.posterAndPreviewWrapper} keyboardShouldPersistTaps={'always'}>
{this.showContext()}
<View style={styles.posterWrapper} pointerEvents="box-none">
<TextInput
style={styles.poster}
editable={true}
placeholder={i18n.t('capture.placeholder')}
placeholderTextColor='#ccc'
underlineColorAndroid='transparent'
onChangeText={this.setText}
value={text}
multiline={true}
selectTextOnFocus={false}
onSelectionChange={this.onSelectionChanges}
{...testID('PostInput')}
/>
</View>
{!params.isRemind ? this.getAttachFeature() : this.getRemind()}
<ScrollView style={styles.posterAndPreviewWrapper} keyboardShouldPersistTaps={'always'} removeClippedSubviews={false}>
{this.getHeader()}
{this.getRemind()}
</ScrollView>
<UserAutocomplete
text={text}
......@@ -243,10 +291,7 @@ export default class CapturePoster extends Component {
/>
<Icon raised name="md-close" type="ionicon" color='#fff' size={22} containerStyle={styles.deleteAttachment} onPress={() => this.deleteAttachment()} {...testID('Attachment Delete Button')} />
</View>}
<CaptureGallery
onSelected={this.onAttachedMedia}
/>
<CaptureTabs onSelectedMedia={this.onAttachedMedia} />
</React.Fragment>
);
}
......@@ -283,6 +328,9 @@ export default class CapturePoster extends Component {
}
}
/**
* Create a remind
*/
async remind() {
const { params } = this.props.navigation.state;
const message = this.props.capture.text;
......@@ -310,7 +358,7 @@ export default class CapturePoster extends Component {
}
/**
* Submit
* Submit post
*/
async submit() {
const attachment = this.props.capture.attachment;
......@@ -400,21 +448,29 @@ export default class CapturePoster extends Component {
}
}
/**
* Set text
* @param {string} text
*/
setText = (text) => {
this.props.capture.setText(text);
this.props.capture.embed.richEmbedCheck(text);
};
/**
* On mature value change
*/
onMature = () => {
const mature = !this.state.mature;
this.setState({ mature });
}
/**
* On nsfw value change
*/
onNsfw = values => {
const nsfw = [...values];
this.setState({ nsfw });
creatorNsfwService.set(nsfw);
}
onShare = network => {
......
......@@ -44,7 +44,7 @@ export default class ChannelFeedStore {
constructor(guid) {
this.guid = guid;
this.buildStores();
this.feedStore = new FeedStore(true);
}
get esFeedfilter () {
......@@ -56,68 +56,11 @@ export default class ChannelFeedStore {
}
}
buildStores() {
// TODO: remove this when es-feeds is in production
this.stores = {
feed: {
list: new OffsetFeedListStore('shallow', true),
},
images: {
list: new OffsetFeedListStore('shallow'),
isTiled: true,
},
videos: {
list: new OffsetFeedListStore('shallow'),
isTiled: true,
},
blogs: {
list: new OffsetFeedListStore('shallow'),
isTiled: false,
},
};
extendObservable(this.stores.feed,{
loading: false
});
extendObservable(this.stores.images,{
loading: false
});
extendObservable(this.stores.videos,{
loading: false
});
extendObservable(this.stores.blogs,{
loading: false
});
if (featuresService.has('es-feeds')) {
this.feedStore = new FeedStore(true);
}
}
@action
setChannel(channel) {
this.channel = channel;
}
get store() {
return this.stores[this.filter]
}
get list() {
return this.stores[this.filter].list;
}
set list(value) {
this.stores[this.filter] = value;
}
get loading() {
return this.stores[this.filter].loading;
}
get isTiled() {
return this.stores[this.filter].isTiled;
}
/**
* Set channel guid
* @param {string} guid
......@@ -143,169 +86,17 @@ export default class ChannelFeedStore {
*/
async loadFeed(refresh = false) {
if (featuresService.has('es-feeds')) {
if (refresh) this.feedStore.clear();
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setLimit(12)
.fetchRemoteOrLocal();
return;
}
if (this.list.cantLoadMore() || this.loading) {
return Promise.resolve();
}
switch (this.filter) {
case 'feed':
await this._loadFeed(refresh);
break;
case 'images':
await this._loadImagesFeed(refresh);
break;
case 'videos':
await this._loadVideosFeed(refresh);
break;
case 'blogs':
await this._loadBlogsFeed(refresh);
break;
}
}
/**
* Load channel feed
*/
async _loadFeed(refresh = false) {
// reference the store because it may change after the await
const store = this.store;
if (!this.channel || store.list.cantLoadMore()) {
return;
}
store.loading = true;
store.list.setErrorLoading(false);
try {
let opts = {
offset: store.list.offset,
limit: 12,
};
if (
this.channel.pinned_posts
&& this.channel.pinned_posts.length
&& !store.list.offset
) {
opts.pinned = this.channel.pinned_posts.join(',');
}
const feed = await channelService.getFeed(this.channel.guid, opts);
if (feed.entities.length > 0) {
feed.entities = ActivityModel.createMany(feed.entities);
this.assignRowKeys(feed);
}
this.list.setList(feed, refresh);
} catch (err) {
// ignore aborts
if (err.code === 'Abort') return;
store.list.setErrorLoading(true);
if (!isNetworkFail(err)) {
logService.exception('[ChannelFeedStore] _loadFeed', err);
}
} finally {
store.loading = false;
}
}
/**
* Load channel images feed
*/
async _loadImagesFeed(refresh = false) {
// reference the store because it may change after the await
const store = this.store;
store.loading = true;
store.list.setErrorLoading(false);
try {
const feed = await channelService.getImageFeed(this.guid, this.list.offset);
feed.entities = ActivityModel.createMany(feed.entities);
this.assignRowKeys(feed, store);
store.list.setList(feed, refresh);
} catch (err) {
// ignore aborts
if (err.code === 'Abort') return;
store.list.setErrorLoading(true);
if (!isNetworkFail(err)) {
logService.exception('[ChannelFeedStore] _loadImagesFeed', err);
}
} finally {
store.loading = false;
}
}
/**
* Load channel videos feed
*/
async _loadVideosFeed(refresh = false) {
// reference the store because it may change after the await
const store = this.store;
store.loading = true;
store.list.setErrorLoading(false);
try {
const feed = await channelService.getVideoFeed(this.guid, this.list.offset);
feed.entities = ActivityModel.createMany(feed.entities);
this.assignRowKeys(feed, store);
store.list.setList(feed, refresh);
} catch (err) {
// ignore aborts
if (err.code === 'Abort') return;
if (!isNetworkFail(err)) {
logService.exception('[ChannelFeedStore] _loadVideosFeed', err);
}
store.list.setErrorLoading(true);
} finally {
store.loading = false;
}
}
/**
* Load channel videos feed
*/
async _loadBlogsFeed(refresh) {
// reference the store because it may change after the await
const store = this.store;
if (refresh) this.feedStore.clear();
store.loading = true;
store.list.setErrorLoading(false);
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setLimit(12)
.fetchRemoteOrLocal();
try {
const feed = await channelService.getBlogFeed(this.guid, this.list.offset);
if (store.list.offset) {
feed.entities.shift();
}
feed.entities = BlogModel.createMany(feed.entities);
this.assignRowKeys(feed, store);
store.list.setList(feed, refresh);
} catch (err) {
store.list.setErrorLoading(true);
if (!isNetworkFail(err)) {
logService.exception('[ChannelFeedStore] _loadBlogsFeed', err);
}
} finally {
store.loading = false;
}
return;
}
@action
clearFeed() {
this.list.clearList();
this.isTiled = false;
this.filter = 'feed';
this.showrewards = false;
this.feedStore.clear();
......@@ -318,33 +109,22 @@ export default class ChannelFeedStore {
return;
}
if (featuresService.has('es-feeds')) {
this.feedStore.clear();
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setLimit(12)
.fetchRemoteOrLocal();
this.feedStore.clear();
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setLimit(12)
.fetchRemoteOrLocal();
return;
}
return;
// reference because it could change after the await
const list = this.list;
//this.list.refresh();
list.clearList();
await this.loadFeed(true);
list.refreshDone();
}
@action
setFilter(filter) {
this.filter = filter;
if (featuresService.has('es-feeds')) {
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setIsTiled(filter === 'images' || filter === 'videos')
.clear()
.fetchRemoteOrLocal();
} else {
this.refresh();
}
this.feedStore.setEndpoint(`api/v2/feeds/container/${this.guid}/${this.esFeedfilter}`)
.setIsTiled(filter === 'images' || filter === 'videos')
.clear()
.fetchRemoteOrLocal();
}
}
......@@ -22,7 +22,6 @@ import { Icon } from 'react-native-elements'
import RewardsCarousel from './carousel/RewardsCarousel';
import ChannelHeader from './header/ChannelHeader';
import Toolbar from './toolbar/Toolbar';
import NewsfeedList from '../newsfeed/NewsfeedList';
import CenteredLoading from '../common/components/CenteredLoading';
import Button from '../common/components/Button';
import colors from '../styles/Colors';
......@@ -66,7 +65,7 @@ export default class ChannelScreen extends Component {
const store = this.props.channel.store(this.guid);
if (params && params.prepend) {
if (store.channel && store.channel.isOwner && store.channel.isOwner()) {
store.feedStore.stores.feed.list.prepend(params.prepend);
store.feedStore.feedStore.prepend(params.prepend);
}
// we clear the parameter to prevent prepend it again on goBack
this.props.navigation.setParams({prepend: null});
......@@ -259,26 +258,18 @@ export default class ChannelScreen extends Component {
const emptyRender = () => <View />;
const list = featuresService.has('es-feeds') ?
<FeedList
feedStore={feed.feedStore}
renderActivity={renderActivity}
header={header}
navigation={this.props.navigation}
emptyMessage={emptyMessage}
/> :
<NewsfeedList
newsfeed={feed}
renderActivity={renderActivity}
header={header}
navigation={this.props.navigation}
emptyMessage={emptyMessage}
/>;
return (
<View style={CommonStyle.flexContainer}>
{!channel.blocked && list}
{!channel.blocked &&
<FeedList
feedStore={feed.feedStore}
renderActivity={renderActivity}
header={header}
navigation={this.props.navigation}
emptyMessage={emptyMessage}
/>}
{/* Not using FlatList breaks header layout */}
{channel.blocked && <FlatList
......
......@@ -9,6 +9,7 @@ import wireService from '../wire/WireService';
import ModelStorageList from '../common/ModelStorageList';
import logService from '../common/services/log.service';
import channelsService from '../common/services/channels.service';
import UserModel from './UserModel';
/**
* Channel Stores
......@@ -49,12 +50,26 @@ class ChannelStores {
}
}
/**
* Add a visited channel to the list
* if the channel is already in the list it moves it to the top
* @param {UserModel} channel
*/
async addVisited(channel) {
result = await this.lastVisited.unshift(channel);
// if it already exist we move it to the beggining
if (result == -1) this.lastVisited.moveFirst(channel.guid);
}
/**
* Get latest visited channels
* @param {number} count
*/
async getVisited(count) {
const result = await this.lastVisited.first(count);
return UserModel.createMany(result);
}
@action
reset() {
this.lastVisited.clear();
......
......@@ -14,6 +14,17 @@ export default class UserModel extends BaseModel {
* @var boolean
*/
@observable blocked;
/**
* @var integer
*/
@observable subscribers_count;
/**
* @var integer
*/
@observable impressions;
/**
* @var boolean
*/
......
......@@ -9,7 +9,7 @@ import {
FlatList,
Keyboard,
Platform,
TextInput,
// TextInput,
TouchableOpacity,
TouchableHighlight,
ActivityIndicator,
......@@ -38,6 +38,9 @@ import i18n from '../common/services/i18n.service';
import blockListService from '../common/services/block-list.service';
import autobind from "../common/helpers/autobind";
// workaround for android copy/paste issue
import TextInput from '../common/components/TextInput';
// types
type Props = {
header?: any,
......@@ -313,7 +316,7 @@ export default class CommentList extends React.Component<Props, State> {
* Render poster
*/
renderPoster() {
if (this.state.hideInput) return null;
if (this.state.hideInput || (!this.props.entity.allow_comments && this.props.entity.type !== "group")) return null;
const attachment = this.props.store.attachment;
......
......@@ -112,4 +112,15 @@ export function updateComment(guid, description) {
return api.post(`api/v1/comments/update/${guid}`, {
description: description
});
}
/**
* Enable/Disable comments
* @param {string} guid
* @param {boolean} state
*/
export function toggleAllowComments(guid, state) {
return api.post(`api/v2/permissions/comments/${guid}`,{
allowed: state
});
}
\ No newline at end of file
......@@ -14,11 +14,18 @@ import logService from './services/log.service';
import channelService from '../channel/ChannelService';
import { revokeBoost, acceptBoost, rejectBoost } from '../boost/BoostService';
import { toggleAllowComments as toggleAllow } from '../comments/CommentsService';
/**
* Base model
*/
export default class BaseModel {
/**
* Enable/Disable comments
*/
@observable allow_comments = true;
/**
* List reference (if the entity belongs to one)
* @var {OffsetListStore}
......@@ -261,4 +268,10 @@ export default class BaseModel {
throw err;
}
}
@action
async toggleAllowComments() {
const data = await toggleAllow(this.guid, !this.allow_comments);
this.allow_comments = !this.allow_comments;
}
}
\ No newline at end of file
......@@ -65,8 +65,10 @@ export default class ModelStorageList {
const guid = this.index.pop();
await AsyncStorage.removeItem(this._getKeyGuid(guid));
}
const list = model.__list;
delete(model.__list);
await this._persist(model);
model._list = list;
return 0;
}
......
......@@ -198,7 +198,7 @@ export default class MediaView extends Component {
source={source}
entity={this.props.entity}
style={[styles.image, { height }]}
loadingIndicator="placeholder"
// loadingIndicator="placeholder"
onError={this.imageError}
imageStyle={styles.innerImage}
/>
......@@ -229,7 +229,7 @@ export default class MediaView extends Component {
source={source}
entity={this.props.entity}
style={styles.image}
loadingIndicator="placeholder"
// loadingIndicator="placeholder"
onError={this.imageError}
imageStyle={styles.innerImage}
/>
......@@ -330,7 +330,7 @@ const styles = StyleSheet.create({
flex: 1,
},
innerImage: {
backgroundColor: 'transparent'
backgroundColor: 'black'
},
videoContainer: {
flex: 1,
......
......@@ -73,6 +73,8 @@ export default class TagInput extends Component {
render() {
let tags = null;
const autoFocus = this.props.noAutofocus ? false : true;
const ViewCmp = this.props.noScroll ? View : ScrollView;
if (!this.props.hideTags) {
tags = <View style={styles.tagContainer}>
{this.props.tags.map((t,i) => <View style={styles.tag} key={i} >
......@@ -82,14 +84,14 @@ export default class TagInput extends Component {
</View>
}
return (
<ScrollView keyboardShouldPersistTaps={'always'}>
<ViewCmp keyboardShouldPersistTaps={'always'}>
{tags}
{this.state.error ? <Text style={styles.error}>{this.state.error}</Text> : null}
<TextInput
autoCapitalize="none"
autoFocus={autoFocus}
style={{height: 35, width: '100%', borderColor: '#ccc', borderBottomWidth: 1, padding: 10}}
ref={r => this.inputRef = r}
style={styles.input}
ref={this.setInputRef}
value={this.state.text}
blurOnSubmit={false}
onChangeText={this.onChangeText}
......@@ -99,14 +101,27 @@ export default class TagInput extends Component {
onSubmitEditing={this.addTag}
onEndEditing={this.addTag}
/>
</ScrollView>
</ViewCmp>
);
}
/**
* Set input ref
* @param {TextInputRef} r
*/
setInputRef = r => this.inputRef = r;
}
const styles = StyleSheet.create({
input: {
height: 35,
width: '100%',
borderColor: '#ccc',
borderBottomWidth: 1,
padding: 10
},
error: {
fontFamily: 'Roboto',
color: 'red',
......
......@@ -112,6 +112,7 @@ export default class TagOptinDrawer extends Component {
</ScrollView>
<View style={styles.inputContainer}>
<TagInput
noScroll
noAutofocus={true}
hideTags={true}
tags={this.props.hashtag.suggested.map(m => m.value)}
......
import React, { Component } from 'react';
import { TextInput } from 'react-native';
/**
* Workaround for copy/paste issue on android
* https://github.com/facebook/react-native/issues/20887
*/
export default class AlternativeTextInput extends React.Component<Props, State> {
static defaultProps = {
editable: true,
}
constructor(props) {
super(props);
this.state = {
editable: !props.editable
};
}
componentDidMount() {
if (this.props.editable) {
setTimeout(() => {
this.setState({ editable: true });
}, 100);
}
}
render() {
const { editable } = this.state;
return <TextInput {...this.props} editable={editable} />;
}
}
\ No newline at end of file
export default {
load: jest.fn(),
fetch: jest.fn(),
}
\ No newline at end of file
export default {
getFromCache : jest.fn(),
garbageCollector : jest.fn(),
deleteFromCache : jest.fn(),
getFromFeed : jest.fn(),
single : jest.fn(),
fetch : jest.fn(),
addEntity : jest.fn(),
mapToModel : jest.fn(),
}
\ No newline at end of file
const feedService = function() {
this.getEntities = jest.fn();
this.prepend = jest.fn();
this.setInjectBoost = jest.fn().mockImplementation(() => this);
this.setLimit = jest.fn().mockImplementation(() => this);
this.setOffset = jest.fn().mockImplementation(() => this);
this.setEndpoint = jest.fn().mockImplementation(() => this);
this.setParams = jest.fn().mockImplementation(() => this);
this.setAsActivities = jest.fn().mockImplementation(() => this);
this.fetch = jest.fn();
this.fetchLocal = jest.fn();
this.fetchRemoteOrLocal = jest.fn();
this.fetchLocalOrRemote = jest.fn();
}
export default feedService;
\ No newline at end of file
export default {
init: jest.fn(),
stop: jest.fn(),
registerToken: jest.fn(),
setBadgeCount: jest.fn(),
setOnInitialNotification: jest.fn(),
handleInitialNotification: jest.fn(),
}
\ No newline at end of file
......@@ -2,5 +2,6 @@ export default {
login: jest.fn(),
logout: jest.fn(),
isLoggedIn: jest.fn(),
setInitialScreen: jest.fn()
setInitialScreen: jest.fn(),
onLogin: jest.fn()
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -46,8 +46,7 @@ class FeaturesService {
* @param {string} feature
*/
has(feature) {
return (typeof this.features[feature] === 'undefined') ||
this.features[feature] === true ||
return this.features[feature] === true ||
(this.features[feature] === 'canary' && sessionService.getUser().canary);
}
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.