...
 
Commits (27)
module.exports = {
"prettier.printWidth": 100,
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
semi: true,
bracketSpacing: true,
trailingComma: 'all',
};
......@@ -38,6 +38,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -98,6 +99,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -174,6 +176,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -279,6 +282,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -350,6 +354,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -417,6 +422,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -490,6 +496,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -556,6 +563,7 @@ exports[`Activity component renders correctly 1`] = `
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......
......@@ -37,6 +37,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......@@ -88,6 +89,7 @@ exports[`Activity screen component renders correctly with an entity as param 2`]
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"confirmEmail": [Function],
"guid": "824853017709780997",
"isOwner": [Function],
"subtype": false,
......
......@@ -80,6 +80,8 @@ exports[`blog card component should renders correctly 1`] = `
}
>
<Text
ellipsizeMode="tail"
numberOfLines={2}
style={
Array [
Object {
......@@ -88,6 +90,9 @@ exports[`blog card component should renders correctly 1`] = `
Object {
"fontWeight": "500",
},
Object {
"flex": 1,
},
]
}
>
......@@ -357,10 +362,12 @@ exports[`blog card component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
......@@ -66,10 +66,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -259,10 +261,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -435,10 +439,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -590,10 +596,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -736,10 +744,12 @@ exports[`blog view screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
......@@ -81,10 +81,12 @@ exports[`Channel screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -346,10 +348,12 @@ exports[`Channel screen component should renders correctly 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -606,10 +610,12 @@ exports[`Channel screen component should show closed channel message 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......@@ -892,10 +898,12 @@ exports[`Channel screen component should show closed channel message 1`] = `
],
"chat": true,
"city": "",
"confirmEmail": [Function],
"container_guid": "0",
"deleted": "0",
"disabled_boost": false,
"dob": "",
"email_confirmed": false,
"eth_wallet": "0x6fffffdadae350ddd5a1777d5a1777a4f39f0317",
"fb": false,
"feature_flags": false,
......
......@@ -264,9 +264,21 @@ exports[`channel header component owner should render correctly 1`] = `
}
}
>
<Text
selectable={true}
/>
<View
style={
Object {
"height": undefined,
}
}
>
<Text
numberOfLines={0}
>
<Text
selectable={true}
/>
</Text>
</View>
</View>
</View>
<View
......
import {Alert} from 'react-native';
import remoteAction from '../../src/common/RemoteAction';
import connectivityService from '../../src/common/services/connectivity.service';
import { ApiError } from '../../src/common/services/api.service';
jest.mock('../../src/common/services/connectivity.service');
describe('remote action', () => {
Alert.alert = jest.fn();
beforeEach(() => {
Alert.alert.mockClear();
connectivityService.isConnected = true;
});
it('should not auto retry on generic error', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new Error('boom');
});
await remoteAction(action);
// should have been called
expect(action).toHaveBeenCalledTimes(1);
});
it('should auto retry on net error', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new TypeError('Network request failed');
});
await remoteAction(action);
// should have been called
expect(action).toHaveBeenCalledTimes(2);
});
it('should auto retry on net error n times', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new TypeError('Network request failed');
});
await remoteAction(action, '', 2);
// should have been called
expect(action).toHaveBeenCalledTimes(3);
});
it('should stop auto retry on success', async () => {
const action = jest.fn();
let tries = 0;
action.mockImplementation(async () => {
tries++;
if (tries > 1) {
return;
}
throw new TypeError('Network request failed');
});
await remoteAction(action, '', 2);
// should have been called
expect(action).toHaveBeenCalledTimes(2);
});
it('should show offline error message', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new TypeError('Network request failed');
});
connectivityService.isConnected = false;
await remoteAction(action, '', 0);
// should have been called
expect(action).toHaveBeenCalledTimes(1);
// should call alert with the correct messages
expect(Alert.alert.mock.calls[0][0]).toBe('Sorry!');
expect(Alert.alert.mock.calls[0][1]).toBe('No Internet Connection');
expect(Alert.alert.mock.calls[0][2][0].text).toBe('Ok');
expect(Alert.alert.mock.calls[0][2][1].text).toBe('Try again');
});
it('should show api errors message', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new ApiError('Some Error');
});
await remoteAction(action, '', 0);
// should have been called
expect(action).toHaveBeenCalledTimes(1);
// should call alert with the correct messages
expect(Alert.alert.mock.calls[0][0]).toBe('Sorry!');
expect(Alert.alert.mock.calls[0][1]).toBe('Some Error');
expect(Alert.alert.mock.calls[0][2][0].text).toBe('Ok');
expect(Alert.alert.mock.calls[0][2][1].text).toBe('Try again');
});
it('should show error message with retry on failure', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new TypeError('Network request failed');
});
await remoteAction(action, '', 0);
// should have been called
expect(action).toHaveBeenCalledTimes(1);
// should call alert with the correct messages
expect(Alert.alert.mock.calls[0][0]).toBe('Sorry!');
expect(Alert.alert.mock.calls[0][1]).toBe('Can\'t reach the server');
expect(Alert.alert.mock.calls[0][2][0].text).toBe('Ok');
expect(Alert.alert.mock.calls[0][2][1].text).toBe('Try again');
});
it('should call the action again if the user tap retry', async () => {
const action = jest.fn();
action.mockImplementation(async () => {
throw new TypeError('Network request failed');
});
await remoteAction(action, '', 0);
// should have been called
expect(action).toHaveBeenCalledTimes(1);
Alert.alert.mock.calls[0][2][1].onPress();
// should have been called again
expect(action).toHaveBeenCalledTimes(2);
});
});
import boostedContentService from "../../../src/common/services/boosted-content.service";
import FeedsService from "../../../src/common/services/feeds.service";
import blockListService from "../../../src/common/services/block-list.service";
jest.mock('../../../src/common/services/feeds.service');
jest.mock('../../../src/common/services/session.service');
jest.mock('../../../src/common/services/block-list.service');
/**
* Tests
*/
describe('Boosted content service', () => {
beforeEach(() => {
blockListService.has.mockClear();
});
it('should fetch the boosts from the server', async () => {
const fakeBoosts = [{guid: 1}, {guid: 2}, {guid: 3}];
blockListService.has.mockReturnValue(false);
const fakeBoosts = [
{guid: 1, ownerObj: {guid: 1}},
{guid: 2, ownerObj: {guid: 2}},
{guid: 3, ownerObj: {guid: 3}},
];
const result = fakeBoosts.map(e => {
e.boosted = true;
return e;
});
boostedContentService.feedsService.getEntities.mockResolvedValue(fakeBoosts);
boostedContentService.feedsService.fetchLocal.mockResolvedValue(true);
// load the boosts
await boostedContentService.load();
// should fetch the feed
expect(boostedContentService.feedsService.setEndpoint).toBeCalledWith('api/v2/boost/feed');
expect(boostedContentService.feedsService.setOffset).toBeCalledWith(0);
expect(boostedContentService.feedsService.setLimit).toBeCalledWith(12);
expect(boostedContentService.feedsService.fetchLocal).toBeCalled();
// should fetch the boosts entities
expect(boostedContentService.feedsService.getEntities).toBeCalled();
// the boosts should be stored in the boosts property
expect(boostedContentService.boosts).toStrictEqual(result);
});
it('should fetch the boosts and filter blocked', async () => {
blockListService.has
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
const fakeBoosts = [
{guid: 1, ownerObj: {guid: 1}},
{guid: 2, ownerObj: {guid: 2}},
{guid: 3, ownerObj: {guid: 3}},
];
const result = fakeBoosts
.map(e => {
e.boosted = true;
return e;
})
.filter(e => e.guid !== 2);
boostedContentService.feedsService.getEntities.mockResolvedValue(fakeBoosts);
boostedContentService.feedsService.fetchLocal.mockResolvedValue(true);
......@@ -25,17 +83,22 @@ describe('Boosted content service', () => {
expect(boostedContentService.feedsService.setLimit).toBeCalledWith(12);
expect(boostedContentService.feedsService.fetchLocal).toBeCalled();
// blocked should be called
expect( blockListService.has).toBeCalled();
// should fetch the boosts entities
expect(boostedContentService.feedsService.getEntities).toBeCalled();
// the boosts should be stored in the boosts property
expect(boostedContentService.boosts).toBe(fakeBoosts);
expect(boostedContentService.boosts).toStrictEqual(result);
});
it('should return next boost and start again when the end is reached', () => {
const fakeBoosts = [{guid: 1}, {guid: 2}, {guid: 3}];
blockListService.has.mockReturnValue(false);
boostedContentService.boosts = fakeBoosts;
// next
......
import service from '../../../src/common/services/deeplinks-router.service';
import { MINDS_DEEPLINK } from '../../../src/config/Config';
import navigationService from '../../../src/navigation/NavigationService';
import service from '../../../src/common/services/deeplinks-router.service';
/**
* Tests
......@@ -10,19 +7,43 @@ import navigationService from '../../../src/navigation/NavigationService';
describe('Deeplinks router service', () => {
navigationService.navigate = jest.fn();
beforeEach(() => {
navigationService.navigate.mockClear();
});
it('should add route', async () => {
expect(service.routes.length).toBe(12)
service.clearRoutes();
expect(service.routes.length).toBe(0);
service.add('crypto/:someparam', 'screen1');
service.add('myurl/:someparam1', 'screen2');
expect(service.routes.length).toBe(14);
service.add('twoparams/:someparam1/:someparam2', 'screen2');
expect(service.routes.length).toBe(3);
service.navigate('http://www.minds.com/crypto/somevalue');
expect(navigationService.navigate).toHaveBeenCalledWith('screen1', {someparam: 'somevalue'});
expect(navigationService.navigate).toHaveBeenCalledWith('screen1', {
someparam: 'somevalue',
});
service.navigate('http://www.minds.com/myurl/somevalue');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {someparam1: 'somevalue'});
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
});
service.navigate('http://www.minds.com/twoparams/somevalue/somevalue1');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
someparam2: 'somevalue1',
});
service.navigate(
'http://www.minds.com/twoparams/somevalue/somevalue1?other=1&other2=2',
);
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
someparam2: 'somevalue1',
other: '1',
other2: '2',
});
service.navigate('http://www.minds.com/myurl/somevalue?other=1');
expect(navigationService.navigate).toHaveBeenCalledWith('screen2', {
someparam1: 'somevalue',
other: '1',
});
});
});
\ No newline at end of file
});
......@@ -492,6 +492,7 @@ exports[`notification component renders correctly for every type 5`] = `
some title
</Text>
</Text>
You have not been charged
</Text>
</View>
......
......@@ -52,6 +52,7 @@ exports[`renders correctly 1`] = `
some title
</Text>
</Text>
You have not been charged
</Text>
</View>
......
......@@ -50,12 +50,13 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="mobile.minds.com" android:path="/"/>
<data android:pathPattern="/channels/..*"/>
android:host="www.minds.com" android:path="/"/>
<data android:pathPattern="/email-confirmation"/>
<!-- <data android:pathPattern="/channels/..*"/>
<data android:pathPattern="/newsfeed/..*"/>
<data android:pathPattern="/groups/profile/..*"/>
<data android:pathPattern="/wallet/..*"/>
<data android:pathPattern="/..*"/>
<data android:pathPattern="/..*"/> -->
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
......
......@@ -6,8 +6,8 @@
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:mobile.minds.com</string>
<string>activitycontinuation:mobile.minds.com</string>
<string>applinks:www.minds.com</string>
<string>activitycontinuation:www.minds.com</string>
</array>
</dict>
</plist>
......@@ -294,7 +294,8 @@
"subscribed":"SUBSCRIBED",
"boostfeed":"BOOSTFEED",
"designYourChannel":"Design your channel",
"empty":"Your newsfeed is empty"
"empty":"Your newsfeed is empty",
"olderThan":"Older than {{period}}"
},
"comments": {
"successRemoving":"Comment removed succesfully",
......@@ -602,6 +603,13 @@
"email":"Invalid email",
"number":"Invalid number"
},
"emailConfirm": {
"confirmed":"Email address confirmed!",
"confirm":"Please confirm your email address. Didn't get it?",
"sendAgain": "Click here to send again.",
"confirmNote": "Note: If you change your email address, it will need to be confirmed again.",
"sent":"Email sent"
},
"imagePicker":{
"gallery":"Choose from gallery",
"camera":"Take new photo",
......
......@@ -14,6 +14,12 @@ import { Alert } from 'react-native';
*/
class UserStore {
@observable me = {};
@observable emailConfirmMessageDismiss = false;
@action
setDissmis(value) {
this.emailConfirmMessageDismiss = value;
}
@action
setUser(user) {
......
......@@ -23,13 +23,25 @@ import Actions from '../newsfeed/activity/Actions';
* Blog Card
*/
export default class BlogCard extends PureComponent {
/**
* Navigate to blog
*/
navToBlog = () => {
if (!this.props.navigation || !this.props.entity.can(FLAG_VIEW, true)) return;
if (!this.props.navigation || !this.props.entity.can(FLAG_VIEW, true)) {
return;
}
return this.props.navigation.push('BlogView', { blog: this.props.entity });
};
/**
* Trim and remove new line char
* @param {string} title
*/
cleanTitle(title) {
if (!title) {
return '';
}
return title.trim().replace(/\n/gm, ' ');
}
/**
......@@ -39,13 +51,14 @@ export default class BlogCard extends PureComponent {
const blog = this.props.entity;
const channel = this.props.entity.ownerObj;
const image = blog.getBannerSource();
const title = this.cleanTitle(blog.title);
return (
<TouchableOpacity onPress={this.navToBlog} style={CS.backgroundWhite}>
<FastImage source={image} style={styles.banner} resizeMode={FastImage.resizeMode.cover} />
<View style={[CS.padding2x]}>
<View style={[CS.columnAlignStart, CS.fullWidth]}>
<Text style={[CS.fontL, CS.fontMedium]}>{blog.title}</Text>
<Text style={[CS.fontL, CS.fontMedium, CS.flexContainer]} numberOfLines={2} ellipsizeMode="tail">{title}</Text>
<View style={[CS.marginBottom2x, CS.marginTop2x, CS.rowJustifyCenter, CS.alignCenter]}>
{ channel && <Avatar
width={24}
......@@ -73,5 +86,5 @@ const styles = StyleSheet.create({
flexDirection: 'row',
height: 150,
width: '100%',
}
},
});
......@@ -43,17 +43,19 @@ export default
@inject('channel', 'subscriptionRequest')
@observer
class ChannelScreen extends Component {
/**
* State
*/
state = {
guid: null
guid: null,
};
/**
* Disable navigation bar
*/
static navigationOptions = {
header: null
}
header: null,
};
/**
* Load data on mount
......@@ -78,6 +80,9 @@ class ChannelScreen extends Component {
}
}
/**
* Initial load
*/
async initialLoad() {
const params = this.props.navigation.state.params;
......@@ -92,6 +97,9 @@ class ChannelScreen extends Component {
}
}
/**
* Component will unmount
*/
componentWillUnmount() {
if (this.disposeEnter) {
this.disposeEnter.remove();
......@@ -100,8 +108,11 @@ class ChannelScreen extends Component {
this.props.channel.store(this.guid).markInactive();
}
/**
* Load channel
* @param {string|UserModel} channelOrGuid
*/
async loadChannel(channelOrGuid) {
const isModel = channelOrGuid instanceof UserModel;
const guid = isModel ? channelOrGuid.guid : channelOrGuid;
const store = this.props.channel.store(guid);
......@@ -160,14 +171,14 @@ class ChannelScreen extends Component {
// load feed now
store.feedStore.loadFeed();
} catch(err) {
} catch (err) {
Alert.alert(
i18n.t('attention'),
i18n.t('channel.notFound'),
[{ text: i18n.t('ok'), onPress: () => this.props.navigation.goBack() }],
{ cancelable: false }
);
};
}
}
get guid() {
......
import {
observable,
action
} from 'mobx';
import {observable, action} from 'mobx';
import {cloneDeep} from 'lodash';
import channelService from './ChannelService';
import wireService from '../wire/WireService';
......@@ -62,6 +60,9 @@ export default class ChannelStore {
@action
async load(defaultChannel) {
if (defaultChannel) {
defaultChannel = cloneDeep(defaultChannel);
}
const channel = await channelsService.get(this.guid, defaultChannel);
if (channel) {
......
......@@ -51,6 +51,26 @@ export default class UserModel extends BaseModel {
*/
@observable mode = 0;
/**
* @var {boolean}
*/
@observable email_confirmed = false;
/**
* Confirm email
* @param {Object} params
*/
confirmEmail = async params => {
// call any api endpoint with the param
try {
await apiService.get('api/v2/entities/', { urn: this.urn, ...params });
this.setEmailConfirmed(true);
return true;
} catch (error) {
return false;
}
};
/**
* Get the user icon time
*/
......@@ -102,6 +122,11 @@ export default class UserModel extends BaseModel {
this.mode = value;
}
@action
setEmailConfirmed(value) {
this.email_confirmed = value;
}
/**
* Is admin
*/
......
......@@ -19,6 +19,7 @@ import {
import Icon from 'react-native-vector-icons/Ionicons';
import FastImage from 'react-native-fast-image';
import * as Progress from 'react-native-progress';
import ReadMore from 'react-native-read-more-text';
import { MINDS_CDN_URI } from '../../config/Config';
import abbrev from '../../common/helpers/abbrev';
......@@ -185,6 +186,28 @@ export default class ChannelHeader extends Component {
setBriefdescription = briefdescription => this.setState({ briefdescription });
setName = name => this.setState({ name });
/**
* Truncated footer render for description
*/
_renderTruncatedFooter = (handlePress) => {
return (
<Text style={[CommonStyle.fontM, CommonStyle.colorPrimary, CommonStyle.marginTop2x]} onPress={handlePress}>
{i18n.t('readMore')}
</Text>
);
}
/**
* Revealed footer render for description
*/
_renderRevealedFooter = (handlePress) => {
return (
<Text style={[CommonStyle.fontM, CommonStyle.colorPrimary, CommonStyle.marginTop2x]} onPress={handlePress}>
{i18n.t('showLess')}
</Text>
);
}
/**
* Render Header
*/
......@@ -272,7 +295,13 @@ export default class ChannelHeader extends Component {
</View>}
{!isEditable &&
<View style={CommonStyle.paddingTop2x}>
<Tags navigation={this.props.navigation}>{channel.briefdescription}</Tags>
<ReadMore
numberOfLines={3}
renderTruncatedFooter={this._renderTruncatedFooter}
renderRevealedFooter={this._renderRevealedFooter}
>
<Tags navigation={this.props.navigation}>{channel.briefdescription}</Tags>
</ReadMore>
</View>
}
</View>
......
......@@ -21,6 +21,7 @@ import { observer, inject } from 'mobx-react/native';
import * as Progress from 'react-native-progress';
import Icon from 'react-native-vector-icons/Ionicons';
import IconMC from 'react-native-vector-icons/MaterialCommunityIcons';
import IconMd from 'react-native-vector-icons/MaterialIcons';
import ActionSheet from 'react-native-actionsheet';
import Comment from './Comment';
......@@ -337,7 +338,17 @@ class CommentList extends React.Component<PropsType, StateType> {
return (
<View>
<View style={[CS.rowJustifyCenter, CS.margin, CS.padding, CS.backgroundWhite, CS.borderRadius12x, CS.borderGreyed, CS.borderHair]} testID={this.props.parent ? 'CommentParentView' : ''}>
<View
style={[
CS.rowJustifyCenter,
CS.margin,
CS.padding,
CS.backgroundWhite,
CS.borderRadius12x,
CS.borderGreyed,
CS.borderHair,
]}
testID={this.props.parent ? 'CommentParentView' : ''}>
<Image source={avatarImg} style={CmpStyle.posterAvatar} />
<TextInput
style={[CS.flexContainer, CS.marginLeft, inputStyle, {paddingVertical: 2}]}
......@@ -359,8 +370,22 @@ class CommentList extends React.Component<PropsType, StateType> {
(comments.saving || attachment.checkingVideoLength) ?
<ActivityIndicator size={'large'} /> :
<View style={[CS.rowJustifyEnd, CS.centered]}>
<TouchableOpacity onPress={this.showAttachment} style={CS.paddingRight2x}><Icon name="md-attach" size={24} style={CS.paddingRight2x} /></TouchableOpacity>
<TouchableOpacity onPress={this.postComment} style={CS.paddingRight2x} testID='PostCommentButton'><Icon name="md-send" size={24} /></TouchableOpacity>
<TouchableOpacity
onPress={this.showAttachment}
style={CS.paddingRight2x}>
<Icon name="md-attach" size={24} style={[CS.paddingRight2x, CS.colorDarkGreyed]} />
</TouchableOpacity>
<TouchableOpacity
onPress={comments.toggleMature}
style={CS.paddingRight2x}>
<IconMd name="explicit" size={24} style={[CS.paddingRight2x, comments.mature ? CS.colorDanger : CS.colorDarkGreyed]} />
</TouchableOpacity>
<TouchableOpacity
onPress={this.postComment}
style={CS.paddingRight2x}
testID="PostCommentButton">
<Icon name="md-send" size={24} style={CS.colorDarkGreyed}/>
</TouchableOpacity>
</View>
}
</View>
......
......@@ -39,6 +39,7 @@ export default class CommentsStore {
@observable loaded = false;
@observable saving = false;
@observable text = '';
@observable mature = 0;
@observable loadingPrevious = false;
@observable loadingNext = false;
......@@ -71,6 +72,11 @@ export default class CommentsStore {
return (this.parent && this.parent.child_path) ? this.parent.child_path : '0:0:0';
}
@action
toggleMature = () => {
this.mature = this.mature ? 0 : 1;
};
/**
* Set the entity
* @param {object} entity
......@@ -332,8 +338,9 @@ export default class CommentsStore {
const comment = {
comment: this.text,
parent_path: this.getParentPath()
}
mature: this.mature,
parent_path: this.getParentPath(),
};
if (this.attachment.guid) {
comment.attachment_guid = this.attachment.guid;
......@@ -347,7 +354,6 @@ export default class CommentsStore {
Object.assign(comment, entity.getClientMetadata());
try {
const data = await postComment(this.guid, comment);
this.pushComment(data.comment);
......
import connectivityService from './services/connectivity.service';
import {isNetworkFail} from './helpers/abortableFetch';
import i18nService from './services/i18n.service';
import {Alert} from 'react-native';
import { isApiError } from './services/api.service';
/**
* Remote action with auto and manual retry
*
* @param {function} action async function that runs the action
* @param {string} actionName translation term (optional)
* @param {number} retries number of auto-retries (0 for no auto retry)
*/
async function remoteAction(action, actionName = '', retries = 1) {
try {
await action();
} catch (error) {
let message;
if (isNetworkFail(error)) {
if (retries > 0) {
remoteAction(action, actionName, --retries);
return;
}
message = connectivityService.isConnected
? i18nService.t('cantReachServer')
: i18nService.t('noInternet');
} else if (isApiError(error)) {
message = error.message;
} else {
message = i18nService.t('errorMessage');
}
if (actionName) {
message = i18nService.t(actionName) + '\n' + message;
}
Alert.alert(
i18nService.t('sorry'),
message,
[
{text: i18nService.t('ok')},
{
text: i18nService.t('tryAgain'),
onPress: () => remoteAction(action, actionName, retries),
},
],
{cancelable: true},
);
}
}
export default remoteAction;
import React, {Component} from 'react';
import {Text, StyleSheet, View} from 'react-native';
import {Text, View} from 'react-native';
import {CommonStyle} from '../../styles/Common';
import Colors from '../../styles/Colors';
import {CommonStyle as CS} from '../../styles/Common';
import i18n from '../services/i18n.service';
import { TouchableOpacity } from 'react-native-gesture-handler';
import {TouchableOpacity} from 'react-native-gesture-handler';
/**
* Blocked Channel
*/
export default class BlockedChannel extends Component {
/**
* Navigate To channel
......@@ -20,53 +22,44 @@ export default class BlockedChannel extends Component {
}
};
/**
* Render
*/
render() {
return (
<View style={[styles.container, CommonStyle.hairLineBottom]}>
<View style={[CommonStyle.flexContainerCenter, styles.insideBox]}>
<Text style={styles.text}>
{i18n.to('channel.blockedNav', null, {
username: (
<Text style={styles.username} onPress={this.navToChannel}>
@{this.props.entity.ownerObj.username}
</Text>
),
})}
<View
style={[
CS.flexContainer,
CS.centered,
CS.padding2x,
CS.backgroundLight,
CS.fullWidth,
]}>
<Text style={[CS.fontXL, CS.colorDarkGreyed, CS.marginTop3x]}>
{i18n.to('channel.blockedNav', null, {
username: (
<Text style={CS.bold} onPress={this.navToChannel}>
@{this.props.entity.ownerObj.username}
</Text>
),
})}
</Text>
<TouchableOpacity
onPress={async () => {
await this.props.entity.unblockOwner();
}}>
<Text
style={[
CS.fontL,
CS.colorPrimary,
CS.marginTop3x,
CS.marginBottom3x,
]}>
{i18n.t('undo')}
</Text>
<TouchableOpacity
onPress={async () => {
await this.props.entity.unblockOwner();
}}>
<Text style={styles.textUndo}>{i18n.t('undo')}</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
overflow: 'visible',
height: 150,
backgroundColor: Colors.lightGreyed,
padding: 5,
},
insideBox: {
borderStyle: 'solid',
borderColor: Colors.darkGreyed,
borderWidth: 0.5,
alignItems: 'center',
},
text: {
color: Colors.darkGreyed,
},
username: {
fontWeight: 'bold',
},
textUndo: {
color: Colors.darkGreyed,
marginTop: 20,
fontWeight: 'bold',
}
});
......@@ -97,11 +97,9 @@ export default class MediaView extends Component {
guid = this.props.entity.guid;
}
const source = {uri: MINDS_API_URI + `api/v1/media/${guid}/play`, headers: api.buildHeaders() };
return (
<View style={styles.videoContainer}>
<MindsVideo video={source} entity={this.props.entity} ref={o => {this.videoPlayer = o}}/>
<MindsVideo entity={this.props.entity} ref={o => {this.videoPlayer = o}}/>
</View>
);
}
......
export default {
has: jest.fn(),
getList: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
};
\ No newline at end of file
import { jsxEmptyExpression } from "@babel/types";
export default {
connectionInfo : {
connectionInfo: {
type: 'unknown',
effectiveType: 'unknown',
},
isConnected: true,
init: jsxEmptyExpression.fn(),
}
\ No newline at end of file
init: jest.fn(),
};
......@@ -6,16 +6,12 @@ import Cancelable from 'promise-cancelable';
* Attacment service
*/
class AttachmentService {
/**
* Attach media file
* @param {object} media
* @param {function} onProgress
*/
attachMedia(media, extra, onProgress=null) {
let type = 'image'
attachMedia(media, extra, onProgress = null) {
const file = {
uri: media.uri,
path: media.path || null,
......@@ -25,8 +21,10 @@ class AttachmentService {
const progress = (e) => {
let pct = e.loaded / e.total;
if (onProgress) onProgress(pct);
}
if (onProgress) {
onProgress(pct);
}
};
let promise;
......@@ -87,6 +85,10 @@ class AttachmentService {
return api.get(`api/v1/media/transcoding/${guid}`);
}
getVideoSources(guid) {
return api.get(`api/v2/media/video/${guid}`);
}
/**
* Capture video
*/
......@@ -98,8 +100,8 @@ class AttachmentService {
uri: response.uri,
path: response.path,
type: 'video/mp4',
fileName: 'image.mp4'
}
fileName: 'image.mp4',
};
}
return response;
......
// @flow
import FeedsService from "./feeds.service";
import logService from "./log.service";
import blockListService from "./block-list.service";
// types
import type ActivityModel from "../../newsfeed/ActivityModel";
......@@ -9,17 +10,28 @@ import type ActivityModel from "../../newsfeed/ActivityModel";
* Boosted content service
*/
class BoostedContentService {
/**
* Offset
* @var {number}
*/
offset: number = -1;
feedsService: FeedsService = new FeedsService;
/**
* Feed service
* @var {FeedsService}
*/
feedsService: FeedsService = new FeedsService();
/**
* Boosts
* @var {Array<ActivityModel>} boosts
*/
boosts: Array<ActivityModel> = [];
/**
* Reload boosts list
*/
load = async(): Promise<any> => {
load = async (): Promise<any> => {
try {
const done = await this.feedsService
.setLimit(12)
......@@ -31,14 +43,24 @@ class BoostedContentService {
if (!done) {
await this.update();
} else {
this.boosts = await this.feedsService.getEntities();
// refresh boost without the wait
this.boosts = this.cleanBoosts(await this.feedsService.getEntities());
this.update();
}
} catch (err) {
logService.exception('[BoostedContentService]', err);
}
};
/**
* Remove blocked channel's boosts and sets boosted to true
* @param {Array<ActivityModel} boosts
*/
cleanBoosts(boosts: Array<ActivityModel>): Array<ActivityModel> {
return boosts.filter((e: ActivityModel) => {
e.boosted = true;
return !blockListService.has(e.ownerObj.guid);
});
}
/**
......@@ -46,7 +68,7 @@ class BoostedContentService {
*/
async update() {
await this.feedsService.fetch();
this.boosts = await this.feedsService.getEntities();
this.boosts = this.cleanBoosts(await this.feedsService.getEntities());
}
/**
......
......@@ -5,7 +5,9 @@ import navigationService from '../../navigation/NavigationService';
* Deeplinks router
*/
class DeeplinksRouter {
/**
* Routes
*/
routes = [];
/**
......@@ -15,6 +17,28 @@ class DeeplinksRouter {
MINDS_DEEPLINK.forEach(r => this.add(r[0], r[1]));
}
/**
* Clear routes
*/
clearRoutes() {
this.routes = [];
}
/**
* Parse params
* @param {string} url
*/
parseQueryParams(url) {
let regex = /[?&]([^=#]+)=([^&#]*)/g,
params = {},
match;
while ((match = regex.exec(url))) {
params[match[1]] = match[2];
}
return params;
}
/**
* Add a new route
* @param {string} url ex: newsfeed/:guid
......@@ -28,7 +52,7 @@ class DeeplinksRouter {
this.routes.push({
screen,
params,
re: new RegExp('^' + url.replace(re, '([^\/]+?)') + '\/?$')
re: new RegExp('^' + url.replace(re, '([^\/]+?)') + '(\/?$|\/?\\?)')
});
}
......@@ -63,8 +87,9 @@ class DeeplinksRouter {
if (match) {
const params = {};
route.params.forEach((v, i) => params[v] = match[i + 1]);
const urlParams = this.parseQueryParams(url);
return { screen: route.screen, params }
return { screen: route.screen, params: {...params, ...urlParams}}
}
}
return null;
......
import apiService from './api.service';
import logService from './log.service';
class EmailConfirmationService {
async send() {
try {
const response = await apiService.post(
'api/v2/email/confirmation/resend',
{},
);
return Boolean(response && response.sent);
} catch (err) {
logService.exception('[EmailConfirmationService] send', err);
return false;
}
}
}
export default new EmailConfirmationService();
// @flow
import logService from './log.service';
import apiService from './api.service';
import { abort, isNetworkFail } from '../helpers/abortableFetch';
import {abort, isNetworkFail} from '../helpers/abortableFetch';
import entitiesService from './entities.service';
import feedsStorage from './sql/feeds.storage';
import { showMessage } from 'react-native-flash-message';
import {showMessage} from 'react-native-flash-message';
import i18n from './i18n.service';
import connectivityService from './connectivity.service';
import Colors from '../../styles/Colors';
......@@ -15,14 +15,13 @@ export type FeedRecordType = {
owner_guid: string,
timestamp: string,
urn: string,
entity?: Object
entity?: Object,
};
/**
* Feed store
*/
export default class FeedsService {
/**
* @var {boolean}
*/
......@@ -51,7 +50,7 @@ export default class FeedsService {
/**
* @var {Object}
*/
params: Object = {sync: 1}
params: Object = {sync: 1};
/**
* @var {Array}
......@@ -73,6 +72,16 @@ export default class FeedsService {
*/
paginated = true;
/**
* @var {number|null}
*/
fallbackAt = null;
/**
* @var {number}
*/
fallbackIndex = -1;
/**
* Get entities from the current page
*/
......@@ -91,9 +100,15 @@ export default class FeedsService {
const feedPage = this.feed.slice(this.offset, end);
const result: Array<any> = await entitiesService.getFromFeed(feedPage, this, this.asActivities);
const result: Array<any> = await entitiesService.getFromFeed(
feedPage,
this,
this.asActivities,
);
if (!this.injectBoost) return result;
if (!this.injectBoost) {
return result;
}
this.injectBoosted(3, result, end);
this.injectBoosted(8, result, end);
......@@ -114,8 +129,10 @@ export default class FeedsService {
*/
injectBoosted(position: number, entities: Array<BaseModel>, end: number) {
if (this.offset <= position && end >= position) {
const boost = boostedContentService.fetch();
if (boost) entities.splice( position + this.offset, 0, boost );
const boost = boostedContentService.fetch();
if (boost) {
entities.splice(position + this.offset, 0, boost);
}
}
}
......@@ -127,10 +144,11 @@ export default class FeedsService {
this.feed.unshift({
owner_guid: entity.owner_guid,
timestamp: Date.now().toString(),
urn: entity.urn
urn: entity.urn,
});
this.offset++;
this.fallbackIndex++;
const plainEntity = entity.toPlainObject();
......@@ -146,6 +164,14 @@ export default class FeedsService {
return this.feed.length > this.limit + this.offset;
}
/**
* Set fallback index
* @param {number} value
*/
setFallbackIndex(value: number) {
this.fallbackIndex = value;
}
/**
* Set feed
* @param {Array<FeedRecordType>} feed
......@@ -235,6 +261,28 @@ export default class FeedsService {
abort(this);
}
/**
* Calculate the index of the fallback
*/
calculateFallbackIndex = () => {
let index = -1;
if (this.fallbackAt) {
index = this.feed.findIndex(
r =>
r.entity &&
r.entity.time_created &&
parseInt(r.entity.time_created, 10) < this.fallbackAt,
);
}
if (index !== -1) {
this.fallbackIndex = index;
} else {
this.fallbackIndex = -1;
}
};
/**
* Fetch
* @param {boolean} more
......@@ -242,17 +290,30 @@ export default class FeedsService {
async fetch(more: boolean = false): Promise<void> {
abort(this);
const params = {...this.params, ...{ limit: 150, as_activities: this.asActivities ? 1 : 0 }};
const params = {
...this.params,
...{limit: 150, as_activities: this.asActivities ? 1 : 0},
};
if (this.paginated && more) params.from_timestamp = this.pagingToken;
if (this.paginated && more) {
params.from_timestamp = this.pagingToken;
}
const response = await apiService.get(this.endpoint, params, this);
if (response.entities && response.entities.length) {
if (more) {
this.feed = this.feed.concat(response.entities);
} else {
this.feed = response.entities;
}
if (response.fallback_at) {
this.fallbackAt = response.fallback_at;
this.calculateFallbackIndex();
} else {
this.fallbackAt = null;
this.fallbackIndex = -1;
}
this.pagingToken = response['load-next'];
} else {
this.endReached = true;
......@@ -281,6 +342,8 @@ export default class FeedsService {
this.pagingToken = (this.feed[this.feed.length - 1].timestamp - 1).toString();
} else {
this.feed = feed.feed;
this.fallbackAt = feed.fallbackAt;
this.fallbackIndex = feed.fallbackIndex;
this.pagingToken = feed.next;
}
return true;
......@@ -299,9 +362,13 @@ export default class FeedsService {
const status = await this.fetchLocal();
try {
if (!status) await this.fetch();
if (!status) {
await this.fetch();
}
} catch (err) {
if (err.code === 'Abort') return;
if (err.code === 'Abort') {
return;
}
if (!isNetworkFail(err)) {
logService.exception('[FeedService]', err);
......@@ -318,13 +385,15 @@ export default class FeedsService {
try {
await this.fetch();
} catch (err) {
if (err.code === 'Abort') return;
if (err.code === 'Abort') {
return;
}
if (!isNetworkFail(err)) {
logService.exception('[FeedService]', err);
}
if (!await this.fetchLocal()) {
if (!(await this.fetchLocal())) {
// if there is no local data rethrow the exception
throw err;
}
......@@ -332,7 +401,9 @@ export default class FeedsService {
showMessage({
floating: true,
position: 'top',
message: (connectivityService.isConnected ? i18n.t('cantReachServer') : i18n.t('noInternet')),
message: connectivityService.isConnected
? i18n.t('cantReachServer')
: i18n.t('noInternet'),
description: i18n.t('showingStored'),
duration: 1300,
backgroundColor: '#FFDD63DD',
......@@ -368,8 +439,10 @@ export default class FeedsService {
clear(): FeedsService {
this.offset = 0;
this.limit = 12;
this.fallbackAt = null;
this.fallbackIndex = -1;
this.pagingToken = '';
this.params = {sync: 1};
this.params = {sync: 1};
this.feed = [];
return this;
}
......
......@@ -25,8 +25,21 @@ export class FeedsStorage {
try {
await this.getDb();
const params = [key, 0, JSON.stringify({feed: this.map(feed.feed), next: feed.pagingToken}), Math.floor(Date.now() / 1000)];
await this.db.executeSql('REPLACE INTO feeds (key, offset, data, updated) values (?,?,?,?)', params);
const params = [
key,
0,
JSON.stringify({
feed: this.map(feed.feed),
next: feed.pagingToken,
fallbackAt: feed.fallbackAt,
fallbackIndex: feed.fallbackIndex,
}),
Math.floor(Date.now() / 1000),
];
await this.db.executeSql(
'REPLACE INTO feeds (key, offset, data, updated) values (?,?,?,?)',
params,
);
} catch (err) {
logService.exception('[FeedsStorage]', err);
}
......@@ -41,7 +54,10 @@ export class FeedsStorage {
try {
const key = this.getKey(feed);
const [result] = await this.db.executeSql('SELECT * FROM feeds WHERE key=? AND offset=?;', [key, 0]);
const [result] = await this.db.executeSql(
'SELECT * FROM feeds WHERE key=? AND offset=?;',
[key, 0],
);
const rows = result.rows.raw();
......
import api from './../../common/services/api.service';
import logService from './log.service';
import i18n from './i18n.service';
import { UserError } from '../UserError';
export function vote(guid, direction, data) {
return api.put('api/v1/votes/' + guid + '/' + direction, data)
.then((data) => {
return { data }
})
.catch(err => {
logService.exception('[VotesService]', err);
throw new UserError(i18n.t('errorMessage'));
})
/**
* Vote an activity
* @param {string} guid
* @param {string} direction up|down
* @param {*} data extra data
*/
export async function vote(guid, direction, data) {
const response = await api.put(
'api/v1/votes/' + guid + '/' + direction,
data,
);
return {data: response};
}
......@@ -48,7 +48,7 @@ export default class FeedStore {
/**
* Viewed store
*/
viewed = new Viewed;
viewed = new Viewed();
/**
* Metadata service
......@@ -58,20 +58,27 @@ export default class FeedStore {
/**
* @var {FeedsService}
*/
feedsService = new FeedsService;
feedsService = new FeedsService();
/**
* The offset of the list
*/
scrollOffset = 0;
/**
* Getter fallback index
*/
get fallbackIndex() {
return this.feedsService.fallbackIndex;
}
/**
* Class constructor
* @param {boolean} includeMetadata include a metadata service
*/
constructor(includeMetadata = false) {
if (includeMetadata) {
this.metadataService = new MetadataService;
this.metadataService = new MetadataService();
}
}
......@@ -279,6 +286,15 @@ export default class FeedStore {
return this;
}
/**
* Set fallback index
* @param {number} value
*/
setFallbackIndex(value: number): FeedStore {
this.feedsService.setFallbackIndex(value);
return this;
}
/**
* Fetch from the endpoint
*/
......
......@@ -23,7 +23,7 @@ export const MINDS_URI_SETTINGS = {
export const MINDS_MAX_VIDEO_LENGTH = 5; // in minutes
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030'
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030';
export const MINDS_CDN_URI = 'https://cdn.minds.com/';
export const MINDS_ASSETS_CDN_URI = 'https://cdn-assets.minds.com/';
......@@ -45,6 +45,7 @@ export const MINDS_FEATURES = {
* Deeplink to screen/params maping
*/
export const MINDS_DEEPLINK = [
['email-confirmation', 'EmailConfirmation'],
['groups/profile/:guid/feed', 'GroupView'],
['groups/profile/:guid', 'GroupView'],
['notifications', 'Notifications'],
......@@ -59,7 +60,7 @@ export const MINDS_DEEPLINK = [
['wallet/tokens/:section', 'Wallet'],
];
export const DISABLE_PASSWORD_INPUTS = true;
export const DISABLE_PASSWORD_INPUTS = false;
// IF TRUE COMMENT THE SMS PERMISSIONS IN ANDROID MANIFEST TOO!!!
export const GOOGLE_PLAY_STORE = DeviceInfo.getBuildNumber() < 1050000000 && Platform.OS == 'android';
......@@ -23,7 +23,7 @@ export const MINDS_URI_SETTINGS = {
export const MINDS_MAX_VIDEO_LENGTH = 5; // in minutes
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030'
export const SOCKET_URI = 'wss://ha-socket-io-us-east-1.minds.com:3030';
export const MINDS_CDN_URI = 'https://cdn.minds.com/';
export const MINDS_ASSETS_CDN_URI = 'https://cdn-assets.minds.com/';
......@@ -45,6 +45,7 @@ export const MINDS_FEATURES = {
* Deeplink to screen/params maping
*/
export const MINDS_DEEPLINK = [
['email-confirmation', 'EmailConfirmation'],
['groups/profile/:guid/feed', 'GroupView'],
['groups/profile/:guid', 'GroupView'],
['notifications', 'Notifications'],
......
......@@ -31,6 +31,7 @@ import GroupsListItem from '../groups/GroupsListItem'
import ErrorBoundary from '../common/components/ErrorBoundary';
import i18n from '../common/services/i18n.service';
import FeedList from '../common/components/FeedList';
import FallbackBoundary from './FallbackBoundary';
/**
* Discovery Feed Screen
......@@ -45,6 +46,34 @@ export default class DiscoveryFeedScreen extends Component {
}
}
/**
* Render activity
*/
renderActivity = row => {
let isLast = this.props.discovery.feedStore.list.entities.length == row.index + 1;
const entity = row.item;
const boundaryText =
this.props.discovery.feedStore.list.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary message={this.cantShowActivity} containerStyle={CS.hairLineBottom}>
{boundaryText && <FallbackBoundary title={boundaryText}/>}
<Activity
entity={entity}
newsfeed={this.props.feedStore}
navigation={this.props.navigation}
autoHeight={false}
isLast={isLast}
/>
</ErrorBoundary>
)
}
/**
* Render
*/
......@@ -53,6 +82,7 @@ export default class DiscoveryFeedScreen extends Component {
return (
<FeedList
renderActivity={this.renderActivity}
feedStore={store}
ListFooterComponent={this.getFooter}
keyExtractor={this.keyExtractor}
......
......@@ -29,7 +29,7 @@ class DiscoveryFeedStore {
this.buildListStores();
}
setFeed(feed) {
setFeed(feed, fallbackIndex) {
this.list.clear();
this.list.viewed.clearViewed();
......@@ -38,6 +38,8 @@ class DiscoveryFeedStore {
.setFeed(feed)
.setOffset(0)
.hydratePage();
this.list.setFallbackIndex(fallbackIndex);
}
/**
......
......@@ -7,7 +7,6 @@ import {
StyleSheet,
Platform,
Text,
FlatList,
Dimensions,
RefreshControl,
View,
......@@ -18,7 +17,6 @@ import {
import { ListItem, Avatar } from 'react-native-elements';
import IonIcon from 'react-native-vector-icons/Ionicons';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Modal from 'react-native-modal'
import {
observer,
......@@ -47,14 +45,15 @@ import ErrorBoundary from '../common/components/ErrorBoundary';
import testID from '../common/helpers/testID';
import i18n from '../common/services/i18n.service';
import { FLAG_VIEW } from '../common/Permissions';
import FallbackBoundary from './FallbackBoundary';
/**
* Discovery screen
*/
export default
@inject('discovery', 'channel')
@observer
export default class DiscoveryScreen extends Component {
class DiscoveryScreen extends Component {
cols = 3;
iconSize = 28;
......@@ -62,13 +61,13 @@ export default class DiscoveryScreen extends Component {
active: false,
showFeed: false,
itemHeight: 0,
q: ''
}
q: '',
};
viewOptsFeed = {
viewAreaCoveragePercentThreshold: 50,
minimumViewTime: 300
}
minimumViewTime: 300,
};
static navigationOptions = {
tabBarIcon: ({ tintColor }) => (
......@@ -524,14 +523,22 @@ export default class DiscoveryScreen extends Component {
*/
navigateToFeed = ({urn}) => {
const index = this.props.discovery.listStore.feedsService.feed.findIndex(e => e.urn === urn);
let fallbackIndex = this.props.discovery.listStore.fallbackIndex;
this.props.discovery.feedStore.setFeed(this.props.discovery.listStore.feedsService.feed.slice(index));
if (fallbackIndex !== -1 && fallbackIndex > index) {
fallbackIndex -= index;
}
this.props.discovery.feedStore.setFeed(
this.props.discovery.listStore.feedsService.feed.slice(index),
fallbackIndex,
);
this.props.navigation.push('DiscoveryFeed', {
'showFeed': index,
title: _.capitalize(this.props.discovery.filters.filter) + ' ' + _.capitalize(this.props.discovery.filters.type)
})
}
});
};
/**
* Render a tile
......@@ -540,12 +547,20 @@ export default class DiscoveryScreen extends Component {
if (!this.state.active && row.item.isGif()) {
return <View style={{ height: this.state.itemHeight, width: this.state.itemHeight }}/>;
}
const boundaryText =
this.props.discovery.listStore.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary message={this.tileError} containerStyle={[CS.centered, {width: this.state.itemHeight, height: this.state.itemHeight}]} textSmall={true}>
<DiscoveryTile
entity={row.item}
size={this.state.itemHeight}
onPress={this.navigateToFeed}
boundaryText={boundaryText}
/>
</ErrorBoundary>
);
......@@ -567,8 +582,16 @@ export default class DiscoveryScreen extends Component {
* Render activity item
*/
renderActivity = (row) => {
const boundaryText =
this.props.discovery.listStore.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary containerStyle={CS.hairLineBottom}>
{boundaryText && <FallbackBoundary title={boundaryText}/>}
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false} />
</ErrorBoundary>
);
......
import {
observable,
action,
computed,
extendObservable
} from 'mobx';
import ActivityModel from '../newsfeed/ActivityModel';
import BlogModel from '../blogs/BlogModel';
import OffsetFeedListStore from '../common/stores/OffsetFeedListStore';
import UserModel from '../channel/UserModel';
import GroupModel from '../groups/GroupModel';
import {action} from 'mobx';
import NewsfeedFilterStore from '../common/stores/NewsfeedFilterStore';
import DiscoveryFeedStore from './DiscoveryFeedStore';
import logService from '../common/services/log.service';
import featuresService from '../common/services/features.service';
import boostedContentService from '../common/services/boosted-content.service';
import FeedStore from '../common/stores/FeedStore';
import appStores from '../../AppStores';
......@@ -22,7 +10,6 @@ import appStores from '../../AppStores';
* Discovery Store
*/
class DiscoveryStore {
/**
* FeedStore
*/
......@@ -88,14 +75,14 @@ class DiscoveryStore {
if (this.filters.type !== 'lastchannels') {
this.fetch();
}
}
};
/**
* On search change
*/
onSearchChange = (searchtext) => {
this.fetch(true);
}
};
fetch(refresh = false) {
const hashtags = appStores.hashtag.hashtag ? encodeURIComponent(appStores.hashtag.hashtag) : '';
......@@ -112,6 +99,7 @@ class DiscoveryStore {
all,
query: this.filters.searchtext,
nsfw: this.filters.nsfw.concat([]),
period_fallback: 1,
})
.fetchRemoteOrLocal(refresh);
}
......
......@@ -104,11 +104,24 @@ class DiscoveryTile extends Component {
<ExplicitOverlay entity={entity} iconSize={45} hideText={true} />
) : null;
const boundary = this.props.boundaryText ? (
<View
style={[
CS.positionAbsoluteTop,
CS.backgroundGreyed,
CS.centered,
styles.boundary,
]}>
<Text>{this.props.boundaryText}</Text>
</View>
) : null;
return (
<TouchableOpacity
onPress={this._onPress}
style={[this.state.style, styles.tile]}>
<View style={[CS.flexContainer, CS.backgroundGreyed]}>
{boundary}
<FastImage
source={url}
style={CS.positionAbsolute}
......@@ -123,6 +136,11 @@ class DiscoveryTile extends Component {
}
const styles = StyleSheet.create({
boundary: {
height: 20,
width: '100%',
zIndex: 1000,
},
tile: {
paddingTop: 1,
paddingBottom: 1,
......
import React from 'react';
import {View, Text} from 'react-native';
import {CommonStyle as CS} from '../styles/Common';
/**
* Fallback boundary for feed views
* @param {object} props
*/
export default function FallbackBoundary(props) {
return (
<View
style={[
CS.flexContainer,
CS.centered,
CS.paddingTop,
CS.paddingBottom,
CS.borderBottomHair,
CS.fullWidth,
CS.borderGreyed,
CS.backgroundLight,
]}>
<Text style={[CS.colorDarkGreyed, CS.fontXL]}>{props.title}</Text>
</View>
);
}
......@@ -8,6 +8,14 @@ import groupsService from './GroupsService';
export default class GroupModel extends BaseModel {
@observable conversationDisabled = false;
/**
* Constructor
*/
constructor(data) {
data.name = String(data.name);
super(data);
}
@action
async toggleConversationDisabled() {
await groupsService.toggleConversationDisabled(this.guid, !this.conversationDisabled);
......
import React, {
Component
} from 'react';
import React, {Component} from 'react';
import {
observer,
inject
} from 'mobx-react/native'
import {observer, inject} from 'mobx-react/native';
import {
MINDS_CDN_URI
} from '../config/Config';
import {MINDS_CDN_URI} from '../config/Config';
import { ListItem, Avatar } from 'react-native-elements';
import {ListItem} from 'react-native-elements';
import Button from '../common/components/Button';
import colors from '../styles/Colors';
import i18n from '../common/services/i18n.service';
import { CommonStyle as CS } from '../styles/Common';
import { FLAG_JOIN } from '../common/Permissions';
import {CommonStyle as CS} from '../styles/Common';
import {FLAG_JOIN} from '../common/Permissions';
export default
@inject('groupView')
@observer
class GroupsListItem extends Component {
state = {
source: null,
};
/**
* Derive state from props
* @param {object} nextProps
* @param {object} prevState
*/
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.group && prevState.group !== nextProps.group) {
return {
source: {
rounded: true,
size: 45,
source: {
uri:
MINDS_CDN_URI +
'fs/v1/avatars/' +
nextProps.group.guid +
'/small/' +
nextProps.group.icontime,
},
},
};
}
return null;
}
/**
* Render
*/
......@@ -33,15 +56,10 @@ class GroupsListItem extends Component {
containerStyle={CS.noBorderBottom}
title={this.props.group.name}
keyExtractor={item => item.rowKey}
avatar={
<Avatar
width={42}
height={42}
rounded
source={{ uri: MINDS_CDN_URI + 'fs/v1/avatars/' + this.props.group.guid + '/small/' + this.props.group.icontime }}
/>
}
subtitle={i18n.t('groups.listMembersCount', {count: this.props.group['members:count']})}
leftAvatar={this.state.source}
subtitle={i18n.t('groups.listMembersCount', {
count: this.props.group['members:count'],
})}
onPress={this._onPress}
hideChevron={!button}
rightIcon={button}
......@@ -54,18 +72,20 @@ class GroupsListItem extends Component {
*/
_onPress = () => {
if (this.props.onPress) {
this.props.onPress(this.props.group)
this.props.onPress(this.props.group);
}
}
};
/**
* Get button
*/
getButton = () => {
return this.props.group['is:member'] ?
<Button text="Leave" onPress={this.leave} color={colors.darkGreyed}/> :
<Button text="Join" onPress={this.join} color={colors.darkGreyed}/>
}
return this.props.group['is:member'] ? (
<Button text="Leave" onPress={this.leave} color={colors.darkGreyed} />
) : (
<Button text="Join" onPress={this.join} color={colors.darkGreyed} />
);
};
/**
* Join the group
......@@ -74,15 +94,12 @@ class GroupsListItem extends Component {
if (!this.props.group.can(FLAG_JOIN, true)) return;
this.props.groupView.setGroup(this.props.group);
this.props.groupView.join(this.props.group.guid);
}
};
/**
* Leave the group
*/
leave = () => {
this.props.groupView.setGroup(this.props.group);
this.props.groupView.leave(this.props.group.guid);
}
};
}
This diff is collapsed.
import BaseModel from '../common/BaseModel';
import {observable} from 'mobx';
/**
* Conversation model
*/
export default class ConversationModel extends BaseModel {
@observable unread = false;
@observable online = false;
//TODO: move decryption logic here
}
......@@ -71,6 +71,7 @@ export default class ConversationScreen extends Component {
let conversation;
if (params.conversation) {
conversation = params.conversation;
conversation.unread = false;
} else {
// open conversation with params.target user (minor guid go first)
if (params.target > this.props.user.me.guid) {
......
import {
observable,
action,
inject
} from 'mobx';
import {
Alert
} from 'react-native';
import messengerService from './MessengerService';
import crypto from './../common/services/crypto.service';
import socket from '../common/services/socket.service';
......@@ -18,7 +13,6 @@ import logService from '../common/services/log.service';
* Messenger Conversation Store
*/
class MessengerConversationStore {
/**
* Messages observable
*/
......
......@@ -90,9 +90,7 @@ class MessengerListStore {
@action
touchConversation = (guid) => {
// search conversation
const index = this.conversations.findIndex((conv) => {
return conv.guid == guid;
})
const index = this.conversations.findIndex(conv => conv.guid === guid);
if (index !== -1) {
const conv = this.conversations[index];
......@@ -136,7 +134,7 @@ class MessengerListStore {
try {
// is a search?
if (this.search && this.newsearch) {
if (this.search && (this.newsearch || reload)) {
this.newsearch = false;
response = await messengerService.searchConversations(this.search, rows, this);
} else {
......
import React, {
Component
} from 'react';
import React, {Component} from 'react';
import {
Text,
View,
Alert,
Image,
Platform,
FlatList,
StyleSheet,
ActivityIndicator,
Dimensions,
} from 'react-native';
import {
inject,
observer
} from 'mobx-react/native'
import {inject, observer} from 'mobx-react/native';
import _ from 'lodash';
......@@ -26,9 +19,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import ConversationView from './conversation/ConversationView';
import SearchView from '../common/components/SearchView';
import { CommonStyle } from '../styles/Common';
import { ComponentsStyle } from '../styles/Components';
import Colors from '../styles/Colors';
import {ComponentsStyle} from '../styles/Components';
import MessengerTabIcon from './MessengerTabIcon';
import ErrorLoading from '../common/components/ErrorLoading';
import i18n from '../common/services/i18n.service';
......@@ -36,24 +27,23 @@ import i18n from '../common/services/i18n.service';
/**
* Messenger Conversarion List Screen
*/
export default
@inject('messengerList')
@observer
export default class MessengerScreen extends Component {
class MessengerScreen extends Component {
state = {
active: false,
}
};
static navigationOptions = {
tabBarIcon: ({ tintColor }) => (
<MessengerTabIcon tintColor={tintColor} />
)
}
tabBarIcon: ({tintColor}) => <MessengerTabIcon tintColor={tintColor} />,
};
/**
* On component will mount
*/
componentDidMount() {
// load list
this.props.messengerList.loadList();
// listen socket on app start
......@@ -185,41 +175,41 @@ export default class MessengerScreen extends Component {
*/
searchChange = (search) => {
this.searchDebouncer(search);
}
};
/**
* Clear and reload
*/
refresh = () => {
this.props.messengerList.refresh();
}
};
/**
* Load more rows
*/
loadMore = () => {
this.props.messengerList.loadList()
}
this.props.messengerList.loadList();
};
/**
* render row
* @param {object} row
*/
renderMessage = (row) => {
renderMessage = row => {
return (
<ConversationView
item={row.item}
styles={styles}
navigation={this.props.navigation}
testID={row.item.username.toUpperCase()}
/>
/>
);
}
};
}
// styles
const styles = StyleSheet.create({
listView: {
listView: {
//paddingTop: 20,
flex: 1
},
......
......@@ -49,6 +49,7 @@ import {withErrorBoundaryScreen} from '../common/components/ErrorBoundary';
import DeleteChannelScreen from '../settings/screens/DeleteChannelScreen';
import DiscoveryFeedScreen from '../discovery/DiscoveryFeedScreen';
import Gathering from '../gathering/Gathering';
import EmailConfirmationScreen from '../onboarding/EmailConfirmationScreen';
/**
* Main stack navigator
......@@ -57,6 +58,9 @@ const Stack = createStackNavigator({
Tabs: {
screen: withErrorBoundaryScreen(TabsScreen),
},
EmailConfirmation: {
screen: withErrorBoundaryScreen(EmailConfirmationScreen)
},
// Logs: {
// screen: LogsScreen
// },
......
......@@ -74,7 +74,6 @@ export default class Activity extends Component {
if (this.props.entity.listRef) {
const offsetToScrollTo = this.props.entity._list.scrollOffset + e.nativeEvent.layout.height;
setTimeout(() => {
this.props.entity.listRef.scrollToOffset({
offset: offsetToScrollTo,
......
......@@ -22,6 +22,7 @@ import testID from '../../../common/helpers/testID';
import i18n from '../../../common/services/i18n.service';
import logService from '../../../common/services/log.service';
import { FLAG_VOTE } from '../../../common/Permissions';
import remoteAction from '../../../common/RemoteAction';
// prevent double tap in touchable
const TouchableOpacityCustom = withPreventDoubleTap(TouchableOpacity);
......@@ -82,22 +83,12 @@ class ThumbUpAction extends Component {
* Toggle thumb
*/
toggleThumb = async () => {
if (!this.props.entity.can(FLAG_VOTE, true)) return;
try {
await this.props.entity.toggleVote(this.direction);
} catch (err) {
logService.exception(`[Thumb${this.direction}Action]`, err)
Alert.alert(
i18n.t('sorry'),
i18n.t('errorMessage') + '\n' + i18n.t('activity.tryAgain'),
[
{text: i18n.t('ok'), onPress: () => {}},
],
{ cancelable: false }
);
if (!this.props.entity.can(FLAG_VOTE, true)) {
return;
}
}
remoteAction(async () => {
await this.props.entity.toggleVote(this.direction);
});
};
}
......@@ -35,14 +35,14 @@ export default class BoostPeerAcceptedView extends BoostGiftView {
}
getType() {
return (this.props.entity.params.type == 'pro') ? i18n.t('usd') : i18n.t('tokens');
return (this.props.entity.params.type === 'pro') ? i18n.t('usd') : i18n.t('tokens');
}
getAmount() {
if (this.props.entity.params.type == 'pro') {
if (this.props.entity.params.type === 'pro') {
return this.props.entity.params.bid;
} else {
return number(token(this.props.entity.params.bid, 18), 0, 3)
return number(token(this.props.entity.params.bid, 18), 0, 3);
}
}
}
\ No newline at end of file
}
......@@ -26,10 +26,9 @@ export default class BoostPeerRejectedView extends BoostPeerAcceptedView {
return (
<View style={styles.bodyContents}>
<Text onPress={() => this.navToBoostConsole({filter:'peer'})}>
<Text style={styles.bold} onPress={this.navToChannel}>@{entity.from.username}</Text> {i18n.t('notification.boostPeerRejected')} <Text style={styles.bold}>{amount} {type}</Text> {description}
{i18n.t('notification.notCharged')}
<Text style={styles.bold} onPress={this.navToChannel}>@{entity.from.username}</Text> {i18n.t('notification.boostPeerRejected')} <Text style={styles.bold}>{amount} {type}</Text> {description} {i18n.t('notification.notCharged')}
</Text>
</View>
)
);
}
}
\ No newline at end of file
}
......@@ -4,7 +4,7 @@ import React, {
import {
Text,
View
View,
} from 'react-native';
import BoostGiftView from './BoostGiftView';
......@@ -34,6 +34,6 @@ export default class BoostRejectedView extends BoostGiftView {
<View style={styles.bodyContents}>
<Text onPress={this.navToBoostConsole}>{i18n.to('notification.boostRejected', {reason}, {description})}</Text>
</View>
)
);
}
}
\ No newline at end of file
}
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { CommonStyle as CS } from '../styles/Common';
import CenteredLoading from '../common/components/CenteredLoading';
import i18n from '../common/services/i18n.service';
import sessionService from '../common/services/session.service';
/**
* Email confirmation screen
*/
export default class EmailConfirmationScreen extends Component {
static navigationOptions = {
title: 'Email confirm',
};
/**
* State
*/
state = {
confirmed: false,
error: false,
};
/**
* Component did mount
*/
componentDidMount() {
this.confirm();
}
/**
* Confirm
*/
confirm = async () => {
this.setState({ error: false });
const result = await sessionService
.getUser()
.confirmEmail(this.props.navigation.state.params);
if (!result) {
this.setState({ error: true });
} else {
this.setState({ confirmed: true });
}
};
/**
* Render body
*/
renderBody() {
if (this.state.error) {
return (
<Text
style={[CS.fontL, CS.textCenter, CS.colorDarkGreyed]}
onPress={this.confirm}>
{i18n.t('errorMessage') + '\n'}
<Text style={[CS.colorPrimary]}>{i18n.t('tryAgain')}</Text>
</Text>
);
}
if (this.state.confirmed) {
return (
<Text
style={[CS.fontXL, CS.textCenter, CS.colorDarkGreyed]}
onPress={() => this.props.navigation.goBack()}>
{i18n.t('emailConfirm.confirmed') + '\n'}
<Text style={[CS.colorPrimary]}>{i18n.t('goback')}</Text>
</Text>
);
}
return <CenteredLoading />;
}
/**
* Render
*/
render() {
return (
<View style={[CS.flexContainer, CS.centered]}>{this.renderBody()}</View>
);
}
}
......@@ -17,10 +17,12 @@ import CenteredLoading from '../../common/components/CenteredLoading';
import Button from '../../common/components/Button';
import { CommonStyle } from '../../styles/Common';
import ModalConfirmPassword from '../../auth/ModalConfirmPassword';
import { inject } from 'mobx-react/native'
/**
* Email settings screen
*/
@inject('user')
export default class EmailScreen extends Component {
static navigationOptions = {
......@@ -31,7 +33,8 @@ export default class EmailScreen extends Component {
email: null,
saving: false,
isVisible: false,
loaded: false
loaded: false,
showConfirmNote: false,
}
constructor(){
......@@ -45,7 +48,7 @@ export default class EmailScreen extends Component {
* Set email value
*/
setEmail = (email) => {
this.setState({email});
this.setState({email, showConfirmNote: true});
}
/**
......@@ -61,9 +64,9 @@ export default class EmailScreen extends Component {
this.props.navigation.goBack();
})
.finally(() => {
this.setState({isVisible:false});
this.setState({saving: false});
this.props.user.me.setEmailConfirmed(false);
})
.catch(() => {
Alert.alert(i18n.t('error'), i18n.t('settings.errorSaving'));
......@@ -83,19 +86,19 @@ export default class EmailScreen extends Component {
}
const email = this.state.email;
const showConfirmNote = this.state.showConfirmNote;
// validate
const error = validator.emailMessage(email);
const message = error ? <FormValidationMessage>{error}</FormValidationMessage> : null;
const confirmNote = showConfirmNote ? <FormValidationMessage>{i18n.t('emailConfirm.confirmNote')}</FormValidationMessage> : null;
return (
<View style={[CommonStyle.flexContainer, CommonStyle.backgroundWhite, CommonStyle.marginTop2x]}>
<Input
label={i18n.t('settings.currentEmail')}
onChangeText={this.setEmail}
value={email}
errorMessage={error}
leftIcon={{ type: 'font-awesome', name: 'envelope-o' }}
/>
<View style={[CommonStyle.flexContainer, CommonStyle.backgroundWhite]}>
<FormLabel labelStyle={CommonStyle.fieldLabel}>{i18n.t('settings.currentEmail')}</FormLabel>
<FormInput onChangeText={this.setEmail} value={email} inputStyle={CommonStyle.fieldTextInput}/>
{message}
{confirmNote}
<Button
text={i18n.t('save').toUpperCase()}
loading={this.state.saving}
......
import React, { Component } from 'react';
import { Text, StyleSheet, View, Alert } from 'react-native';
import i18n from '../common/services/i18n.service';
import emailConfirmationService from '../common/services/email-confirmation.service';
import IonIcon from 'react-native-vector-icons/Ionicons';
import { CommonStyle as CS } from '../styles/Common';
import { observer, inject } from 'mobx-react/native';
/**
* Email Confirmation Message
*/
export default
@inject('user')
@observer
class EmailConfirmation extends Component {
/**
* Send confirmation email
*/
send = async () => {
if (await emailConfirmationService.send()) {
Alert.alert(i18n.t('emailConfirm.sent'));
} else {
Alert.alert(i18n.t('pleaseTryAgain'));
}
};
/**
* Dismiss message
*/
dismiss = () => {
this.props.user.setDissmis(true);
};
/**
* Render
*/
render() {
const show =
!this.props.user.emailConfirmMessageDismiss &&
this.props.user.me.email_confirmed === false;
if (!show) {
return null;
}
return (
<View style={styles.container}>
<Text style={[CS.fontM, CS.colorWhite]}>
{i18n.t('emailConfirm.confirm')}
</Text>
<Text style={[CS.bold, CS.colorWhite]} onPress={this.send}>
{i18n.t('emailConfirm.sendAgain')}
</Text>
<IonIcon
style={[styles.modalCloseIcon, CS.colorWhite]}
size={28}
name="ios-close"
onPress={this.dismiss}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#4690df',
height: 40,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
},
modalCloseIcon: {
position: 'absolute',
alignSelf: 'flex-end',
paddingRight: 15,
},
});
......@@ -19,13 +19,15 @@ import featuresService from '../common/services/features.service';
import { SafeAreaView } from 'react-navigation';
import isIphoneX from '../common/helpers/isIphoneX';
import testID from '../common/helpers/testID';
import EmailConfirmation from './EmailConfirmation';
const forceInset = isIphoneX ? {top: 32} : null
export default
@inject('user')
@inject('wallet')
@observer
export default class Topbar extends Component {
class Topbar extends Component {
componentDidMount() {
this.props.wallet.refresh();
......@@ -36,9 +38,9 @@ export default class Topbar extends Component {
<SafeAreaView style={styles.container} forceInset={forceInset}>
<View style={styles.topbar}>
{ featuresService.has('crypto') &&
<TouchableOpacity
onPress={() => this.props.navigation.navigate('BoostConsole', { navigation: this.props.navigation })}
{ featuresService.has('crypto') &&
<TouchableOpacity
onPress={() => this.props.navigation.navigate('BoostConsole', { navigation: this.props.navigation })}
{...testID('boost-console button')} >
<View style={styles.topbarLeft}>
<Icon name="trending-up" size={22} color='#444' style={ styles.button }/>
......@@ -57,7 +59,6 @@ export default class Topbar extends Component {
testID="AvatarButton"
/> }
</View>
<TouchableOpacity onPress={() => this.props.navigation.navigate('More', { navigation: this.props.navigation })} {...testID('Main menu button')}>
<View style={styles.topbarRight}>
<Icon name="menu" size={22} color='#444' style={ styles.button }/>
......@@ -65,6 +66,7 @@ export default class Topbar extends Component {
</TouchableOpacity>
</View>
<EmailConfirmation user={this.props.user} />
</SafeAreaView>
);
}
......@@ -81,7 +83,7 @@ const styles = StyleSheet.create({
container: {
height: topbarHeight,
display: 'flex',
flexDirection: 'row',
flexDirection: 'column',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#EEE',
backgroundColor: '#FFFFFF',
......
......@@ -8261,7 +8261,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
......@@ -8595,6 +8595,19 @@ react-native-device-log@Minds/react-native-device-log#74f06b09c6656aa228a9a3a474
react-native-invertible-scroll-view "^1.1.1"
stacktrace-parser "^0.1.4"
react-native-elements@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-native-elements/-/react-native-elements-1.1.0.tgz#f99bcda4459a886f3ab4591c684c099d37aedf2b"
integrity sha512-n1eOL0kUdlH01zX7bn1p7qhYXn7kquqxYQ0oWlxoAck9t5Db/KeK5ViOsAk8seYSvAG6Pe7OxgzRFnMfFhng0Q==
dependencies:
color "^3.1.0"
deepmerge "^3.1.0"
hoist-non-react-statics "^3.1.0"
opencollective-postinstall "^2.0.0"
prop-types "^15.5.8"
react-native-ratings "^6.3.0"
react-native-status-bar-height "^2.2.0"
react-native-elements@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/react-native-elements/-/react-native-elements-1.2.7.tgz#1eca2db715c41722aeb67aea62bd2a4621adb134"
......@@ -8784,6 +8797,10 @@ react-native-ratings@^6.3.0:
lodash "^4.17.4"
prop-types "^15.5.10"
react-native-read-more-text@devbelieve365/react-native-read-more-text:
version "1.0.0"
resolved "https://codeload.github.com/devbelieve365/react-native-read-more-text/tar.gz/7828b78bd632feeb3f5f6bb3fa1b61021ded900f"
react-native-reanimated@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-1.4.0.tgz#7f1acbf9be08492d834f512700570978052be2f9"
......@@ -9339,6 +9356,12 @@ rn-apk@^0.2.9:
resolved "https://registry.yarnpkg.com/rn-apk/-/rn-apk-0.2.9.tgz#6aec783dd64cdf6074b0a590cc51261999351b79"
integrity sha512-JcqO9raQWdjQAaRKjz6OFdE6G4GzZnqhqMLRyKHQ9WBaYFzhNpzujTyTr0T9cngvMJ1JOmHfxG1U8DFHf7QU3A==
"rne-modal-tooltip@gist:b28c003d87c619674def0878473338a0":
version "1.1.0"
resolved "https://gist.github.com/b28c003d87c619674def0878473338a0.git#b68c9d067545ce72b0aa4c9bf5a5fea90d136bc0"
dependencies:
react-native-elements "1.1.0"
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
......