...
 
Commits (80)
......@@ -68,9 +68,19 @@ jobs:
- restore_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
# remove detox from CI until is fixed
# - run:
# name: Install detox
# command:
# |
# HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew
# HOMEBREW_NO_AUTO_UPDATE=1 brew install --HEAD applesimutils
# npm install -g detox-cli
# npm install -g detox
# not using a workspace here as Node and Yarn versions
# differ between our macOS executor image and the Docker containers above
- run: yarn install
- run: yarn install --frozen-lockfile
- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
......@@ -107,6 +117,11 @@ jobs:
paths:
- ios/Pods
# remove detox from CI until is fixed
# Run e2e
# - run: detox build -c ios.sim.release
# - run: detox test -c ios.sim.release --cleanup
### TODO- get tests running with fastlane
......
......@@ -36,29 +36,6 @@ build:android:
expire_in: 7 days
when: on_success
e2e:browserstacks:
image: node:10.16.3
stage: e2e
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- yarn install
- export bsAPP=`curl -u "${bsUSER}:${bsKEY}" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@./Minds-$CI_BUILD_REF_SLUG.apk"| grep -o 'bs\:\/\/.*"' | sed 's/.$//'`
- yarn e2e
tags:
- docker
dependencies:
- build:android
only:
refs:
- /^stable-*/
- /^release-*/
- /^feat-*/
- /^test-*/
allow_failure: true
deploy:s3:
image: minds/ci:latest
stage: deploy
......
......@@ -15,6 +15,7 @@ import {
} from 'mobx-react/native'; // import from mobx-react/native instead of mobx-react fix test
import NavigationService from './src/navigation/NavigationService';
import RNBootSplash from "react-native-bootsplash";
import {
BackHandler,
......@@ -24,6 +25,7 @@ import {
Text,
Alert,
Clipboard,
StatusBar,
} from 'react-native';
import FlashMessage from 'react-native-flash-message';
......@@ -63,6 +65,8 @@ import boostedContentService from './src/common/services/boosted-content.service
let deepLinkUrl = '';
const statusBarStyle = Platform.OS === 'ios' ? 'dark-content' : 'default';
// init push service
pushService.init();
......@@ -94,6 +98,10 @@ sessionService.onLogin(async () => {
pushService.registerToken();
logService.info('[App] navigating to initial screen', sessionService.initialScreen);
// hide splash
RNBootSplash.hide({ duration: 250 });
NavigationService.navigate(sessionService.initialScreen);
// check onboarding progress and navigate if necessary
......@@ -135,13 +143,10 @@ sessionService.onLogin(async () => {
//on app logout
sessionService.onLogout(() => {
// clear app badge
badgeService.setUnreadConversations(0);
badgeService.setUnreadNotifications(0);
// clear minds settings
mindsService.clear();
// clear offline cache
entitiesStorage.removeAll();
feedsStorage.removeAll();
......@@ -181,10 +186,14 @@ export default class App extends Component<Props, State> {
}
/**
* On component will mount
* contructor
*/
componentWillMount() {
if (!Text.defaultProps) Text.defaultProps = {};
constructor(props) {
super(props);
if (!Text.defaultProps) {
Text.defaultProps = {};
}
Text.defaultProps.style = {
fontFamily: 'Roboto',
color: '#444',
......@@ -213,6 +222,7 @@ export default class App extends Component<Props, State> {
if (!token) {
logService.info('[App] there is no active session');
RNBootSplash.hide({ duration: 250 });
NavigationService.navigate('Login');
} else {
logService.info('[App] session initialized');
......@@ -289,6 +299,7 @@ export default class App extends Component<Props, State> {
const app = (
<Provider key="app" {...stores}>
<ErrorBoundary message="An error occurred" containerStyle={CS.centered}>
<StatusBar barStyle={statusBarStyle} />
<NavigationStack
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
......
......@@ -39,7 +39,9 @@ if (process.env.JEST_WORKER_ID === undefined) {
return null;
}
// only log api 500 errors
if (isApiError(hint.originalException) && hint.originalException.status < 500) {
if (isApiError(hint.originalException) &&
(isNaN(hint.originalException.status) || hint.originalException.status < 500)
) {
return null;
}
}
......
......@@ -28,14 +28,31 @@
- iOS
- Android
## Building
## Install dependencies
- `yarn install`
- `cd ios && pod install` (iOS only)
## Building
- `yarn android` or `yarn ios`
## Testing
- `yarn test`
## Testing e2e (macOS)
Install the detox cli
- `brew tap wix/brew`
- `brew install applesimutils`
- `yarn global add detox-cli`
Run the tests
- `detox build -c ios.sim.debug`
- `detox test -c ios.sim.debug`
You can use -c ios.sim.release for e2e test a production build
### _Copyright Minds 2018_
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// should ask for permissions
const permmision = await driver.waitForElementById('com.android.packageinstaller:id/permission_allow_button', wd.asserters.isDisplayed, 10000)
// we accept
permmision.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// select first image
const firstImage = await driver.waitForElementByAccessibilityId('Gallery image/jpeg', wd.asserters.isDisplayed, 5000);
await firstImage.click();
await sleep(3000);
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, amount) => {
const lockButton = await driver.waitForElementByAccessibilityId('Post lock button', wd.asserters.isDisplayed, 5000);
await lockButton.click();
const postInput = await driver.waitForElementByAccessibilityId('Poster lock amount input', wd.asserters.isDisplayed, 5000);
await postInput.type(amount);
// we press post button
const postButton = await driver.elementByAccessibilityId('Poster lock done button');
await postButton.click();
}
\ No newline at end of file
export default async(driver) => {
const username = await driver.elementByAccessibilityId('username input');
const password = await driver.elementByAccessibilityId('password input');
const loginButton = await driver.elementByAccessibilityId('login button');
await username.type(process.env.loginUser);
await password.type(process.env.loginPass);
await loginButton.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, text) => {
// post screen must be shown
const postInput = await driver.waitForElementByAccessibilityId('PostInput', wd.asserters.isDisplayed, 5000);
await postInput.type(text);
// we press post button
const postButton = await driver.elementByAccessibilityId('Capture Post Button');
await postButton.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// tap the capture button
const button = await driver.waitForElementByAccessibilityId('CaptureButton', wd.asserters.isDisplayed, 10000);
button.click();
return button;
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, options) => {
// tap the toggle button
const button = await driver.waitForElementByAccessibilityId('NSFW button', wd.asserters.isDisplayed, 5000);
await button.click();
// wait until the menu is shown
await sleep(500);
for (let index = 0; index < options.length; index++) {
const name = options[index];
const element = await driver.elementByAccessibilityId(`NSFW ${name}`);
await element.click();
}
let action = new wd.TouchAction(driver);
action.tap({x:100, y:170});
await action.release().perform();
}
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import post from './actions/post';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
import attachPostGalleryImage from './actions/attachPostGalleryImage';
import selectNsfw from './actions/selectNsfw';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Activity flow tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and see it in the newsfeed', async () => {
const str = 'My e2e activity';
// press capture button
await pressCapture(driver);
// accept gallery permissions
await acceptPermissions(driver);
// make the post
await post(driver, str);
// should post and return to the newsfeed
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[2]');
expect(await textElement.text()).toBe(str);
});
it('should like the post', async() => {
const likeButton = await driver.waitForElementByAccessibilityId('Thumb up activity button', 5000);
await likeButton.click();
const likeCount = await driver.waitForElementByAccessibilityId('Thumb up count', 5000);
expect(await likeCount.text()).toBe('1');
});
it('should unlike the post', async() => {
const likeButton = await driver.waitForElementByAccessibilityId('Thumb down activity button', 5000);
await likeButton.click();
const likeCount = await driver.waitForElementByAccessibilityId('Thumb down count', 5000);
expect(await likeCount.text()).toBe('1');
});
});
\ No newline at end of file
import factory from '../tests-helpers/e2e-driver.factory';
const customCapabilities = {
'device' : 'Samsung Galaxy S9',
'os_version' : '8.0'
};
let driver, capabilities;
if (process.env.e2elocal) {
[driver, capabilities] = factory('androidLocal', {});
} else {
[driver, capabilities] = factory('browserStack', customCapabilities);
}
export {driver, capabilities} ;
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import { driver, capabilities} from './config';
import post from './actions/post';
import login from './actions/login';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
describe('Discovery post edit flow', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and go to discovery', async () => {
const str = 'My e2e post #mye2epost';
await pressCapture(driver);
await acceptPermissions(driver);
// make the post
await post(driver, str);
// move to discovery
const discoveryTab = await driver.waitForElementByAccessibilityId('Discovery tab button', wd.asserters.isDisplayed, 10000);
await discoveryTab.click();
});
it('should search for the post', async () => {
// select all list
const all = await driver.waitForElementByAccessibilityId('Discovery All', wd.asserters.isDisplayed, 5000);
await all.click();
await sleep(500);
// select latest filter
const latest = await driver.waitForElementByAccessibilityId('Filter latest button', wd.asserters.isDisplayed, 1000);
await latest.click();
await sleep(500);
// search the post
const search = await driver.waitForElementByAccessibilityId('Discovery Search Input', wd.asserters.isDisplayed, 10000);
await search.type('mye2epost');
await sleep(4000);
// activity menu button
const activityMenu = await driver.waitForElementByAccessibilityId('Activity Menu button', wd.asserters.isDisplayed, 5000);
await activityMenu.click();
await sleep(500);
// tap edit
const edit = await driver.waitForElementByAndroidUIAutomator('new UiSelector().text("Edit")', wd.asserters.isDisplayed, 5000);
await edit.click();
// change the text
const editorInput = await driver.waitForElementByAccessibilityId('Post editor input', wd.asserters.isDisplayed, 5000);
await editorInput.type(' edited!');
// tap save
const save = await driver.waitForElementByAccessibilityId('Post editor save button', wd.asserters.isDisplayed, 5000);
await save.click();
// confirm activity text changed
await driver.waitForElementByAndroidUIAutomator('new UiSelector().text("My e2e post #mye2epost edited!")', wd.asserters.isDisplayed, 5000);
});
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import { driver, capabilities} from './config';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
describe('Login flow', () => {
let username, password, loginButton;
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
});
afterAll(async () => {
await driver.quit();
});
it('should shows login error on wrong credentials', async () => {
expect(await driver.hasElementByAccessibilityId('username input')).toBe(true);
expect(await driver.hasElementByAccessibilityId('password input')).toBe(true);
username = await driver.elementByAccessibilityId('username input');
await username.type('myuser');
password = await driver.elementByAccessibilityId('password input');
await password.type('mypass');
loginButton = await driver.elementByAccessibilityId('login button');
await loginButton.click();
// message should appear
await driver.waitForElementByAccessibilityId('loginMsg', wd.asserters.isDisplayed, 5000);
const textElement = await driver.elementByAccessibilityId('loginMsg');
expect(await textElement.text()).toBe('The user credentials were incorrect.');
});
it('should go to newsfeed on successful login', async () => {
// try successfull login
await username.type(process.env.loginUser);
await password.type(process.env.loginPass);
await loginButton.click();
// should open the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 5000);
});
it('should go to login after logout', async () => {
const menu = await driver.elementByAccessibilityId('Main menu button');
// tap menu button
await menu.click();
const logout = await driver.waitForElementByAccessibilityId('Logout', wd.asserters.isDisplayed, 5000);
// tap logout
logout.click();
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
})
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import post from './actions/post';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
import attachPostGalleryImage from './actions/attachPostGalleryImage';
import selectNsfw from './actions/selectNsfw';
import lockPost from './actions/lockPost';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Post flow tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and see it in the newsfeed', async () => {
const str = 'My e2e post #mye2epost';
// press capture button
await pressCapture(driver);
// accept gallery permissions
await acceptPermissions(driver);
// make the post
await post(driver, str);
// should post and return to the newsfeed
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[2]');
expect(await textElement.text()).toBe(str);
});
it('should remind the previuos post and see it in the newsfeed', async () => {
const str = 'Reminding my own post';
const remindButton = await driver.waitForElementByAccessibilityId('Remind activity button', 5000);
// tap remind
await remindButton.click();
// make the post
await post(driver, str);
// should post and return to the newsfeed
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[2]');
expect(await textElement.text()).toBe(str);
});
it('should post a nsfw and see it in the newsfeed', async () => {
const str = 'My e2e post #mye2epost';
// press capture button
await pressCapture(driver);
// select nsfw
await selectNsfw(driver, ['Nudity', 'Pornography']);
// make the post
await post(driver, str);
// should post and return to the newsfeed
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[2]');
expect(await textElement.text()).toBe(str);
});
it('should post paywalled content', async () => {
// press capture button
await pressCapture(driver);
// deselect nsfw
await selectNsfw(driver, ['Nudity', 'Pornography']);
const str = 'pay me something';
await lockPost(driver, '1');
// make the post with image and no permissions wait
await post(driver, str);
await sleep(1000);
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[3]');
expect(await textElement.text()).toBe('Locked');
});
it('should post an image and see it in the newsfeed', async () => {
const str = 'My e2e post image #mye2epostimage';
// press capture button
await pressCapture(driver);
// attach image
await attachPostGalleryImage(driver);
// make the post with image and no permissions wait
await post(driver, str);
// should post and return to the newsfeed
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[2]');
expect(await textElement.text()).toBe(str);
});
it('should open the images in full screen after tap', async () => {
// 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.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();
});
it('should upload an image and cancel it', async () => {
// press capture button
await pressCapture(driver);
// attach image
await attachPostGalleryImage(driver);
await sleep(5000);
// we press post button
const deleteButton = await driver.elementByAccessibilityId('Attachment Delete Button');
await deleteButton.click();
await sleep(1000);
// should fail to find the delete button
return expect(driver.elementByAccessibilityId('Attachment Delete Button')).rejects.toHaveProperty('status', 7);
});
it('should return to the newsfeed', async () => {
// tap the back button
await driver.back();
// should open the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 5000);
});
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Top-bar tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should open the boost console', async () => {
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/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');
back.click();
});
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/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/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 () => {
const button = await driver.waitForElementByAccessibilityId('Main menu button', wd.asserters.isDisplayed, 5000);
await button.click();
const logoutButton = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Logout"]/android.widget.TextView[2]');
expect(await logoutButton.text()).toBe('Logout');
const back = await driver.waitForElementByXPath('//android.widget.Button[@content-desc="Go back"]/android.view.ViewGroup/android.widget.ImageView');
back.click();
});
});
......@@ -9,6 +9,7 @@ import {
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
// mock backhandler
......
......@@ -29,6 +29,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -36,6 +37,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -87,6 +89,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -94,6 +97,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -161,6 +165,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -168,6 +173,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -263,6 +269,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -270,6 +277,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -332,6 +340,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -339,6 +348,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -397,6 +407,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -404,6 +415,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -468,6 +480,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -475,6 +488,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -532,6 +546,7 @@ exports[`Activity component renders correctly 1`] = `
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -539,6 +554,7 @@ exports[`Activity component renders correctly 1`] = `
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......
......@@ -30,6 +30,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -37,6 +38,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......@@ -79,6 +81,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"isOwner": [Function],
"is_visible": true,
"mature": false,
"mature_visibility": false,
......@@ -86,6 +89,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"ownerObj": UserModel {
"__list": null,
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
"time_created": "1522036284",
"type": "user",
......
......@@ -100,6 +100,22 @@ describe('auth service logout', () => {
expect(session.logout.mock.calls.length).toEqual(1);
});
it('should clear cookies on logout', async () => {
api.post.mockResolvedValue(true);
const res = await authService.logout();
// assert on the response
expect(res).toEqual(true);
// call session logout one time
expect(session.logout.mock.calls.length).toBe(1);
// should clear cookies
expect(api.clearCookies).toBeCalled();
});
it('logout returns errors', async () => {
const response = {status: 'error', error: 'some error'};
......
......@@ -35,7 +35,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="username input"
testID="usernameInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -67,7 +67,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="password input"
testID="userPasswordInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -251,7 +251,7 @@ exports[`LoginForm component should renders correctly 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={null}
testID="login button"
testID="loginButton"
>
<View
pointerEvents="box-only"
......
......@@ -37,6 +37,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"isOwner": [Function],
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -79,6 +80,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -186,9 +188,8 @@ exports[`blog view screen component should renders correctly 1`] = `
style={
Object {
"color": "#444",
"fontFamily": "Roboto",
"fontFamily": "Roboto-Black",
"fontSize": 22,
"fontWeight": "800",
"paddingBottom": 8,
"paddingLeft": 12,
"paddingRight": 12,
......@@ -229,6 +230,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"isOwner": [Function],
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -271,6 +273,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -403,6 +406,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"isOwner": [Function],
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -445,6 +449,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -556,6 +561,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"isOwner": [Function],
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -598,6 +604,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -700,6 +707,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"isOwner": [Function],
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -742,6 +750,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......
......@@ -94,6 +94,7 @@ exports[`Channel screen component should renders correctly 1`] = `
"guid": 1,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -358,6 +359,7 @@ exports[`Channel screen component should renders correctly 1`] = `
"guid": 1,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -618,6 +620,7 @@ exports[`Channel screen component should show closed channel message 1`] = `
"guid": 1,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -903,6 +906,7 @@ exports[`Channel screen component should show closed channel message 1`] = `
"guid": 1,
"icontime": "1523515420",
"impressions": 100,
"isOwner": [Function],
"language": "en",
"legacy_guid": false,
"mature": "1",
......
......@@ -208,7 +208,7 @@ exports[`channel header component owner should render correctly 1`] = `
}
>
<View
accessibilityLabel="Edit your channel settings"
accessibilityLabel="Subscribe to this channel"
accessible={true}
focusable={true}
isTVSelectable={true}
......@@ -246,7 +246,56 @@ exports[`channel header component owner should render correctly 1`] = `
}
>
Edit
Subscribe
</Text>
</View>
<View
accessibilityLabel="More"
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"alignItems": "center",
"backgroundColor": "white",
"borderColor": "#4690D6",
"borderRadius": 20,
"borderWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"margin": 4,
"opacity": 1,
"padding": 4,
}
}
>
<Text
style={
Array [
Object {
"color": "#4690D6",
},
Array [
Object {
"marginLeft": 5,
},
Object {
"marginRight": 5,
},
],
]
}
>
More
</Text>
</View>
......
import videoPlayerService from '../../../src/common/services/video-player.service';
const mockPlayerRef1 = {
pause: jest.fn()
}
const mockPlayerRef2 = {
pause: jest.fn()
}
/**
* Tests
*/
describe('Video player service', () => {
beforeEach(() => {
mockPlayerRef1.pause.mockClear();
mockPlayerRef2.pause.mockClear();
});
it('should set the current ref', () => {
expect(videoPlayerService.current).toBe(null);
videoPlayerService.setCurrent(mockPlayerRef1);
expect(videoPlayerService.current).toBe(mockPlayerRef1);
});
it('should pause the previous video', () => {
videoPlayerService.setCurrent(mockPlayerRef2);
expect(videoPlayerService.current).toBe(mockPlayerRef2);
expect(mockPlayerRef1.pause).toBeCalled();
});
it('should clear the ref', () => {
videoPlayerService.clear();
expect(videoPlayerService.current).toBe(null);
});
});
\ No newline at end of file
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import ReferralCompleteView from '../../../../src/notifications/notification/view/ReferralCompleteView';
import styles from '../../../../src/notifications/notification/style';
// fake data generation
import boostNotificationFactory from '../../../../__mocks__/fake/notifications/BoostNotificationFactory';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const entity = boostNotificationFactory('referral_complete');
const notification = renderer.create(
<ReferralCompleteView styles={styles} entity={entity}/>
).toJSON();
expect(notification).toMatchSnapshot();
});
\ No newline at end of file
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import ReferralPendingView from '../../../../src/notifications/notification/view/ReferralPendingView';
import styles from '../../../../src/notifications/notification/style';
// fake data generation
import boostNotificationFactory from '../../../../__mocks__/fake/notifications/BoostNotificationFactory';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const entity = boostNotificationFactory('referral_pending');
const notification = renderer.create(
<ReferralPendingView styles={styles} entity={entity}/>
).toJSON();
expect(notification).toMatchSnapshot();
});
\ No newline at end of file
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import ReferralPingView from '../../../../src/notifications/notification/view/ReferralPingView';
import styles from '../../../../src/notifications/notification/style';
// fake data generation
import boostNotificationFactory from '../../../../__mocks__/fake/notifications/BoostNotificationFactory';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const entity = boostNotificationFactory('referral_ping');
const notification = renderer.create(
<ReferralPingView styles={styles} entity={entity}/>
).toJSON();
expect(notification).toMatchSnapshot();
});
\ No newline at end of file
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<View
style={
Object {
"flexDirection": "row",
"flexWrap": "wrap",
}
}
>
<Text
onPress={[Function]}
>
You've earned tokens for the completed referral of
<Text
style={
Object {
"color": "#444",
"fontWeight": "bold",
}
}
>
someUser
</Text>
</Text>
</View>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<View
style={
Object {
"flexDirection": "row",
"flexWrap": "wrap",
}
}
>
<Text
onPress={[Function]}
>
You have a pending referral!
<Text
style={
Object {
"color": "#444",
"fontWeight": "bold",
}
}
>
someUser
</Text>
used your referral link when they signed up for Minds. You'll get tokens once they join the rewards program and set up their wallet
</Text>
</View>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<View
style={
Object {
"flexDirection": "row",
"flexWrap": "wrap",
}
}
>
<Text
onPress={[Function]}
>
Free tokens are waiting for you! Once you join the rewards program by setting up your Minds wallet, both you and
<Text
style={
Object {
"color": "#444",
"fontWeight": "bold",
}
}
>
someUser
</Text>
will earn tokens for your referral
</Text>
</View>
`;
......@@ -19,9 +19,38 @@ exports[`WithdrawScreen renders correctly 1`] = `
}
}
>
You can request to withdraw your OffChain token rewards to your OnChain address below.
Note: a small amount of ETH will be charged to cover the transaction fee.
Withdrawals may take up to a few hours to complete
You can request to withdraw up to 0 tokens from your rewards to your
<Text
style={
Object {
"fontWeight": "700",
}
}
>
OnChain
</Text>
wallet.
<Text
style={
Object {
"fontSize": 11,
}
}
>
Note: a small amount of ETH will be charged to cover the transaction fee. Withdrawals
<Text
style={
Object {
"fontWeight": "700",
}
}
>
go through an approval process
</Text>
and may take up to 72 hours to complete
</Text>
</Text>
<View
......@@ -128,9 +157,38 @@ exports[`WithdrawScreen renders correctly 1`] = `
}
}
>
You can request to withdraw your OffChain token rewards to your OnChain address below.
Note: a small amount of ETH will be charged to cover the transaction fee.
Withdrawals may take up to a few hours to complete
You can request to withdraw up to 0 tokens from your rewards to your
<Text
style={
Object {
"fontWeight": "700",
}
}
>
OnChain
</Text>
wallet.
<Text
style={
Object {
"fontSize": 11,
}
}
>
Note: a small amount of ETH will be charged to cover the transaction fee. Withdrawals
<Text
style={
Object {
"fontWeight": "700",
}
}
>
go through an approval process
</Text>
and may take up to 72 hours to complete
</Text>
</Text>
<View
......
......@@ -80,6 +80,7 @@ project.ext.react = [
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
/**
* Set this to true to create two separate APKs instead of one:
......
......@@ -40,10 +40,9 @@
android:label="@string/app_name"
android:screenOrientation="portrait"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
<intent-filter>
......@@ -75,6 +74,15 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<activity
android:name="com.zoontek.rnbootsplash.RNBootSplashActivity"
android:theme="@style/BootTheme"> <!-- apply the theme you created at step 3. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_stat_name" />
<provider
android:name="androidx.core.content.FileProvider"
......
package com.minds.mobile;
import android.os.Bundle;
import com.zoontek.rnbootsplash.RNBootSplash;
import com.facebook.react.ReactActivity;
// image picker imports
......@@ -43,6 +45,12 @@ public class MainActivity extends ReactActivity implements OnImagePickerPermissi
};
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
RNBootSplash.show(R.drawable.bootsplash, MainActivity.this);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
......
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- the background color. it can be a system color or a custom one defined in colors.xml -->
<item android:drawable="@android:color/white" />
<item>
<!-- the app logo, centered horizontally and vertically -->
<bitmap
android:src="@mipmap/ic_logo"
android:gravity="center" />
</item>
</layer-list>
\ No newline at end of file
......@@ -6,4 +6,10 @@
<!-- Customize your theme here. -->
</style>
<style name="BootTheme" parent="AppTheme">
<!-- set bootsplash.xml as activity background -->
<item name="android:background">@drawable/bootsplash</item>
</style>
</resources>
......@@ -26,10 +26,10 @@ org.gradle.jvmargs=-Xmx2048m
systemProp.org.gradle.internal.http.connectionTimeout=180000
systemProp.org.gradle.internal.http.socketTimeout=180000
versionName=3.12.0
versionName=3.12.1
# CUSTOM
versionCode=1050000016
versionCode=1050000017
# PLAY STORE
# versionCode=310034
# versionCode=310035
/**
* Login action
* @param {string} username
* @param {string} password
*/
export default async function(username, password) {
await element(by.id('usernameInput')).typeText(username);
await element(by.id('userPasswordInput')).typeText(password);
await element(by.id('loginButton')).tap();
}
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
const assignReporter = require('detox/runners/jest/assignReporter');
// Set the default timeout
jest.setTimeout(120000);
jasmine.getEnv().addReporter(adapter);
// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
// This is strictly optional.
jasmine.getEnv().addReporter(specReporter);
// This will post which device has assigned to run a suite, which can be useful in a multiple-worker tests run.
// This is strictly optional.
jasmine.getEnv().addReporter(assignReporter);
beforeAll(async () => {
await detox.init(config, { launchApp: false });
});
beforeEach(async () => {
await adapter.beforeEach();
});
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});
import login from "./actions/login";
import sleep from '../src/common/helpers/sleep';
describe('Login Flow', () => {
beforeEach(async () => {
await device.launchApp({
newInstance: true,
permissions: {
notifications: 'YES',
},
});
});
it('should show error', async () => {
// should login successfully
await expect(element(by.id('usernameInput'))).toBeVisible();
// we moved the login logic to an action to avoid code duplication
await login('bad', 'credentials');
await sleep(1000);
// it should show the error message
// according to the detox docs it should be toHaveText but it only works with toHaveLabel
await expect(element(by.id('loginMsg'))).toHaveLabel('The user credentials were incorrect.');
});
it('should login successfully', async () => {
// should login successfully
await expect(element(by.id('usernameInput'))).toBeVisible();
// we moved the login logic to an action to avoid code duplication
await login(process.env.loginUser, process.env.loginPass);
// it should show the newsfeed screen
await expect(element(by.id('NewsfeedScreen'))).toBeVisible();
});
});
// created this file because the bundler is not reading index for some reason
import 'react-native-gesture-handler'; // fix ongesture handler error
import "@hawkingnetwork/node-libs-react-native/globals";
import "./global";
import { AppRegistry } from 'react-native';
import App from './App';
import { useScreens } from 'react-native-screens';
useScreens();
// const modules = require.getModules();
// const moduleIds = Object.keys(modules);
// const loadedModuleNames = moduleIds
// .filter(moduleId => modules[moduleId].isInitialized)
// .map(moduleId => modules[moduleId].verboseName);
// const waitingModuleNames = moduleIds
// .filter(moduleId => !modules[moduleId].isInitialized)
// .map(moduleId => modules[moduleId].verboseName);
// // make sure that the modules you expect to be waiting are actually waiting
// console.log(
// 'loaded:',
// loadedModuleNames.length,
// 'waiting:',
// waitingModuleNames.length
// );
// // grab this text blob, and put it in a file named packager/modulePaths.js
// console.log(`module.exports = ${JSON.stringify(loadedModuleNames.sort())};`);
AppRegistry.registerComponent('Minds', () => App);
\ No newline at end of file
// created this file because the bundler is not reading index for some reason
import 'react-native-gesture-handler'; // fix ongesture handler error
import "@hawkingnetwork/node-libs-react-native/globals";
import "./global";
import { AppRegistry } from 'react-native';
import { AppRegistry, Platform } from 'react-native';
import App from './App';
import { useScreens } from 'react-native-screens';
useScreens();
useScreens(Platform.OS !== 'ios');
// const modules = require.getModules();
// const moduleIds = Object.keys(modules);
......
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.12.0</string>
<string>3.12.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.12.0</string>
<string>3.12.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
......
......@@ -5,7 +5,6 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* MindsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* MindsTests.m */; };
0EA47AA13B1C4DFC8DCD3912 /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BFA6449C294D4B2FA6E84139 /* Roboto-Regular.ttf */; };
......@@ -32,6 +31,7 @@
FD04EC43AFE79D3DE060DD43 /* libPods-Minds-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 728C16C16413E9B0E10971C9 /* libPods-Minds-tvOS.a */; };
FE6E8EAA158246D09E0FEA9D /* Roboto-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2CD2622D204E4B0286943196 /* Roboto-MediumItalic.ttf */; };
FFEB1A966BF04513973FCA99 /* Roboto-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73818081AF2E4726B1E7FBBC /* Roboto-LightItalic.ttf */; };
B080D11FCFB44B31B8AA0E13 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DF9BB306F046229D6C90DA /* libz.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -92,6 +92,7 @@
E0F0DBA3E9FE7A03AF5DCCC7 /* Pods-Minds.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Minds.debug.xcconfig"; path = "Target Support Files/Pods-Minds/Pods-Minds.debug.xcconfig"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
37DF9BB306F046229D6C90DA /* libz.tbd */ = {isa = PBXFileReference; name = "libz.tbd"; path = "usr/lib/libz.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -108,6 +109,7 @@
buildActionMask = 2147483647;
files = (
4DBD76CA42C18451868A4BD6 /* libPods-Minds.a in Frameworks */,
B080D11FCFB44B31B8AA0E13 /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -171,6 +173,7 @@
728C16C16413E9B0E10971C9 /* libPods-Minds-tvOS.a */,
CB3378F9EB6781ED5A558E01 /* libPods-Minds-tvOSTests.a */,
22BFDDB4D04FFCBEAEDF3516 /* libPods-MindsTests.a */,
37DF9BB306F046229D6C90DA /* libz.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
......@@ -243,6 +246,14 @@
path = Pods;
sourceTree = "<group>";
};
B05B1CF8A35D4DBAB32376F4 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
path = Application;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
......@@ -277,6 +288,7 @@
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
5D4283786B433FEB77EB2907 /* [CP] Embed Pods Frameworks */,
521352FB382A6F4552BD3ED7 /* [CP] Copy Pods Resources */,
DF23D9AE37694D50863E721D /* Upload Debug Symbols to Sentry */,
);
buildRules = (
);
......@@ -440,7 +452,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport NODE_BINARY=node\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh";
};
296731C110F3A1661160366F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
......@@ -476,7 +488,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh";
shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport NODE_BINARY=node\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh";
};
368FB13BAB274FAAEBBB6F32 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
......@@ -650,6 +662,20 @@
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
showEnvVarsInLog = 0;
};
DF23D9AE37694D50863E721D /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Upload Debug Symbols to Sentry";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "export SENTRY_PROPERTIES=sentry.properties\n../node_modules/@sentry/cli/bin/sentry-cli upload-dsym";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
......@@ -766,7 +792,7 @@
CODE_SIGN_ENTITLEMENTS = Minds/Minds.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 201907230162;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = 35U3998VRZ;
ENABLE_BITCODE = NO;
......@@ -793,7 +819,7 @@
CODE_SIGN_ENTITLEMENTS = Minds/Minds.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 201907230162;
DEVELOPMENT_TEAM = 35U3998VRZ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Minds/Info.plist;
......
......@@ -7,6 +7,7 @@
#import "RNNotifications.h"
#import "AppDelegate.h"
#import "RNBootSplash.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
......@@ -29,6 +30,9 @@
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[RNBootSplash show:@"LaunchScreen" inView:rootView];
[RNNotifications startMonitorNotifications];
return YES;
}
......
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
......@@ -14,10 +12,19 @@
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Logo" id="Cdt-IG-Ocz">
<rect key="frame" x="121" y="176" width="239.99999999999991" height="128.00000000000023"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
<resources>
<image name="Logo" width="1878" height="691"/>
</resources>
</document>
......@@ -3,4 +3,4 @@
"version" : 1,
"author" : "xcode"
}
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "universal",
"filename" : "logo.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.12.0</string>
<string>3.12.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
......@@ -36,7 +36,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>201907230145</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
......@@ -123,6 +123,8 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
......
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>3.12.0</string>
<string>3.12.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
......
......@@ -35,6 +35,8 @@ target 'Minds' do
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
pod 'RNSentry', :path => '../node_modules/@sentry/react-native'
target 'MindsTests' do
inherit! :search_paths
# Pods for testing
......
......@@ -196,7 +196,7 @@ PODS:
- React-jsinspector (0.61.4)
- react-native-cameraroll (1.3.0):
- React
- react-native-image-picker (1.1.0):
- react-native-image-picker (0.28.1):
- React
- react-native-jitsi-meet (2.0.1):
- JitsiMeetSDK (= 2.4.0)
......@@ -255,6 +255,8 @@ PODS:
- React
- ReactNativeExceptionHandler (2.10.8):
- React
- RNBootSplash (1.0.3):
- React
- RNCAsyncStorage (1.6.2):
- React
- RNConvertPhAsset (1.0.3):
......@@ -277,7 +279,7 @@ PODS:
- React
- RNReanimated (1.4.0):
- React
- RNScreens (1.0.0-alpha.23):
- RNScreens (2.0.0-alpha.11):
- React
- RNSentry (1.0.9):
- React
......@@ -339,6 +341,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "ReactNativeART (from `../node_modules/@react-native-community/art`)"
- ReactNativeExceptionHandler (from `../node_modules/react-native-exception-handler`)
- RNBootSplash (from `../node_modules/react-native-bootsplash`)
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
- RNConvertPhAsset (from `../node_modules/react-native-convert-ph-asset`)
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
......@@ -438,6 +441,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/art"
ReactNativeExceptionHandler:
:path: "../node_modules/react-native-exception-handler"
RNBootSplash:
:path: "../node_modules/react-native-bootsplash"
RNCAsyncStorage:
:path: "../node_modules/@react-native-community/async-storage"
RNConvertPhAsset:
......@@ -491,7 +496,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 8dfb73b987afa9324e4009bdce62a18ce23d983c
React-jsinspector: d15478d0a8ada19864aa4d1cc1c697b41b3fa92f
react-native-cameraroll: 463aff54e37cff27ea76eb792e6f1fa43b876320
react-native-image-picker: 7a85cf7b0a53845f03ae52fb4592a2748ded069b
react-native-image-picker: fd93361c666f397bdf72f9c6c23f13d2685b9173
react-native-jitsi-meet: becd37e8fa1c5f3321b9222c232d190a36f90880
react-native-netinfo: fa32a5bb986924e9be82a261c262039042dde81e
react-native-notifications: 163ddedac6fcc8d850ea15b06abdadcacdff00f1
......@@ -511,6 +516,7 @@ SPEC CHECKSUMS:
ReactCommon: a6a294e7028ed67b926d29551aa9394fd989c24c
ReactNativeART: 103929e284be663b5a2f921ed912821f04120a70
ReactNativeExceptionHandler: 8025d98049c25f186835a3af732dd7c9974d6dce
RNBootSplash: 161de9d2b5dc2af37c2777063b8e6d844ab2a6ff
RNCAsyncStorage: 60a80e72d95bf02a01cace55d3697d9724f0d77f
RNConvertPhAsset: 9b366b8a1abc194b76572712c6f7dd89c9e4e37f
RNDeviceInfo: 687c1b2ab6d86ff1ca1208783320cd144138c7f2
......@@ -521,7 +527,7 @@ SPEC CHECKSUMS:
RNGestureHandler: a4ddde1ffc6e590c8127b8b7eabfdade45475c74
RNLocalize: 07eb7a91d10021cdf59d80061ebf3adb8a5b5688
RNReanimated: b2ab0b693dddd2339bd2f300e770f6302d2e960c
RNScreens: f28b48b8345f2f5f39ed6195518291515032a788
RNScreens: ad3661f864ef18d952e9a4799b6791683e33c1fc
RNSentry: 2803ba8c8129dcf26b79e9b4d8c80168be6e4390
RNShare: 8b171d4b43c1d886917fdd303bf7a4b87167b05c
RNSVG: f6177f8d7c095fada7cfee2e4bb7388ba426064c
......
app_identifier("com.minds.mobile") # The bundle identifier of your app
apple_id("mark@minds.com") # Your Apple email address
apple_id("msantang78@gmail.com") # Your Apple email address
itc_team_id("1312581") # App Store Connect Team ID
team_id("35U3998VRZ") # Developer Portal Team ID
......
const config = {
"automock": false,
"cacheDirectory": ".jest/cache",
"testRegex": "./__e2e__/.*-test.js$",
"preset": "react-native",
"testPathIgnorePatterns": [
"/node_modules/"
],
"transformIgnorePatterns": [
"node_modules/(?!react-native|react-navigation|vargs)/"
],
"moduleNameMapper": {
"^image![a-zA-Z0-9$_-]+$": "GlobalImageStub",
"^[@./a-zA-Z0-9$_-]+\\.(png|gif)$": "RelativeImageStub"
},
"snapshotSerializers": [
"./node_modules/enzyme-to-json/serializer"
]
}
module.exports = config
\ No newline at end of file
......@@ -315,6 +315,9 @@
"errorRemoving":"Error removing comment"
},
"notification":{
"referralPing":"Free tokens are waiting for you! Once you join the rewards program by setting up your Minds wallet, both you and &{user}& will earn tokens for your referral",
"referralPending":"You have a pending referral! &{user}& used your referral link when they signed up for Minds. You'll get tokens once they join the rewards program and set up their wallet",
"referralComplete":"You've earned tokens for the completed referral of &{user}&",
"boostAccepted":"{{count}} tokens &{description}& were accepted.",
"boostCompleted":"{{impressions}}/{{impressions}} views &{description}& have been met.",
"boostGiftView":"{{name}} gifted you {{impressions}} views &{description}&",
......@@ -588,7 +591,9 @@
"errorReadingStatus":"Error reading withdrawal status",
"errorOnlyOnceDay":"You can only withdraw once a day",
"errorWithdrawing":"Error withdrawing tokens",
"youCanRequest":"You can request to withdraw your OffChain token rewards to your OnChain address below.\n Note: a small amount of ETH will be charged to cover the transaction fee.\n Withdrawals may take up to a few hours to complete",
"youCanRequest1":"You can request to withdraw up to {{amount}} tokens from your rewards to your &{onchain}& wallet.\n &{note}&",
"youCanRequest2":"Note: a small amount of ETH will be charged to cover the transaction fee. Withdrawals &{approval}& and may take up to 72 hours to complete",
"youCanRequest3":"go through an approval process",
"holdingMessage":"{{amount}} tokens are unavailable due to credit card payment. They will be released after 30 days the payment occurred.",
"amount":"Amount",
"errorReadingBalances":"Error reading balances",
......
......@@ -506,7 +506,9 @@
"errorReadingStatus": "Error leyendo el estado del retiro",
"errorOnlyOnceDay": "Tu puedes retirar solo una vez por día",
"errorWithdrawing": "Error retirando tokens",
"youCanRequest": "Puede solicitar retirar sus recompensas de token de OffChain a su dirección de OnChain a continuación.\n Nota: se cobrará una pequeña cantidad de ETH para cubrir la tarifa de la transacción.\n Los retiros pueden tardar hasta unas pocas horas en completarse.",
"youCanRequest1":"Puedes solicitar retirar hasta {{amount}} tokens de tus recomensas a tu dirección de &{onchain}&.\n &{note}&",
"youCanRequest2":"Nota: se cobrará una pequeña cantidad de ETH para cubrir la tarifa de la transacción. Los retiros &{approval}& y pueden tardar hasta 72 horas en completarse",
"youCanRequest3":"pasan por un proceso de aprobación",
"holdingMessage": "{{amount}} tokens no están disponibles debido al pago con tarjeta de crédito. Serán liberados después de 30 días de ocurrido el pago.",
"amount": "Monto",
"errorReadingBalances": "Error leyendo saldos",
......
{
"name": "Minds",
"version": "0.0.1",
"version": "3.12.0",
"private": true,
"scripts": {
"android": "react-native run-android",
......@@ -11,8 +11,6 @@
"update-settings": "ts-node tasks/update-settings.js",
"test": "jest",
"locale": "ts-node tasks/poeditor.js",
"e2e": "jest -c jest.e2e.config.js",
"e2e-local": "e2elocal=1 jest -c jest.e2e.config.js",
"preinstall": "git config core.hooksPath .githooks",
"postinstall": "jetify"
},
......@@ -20,7 +18,7 @@
"@hawkingnetwork/node-libs-react-native": "^1.0.10",
"@react-native-community/art": "^1.0.2",
"@react-native-community/async-storage": "^1.3.4",
"@react-native-community/cameraroll": "^1.2.1",
"@react-native-community/cameraroll": "^1.3.0",
"@react-native-community/netinfo": "^4.4.0",
"@sentry/react-native": "^1.0.9",
"crypto-js": "^3.1.9-1",
......@@ -38,6 +36,7 @@
"react-native": "0.61.4",
"react-native-actionsheet": "^2.4.2",
"react-native-animatable": "^1.3.3",
"react-native-bootsplash": "^1.0.3",
"react-native-collapsible-header-views": "^1.0.2",
"react-native-convert-ph-asset": "^1.0.3",
"react-native-device-info": "^4.0.1",
......@@ -65,7 +64,8 @@
"react-native-qrcode-svg": "^5.2.0",
"react-native-randombytes": "^3.5.3",
"react-native-reanimated": "^1.3.0",
"react-native-screens": "^1.0.0-alpha.23",
"react-native-redash": "^8.6.0",
"react-native-screens": "^2.0.0-alpha.11",
"react-native-share": "^2.0.0",
"react-native-snap-carousel": "^3.8.2",
"react-native-sqlite-storage": "^4.1.0",
......@@ -90,6 +90,7 @@
"@react-native-community/eslint-config": "^0.0.5",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^24.9.0",
"detox": "^14.7.1",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.4.0",
......@@ -102,5 +103,26 @@
"react-test-renderer": "16.9.0",
"ts-node": "^8.4.1",
"typescript": "^3.7.2"
},
"detox": {
"configurations": {
"ios.sim.debug": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/Minds.app",
"build": "xcodebuild -workspace ios/Minds.xcworkspace -scheme Minds -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
}
},
"ios.sim.release": {
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/Minds.app",
"build": "xcodebuild -workspace ios/Minds.xcworkspace -scheme Minds -configuration Release -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
}
}
},
"test-runner": "jest"
}
}
......@@ -3,7 +3,6 @@ import React, {
} from 'react';
import {
Image,
View,
} from 'react-native';
......@@ -18,13 +17,7 @@ export default class LoadingScreen extends Component {
render() {
return (
<View style={[CommonStyle.backgroundWhite ,CommonStyle.flexContainerCenter, CommonStyle.padding2x]}>
<Image
resizeMode={"contain"}
style={ComponentsStyle.logo}
source={require('./assets/logos/logo.png')}
/>
</View>
<View style={[CommonStyle.backgroundWhite ,CommonStyle.flexContainerCenter, CommonStyle.padding2x]}/>
);
}
}
\ No newline at end of file
......@@ -28,6 +28,10 @@ class AuthService {
try {
let resp = await api.delete('api/v2/oauth/token');
session.logout();
// Fixes autosubscribe issue on register
await api.clearCookies();
return true;
} catch (err) {
logService.exception('[AuthService] logout', err);
......
......@@ -8,7 +8,6 @@ import {
View,
Text,
// TextInput,
StyleSheet,
KeyboardAvoidingView,
} from 'react-native';
......@@ -21,7 +20,6 @@ import { ComponentsStyle } from '../styles/Components';
import { Button } from 'react-native-elements'
import i18n from '../common/services/i18n.service';
import testID from '../common/helpers/testID';
import logService from '../common/services/log.service';
import ModalPicker from '../common/components/ModalPicker';
......@@ -32,7 +30,9 @@ import TextInput from '../common/components/TextInput';
* Login Form
*/
export default class LoginForm extends Component {
/**
* State
*/
state = {
username: '',
password: '',
......@@ -44,19 +44,21 @@ export default class LoginForm extends Component {
showLanguages: false,
};
componentWillMount() {
this.setState({
language: i18n.getCurrentLocale()
});
/**
* Constructor
*/
constructor(props) {
super(props);
this.state.language = i18n.getCurrentLocale();
}
/**
* Render
*/
render() {
const msg = (this.state.msg) ? <Animatable.Text animation="bounceInLeft" style={[CommonStyle.colorLight, { textAlign: 'center' }]} {...testID('loginMsg')}>{this.state.msg}</Animatable.Text>:null;
const msg = this.state.msg ? (
<Animatable.Text animation="bounceInLeft" style={[CommonStyle.colorLight, { textAlign: 'center' }]} testID="loginMsg">{this.state.msg}</Animatable.Text>
) : null;
const inputs = this.getInputs();
const buttons = this.getButtons();
......@@ -89,9 +91,12 @@ export default class LoginForm extends Component {
);
}
/**
* Show languages
*/
showLanguages = () => {
this.setState({showLanguages: true});
}
};
/**
* Language selected
......@@ -99,12 +104,18 @@ export default class LoginForm extends Component {
languageSelected = (language) => {
this.setState({language, showLanguages: false});
i18n.setLocale(language);
}
};
/**
* Cancel language selection
*/
cancel = () => {
this.setState({showLanguages: false});
}
};
/**
* Returns the buttons
*/
getButtons() {
const buttons = [
<Button
......@@ -119,9 +130,9 @@ export default class LoginForm extends Component {
loadingRight={true}
disabled={this.state.inProgress}
disabledStyle={CommonStyle.backgroundTransparent}
{...testID('login button')}
testID="loginButton"
/>
]
];
if (!this.state.twoFactorToken) {
buttons.unshift(
......@@ -140,6 +151,9 @@ export default class LoginForm extends Component {
return buttons;
}
/**
* Return the inputs for the form
*/
getInputs() {
if (this.state.twoFactorToken) {
return (
......@@ -149,7 +163,7 @@ export default class LoginForm extends Component {
returnKeyType={'done'}
placeholderTextColor="#444"
underlineColorAndroid='transparent'
onChangeText={(value) => this.setState({ twoFactorCode: value })}
onChangeText={this.setTwoFactor}
autoCapitalize={'none'}
value={this.state.twoFactorCode}
/>
......@@ -162,11 +176,11 @@ export default class LoginForm extends Component {
returnKeyType={'done'}
placeholderTextColor="#444"
underlineColorAndroid='transparent'
onChangeText={(value) => this.setState({ username: value })}
onChangeText={this.setUsername}
autoCapitalize={'none'}
value={this.state.username.trim()}
value={this.state.username}
key={1}
{...testID('username input')}
testID="usernameInput"
/>,
<View key={2}>
<TextInput
......@@ -177,24 +191,62 @@ export default class LoginForm extends Component {
returnKeyType={'done'}
placeholderTextColor="#444"
underlineColorAndroid='transparent'
onChangeText={(value) => this.setState({ password: value })}
onChangeText={this.setPassword}
value={this.state.password}
{...testID('password input')}
testID="userPasswordInput"
/>
<Icon
name={this.state.hidePassword ? 'md-eye' : 'md-eye-off'}
size={25}
style={ComponentsStyle.loginInputIcon}
onPress={this.toggleHidePassword}
/>
<Icon name={this.state.hidePassword ? 'md-eye' : 'md-eye-off'} size={25} style={ComponentsStyle.loginInputIcon} onPress={this.toggleHidePassword}/>
</View>
];
}
}
/**
* Set two factor
* @param {string} value
*/
setTwoFactor = value => {
const twoFactorCode = String(value).trim();
this.setState({twoFactorCode});
};
/**
* Set two factor
* @param {string} value
*/
setUsername = value => {
const username = String(value).trim();
this.setState({username});
};
/**
* Set two factor
* @param {string} value
*/
setPassword = value => {
const password = String(value).trim();
this.setState({password});
};
/**
* Set two factor
* @param {string} value
*/
toggleHidePassword = () => {
this.setState({hidePassword: !this.state.hidePassword});
}
};
/**
* Handle forgot password
*/
onForgotPress = () => {
this.props.onForgot()
}
this.props.onForgot();
};
/**
* On login press
......
......@@ -239,7 +239,8 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
link: {
fontWeight: '800',
// fontWeight: '800',
fontFamily: 'Roboto-Black', // workaround android ignoring >= 800
},
warning: {
marginTop: 10,
......
......@@ -497,7 +497,8 @@ const styles = StyleSheet.create({
label: {
color: '#444',
fontSize: 16,
fontWeight: '800',
// fontWeight: '800',
fontFamily: 'Roboto-Black', // workaround android ignoring >= 800
},
supportingTextContainer: {
flexDirection: 'row',
......
......@@ -135,6 +135,7 @@ export default class BlockchainWalletImportScreen extends Component {
const styles = StyleSheet.create({
title: {
fontWeight: '800',
fontFamily: 'Roboto-Black', // workaround android ignoring >= 800
fontSize: 18,
color: '#444',
marginBottom: 8,
......
......@@ -159,7 +159,8 @@ const styles = StyleSheet.create({
label: {
paddingBottom: 3,
fontSize: 16,
fontWeight: '800',
// fontWeight: '800',
fontFamily: 'Roboto-Black', // workaround android ignoring >= 800
letterSpacing: 1,
},
listAliasHighlight: {
......@@ -190,7 +191,7 @@ const styles = StyleSheet.create({
paddingRight: 3,
color: 'green',
fontSize: 20,
fontWeight: '800',
fontWeight: '700',
textAlign: 'right',
},
eth: {
......
......@@ -153,9 +153,12 @@ export default class BlogViewHTML extends Component {
}
if (html.indexOf('<iframe') >= 0) {
html = html.replace('<iframe', '<div class="iframewrapper"><iframe');
html = html.replace('</iframe>', '</iframe></div>');
html = html.replace('src="//', 'src="https://');
const iframeOpen = new RegExp(/\<iframe/g);
const iframeClose = new RegExp(/\<\/iframe\>/g);
const badSrc = new RegExp(/src=\"\/\//g);
html = html.replace(iframeOpen, '<div class="iframewrapper"><iframe');
html = html.replace(iframeClose, '</iframe></div>');
html = html.replace(badSrc, 'src="https://');
}
return `<!DOCTYPE html><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
......
......@@ -319,7 +319,8 @@ const styles = StyleSheet.create({
fontSize: 22,
color: '#444',
fontFamily: 'Roboto',
fontWeight: '800',
// fontWeight: '800',
fontFamily: 'Roboto-Black', // workaround android ignoring >= 800
},
ownerBlockContainer: {
margin: 8,
......
......@@ -31,9 +31,7 @@ export default
@observer
class ChannelActions extends Component {
state = {
scheduledCount: '',
}
state = {}
componentDidMount() {
this.getScheduledCount();
......@@ -112,8 +110,7 @@ class ChannelActions extends Component {
getScheduledCount = async () => {
if (featuresService.has('post-scheduler')) {
const count = await this.props.store.feedStore.getScheduledCount();
this.setState({ scheduledCount: count });
await this.props.store.feedStore.getScheduledCount();
}
}
......@@ -149,7 +146,7 @@ class ChannelActions extends Component {
<ButtonCustom
onPress={this.onViewScheduledAction}
accessibilityLabel={i18n.t('channel.viewScheduled')}
text={`${i18n.t('channel.viewScheduled')}: ${this.state.scheduledCount}`}
text={`${i18n.t('channel.viewScheduled')}: ${this.props.store.feedStore.feedStore.scheduledCount}`}
loading={this.state.saving}
inverted={this.props.store.feedStore.endpoint == this.props.store.feedStore.scheduledEndpoint ? true : undefined}
/>
......
......@@ -3,7 +3,6 @@ import {
action,
} from 'mobx'
import channelService from './ChannelService';
import FeedStore from '../common/stores/FeedStore';
/**
......@@ -87,16 +86,7 @@ export default class ChannelFeedStore {
* Get channel scheduled activities count
*/
async getScheduledCount() {
const count = await channelService.getScheduledCount(this.guid);
return count;
}
/**
* Get channel scheduled activities count
*/
async getScheduledCount() {
const count = await channelService.getScheduledCount(this.guid);
return count;
await this.feedStore.getScheduledCount(this.guid);
}
@action
......
......@@ -93,7 +93,7 @@ class Comment extends Component {
<CommentEditor setEditing={this.setEditing} comment={comment} store={this.props.store}/>
:
<DoubleTapText style={styles.message} selectable={true} onDoubleTap={this.showActions} selectable={false} onLongPress={this.showActions}>
<Text style={styles.username}>@{comment.ownerObj.username} </Text>
<Text style={styles.username} onPress={this._navToChannel} >@{comment.ownerObj.username} </Text>
{ comment.description &&
<Tags
navigation={this.props.navigation}
......@@ -135,6 +135,7 @@ class Comment extends Component {
</View>
<CommentActionSheet
entity={this.props.entity}
comment={this.props.comment}
onSelection={this.onSelection}
ref={this.actionSheetRef}
......@@ -300,7 +301,8 @@ const styles = StyleSheet.create({
fontSize: 14,
},
username: {
fontWeight: '800',
// fontWeight: '800',
fontFamily: 'Roboto-Black',
paddingRight: 8,
color: '#444',
},
......
......@@ -90,6 +90,8 @@ export default class CommentActionSheet extends Component {
} else {
actions.push( i18n.t('removeExplicit') )
}
} else if (this.props.entity.isOwner()) {
actions.push( i18n.t('delete') );
}
actions.push( i18n.t('report') );
......
......@@ -81,6 +81,11 @@ export default class BaseModel {
constructor(data) {
Object.assign(this, data);
// Some users have a number as username and engine return them as a number
if (this.username) {
this.username = this.username.toString();
}
// create childs instances
const childs = this.childModels()
for (var prop in childs) {
......@@ -90,6 +95,13 @@ export default class BaseModel {
}
}
/**
* Return if the current user is the owner of the activity
*/
isOwner = () => {
return this.ownerObj && sessionService.guid === this.ownerObj.guid;
}
/**
* Update model data
* @param {Object} data
......
import React from 'react';
import { View, StyleSheet, Animated, Easing, Dimensions } from 'react-native';
const { height, width } = Dimensions.get('window');
export default class Pulse extends React.Component {
constructor(props) {
super(props);
this.anim = new Animated.Value(0);
}
componentDidMount() {
Animated.timing(this.anim, {
toValue: 1,
duration: this.props.interval,
easing: Easing.in,
})
.start();
}
render() {
const { size, pulseMaxSize, borderColor, backgroundColor, getStyle } = this.props;
return (
<View style={[styles.circleWrapper, {
width: pulseMaxSize,
height: pulseMaxSize,
marginLeft: -pulseMaxSize/2,
marginTop: -pulseMaxSize/2,
}]}>
<Animated.View
style={[styles.circle, {
borderColor,
backgroundColor,
width: this.anim.interpolate({
inputRange: [0, 1],
outputRange: [size, pulseMaxSize]
}),
height: this.anim.interpolate({
inputRange: [0, 1],
outputRange: [size, pulseMaxSize]
}),
borderRadius: pulseMaxSize/2,
opacity: this.anim.interpolate({
inputRange: [0, 1],
outputRange: [1, 0]
})
}, getStyle && getStyle(this.anim)]}
/>
</View>
);
}
import { View, StyleSheet } from 'react-native';
import Animated, { Easing } from "react-native-reanimated";
import { bInterpolate, loop } from "react-native-redash";
const { Value, useCode, set } = Animated;
export default function(props) {
const animation = new Value(0);
useCode(
set(
animation,
loop({
toValue: 1,
duration: 1000,
easing: Easing.in(Easing.ease),
}),
),
[animation],
);
const scale = bInterpolate(animation, 1, 1.3);
const opacity = bInterpolate(animation, 1, 0);
const pulseMaxSize = Math.round(1.3 * props.size);
return (
<View
style={[
styles.circleWrapper,
{
width: pulseMaxSize,
height: pulseMaxSize,
marginLeft: -pulseMaxSize/2,
marginTop: -pulseMaxSize/2,
}
]}
>
<Animated.View
style={{
transform: [{scale}],
backgroundColor: 'red',
borderRadius: props.size / 2,
width: props.size,
height: props.size,
opacity,
}}
/>
</View>
);
}
const styles = StyleSheet.create({
circleWrapper: {
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
// left: width/8,
// top: height/2,
},
circle: {
borderWidth: 4 * StyleSheet.hairlineWidth,
},
circleWrapper: {
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
// left: width/8,
// top: height/2,
},
circle: {
borderWidth: 4 * StyleSheet.hairlineWidth,
},
});
\ No newline at end of file
import React from 'react';
import { View, Image, TouchableOpacity, Animated, Easing } from 'react-native';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import Pulse from './Pulse';
import FastImage from 'react-native-fast-image';
/**
* Based on https://github.com/wissile/react-native-pulse-anim
* Pulse avatar
*/
export default class PulseAnimAvatar extends React.Component {
constructor(props) {
super(props);
state = {};
this.state = {
circles: []
};
/**
* Derive state from props
* @param {object} nextProps
* @param {object} prevState
*/
static getDerivedStateFromProps(nextProps, prevState) {
if (
nextProps.size !== prevState.size ||
nextProps.avatar !== prevState.avatar
) {
return {
sizeStyle: {
width: nextProps.size,
height: nextProps.size,
},
imageStyle: {
width: nextProps.size,
height: nextProps.size,
borderRadius: nextProps.size / 2,
backgroundColor: nextProps.avatarBackgroundColor
},
avatarUri: {
uri: nextProps.avatar
},
};
}
return null;
}
this.counter = 1;
this.setInterval = null;
this.anim = new Animated.Value(1);
}
/**
* Render
*/
render() {
const {onPress} = this.props;
componentDidMount() {
this.setCircleInterval();
}
componentWillUnmount() {
clearInterval(this.setInterval);
}
setCircleInterval() {
this.setInterval = setInterval(this.addCircle.bind(this), this.props.interval);
this.addCircle();
}
addCircle() {
this.setState({ circles: [...this.state.circles, this.counter] });
this.counter++;
}
onPressIn() {
Animated.timing(this.anim, {
toValue: this.props.pressInValue,
duration: this.props.pressDuration,
easing: this.props.pressInEasing,
}).start(() => clearInterval(this.setInterval));
}
onPressOut() {
Animated.timing(this.anim, {
toValue: 1,
duration: this.props.pressDuration,
easing: this.props.pressOutEasing,
}).start(this.setCircleInterval.bind(this));
}
render() {
const { size, avatar, avatarBackgroundColor, interval, onPress } = this.props;
return (
<View style={{
// flex: 1,
backgroundColor: 'transparent',
// width: size,
// height: size,
justifyContent: 'center',
alignItems: 'center',
}}>
{this.state.circles.map((circle) => (
<Pulse
key={circle}
{...this.props}
/>
))}
<TouchableOpacity
activeOpacity={.5}
// onPressIn={this.onPressIn.bind(this)}
// onPressOut={this.onPressOut.bind(this)}
return (
<View style={styles.main}>
<Pulse
{...this.props}
/>
<TouchableOpacity
activeOpacity={.5}
onPress={onPress}
style={{
width: size,
height: size,
}}
>
<FastImage
source={{ uri: avatar }}
style={{
width: size,
height: size,
borderRadius: size/2,
backgroundColor: avatarBackgroundColor,
...this.props.style
}}
/>
</TouchableOpacity>
</View>
);
}
style={this.state.sizeStyle}
>
<FastImage
source={this.state.avatarUri}
style={[this.state.imageStyle, this.props.style]}
/>
</TouchableOpacity>
</View>
);
}
}
PulseAnimAvatar.defaultProps = {
interval: 2000,
size: 100,
pulseMaxSize: 200,
avatar: undefined,
avatarBackgroundColor: 'transparent',
pressInValue: 0.8,
pressDuration: 150,
pressInEasing: Easing.in,
pressOutEasing: Easing.in,
borderColor: '#1c1d1f',
backgroundColor: '#D1D1D1',
getStyle: undefined,
};
\ No newline at end of file
style: null,
};
const styles = StyleSheet.create({
main: {
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
},
});
......@@ -8,6 +8,8 @@ import abortableFetch from '../helpers/abortableFetch';
import { Version } from '../../config/Version';
import logService from './log.service';
import * as Sentry from '@sentry/react-native';
/**
* Api Error
*/
......@@ -29,6 +31,16 @@ export const isApiForbidden = function(err) {
* Api service
*/
class ApiService {
async parseJSON(response) {
try {
return await response.json();
} catch (error) {
Sentry.captureMessage(`ISSUE #1572 URL: ${response.url}, STATUS: ${response.status} STATUSTEXT: ${response.statusText}`);
throw error;
}
}
/**
* Clear cookies
*/
......@@ -113,7 +125,7 @@ class ApiService {
}
// Convert from JSON
const data = await response.json();
const data = await this.parseJSON(response);
// Failed on API side
if (data.status != 'success') {
......@@ -144,7 +156,7 @@ class ApiService {
}
// Convert from JSON
const data = await response.json();
const data = await this.parseJSON(response);
// Failed on API side
if (data.status != 'success') {
......@@ -175,7 +187,7 @@ class ApiService {
}
// Convert from JSON
const data = await response.json();
const data = await this.parseJSON(response);
// Failed on API side
if (data.status === 'error') {
......@@ -251,7 +263,7 @@ class ApiService {
}
// Convert from JSON
const data = await response.json();
const data = await this.parseJSON(response);
// Failed on API side
if (data.status === 'error') {
......
......@@ -114,12 +114,13 @@ class EntitiesService {
urnsToFetch,
localEntities.map((m: any): string => m.urn),
);
// we add to resync list
localEntities.forEach((entity: any) => {
urnsToResync.push(entity.urn);
this.addEntity(entity, false)
});
}
// we add to resync list
localEntities.forEach((entity: any) => {
urnsToResync.push(entity.urn);
this.addEntity(entity, false)
});
}
// Fetch entities we don't have
......
......@@ -4,6 +4,11 @@ import api from './../../common/services/api.service';
* Gathering service
*/
class GatheringService {
keepAliveInterval = null;
get isActive() {
return this.keepAliveInterval !== null;
}
/**
* Start keep alive pooling
*/
......@@ -16,6 +21,7 @@ class GatheringService {
*/
stopKeepAlive() {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
}
/**
......
......@@ -69,13 +69,18 @@ class LogService {
}
exception(prepend, error) {
if (!error) {
error = prepend;
prepend = null;
}
if (!isNetworkFail(error) && !isUserError(error) && !isAbort(error) && (!this.isApiError(error) || this.isUnexpectedError(error))) {
if (
error instanceof Error &&
!isNetworkFail(error) &&
!isUserError(error) &&
!isAbort(error) &&
(!this.isApiError(error) || this.isUnexpectedError(error))
) {
// report the issue to sentry
Sentry.captureException(error);
......
......@@ -51,7 +51,8 @@ export default class Router {
} else if (entity_type[0] === 'object') {
navigation.push('Activity', { guid: data.json.entity_guid });
} else {
logService.exception('[DeepLinkRouter] Unknown notification:', entity_type, data);
const err = new Error(`[DeepLinkRouter] Unknown notification, entity_type: ${entity_type}`);
logService.exception('[DeepLinkRouter] Unknown notification:', err);
}
break;
......
/**
* Video Player Service
*/
class VideoPlayerService {
/**
* current playing video player reference
*/
current = null;
/**
* Set current player reference
* @param {MindsVideo} videoPlayerRef
*/
setCurrent(videoPlayerRef) {
if (this.current && this.current !== videoPlayerRef) {
this.current.pause();
}
this.current = videoPlayerRef;
}
/**
* Clear the current player ref
*/
clear() {
this.current = null;
}
}
export default new VideoPlayerService();
......@@ -32,7 +32,7 @@ export default class AttachmentStore {
if (this.transcoding) {
return;
}
console.log('ATTACHING', media, extra);
if (this.uploading) {
// abort current upload
this.cancelCurrentUpload();
......@@ -52,26 +52,34 @@ export default class AttachmentStore {
this.setHasAttachment(true);
// correctly handle videos from ph:// paths on ios
if (
Platform.OS === 'ios' &&
media.type === 'video' &&
media.uri.startsWith('ph://')
) {
try {
this.transcoding = true;
const converted = await RNConvertPhAsset.convertVideoFromUrl({
url: media.uri,
convertTo: 'm4v',
quality: 'high',
});
media.type = converted.mimeType;
media.uri = converted.path;
media.filename = converted.filename;
} catch (error) {
Alert.alert('Error reading the video', 'Please try again');
} finally {
this.transcoding = false;
if (Platform.OS === 'ios') {
// correctly handle videos from ph:// paths on ios
if (media.type === 'video' && media.uri.startsWith('ph://')) {
try {
this.transcoding = true;
const converted = await RNConvertPhAsset.convertVideoFromUrl({
url: media.uri,
convertTo: 'm4v',
quality: 'high',
});
media.type = converted.mimeType;
media.uri = converted.path;
media.filename = converted.filename;
} catch (error) {
Alert.alert('Error reading the video', 'Please try again');
} finally {
this.transcoding = false;
}
}
// fix camera roll gif issue
if (media.type === 'image' && media.fileName) {
const extension = media.fileName.split('.').pop();
if (extension && extension.toLowerCase() === 'gif') {
media.type = 'image/gif';
const appleId = media.uri.substring(5, 41);
media.uri = `assets-library://asset/asset.GIF?id=${appleId}&ext=GIF`;
}
}
}
......
......@@ -5,12 +5,15 @@ import Viewed from './Viewed';
import MetadataService from '../services/metadata.service';
import FeedsService from '../services/feeds.service';
import connectivityService from '../services/connectivity.service';
import channelService from '../../channel/ChannelService';
/**
* Feed store
*/
export default class FeedStore {
@observable scheduledCount = '';
/**
* Refreshing
*/
......@@ -143,6 +146,9 @@ export default class FeedStore {
entity._list = this;
this.entities.unshift(entity);
this.feedsService.prepend(entity);
if (entity.isScheduled()) {
this.setScheduledCount(this.scheduledCount + 1);
}
}
/**
......@@ -162,6 +168,9 @@ export default class FeedStore {
const index = this.entities.findIndex(e => e === entity);
if (index < 0) return;
this.removeIndex(index);
if (entity.isScheduled()) {
this.setScheduledCount(this.scheduledCount - 1);
}
}
/**
......@@ -423,4 +432,25 @@ export default class FeedStore {
this.feedsService.setOffset(0);
return this;
}
/**
* Reset store and service data
*/
reset() {
this.clear();
this.feedsService.clear();
}
/**
* Get channel scheduled activities count
*/
async getScheduledCount(guid) {
const count = await channelService.getScheduledCount(guid);
this.setScheduledCount(count);
}
@action
setScheduledCount(count) {
this.scheduledCount = count;
}
}
......@@ -121,6 +121,10 @@ class DiscoveryStore {
*/
@action
reload() {
// ignore reload for latest channels
if (this.filters.type === 'lastchannels') {
return;
}
this.listStore.clear();
this.fetch();
}
......
import React from 'react';
import { View } from 'react-native';
import { View, BackHandler } from 'react-native';
import JitsiMeet, { JitsiMeetView } from 'react-native-jitsi-meet';
import { CommonStyle } from '../styles/Common';
import sessionService from '../common/services/session.service';
......@@ -13,34 +13,63 @@ class Gathering extends React.Component {
* Remove navigation header
*/
static navigationOptions = {
header: null
header: null,
};
/**
* Constructor
*/
constructor(props) {
super(props);
// we disable the back button until the video call is started
// to prevent an inconsistent behavior
this.backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => true,
);
}
/**
* Component did mount
*/
componentDidMount() {
const entity = this.props.navigation.getParam('entity');
setTimeout(async () => {
const url = await gatheringService.getRoomName(entity);
const user = sessionService.getUser();
const avatar = user.getAvatarSource().uri;
this.init();
}
JitsiMeet.callWithUserInfo(url, avatar, user.name, entity.name);
}, 1000);
/**
* Init gathering
*/
async init() {
if (!gatheringService.isActive) {
const entity = this.props.navigation.getParam('entity');
this.timer = setTimeout(async () => {
const url = await gatheringService.getRoomName(entity);
const user = sessionService.getUser();
const avatar = user.getAvatarSource().uri;
JitsiMeet.callWithUserInfo(url, avatar, user.name, entity.name);
}, 300);
}
}
/**
* Component will unmount
*/
componentWillUnmount() {
if (this.backHandler) {
this.backHandler.remove();
this.backHandler = null;
}
if (this.timer) {
clearTimeout(this.timer);
}
JitsiMeet.endCall();
}
/**
* On conference terminated
*/
onConferenceTerminated = nativeEvent => {
onConferenceTerminated = event => {
gatheringService.stopKeepAlive();
this.props.navigation.goBack();
};
......@@ -48,16 +77,26 @@ class Gathering extends React.Component {
/**
* On conference joined
*/
onConferenceJoined = nativeEvent => {
onConferenceJoined = event => {
gatheringService.startKeepAlive();
if (this.backHandler) {
this.backHandler.remove();
this.backHandler = null;
// the back button should end the call instead of return to previous screen
this.backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => {
JitsiMeet.endCall();
return true;
},
);
}
};
/**
* On conference will join
*/
onConferenceWillJoin = nativeEvent => {
/* Conference will join event */
};
onConferenceWillJoin = event => {};
/**
* Render
......
......@@ -29,6 +29,7 @@ import ExplicitImage from '../common/components/explicit/ExplicitImage';
import logService from '../common/services/log.service';
import i18n from '../common/services/i18n.service';
import attachmentService from '../common/services/attachment.service';
import videoPlayerService from '../common/services/video-player.service';
const isIOS = Platform.OS === 'ios';
......@@ -89,6 +90,9 @@ class MindsVideo extends Component {
*/
componentWillUnmount() {
this.onScreenBlur.remove();
if (videoPlayerService.current === this) {
videoPlayerService.clear();
}
}
onVideoEnd = () => {
......@@ -106,13 +110,13 @@ class MindsVideo extends Component {
}
this.setState({loaded: false, currentTime: current, duration: e.duration});
this.player.seek(current)
this.player.seek(current);
this.onLoadEnd();
}
onLoadStart = () => {
this.setState({ error: false, inProgress: true, });
this.setState({error: false, inProgress: true});
};
onError = async err => {
......@@ -123,31 +127,31 @@ class MindsVideo extends Component {
this.setState({transcoding: true});
} else {
logService.exception('[MindsVideo]', new Error(err));
this.setState({ error: true, inProgress: false, });
this.setState({error: true, inProgress: false});
}
} catch (error) {
logService.exception('[MindsVideo]', new Error(error));
this.setState({ error: true, inProgress: false, });
this.setState({error: true, inProgress: false});
}
};
onLoadEnd = () => {
this.setState({ error: false, inProgress: false, });
this.setState({error: false, inProgress: false});
};
toggleVolume = () => {
const v = this.state.volume ? 0 : 1;
this.setState({volume: v});
}
};
onProgress = (e) => {
onProgress = e => {
this.setState({currentTime: e.currentTime});
}
};
onBackward(currentTime) {
let newTime = Math.max(currentTime - FORWARD_DURATION, 0);
this.player.seek(newTime);
this.setState({currentTime: newTime})
this.setState({currentTime: newTime});
}
onForward(currentTime, duration) {
......@@ -180,15 +184,13 @@ class MindsVideo extends Component {
}
play = () => {
this.setState({
showOverlay: false,
});
videoPlayerService.setCurrent(this);
this.setState({
active: true,
showOverlay: false,
paused: false,
});
}
};
pause = () => {
this.setState({
......@@ -247,6 +249,13 @@ class MindsVideo extends Component {
}
}
/**
* Set the reference to the video player
*/
setRef = (ref) => {
this.player = ref;
};
/**
* Get video component or thumb
*/
......@@ -257,9 +266,7 @@ class MindsVideo extends Component {
if (this.state.active || !thumb_uri) {
return (
<Video
ref={(ref) => {
this.player = ref
}}
ref={this.setRef}
volume={parseFloat(this.state.volume)}
onEnd={this.onVideoEnd}
onLoadStart={this.onLoadStart}
......@@ -301,7 +308,7 @@ class MindsVideo extends Component {
const entity = this.props.entity;
let {currentTime, duration, paused} = this.state;
const mustShow = (this.state.showOverlay && !isIOS) || this.state.paused;
const mustShow = (this.state.showOverlay && !isIOS) || this.state.paused && entity;
if (mustShow) {
const completedPercentage = this.getCurrentTimePercentage(currentTime, duration) * 100;
......
......@@ -38,11 +38,6 @@ export default class ConversationView extends Component {
let unread = item.unread ? <Icon style={styles.icons} name='md-notifications' color='#4caf50' size={19} /> : null;
let online = item.online ? <Icon style={styles.icons} name='md-radio-button-on' color='#2196f3' size={19} /> : null;
// Added to capture information about /issues/1203549247/?project=1538735
if (item.username && !item.username.toUpperCase) {
Sentry.captureMessage('ISSUE 1203549247 No username on ' + item.guid + ' name: ' + item.name);
}
return (
<TouchableOpacity style={styles.row} onPress={this._navToConversation}>
<Image source={avatarImg} style={styles.avatar} />
......
......@@ -2,6 +2,14 @@ import { NavigationActions, StackActions, SwitchActions } from 'react-navigation
let _navigator;
function getStateFrom(nav) {
let state = nav.routes[nav.index];
if (state.routes) {
state = getStateFrom(state);
}
return state;
}
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
......@@ -11,7 +19,7 @@ function getState() {
}
function getCurrentState() {
return _navigator.state.nav.routes[_navigator.state.nav.index];
return getStateFrom(_navigator.state.nav);
}
function navigate(routeName, params) {
......
......@@ -104,13 +104,6 @@ export default class ActivityModel extends BaseModel {
FastImage.preload([this.getThumbSource(size)]);
}
/**
* Return if the current user is the owner of the activity
*/
isOwner() {
return sessionService.guid == this.ownerObj.guid;
}
shouldBeBlured() {
const user = sessionService.getUser();
......
......@@ -18,7 +18,6 @@ import CaptureFab from '../capture/CaptureFab';
import stores from '../../AppStores';
import { CommonStyle } from '../styles/Common';
import GroupsBar from '../groups/GroupsBar';
import testID from '../common/helpers/testID';
import FeedList from '../common/components/FeedList';
import featuresService from '../common/services/features.service';
......@@ -109,7 +108,7 @@ export default class NewsfeedScreen extends Component {
if (newsfeed.filter == 'subscribed') {
return (
<View style={CommonStyle.flexContainer} {...testID('Newsfeed Screen')}>
<View style={CommonStyle.flexContainer} testID="NewsfeedScreen">
<FeedList
ref={newsfeed.setListRef}
feedStore={newsfeed.feedStore}
......@@ -122,7 +121,7 @@ export default class NewsfeedScreen extends Component {
}
return (
<View style={CommonStyle.flexContainer} {...testID('Newsfeed Screen')}>
<View style={CommonStyle.flexContainer} testID="NewsfeedScreen">
<NewsfeedList
newsfeed={newsfeed}
header={header}
......
......@@ -39,10 +39,6 @@ class NewsfeedStore {
*/
constructor() {
this.buildStores();
this.feedStore
.setEndpoint(`api/v2/feeds/subscribed/activities`)
.setInjectBoost(true)
.setLimit(12);
}
/**
......@@ -68,6 +64,11 @@ class NewsfeedStore {
this.list.getMetadataService()
.setSource('feed/boosts')
.setMedium('featured-content');
this.feedStore
.setEndpoint('api/v2/feeds/subscribed/activities')
.setInjectBoost(true)
.setLimit(12);
}
/**
......@@ -167,12 +168,12 @@ class NewsfeedStore {
@action
reset() {
this.feedStore.reset();
this.buildStores();
this.filter = 'subscribed';
this.boosts = [];
this.loading = false;
this.loadingBoost = false;
this.feedStore.clear();
}
}
......
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.