...
 
Commits (9)
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
[*.js]
indent_style = space
indent_size = 2
\ No newline at end of file
indent_size = 2
trim_trailing_whitespace = true
prettier.singleQuote = true
\ No newline at end of file
module.exports = {
"parser": "babel-eslint",
"plugins": [
"react-native",
"flowtype"
],
"extends": ["plugin:react-native/all"],
"rules": {
"flowtype/boolean-style": [
2,
"boolean"
],
"flowtype/define-flow-type": 1,
"flowtype/delimiter-dangle": [
2,
"never"
],
"flowtype/generic-spacing": [
2,
"never"
],
"flowtype/no-mixed": 0,
"flowtype/no-primitive-constructor-types": 2,
"flowtype/no-types-missing-file-annotation": 2,
"flowtype/no-weak-types": 0,
"flowtype/object-type-delimiter": [
2,
"comma"
],
"flowtype/require-parameter-type": 2,
"flowtype/require-readonly-react-props": 0,
"flowtype/require-return-type": [
2,
"always",
{
"annotateUndefined": "never"
}
],
"flowtype/require-valid-file-annotation": 2,
"flowtype/semi": [
2,
"always"
],
"flowtype/space-after-type-colon": [
2,
"always"
],
"flowtype/space-before-generic-bracket": [
2,
"never"
],
"flowtype/space-before-type-colon": [
2,
"never"
],
"flowtype/type-id-match": [
2,
"^([A-Z][a-z0-9]+)+Type$"
],
"flowtype/union-intersection-spacing": [
2,
"always"
],
"flowtype/use-flow-type": 1,
"flowtype/valid-syntax": 1
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}
\ No newline at end of file
......@@ -5,25 +5,20 @@ import { shallow } from 'enzyme';
import ActivityEditor from '../../../src/newsfeed/activity/ActivityEditor';
import { activitiesServiceFaker } from '../../../__mocks__/fake/ActivitiesFaker';
import ActivityModel from '../../../src/newsfeed/ActivityModel';
describe('Activity editor component', () => {
let user, comments, entity, screen, newsfeed, toggleEdit;
let user, comments, entity, screen, newsfeed, toggleEdit, activity;
beforeEach(() => {
newsfeed = {
list: {
updateActivity: jest.fn()
}
}
newsfeed.list.updateActivity.mockResolvedValue(true);
const navigation = { navigate: jest.fn() };
let activityResponse = activitiesServiceFaker().load(1);
activity = new ActivityModel(activityResponse.activities[0])
toggleEdit = jest.fn();
screen = shallow(
<ActivityEditor entity={activityResponse.activities[0]} toggleEdit={toggleEdit} navigation={navigation} newsfeed={newsfeed}/>
<ActivityEditor entity={activity} toggleEdit={toggleEdit} navigation={navigation} newsfeed={newsfeed}/>
);
jest.runAllTimers();
......@@ -45,7 +40,7 @@ describe('Activity editor component', () => {
screen.update()
let instance = screen.instance();
expect(instance.state.text).toBe('Message');
const spy = jest.spyOn(instance.props.newsfeed.list, 'updateActivity');
const spy = jest.spyOn(activity, 'updateActivity');
screen.find('Button').at(1).props().onPress();
expect(spy).toHaveBeenCalled();
......
......@@ -17,6 +17,7 @@ exports[`Activity component renders correctly 1`] = `
<Pinned
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -32,6 +33,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -71,6 +73,7 @@ exports[`Activity component renders correctly 1`] = `
<OwnerBlock
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -86,6 +89,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -141,6 +145,7 @@ exports[`Activity component renders correctly 1`] = `
<ActivityActionSheet
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -156,6 +161,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -239,6 +245,7 @@ exports[`Activity component renders correctly 1`] = `
<ExplicitText
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -254,6 +261,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -304,6 +312,7 @@ exports[`Activity component renders correctly 1`] = `
<Translate
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -319,6 +328,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -365,6 +375,7 @@ exports[`Activity component renders correctly 1`] = `
<MediaView
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -380,6 +391,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -432,6 +444,7 @@ exports[`Activity component renders correctly 1`] = `
<Actions
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -447,6 +460,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -492,6 +506,7 @@ exports[`Activity component renders correctly 1`] = `
<ActivityMetrics
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -507,6 +522,7 @@ exports[`Activity component renders correctly 1`] = `
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......
......@@ -94,6 +94,7 @@ exports[`Activity editor component renders correctly 1`] = `
/>
<Button
color="#4690D6"
disabled={false}
inverted={true}
loading={false}
onPress={[Function]}
......
......@@ -16,6 +16,7 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
<CommentList
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -31,6 +32,7 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -61,6 +63,7 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
autoHeight={false}
entity={
ActivityModel {
"__list": null,
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
......@@ -76,6 +79,7 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"__list": null,
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
......@@ -142,88 +146,6 @@ exports[`Activity screen component renders correctly with an entity as param 1`]
},
}
}
newsfeed={
NewsfeedStore {
"boosts": Array [],
"filter": "subscribed",
"loadingBoost": true,
"service": NewsfeedService {
"_getFeed": [MockFunction],
"getBoosts": [MockFunction],
"getFeed": [MockFunction],
"getFeedChannel": [MockFunction],
"getFeedFromService": [MockFunction],
"getFeedLegacy": [MockFunction],
"getFeedSuggested": [MockFunction],
},
"stores": Object {
"boostfeed": Object {
"list": OffsetFeedListStore {
"entities": Array [],
"errorLoading": false,
"loaded": false,
"offset": "",
"refreshing": false,
"saving": false,
},
"loading": false,
},
"subscribed": Object {
"list": OffsetFeedListStore {
"entities": Array [
ActivityModel {
"attachment_guid": false,
"blurb": false,
"comments:count": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"getThumbSource": [Function],
"guid": "activityguid0",
"is_visible": true,
"mature": false,
"mature_visibility": false,
"message": "Message",
"ownerObj": UserModel {
"getAvatarSource": [Function],
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"paywall": undefined,
"perma_url": false,
"pinned": undefined,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"thumbs:down:count": undefined,
"thumbs:down:user_guids": undefined,
"thumbs:up:count": undefined,
"thumbs:up:user_guids": undefined,
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
"tokens": 1000000000000000000,
},
},
],
"errorLoading": false,
"loaded": false,
"offset": "",
"refreshing": false,
"saving": false,
},
"loading": false,
},
},
"viewed": Array [],
}
}
/>
}
navigation={
......@@ -418,6 +340,7 @@ exports[`Activity screen component should show loader until it loads the activit
<CommentList
entity={
ActivityModel {
"__list": null,
"comments:count": undefined,
"edited": undefined,
"is_visible": true,
......@@ -436,6 +359,7 @@ exports[`Activity screen component should show loader until it loads the activit
autoHeight={false}
entity={
ActivityModel {
"__list": null,
"comments:count": undefined,
"edited": undefined,
"is_visible": true,
......@@ -460,45 +384,6 @@ exports[`Activity screen component should show loader until it loads the activit
},
}
}
newsfeed={
NewsfeedStore {
"loadingBoost": true,
"service": NewsfeedService {
"_getFeed": [MockFunction],
"getBoosts": [MockFunction],
"getFeed": [MockFunction],
"getFeedChannel": [MockFunction],
"getFeedFromService": [MockFunction],
"getFeedLegacy": [MockFunction],
"getFeedSuggested": [MockFunction],
},
"stores": Object {
"boostfeed": Object {
"list": OffsetFeedListStore {
"entities": Array [],
"errorLoading": false,
"loaded": false,
"offset": "",
"refreshing": false,
"saving": false,
},
"loading": false,
},
"subscribed": Object {
"list": OffsetFeedListStore {
"entities": Array [],
"errorLoading": false,
"loaded": false,
"offset": "",
"refreshing": false,
"saving": false,
},
"loading": false,
},
},
"viewed": Array [],
}
}
/>
}
navigation={
......
......@@ -16,6 +16,7 @@ exports[`blog view screen component should renders correctly 1`] = `
<CommentList
entity={
BlogModel {
"__list": null,
"access_id": "2",
"boost_rejection_reason": -1,
"categories": Array [],
......@@ -40,6 +41,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"mature": false,
"monetized": false,
"ownerObj": UserModel {
"__list": null,
"access_id": "2",
"banned": "no",
"blocked": undefined,
......@@ -197,6 +199,7 @@ exports[`blog view screen component should renders correctly 1`] = `
<OwnerBlock
entity={
BlogModel {
"__list": null,
"access_id": "2",
"boost_rejection_reason": -1,
"categories": Array [],
......@@ -221,6 +224,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"mature": false,
"monetized": false,
"ownerObj": UserModel {
"__list": null,
"access_id": "2",
"banned": "no",
"blocked": undefined,
......@@ -360,6 +364,7 @@ exports[`blog view screen component should renders correctly 1`] = `
<RemindAction
entity={
BlogModel {
"__list": null,
"access_id": "2",
"boost_rejection_reason": -1,
"categories": Array [],
......@@ -384,6 +389,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"mature": false,
"monetized": false,
"ownerObj": UserModel {
"__list": null,
"access_id": "2",
"banned": "no",
"blocked": undefined,
......@@ -503,6 +509,7 @@ exports[`blog view screen component should renders correctly 1`] = `
<ThumbUpAction
entity={
BlogModel {
"__list": null,
"access_id": "2",
"boost_rejection_reason": -1,
"categories": Array [],
......@@ -527,6 +534,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"mature": false,
"monetized": false,
"ownerObj": UserModel {
"__list": null,
"access_id": "2",
"banned": "no",
"blocked": undefined,
......@@ -642,6 +650,7 @@ exports[`blog view screen component should renders correctly 1`] = `
<ThumbDownAction
entity={
BlogModel {
"__list": null,
"access_id": "2",
"boost_rejection_reason": -1,
"categories": Array [],
......@@ -666,6 +675,7 @@ exports[`blog view screen component should renders correctly 1`] = `
"mature": false,
"monetized": false,
"ownerObj": UserModel {
"__list": null,
"access_id": "2",
"banned": "no",
"blocked": undefined,
......
......@@ -21,6 +21,7 @@ describe('channel actions component', () => {
beforeEach(() => {
store = new ChannelStore();
store.channel = new UserModel(userFaker(1));
store.channel.toggleSubscription = jest.fn();
});
it('should renders correctly', () => {
......@@ -29,7 +30,7 @@ describe('channel actions component', () => {
features.has.mockReturnValue(true);
const component = renderer.create(
<ChannelActions channel={store} />
<ChannelActions store={store} />
).toJSON();
expect(component).toMatchSnapshot();
......@@ -38,7 +39,7 @@ describe('channel actions component', () => {
it('should show the correct options', () => {
const wrapper = shallow(
<ChannelActions channel={store} />
<ChannelActions store={store} />
);
let opt = wrapper.instance().getOptions();
......@@ -61,14 +62,14 @@ describe('channel actions component', () => {
const navigation = {push: jest.fn()};
const wrapper = shallow(
<ChannelActions channel={store} navigation={navigation}/>
<ChannelActions store={store} navigation={navigation}/>
);
store.channel.subscribed = true;
store.channel.blocked = true;
opt = wrapper.instance().makeAction(1);
expect(store.subscribe).toBeCalled();
expect(store.channel.toggleSubscription).toBeCalled();
opt = wrapper.instance().makeAction(2);
expect(store.toggleBlock).toBeCalled();
......
......@@ -14,22 +14,6 @@ exports[`Channel screen component should renders correctly 1`] = `
header={
<View>
<ChannelHeader
channel={
Object {
"channel": Object {
"blocked": false,
"guid": "125",
},
"feedStore": Object {
"setChannel": [MockFunction],
},
"load": [MockFunction],
"rewards": Object {
"merged": Array [],
},
"setChannel": [MockFunction],
}
}
navigation={
Object {
"navigate": [MockFunction],
......@@ -71,6 +55,22 @@ exports[`Channel screen component should renders correctly 1`] = `
},
}
}
store={
Object {
"channel": Object {
"blocked": false,
"guid": "125",
},
"feedStore": Object {
"setChannel": [MockFunction],
},
"load": [MockFunction],
"rewards": Object {
"merged": Array [],
},
"setChannel": [MockFunction],
}
}
styles={
Object {
"avatar": Object {
......
......@@ -43,7 +43,7 @@ describe('channel header component owner', () => {
store.loaded = true;
component = renderer.create(
<ChannelHeader.wrappedComponent channel={store} user={userStore} navigation={navigation} styles={{}}/>
<ChannelHeader.wrappedComponent store={store} user={userStore} navigation={navigation} styles={{}}/>
)
});
......
......@@ -698,6 +698,7 @@ exports[`channel subscribers component should render correctly 1`] = `
],
"errorLoading": false,
"loaded": true,
"metadataServie": null,
"offset": "",
"refreshing": false,
},
......@@ -1059,6 +1060,7 @@ exports[`channel subscribers component should render correctly 1`] = `
],
"errorLoading": false,
"loaded": true,
"metadataServie": null,
"offset": "",
"refreshing": false,
},
......@@ -1420,6 +1422,7 @@ exports[`channel subscribers component should render correctly 1`] = `
],
"errorLoading": false,
"loaded": true,
"metadataServie": null,
"offset": "",
"refreshing": false,
},
......
......@@ -240,6 +240,7 @@ exports[`newsfeed list screen component should renders correctly 1`] = `
}
viewabilityConfig={
Object {
"minimumViewTime": 300,
"viewAreaCoveragePercentThreshold": 50,
}
}
......
......@@ -65,4 +65,5 @@ decorate(BlogModel, {
'thumbs:up:count': observable,
'thumbs:down:user_guids': observable,
'thumbs:up:user_guids': observable,
'description': observable
});
\ No newline at end of file
......@@ -83,10 +83,13 @@ export default class BlogsViewScreen extends Component {
*/
async componentDidMount() {
const params = this.props.navigation.state.params;
try {
if (params.blog) {
if (params.blog._list && params.blog._list.metadataServie) {
params.blog._list.metadataServie.pushSource('single');
}
this.props.blogsView.setBlog(params.blog);
if (!params.blog.description) {
await this.props.blogsView.loadBlog(params.blog.guid);
}
......@@ -116,6 +119,10 @@ export default class BlogsViewScreen extends Component {
* On component will unmount
*/
componentWillUnmount() {
const blog = this.props.blogsView.blog;
if (blog._list && blog._list.metadataServie) {
blog._list.metadataServie.popSource();
}
this.props.blogsView.reset();
}
......@@ -168,7 +175,13 @@ export default class BlogsViewScreen extends Component {
*/
render() {
if (!this.props.blogsView.blog) return <CenteredLoading />;
if (!this.props.blogsView.blog) {
return <CenteredLoading />;
} else {
// force observe on description
const desc = this.props.blogsView.blog.description;
}
return (
<View style={[CS.flexContainer, CS.backgroundWhite]}>
{
......
......@@ -11,7 +11,7 @@ import BlogModel from './BlogModel';
*/
class BlogsViewStore {
@observable blog = null;
@observable.ref blog = null;
/**
* Load blog
......@@ -21,7 +21,12 @@ class BlogsViewStore {
loadBlog(guid) {
return blogService.loadEntity(guid)
.then(result => {
this.setBlog(result.blog);
// keep the _list if the entity has one
if (this.blog) {
this.blog.update(result.blog);
} else {
this.setBlog(result.blog);
}
});
}
......
......@@ -21,6 +21,7 @@ import ActivityModel from '../newsfeed/ActivityModel';
import BlogModel from '../blogs/BlogModel';
import UserModel from '../channel/UserModel';
import GroupCard from '../groups/card/GroupCard';
import GroupModel from '../groups/GroupModel';
/**
* Boost console item
......@@ -31,7 +32,6 @@ export default class Boost extends Component {
* Render
*/
render() {
return (
<View style={styles.container}>
{ this.renderEntity() }
......@@ -53,14 +53,14 @@ export default class Boost extends Component {
case 'user':
return <ChannelCard entity={UserModel.create(entity)} navigation={this.props.navigation} />;
case 'group':
return <GroupCard entity={UserModel.create(entity)} navigation={this.props.navigation} />;
return <GroupCard entity={GroupModel.create(entity)} navigation={this.props.navigation} />;
case 'object':
switch (entity.subtype) {
case 'blog':
return <BlogCard entity={BlogModel.create(entity)} navigation={this.props.navigation} />;
case 'image':
return <ImageCard entity={entity} navigation={this.props.navigation} />;
return <ImageCard entity={ActivityModel.create(entity)} navigation={this.props.navigation} />;
case 'video':
return <VideoCard entity={ActivityModel.create(entity)} navigation={this.props.navigation} />
}
......
......@@ -152,7 +152,7 @@ export default class BoostActionBar extends Component {
buttons.push(
<View style={CS.flexColumnCentered} key="revoke">
<TouchableHighlight
onPress={() => { this.props.boost.list.revoke(this.props.entity.guid, this.props.boost.filter)}}
onPress={() => { this.props.entity.revoke(this.props.boost.filter)}}
underlayColor = 'transparent'
style = {ComponentsStyle.redbutton}
>
......@@ -166,7 +166,7 @@ export default class BoostActionBar extends Component {
buttons.push(
<View style={CS.flexColumnCentered} key="reject">
<TouchableHighlight
onPress={() => { this.props.boost.list.reject(this.props.entity.guid)}}
onPress={() => { this.props.entity.reject()}}
underlayColor = 'transparent'
style = {ComponentsStyle.redbutton}
>
......@@ -180,7 +180,7 @@ export default class BoostActionBar extends Component {
buttons.push(
<View style={CS.flexColumnCentered} key="accept">
<TouchableHighlight
onPress={() => { this.props.boost.list.accept(this.props.entity.guid)}}
onPress={() => { this.props.entity.accept()}}
underlayColor = 'transparent'
style = {ComponentsStyle.bluebutton}
>
......
......@@ -286,7 +286,13 @@ export default class CapturePoster extends Component {
async remind() {
const { params } = this.props.navigation.state;
const message = this.props.capture.text;
const post = {message};
const metadata = params.entity.getClientMetadata();
const post = {
message,
...metadata
};
let group = this.props.navigation.state.params ? this.props.navigation.state.params.group : null
if(HashtagService.slice(message).length > HashtagService.maxHashtags){ //if hashtag count greater than 5
......
......@@ -50,11 +50,11 @@ export default class ChannelActions extends Component {
getOptions() {
let options = [ i18n.t('cancel') ];
if(this.props.channel.channel.subscribed){
if(this.props.store.channel.subscribed){
options.push( i18n.t('channel.unsubscribe') );
}
if (!this.props.channel.channel.blocked) {
if (!this.props.store.channel.blocked) {
options.push( i18n.t('channel.block') );
} else {
options.push( i18n.t('channel.unblock') );
......@@ -71,16 +71,16 @@ export default class ChannelActions extends Component {
let selected = options[option];
switch (selected) {
case i18n.t('channel.unsubscribe'):
this.props.channel.subscribe();
this.props.store.channel.toggleSubscription();
break;
case i18n.t('channel.block'):
this.props.channel.toggleBlock();
this.props.store.toggleBlock();
break;
case i18n.t('channel.unblock'):
this.props.channel.toggleBlock();
this.props.store.toggleBlock();
break;
case i18n.t('channel.report'):
this.props.navigation.push('Report', { entity: this.props.channel.channel });
this.props.navigation.push('Report', { entity: this.props.store.channel });
break;
}
}
......@@ -90,12 +90,12 @@ export default class ChannelActions extends Component {
*/
render() {
const channel = this.props.channel.channel;
const channel = this.props.store.channel;
const showWire = !channel.blocked && !channel.isOwner() && featuresService.has('crypto');
return (
<View style={styles.wrapper}>
{!!showWire && <WireAction owner={this.props.channel.channel} navigation={this.props.navigation}/>}
{!!showWire && <WireAction owner={this.props.store.channel} navigation={this.props.navigation}/>}
<Icon name="md-settings" style={ styles.icon } onPress={() => this.showActionSheet()} size={24} />
<ActionSheet
ref={o => this.ActionSheet = o}
......
......@@ -29,8 +29,6 @@ export default class ChannelFeedStore {
channel;
viewed = [];
/**
* Channel guid
*/
......@@ -44,7 +42,7 @@ export default class ChannelFeedStore {
buildStores() {
this.stores = {
feed: {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
},
images: {
list: new OffsetFeedListStore('shallow'),
......@@ -117,21 +115,6 @@ export default class ChannelFeedStore {
});
}
@action
async addViewed(entity) {
if(this.viewed.indexOf(entity.guid) < 0) {
let response;
try {
response = await setViewed(entity);
if (response) {
this.viewed.push(entity.guid);
}
} catch (e) {
throw new Error('There was an issue storing the view');
}
}
}
/**
* Load selected feed
* @param {boolean} refresh
......
......@@ -72,8 +72,8 @@ export default class ChannelScreen extends Component {
try {
await this.initialLoad();
} catch (e) {
logService.exception(e);
} catch (err) {
logService.exception('[ChannelScreen]', err);
}
}
......@@ -105,7 +105,7 @@ export default class ChannelScreen extends Component {
const store = this.props.channel.store(guid);
try {
const channel = await store.load();
const channel = await store.load(true);
if (channel) {
this.props.channel.addVisited(channel);
}
......@@ -218,7 +218,7 @@ export default class ChannelScreen extends Component {
<View>
<ChannelHeader
styles={styles}
channel={store}
store={store}
navigation={this.props.navigation}
/>
......
......@@ -31,12 +31,14 @@ class ChannelService {
/**
* Subscribe to Channel
* @param {string} guid
* @param {boolean} value
* @param {Object} data
*/
toggleSubscription(guid, value) {
toggleSubscription(guid, value, data = {}) {
if (value) {
return api.post('api/v1/subscribe/' + guid);
return api.post('api/v1/subscribe/' + guid, data);
} else {
return api.delete('api/v1/subscribe/' + guid);
return api.delete('api/v1/subscribe/' + guid, data);
}
}
......
......@@ -60,31 +60,20 @@ export default class ChannelStore {
}
@action
async load() {
async load(update = false) {
const { channel } = await channelService.load(this.guid);
if (channel) {
this.loaded = true;
this.setChannel(channel);
if (update && this.channel.update) {
this.channel.update(channel);
} else {
this.setChannel(channel);
}
return channel;
}
return false;
}
@action
subscribe() {
let value = !this.channel.subscribed;
this.channel.subscribed = value;
return channelService.toggleSubscription(this.channel.guid, value)
.then(response => {
this.channel.subscribed = value;
})
.catch(err => {
this.channel.subscribed = !value;
logService.exception('[ChannelStore] subscribe', err);
});
}
@action
toggleBlock() {
let value = !this.channel.blocked;
......
import { observable, decorate, action } from 'mobx';
import { observable, decorate, action, runInAction } from 'mobx';
import { MINDS_CDN_URI, GOOGLE_PLAY_STORE } from '../config/Config';
import api from '../common/services/api.service';
import BaseModel from '../common/BaseModel';
import stores from '../../AppStores';
import ChannelService from './ChannelService';
/**
* User model
......@@ -36,6 +37,21 @@ export default class UserModel extends BaseModel {
this.mature_visibility = !this.mature_visibility;
}
@action
async toggleSubscription() {
const value = !this.subscribed;
this.subscribed = value;
try {
const metadata = this.getClientMetadata();
await ChannelService.toggleSubscription(this.guid, value, metadata)
} catch (err) {
runInAction(() => {
this.subscribed = !value;
});
throw err;
}
}
/**
* current user is owner of the channel
*/
......
......@@ -29,8 +29,7 @@ import FastImage from 'react-native-fast-image';
export default class ChannelCard extends Component {
subscribe() {
let channel = this.props.entity;
this.props.channel.subscribe(channel.guid);
this.props.entity.toggleSubscription();
}
/**
......
......@@ -74,7 +74,7 @@ export default class ChannelHeader extends Component {
return {uri: this.state.preview_banner};
}
return this.props.channel.channel.getBannerSource();
return this.props.store.channel.getBannerSource();
}
/**
......@@ -85,7 +85,7 @@ export default class ChannelHeader extends Component {
return { uri: this.state.preview_avatar };
}
return this.props.channel.channel.getAvatarSource('large');
return this.props.store.channel.getAvatarSource('large');
}
/**
......@@ -93,18 +93,18 @@ export default class ChannelHeader extends Component {
*/
_navToConversation() {
if (this.props.navigation) {
this.props.navigation.push('Conversation', { conversation: { guid : this.props.channel.channel.guid + ':' + session.guid } });
this.props.navigation.push('Conversation', { conversation: { guid : this.props.store.channel.guid + ':' + session.guid } });
}
}
_navToSubscribers() {
if (this.props.navigation) {
this.props.navigation.push('Subscribers', { guid : this.props.channel.channel.guid });
this.props.navigation.push('Subscribers', { guid : this.props.store.channel.guid });
}
}
componentDidMount() {
const isOwner = session.guid === this.props.channel.channel.guid;
const isOwner = session.guid === this.props.store.channel.guid;
if(isOwner) this.props.onboarding.getProgress();
}
......@@ -122,7 +122,7 @@ export default class ChannelHeader extends Component {
this.setState({saving: true});
const response = await this.props.channel.save(payload);
const response = await this.props.store.save(payload);
if (response === true) {
this.props.user.load();
......@@ -143,8 +143,8 @@ export default class ChannelHeader extends Component {
edit: true,
preview_avatar: null,
preview_banner: null,
briefdescription: this.props.channel.channel.briefdescription,
name: this.props.channel.channel.name
briefdescription: this.props.store.channel.briefdescription,
name: this.props.store.channel.name
});
}
}
......@@ -154,9 +154,9 @@ export default class ChannelHeader extends Component {
*/
getActionButton() {
const styles = this.props.styles;
if (!this.props.channel.loaded && session.guid !== this.props.channel.channel.guid )
if (!this.props.store.loaded && session.guid !== this.props.store.channel.guid )
return null;
if (session.guid === this.props.channel.channel.guid) {
if (session.guid === this.props.store.channel.guid) {
return (
<ButtonCustom
onPress={this.onEditAction}
......@@ -165,7 +165,7 @@ export default class ChannelHeader extends Component {
loading={this.state.saving}
/>
);
} else if (!!this.props.channel.channel.subscribed) {
} else if (!!this.props.store.channel.subscribed) {
return (
<TouchableHighlightCustom
onPress={() => { this._navToConversation() }}
......@@ -176,7 +176,7 @@ export default class ChannelHeader extends Component {
<Text style={{ color: colors.primary }} > {i18n.t('channel.message')} </Text>
</TouchableHighlightCustom>
);
} else if (session.guid !== this.props.channel.channel.guid) {
} else if (session.guid !== this.props.store.channel.guid) {
return (
<TouchableHighlightCustom
onPress={() => { this.subscribe() }}
......@@ -187,7 +187,7 @@ export default class ChannelHeader extends Component {
<Text style={{ color: colors.primary }} > {i18n.t('channel.subscribe').toUpperCase()} </Text>
</TouchableHighlightCustom>
);
} else if (this.props.channel.isUploading) {
} else if (this.props.store.isUploading) {
return (
<ActivityIndicator size="small" />
)
......@@ -195,8 +195,7 @@ export default class ChannelHeader extends Component {
}
subscribe() {
let channel = this.props.channel.channel;
this.props.channel.subscribe();
this.props.store.channel.toggleSubscription();
}
changeBannerAction = async () => {
......@@ -239,12 +238,12 @@ export default class ChannelHeader extends Component {
* Render Header
*/
render() {
const isOwner = session.guid === this.props.channel.channel.guid;
const channel = this.props.channel.channel;
const isOwner = session.guid === this.props.store.channel.guid;
const channel = this.props.store.channel;
const styles = this.props.styles;
const avatar = this.getAvatar();
const iurl = this.getBannerFromChannel();
const isUploading = this.props.channel.isUploading;
const isUploading = this.props.store.isUploading;
const isEditable = this.state.edit && !isUploading;
return (
......@@ -255,8 +254,8 @@ export default class ChannelHeader extends Component {
{isEditable && <View style={styles.tapOverlayView}>
<Icon name="md-create" size={30} color="#fff" />
</View>}
{(isUploading && this.props.channel.bannerProgress) ? <View style={styles.tapOverlayView}>
<Progress.Pie progress={this.props.channel.bannerProgress} size={36} />
{(isUploading && this.props.store.bannerProgress) ? <View style={styles.tapOverlayView}>
<Progress.Pie progress={this.props.store.bannerProgress} size={36} />
</View>:null}
</TouchableCustom>
......@@ -299,7 +298,7 @@ export default class ChannelHeader extends Component {
<View style={styles.buttonscol}>
{ !channel.blocked && this.getActionButton() }
{ session.guid !== channel.guid?
<ChannelActions navigation={this.props.navigation} channel={this.props.channel} me={session}></ChannelActions> : <View></View>
<ChannelActions navigation={this.props.navigation} store={this.props.store} me={session}></ChannelActions> : <View></View>
}
</View>
</View>
......@@ -326,8 +325,8 @@ export default class ChannelHeader extends Component {
{isEditable && <View style={[styles.tapOverlayView, styles.wrappedAvatarOverlayView]}>
<Icon name="md-create" size={30} color="#fff" />
</View>}
{(isUploading && this.props.channel.avatarProgress) ? <View style={[styles.tapOverlayView, styles.wrappedAvatarOverlayView]}>
<Progress.Pie progress={this.props.channel.avatarProgress} size={36} />
{(isUploading && this.props.store.avatarProgress) ? <View style={[styles.tapOverlayView, styles.wrappedAvatarOverlayView]}>
<Progress.Pie progress={this.props.store.avatarProgress} size={36} />
</View>: null}
</TouchableCustom>
{isOwner && this.props.onboarding.percentage < 1 ? <CompleteProfile progress={this.props.onboarding.percentage}/>: null}
......
import React, { Component } from 'react';
// @flow
import * as React from 'react';
import type { ViewLayoutEvent } from 'react-native/Libraries/Components/View/ViewPropTypes';
import {
View,
......@@ -40,6 +42,7 @@ import autobind from "../common/helpers/autobind";
type Props = {
header?: any,
parent?: any,
keyboardVerticalOffset?: any,
entity: any,
store: any,
user: any,
......@@ -50,6 +53,8 @@ type Props = {
type State = {
focused: boolean,
hideInput: boolean,
guid: ?string,
blockedChannels: Array<string>,
selection: {
start: number,
end: number
......@@ -83,11 +88,12 @@ function getEntityGuid(entity) {
@inject('user')
@observer
export default class CommentList extends React.Component<Props, State> {
listRef: any;
listRef: ?React.ElementRef<typeof FlatList>;
textInput: any;
actionAttachmentSheet: ?ActionSheet;
actionSheet: ?ActionSheet;
keyboardDidShowListener: any;
keyboardDidHideListener: any;
focusedChild: number = -1;
focusedOffset: number = 0;
height: number = 0;
......@@ -140,7 +146,7 @@ export default class CommentList extends React.Component<Props, State> {
if (store.text.trim() == '' && !store.attachment.hasAttachment) return;
Keyboard.dismiss();
if (!store.saving){
await store.post();
await store.post(this.props.entity);
if (!this.props.parent) this.scrollToBottom();
}
}
......@@ -175,7 +181,7 @@ export default class CommentList extends React.Component<Props, State> {
}
}
onLayout = (e) => {
onLayout = (e: ViewLayoutEvent) => {
if (!this.props.parent) {
this.height = e.nativeEvent.layout.height || 0;
}
......@@ -221,7 +227,7 @@ export default class CommentList extends React.Component<Props, State> {
}
}
onChildFocus = (item, offset) => {
onChildFocus = (item: CommentType, offset: number) => {
if (!offset) offset = 0;
const comments = this.getComments();
......@@ -279,7 +285,7 @@ export default class CommentList extends React.Component<Props, State> {
/**
* Load comments
*/
loadComments = async (loadingMore = false, descending = true) => {
loadComments = async (loadingMore: boolean = false, descending: boolean = true) => {
await this.loadBlockedChannels();
let guid;
......@@ -299,7 +305,6 @@ export default class CommentList extends React.Component<Props, State> {
}
}
@autobind
async loadBlockedChannels() {
this.setState({
......@@ -384,7 +389,7 @@ export default class CommentList extends React.Component<Props, State> {
/**
* Render comments
*/
renderComment = (row: any) => {
renderComment = (row: any): React.Element<Comment> => {
const comment = row.item;
const comments = this.props.store;
......@@ -438,13 +443,17 @@ export default class CommentList extends React.Component<Props, State> {
/**
* Refresh comments
*/
refresh = async () => {
refreshAsync = async () => {
this.props.store.refresh();
await this.loadComments();
this.props.store.refreshDone();
}
getErrorLoading(errorLoading, descending) {
refresh() {
this.refreshAsync();
}
getErrorLoading(errorLoading: boolean, descending: boolean) {
if (errorLoading) {
const message = this.props.store.comments.length ?
(i18n.t('cantLoadMore') + '\n' + i18n.t('tryAgain')) :
......@@ -495,12 +504,12 @@ export default class CommentList extends React.Component<Props, State> {
/**
* @param {object} ref
*/
setListRef = ref => this.listRef = ref;
setListRef = (ref: ?React.ElementRef<typeof FlatList>) => this.listRef = ref;
/**
* @param {object} ref
*/
setActionSheetRef = o => this.actionAttachmentSheet = o;
setActionSheetRef = (o: ActionSheet) => this.actionAttachmentSheet = o;
/**
* Render
......
......@@ -50,7 +50,6 @@ export default class CommentModel extends ActivityModel {
}
return { uri: MINDS_CDN_URI + 'fs/v1/thumbnail/' + (this.attachment_guid || this.guid) + '/' + size };
}
}
/**
......
......@@ -23,6 +23,7 @@ import {toggleExplicit} from '../newsfeed/NewsfeedService';
import RichEmbedStore from '../common/stores/RichEmbedStore';
import logService from '../common/services/log.service';
import NavigationService from '../navigation/NavigationService';
import BaseModel from '../common/BaseModel';
const COMMENTS_PAGE_SIZE = 6;
......@@ -31,7 +32,7 @@ const COMMENTS_PAGE_SIZE = 6;
*/
export default class CommentsStore {
@observable comments = [];
@observable.shallow comments = [];
@observable refreshing = false;
@observable loaded = false;
@observable saving = false;
......@@ -298,10 +299,10 @@ export default class CommentsStore {
}
if (response.comments) {
const comments = CommentModel.createMany(response.comments)
const comments = CommentModel.createMany(response.comments);
// check and build child comments store if necessary
comments.forEach(c => c.buildCommentsStore(this.parent))
comments.forEach(c => c.buildCommentsStore(this.parent));
if (descending) {
comments.reverse().forEach(c => this.comments.unshift(c));
......@@ -317,14 +318,14 @@ export default class CommentsStore {
* @param {object} comment
*/
@action
setComment(comment) {
pushComment(comment) {
this.comments.push(CommentModel.create(comment));
}
/**
* Post comment
*/
async post() {
async post(entity: BaseModel) {
this.saving = true;
const comment = {
......@@ -340,11 +341,14 @@ export default class CommentsStore {
Object.assign(comment, this.embed.meta);
}
// Add client metada if available
Object.assign(comment, entity.getClientMetadata());
try {
const data = await postComment(this.guid, comment);
this.setComment(data.comment);
this.pushComment(data.comment);
this.setText('');
this.embed.clearRichEmbedAction();
this.attachment.clear();
......
import {
extendShallowObservable,
extendObservable,
decorate,
observable,
action,
computed,
runInAction,
} from 'mobx';
import _ from 'lodash';
import sessionService from './services/session.service';
import { vote } from './services/votes.service';
import { toggleExplicit, toggleUserBlock, update } from '../newsfeed/NewsfeedService';
import logService from './services/log.service';
import channelService from '../channel/ChannelService';
import { revokeBoost, acceptBoost, rejectBoost } from '../boost/BoostService';
/**
* Base model
*/
export default class BaseModel {
/**
* List reference (if the entity belongs to one)
* @var {OffsetListStore}
*/
__list = null;
/**
* List reference setter
*/
set _list(value) {
this.__list = value;
}
/**
* List reference getter
*/
get _list() {
return this.__list;
}
/**
* Child models classes
*/
......@@ -37,6 +61,28 @@ export default class BaseModel {
}
}
/**
* Update model data
* @param {Object} data
*/
@action
update(data) {
const childs = this.childModels();
Object.getOwnPropertyNames(this).forEach(key => {
if (data[key]) {
if (childs[key]) {
// we update the child model
this[key].update(data[key]);
} else {
// we assign the property
this[key] = data[key];
}
}
});
}
/**
* Create an instance
* @param {object} data
......@@ -133,11 +179,11 @@ export default class BaseModel {
this['thumbs:' + direction + ':count'] += delta;
// class service
const params = this.getClientMetadata();
try {
await vote(this.guid, direction)
await vote(this.guid, direction, params)
} catch (err) {
alert(err);
if (!voted) {
this['thumbs:' + direction + ':user_guids'] = guids.filter(function (item) {
return item !== sessionService.guid;
......@@ -146,7 +192,73 @@ export default class BaseModel {
this['thumbs:' + direction + ':user_guids'] = [sessionService.guid, ...guids];
}
this['thumbs:' + direction + ':count'] -= delta;
throw err;
}
}
getClientMetadata() {
return (this._list && this._list.metadataServie) ? this._list.metadataServie.getEntityMeta(this) : {};
}
/**
* Block owner
*/
blockOwner() {
if (!this.ownerObj) throw new Error('This entity has no owner');
return channelService.toggleBlock(this.ownerObj.guid, true);
}
/**
* Unblock owner
*/
unblockOwner() {
if (!this.ownerObj) throw new Error('This entity has no owner');
return channelService.toggleBlock(this.ownerObj.guid, false);
}
@action
async toggleExplicit() {
let value = !this.mature;
try {
await toggleExplicit(this.guid, value);
this.mature = value;
} catch (err) {
this.mature = !value;
logService.exception('[BaseModel]', err);
throw err;
}
}
@action
async reject() {
try {
await rejectBoost(this.guid);
this.state = 'rejected';
} catch (err) {
logService.exception('[BaseModel]', err);
throw err;
}
}
@action
async accept() {
try {
await acceptBoost(this.guid);
this.state = 'accepted';
} catch (err) {
logService.exception('[BaseModel]', err);
throw err;
}
}
@action
async revoke(filter) {
try {
await revokeBoost(this.guid, filter);
this.state = 'revoked';
} catch (err) {
logService.exception('[BaseModel]', err);
throw err;
}
}
}
\ No newline at end of file
import moment from 'moment';
import sessionService from './session.service';
import hashCode from '../helpers/hash-code';
import NavigationService from '../../navigation/NavigationService';
/**
* Metadata service for analytics
*/
class MetadataService {
/**
* @var {String} source
*/
_source = ['feed/subscribed'];
/**
* @var {String} medium
*/
medium = 'feed';
/**
* @var {String} campaign
*/
campaign = '';
/**
* @var {moment} deltaBegin
*/
deltaBegin = null;
/**
* @var {String} salt
*/
salt = null;
/**
* Source getter
*/
get source() {
return this._source[this._source.length - 1];
}
/**
* Source setter
*/
set source(value) {
this._source[this._source.length - 1] = value;
}
/**
* Push source
* @param {String} value
*/
pushSource(value) {
this._source.push(value);
}
/**
* Pop source
* @returns {String}
*/
popSource() {
return this._source.pop();
}
/**
* Cosntructor
*/
constructor() {
sessionService.onLogin(() => {
this.initDelta();
});
}
/**
* @var {function} entityMapper maps entities properties to metadata
*/
entityMapper = (entity) => ({
position: entity._list ? entity._list.getIndex(entity) + 1 : 0,
medium: entity.boosted ? 'featured-content' : 'feed',
campaign: entity.boosted_guid ? entity.urn : ''
})
/**
* Init delta timer
*/
initDelta() {
this.deltaBegin = moment();
}
/**
* Set the entities mapper function
* @param {Function} fn
*/
setEntityMapper(fn) {
this.entityMapper = fn;
return this;
}
/**
* Get delta in seconds
* @returns {integer} seconds
*/
getDelta() {
const deltaEnd = moment();
const delta = moment.duration(deltaEnd.diff(this.deltaBegin));
return delta.seconds();
}
/**
* Set source
* @param {String} source
*/
setSource(source) {
this.source = source;
this.salt = (Math.random()).toString(36).replace(/[^a-z]+/g, '');
return this;
}
/**
* Set medium
* @param {String} medium
*/
setMedium(medium) {
this.medium = medium;
return this;
}
/**
* Set campaign
* @param {String} campaign
*/
setCampaign(campaign) {
this.campaign = campaign;
return this;
}
setPosition(position) {
this.position = position;
return this;
}
/**
* Build the page token
*/
buildPageToken() {
const user = sessionService.getUser();
const tokenParts = [
this.salt, // NOTE: Salt + hash so individual user activity can't be tracked
this.getCurrentRoute(),
user.guid || '000000000000000000',
this.deltaBegin.format('X') || '',
];
return hashCode(tokenParts.join(':'), 5);
}
/**
* Get current visible route
*/
getCurrentRoute() {
const state = NavigationService.getCurrentState();
if (state.routeName === 'Tabs') {
return state.routes[state.index].routeName;
}
return state.routeName;
}
/**
* returns the client metadata
* @param {Object|undefined} overrides
*/
dto(overrides) {
if (!this.deltaBegin) {
// There's no client meta in this component branch.
return {};
}
return {
client_meta: this.build(overrides),
};
}
/**
* Get the metadata for the entity
* @param {BaseModel} entity
*/
getEntityMeta(entity) {
const overrides = this.entityMapper(entity);
return this.dto(overrides);
}
/**
* Build metadata
* @param {Object} overrides
*/
build(overrides = {}) {
return {
platform: 'mobile',
page_token: this.buildPageToken(),
delta: this.getDelta(),
source: this.source,
medium: this.medium,
campaign: this.campaign,
position: this.position,
...overrides
};
}
}
export default MetadataService;
......@@ -2,8 +2,8 @@ import api from './../../common/services/api.service';
import logService from './log.service';
import i18n from './i18n.service';
export function vote(guid, direction) {
return api.put('api/v1/votes/' + guid + '/' + direction)
export function vote(guid, direction, data) {
return api.put('api/v1/votes/' + guid + '/' + direction, data)
.then((data) => {
return { data }
})
......
import { observable, action } from 'mobx';
import { getFeed, toggleComments, follow, unfollow , toggleExplicit, toggleFeatured, deleteItem, monetize, update} from '../../newsfeed/NewsfeedService';
import {toggleComments, follow, unfollow, toggleFeatured, monetize, update, setViewed} from '../../newsfeed/NewsfeedService';
import channelService from '../../channel/ChannelService';
import OffsetListStore from './OffsetListStore';
import logService from '../services/log.service';
/**
* Common infinite scroll list
* Infinite scroll list that inform viewed
*/
export default class OffsetFeedListStore extends OffsetListStore {
/*Activity Methods */
@observable saving = false;
@action
toggleCommentsAction(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
let value = !entity.comments_disabled;
return toggleComments(guid, value)
.then(action(response => {
this.entities[index] = response.entity;
}))
.catch(action(err => {
entity.comments_disabled = !value;
this.entities[index] = entity;
logService.exception('[OffsetFeedListStore]', err);
}));
}
}
// @action
// toggleFeaturedStore(guid, category) {
// let index = this.entities.findIndex(x => x.guid == guid);
// if(index >= 0) {
// let entity = this.entities[index];
// let value = !entity.featured;
// return toggleFeatured(guid, value, category)
// .then(action(response => {
// entity.featured = value;
// this.entities[index] = entity;
// }))
// .catch(action(err => {
// logService.exception('[OffsetFeedListStore]', err);
// }));
// }
// }
@action
newsfeedToggleExplicit(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
let value = !entity.mature;
return toggleExplicit(guid, value)
.then(action(response => {
entity.mature = value;
this.entities[index] = entity;
}))
.catch(action(err => {
entity.mature = !value;
this.entities[index] = entity;
logService.exception('[OffsetFeedListStore]', err);
}));
}
}
@action
newsfeedToogleFollow(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
const entity = this.entities[index];
const method = entity['is:following'] ? unfollow : follow;
return method(guid)
.then(action(response => {
entity['is:following'] = !entity['is:following'];
this.entities[index] = entity;
}))
.catch(action(err => {
entity['is:following'] = !entity['is:following'];
this.entities[index] = entity;
logService.exception('[OffsetFeedListStore]', err);
}));
}
}
/**
* @var {Map} viewed viewed entities list
*/
viewed = new Map();
@action
toggleMonetization(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
let value = !entity.monetized;
return monetize(guid, value)
.then(action(response => {
entity.monetized = value;
this.entities[index] = entity;
}))
.catch(action(err => {
entity.monetized = !value;
this.entities[index] = entity;
logService.exception('[OffsetFeedListStore]', err);
}));
}
/**
* Clear viewed list
*/
clearViewed() {
this.viewed.clear();
}
@action
newsfeedToggleSubscription(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
let value = !entity.ownerObj.subscribed;
entity.ownerObj.subscribed = value;
return channelService.toggleSubscription(entity.ownerObj.guid, value)
.then(action(response => {
entity.ownerObj.subscribed = value;
this.entities[index] = entity;
}))
.catch(action(err => {
entity.ownerObj.subscribed = !value;
this.entities[index] = entity;
logService.exception('[OffsetFeedListStore]', err);
}));
}
async clearList(updateLoaded = true) {
this.clearViewed();
return super.clearList(updateLoaded);
}
@action
deleteEntity(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
return deleteItem(guid)
.then(action(response => {
this.entities.splice(index, 1);
}))
.catch(action(err => {
logService.exception('[OffsetFeedListStore]', err);
}));
}
async refresh() {
this.clearViewed();
return super.refresh();
}
@action
updateActivity(activity, data = {}) {
this.saving = true;
if (data) {
for (const field in data) {
activity[field] = data[field];
/**
* Add an entity to the viewed list and inform to the backend
* @param {BaseModel} entity
*/
async addViewed(entity) {
if (!this.viewed.get(entity.guid)) {
this.viewed.set(entity.guid, true);
let response;
try {
const meta = this.metadataServie ? this.metadataServie.getEntityMeta(entity) : {};
response = await setViewed(entity, meta);
} catch (e) {
this.viewed.delete(entity.guid);
throw new Error('There was an issue storing the view');
}
}
return update(activity)
.finally(action(() => {
this.saving = false;
}))
.then(() => {
this.setActivityMessage(activity, message);
});
}
@action
setActivityMessage(activity, message) {
activity.message = message;
activity.edited = 1;
}
}
......@@ -2,6 +2,7 @@ import { observable, action, extendObservable } from 'mobx'
import channelService from '../../channel/ChannelService';
import { revokeBoost, rejectBoost, acceptBoost} from '../../boost/BoostService';
import logService from '../services/log.service';
import MetadataService from '../services/metadata.service';
/**
* Common infinite scroll list
......@@ -28,11 +29,17 @@ export default class OffsetListStore {
*/
@observable offset = '';
/**
* Metadata service
*/
metadataServie = null;
/**
* Constructor
* @param {string} 'shallow'|'ref'|null
* @param {string} type 'shallow'|'ref'|null
* @param {boolean} includeMetadata
*/
constructor(type = null) {
constructor(type = null, includeMetadata = false) {
if (type) {
extendObservable(this,{
entities: [],
......@@ -46,18 +53,40 @@ export default class OffsetListStore {
entities: observable
});
}
if (includeMetadata) {
this.metadataServie = new MetadataService;
}
}
/**
* Get metadata service
* @returns {MetadataService}
*/
getMetadataService() {
return this.metadataServie;
}
/**
* Set or add to the list
* @param {Object} list
* @param {boolean} replace
*/
@action
setList(list, replace = false) {
if (list.entities && replace) {
this.entities = list.entities;
}
if (list.entities && !replace) {
list.entities.forEach(element => {
this.entities.push(element);
});
if (list.entities) {
if (replace) {
list.entities.forEach((entity) => {
entity._list = this;
});
this.entities = list.entities;
} else {
list.entities.forEach((entity) => {
entity._list = this;
this.entities.push(entity);
});
}
}
this.loaded = true;
......@@ -71,9 +100,25 @@ export default class OffsetListStore {
@action
prepend(entity) {
entity._list = this;
this.entities.unshift(entity);
}
@action
removeIndex(index) {
this.entities.splice(index, 1);
}
remove(entity) {
const index = this.entities.findIndex(e => e === entity);
if (index < 0) return;
this.removeIndex(index);
}
getIndex(entity) {
return this.entities.findIndex(e => e === entity);
}
@action
async clearList(updateLoaded = true) {
this.entities = [];
......@@ -104,72 +149,4 @@ export default class OffsetListStore {
cantLoadMore() {
return this.loaded && !this.offset && !this.refreshing;
}
@action
toggleSubscription(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
let value = !entity.subscribed;
entity.subscribed = value;
return channelService.toggleSubscription(entity.guid, value)
.then(action(response => {
entity.subscribed = value;
this.entities[index] = entity;
}))
.catch(action(err => {
entity.subscribed = !value;
this.entities[index] = entity;
logService.exception('[OffsetListStore]', err);
}));
}
}
@action
reject(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
return rejectBoost(guid)
.then(action(response => {
entity.state = 'rejected';
this.entities[index] = entity;
}))
.catch(action(err => {
logService.exception('[OffsetListStore]', err);
}));
}
}
@action
accept(guid) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
return acceptBoost(guid)
.then(action(response => {
entity.state = 'accepted';
this.entities[index] = entity;
}))
.catch(action(err => {
logService.exception('[OffsetListStore]', err);
}));
}
}
@action
revoke(guid, filter) {
let index = this.entities.findIndex(x => x.guid == guid);
if(index >= 0) {
let entity = this.entities[index];
return revokeBoost(guid, filter)
.then(action(response => {
entity.state = 'revoked';
this.entities[index] = entity;
}))
.catch(action(err => {
logService.exception('[OffsetListStore]', err);
}));
}
}
}
\ No newline at end of file
......@@ -44,6 +44,20 @@ export default class DiscoveryFeedScreen extends Component {
}
}
viewOptsFeed = {
viewAreaCoveragePercentThreshold: 50,
minimumViewTime: 300
}
/**
* On viewavleItemsChange
*/
onViewableItemsChanged = ({viewableItems}) => {
viewableItems.forEach((item) => {
this.props.discovery.feedStore.list.addViewed(item.item);
});
}
/**
* Render
*/
......@@ -64,6 +78,8 @@ export default class DiscoveryFeedScreen extends Component {
windowSize={11}
removeClippedSubviews={false}
keyboardShouldPersistTaps={'handled'}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewOptsFeed}
/>
)
}
......@@ -134,7 +150,7 @@ export default class DiscoveryFeedScreen extends Component {
renderActivity = (row) => {
return (
<ErrorBoundary containerStyle={CS.hairLineBottom}>
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false} newsfeed={this.props.discovery}/>
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false}/>
</ErrorBoundary>
);
}
......
......@@ -33,6 +33,7 @@ class DiscoveryFeedStore {
setFeed(entities, offset) {
this.list.clearList();
this.list.clearViewed();
this.list.setList({entities, offset});
}
......@@ -40,7 +41,11 @@ class DiscoveryFeedStore {
* Build lists stores
*/
buildListStores() {
this.list = new OffsetFeedListStore('shallow')
this.list = new OffsetFeedListStore('shallow', true);
this.list.getMetadataService()
.setSource('feed/discovery')
.setMedium('feed');
}
@action
......
......@@ -67,6 +67,11 @@ export default class DiscoveryScreen extends Component {
q: ''
}
viewOptsFeed = {
viewAreaCoveragePercentThreshold: 50,
minimumViewTime: 300
}
static navigationOptions = {
tabBarIcon: ({ tintColor }) => (
<Icon name="search" size={24} color={tintColor} />
......@@ -158,12 +163,25 @@ export default class DiscoveryScreen extends Component {
* On viewable items change in the list
*/
onViewableItemsChanged = (change) => {
if (this.props.discovery.filters.type == 'images' && isIos) {
change.changed.forEach(c => {
if (c.item.gif) {
c.item.setVisible(c.isViewable);
}
})
switch (this.props.discovery.filters.type ) {
case 'images':
change.changed.forEach(c => {
if (c.item.isGif()) {
c.item.setVisible(c.isViewable);
}
})
break;
case 'blogs':
case 'activities':
change.viewableItems.forEach((item) => {
this.props.discovery.list.addViewed(item.item);
});
break;
default:
break;
}
}
......@@ -232,6 +250,7 @@ export default class DiscoveryScreen extends Component {
columnWrapperStyle={columnWrapperStyle}
keyboardShouldPersistTaps={'handled'}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.cols == 3 ? undefined : this.viewOptsFeed}
/>
);
......@@ -517,10 +536,10 @@ export default class DiscoveryScreen extends Component {
*/
renderTile = (row) => {
if (!this.state.active && row.item.isGif()) {
return <View style={{ height: this.state.itemHeight, width: this.state.itemHeight, backgroundColor: colors.greyed }}/>;
return <View style={{ height: this.state.itemHeight, width: this.state.itemHeight }}/>;
}
return (
<ErrorBoundary message={this.tileError} containerStyle={[CS.centered, {width: this.state.itemHeight, height:this.state.itemHeight}]} textSmall={true}>
<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}
......@@ -548,7 +567,7 @@ export default class DiscoveryScreen extends Component {
renderActivity = (row) => {
return (
<ErrorBoundary containerStyle={CS.hairLineBottom}>
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false} newsfeed={this.props.discovery}/>
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false} />
</ErrorBoundary>
);
}
......
......@@ -73,10 +73,10 @@ class DiscoveryStore {
list: new OffsetFeedListStore('shallow'),
},
'blogs': {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
},
'channels': {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
},
'groups': {
list: new OffsetFeedListStore('shallow'),
......@@ -85,7 +85,7 @@ class DiscoveryStore {
list: new OffsetFeedListStore('shallow'),
},
'activities': {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
}
};
extendObservable(this.stores.images, {
......@@ -109,6 +109,18 @@ class DiscoveryStore {
extendObservable(this.stores.activities, {
loading: false
});
this.stores.activities.list.getMetadataService()
.setSource('feed/discovery')
.setMedium('feed');
this.stores.blogs.list.getMetadataService()
.setSource('feed/discovery')
.setMedium('feed');
this.stores.channels.list.getMetadataService()
.setSource('feed/discovery')
.setMedium('feed');
}
@action
......
......@@ -66,11 +66,18 @@ export default class DiscoveryTile extends Component {
this.props.onLoadEnd && this.props.onLoadEnd();
}
/**
* Render
*/
render() {
if (this.state.error) return this.errorRender();
const entity = this.props.entity;
if (!entity.is_visible){
return null;
}
const url = entity.getThumbSource();
// load gif with lower priority
......
......@@ -39,7 +39,7 @@ export default class DiscoveryUser extends Component {
_navToChannel = () => {
Keyboard.dismiss();
if (this.props.navigation) {
this.props.navigation.push('Channel', { guid: this.props.entity.item.guid });
this.props.navigation.push('Channel', { guid: this.props.entity.item.guid, entity: this.props.entity.item });
}
}
......@@ -57,14 +57,9 @@ export default class DiscoveryUser extends Component {
_toggleSusbcribed() {
const item = this.props.entity.item;
if (this.props.store.members){
this.props.store.members.toggleSubscription(item.guid);
} else if (this.props.store.list) {
this.props.store.list.toggleSubscription(item.guid);
}
this.props.entity.item.toggleSubscription();
}
renderRightButton() {
const item = this.props.entity.item;
if (this.props.user.me.guid === item.guid || this.props.hideButtons) {
......
......@@ -22,7 +22,7 @@ class GroupViewStore {
/**
* List feed store
*/
@observable list = new OffsetFeedListStore();
@observable list = new OffsetFeedListStore('shallow', true);
/**
* List Members
......@@ -56,12 +56,14 @@ class GroupViewStore {
*/
memberSearch = '';
/**
* List loading
*/
viewed = [];
guid = '';
constructor() {
this.list.getMetadataService()
.setSource('feed/groups')
.setMedium('feed');
}
/**
* Set guid
* @param {stirng} guid
......@@ -166,22 +168,6 @@ class GroupViewStore {
}
}
@action
async addViewed(entity) {
if(this.viewed.indexOf(entity.guid) < 0) {
let response;
try {
response = await setViewed(entity);
if (response) {
this.viewed.push(entity.guid);
}
} catch (e) {
throw new Error('There was an issue storing the view');
}
}
}
/**
* Load one group
* @param {string} guid
......@@ -190,6 +176,7 @@ class GroupViewStore {
return groupsService.loadEntity(guid)
.then(group => {
this.setGroup(group);
this.list.clearViewed();
return group;
});
}
......
import { runInAction, action, observable, decorate } from 'mobx';
import { runInAction, action, observable, decorate, toJS } from 'mobx';
import FastImage from 'react-native-fast-image'
import BaseModel from '../common/BaseModel';
......@@ -6,7 +6,7 @@ import UserModel from '../channel/UserModel';
import wireService from '../wire/WireService';
import { thumbActivity } from './activity/ActionsService';
import sessionService from '../common/services/session.service';
import { setPinPost } from '../newsfeed/NewsfeedService';
import { setPinPost, deleteItem, unfollow, follow, update } from '../newsfeed/NewsfeedService';
import api from '../common/services/api.service';
import {
......@@ -15,6 +15,7 @@ import {
MINDS_URI
} from '../config/Config';
import i18n from '../common/services/i18n.service';
import logService from '../common/services/log.service';
/**
* Activity model
......@@ -29,6 +30,26 @@ export default class ActivityModel extends BaseModel {
*/
@observable is_visible = true;
/**
* List reference setter
*/
set _list(value) {
this.__list = value;
// the reminded object need access to the metadata service too
if (this.remind_object) {
this.remind_object._list = value;
}
}
/**
* List reference getter
*/
get _list() {
return this.__list;
}
/**
* Child models
*/
......@@ -181,6 +202,58 @@ export default class ActivityModel extends BaseModel {
alert(i18n.t('errorPinnedPost'));
}
}
@action
async deleteEntity() {
try {
await deleteItem(this.guid)
if (this._list) {
runInAction(() => {
this._list.remove(this);
});
}
} catch (err) {
logService.exception('[ActivityModel]', err);
throw err;
}
}
@action
async toogleFollow() {
const method = this['is:following'] ? unfollow : follow;
try {
await method(this.guid)
runInAction(() => {
this['is:following'] = !this['is:following'];
});
} catch (err) {
logService.exception('[OffsetFeedListStore]', err);
throw err;
}
}
@action
async updateActivity(data = {}) {
const list = this._list;
delete(this._list);
const entity = toJS(this);
this._list = list;
if (data) {
for (const field in data) {
entity[field] = data[field];
}
}
await update(entity);
this.setEdited(entity.message);
}
@action
setEdited(message) {
this.message = message
this.edited = 1;
}
}
/**
......
......@@ -46,16 +46,16 @@ export default class ActivityScreen extends Component {
super(props);
const params = props.navigation.state.params;
this.store = params.store ? params.store : new NewsfeedStore();
this.comments = commentsStoreProvider.get();
if (params.entity && (params.entity.guid || params.entity.entity_guid)) {
this.entityStore.setEntity(ActivityModel.checkOrCreate(params.entity));
let index = this.store.list.entities.findIndex(x => x.guid == this.entityStore.entity.guid);
const entity = this.entityStore.entity;
if (index === -1) {
this.store.list.entities.push(this.entityStore.entity);
if (entity._list && entity._list.metadataServie) {
entity._list.metadataServie.pushSource('single');
}
}
}
......@@ -69,7 +69,13 @@ export default class ActivityScreen extends Component {
if (!this.entityStore.entity || params.hydrate) {
try {
const resp = await getSingle(params.guid || params.entity.guid || params.entity.entity_guid);
await this.entityStore.setEntity(ActivityModel.checkOrCreate(resp.activity));
// if it has a list asigned we set it to the new entity
if (this.entityStore.entity) {
this.entityStore.entity.update(resp.activity);
} else {
this.entityStore.setEntity(ActivityModel.checkOrCreate(resp.activity));
}
} catch (e) {
this.setState({error: true});
logService.exception('[ActivityScreen]',e);
......@@ -78,6 +84,17 @@ export default class ActivityScreen extends Component {
}
}
/**
* Component will unmount
*/
componentWillUnmount() {
const entity = this.entityStore.entity;
if (entity._list && entity._list.metadataServie) {
entity._list.metadataServie.popSource();
}
}
/**
* Get header
*/
......@@ -86,7 +103,6 @@ export default class ActivityScreen extends Component {
<Activity
ref={o => this.activity = o}
entity={ this.entityStore.entity }
newsfeed={ this.store }
navigation={ this.props.navigation }
autoHeight={false}
/> : null;
......
......@@ -26,7 +26,8 @@ export default class NewsfeedList extends Component {
nextBoostedId = 1;
viewOpts = {
viewAreaCoveragePercentThreshold: 50
viewAreaCoveragePercentThreshold: 50,
minimumViewTime: 300
}
state = {
itemHeight: 0,
......@@ -167,8 +168,14 @@ export default class NewsfeedList extends Component {
);
}
/**
* Key extractor for list items
*/
keyExtractor = item => item.rowKey;
/**
* Get footer
*/
getFooter() {
if (this.props.newsfeed.loading && !this.props.newsfeed.list.refreshing){
......@@ -184,6 +191,9 @@ export default class NewsfeedList extends Component {
return null;
}
/**
* Get error loading component
*/
getErrorLoading()
{
const message = this.props.newsfeed.list.entities.length ?
......@@ -193,12 +203,12 @@ export default class NewsfeedList extends Component {
return <ErrorLoading message={message} tryAgain={this.loadFeedForce}/>
}
/**
* On viewable item changed
*/
onViewableItemsChanged = ({viewableItems}) => {
viewableItems.forEach((item) => {
const { isViewable, key } = item;
if (isViewable ) {
this.props.newsfeed.addViewed(item.item);
}
this.props.newsfeed.list.addViewed(item.item);
});
}
......@@ -210,6 +220,9 @@ export default class NewsfeedList extends Component {
this.props.newsfeed.loadFeed();
}
/**
* Force feed load
*/
loadFeedForce = () => {
this.props.newsfeed.loadFeed();
}
......
......@@ -6,6 +6,7 @@ import stores from '../../AppStores';
import blockListService from '../common/services/block-list.service';
import feedsService from '../common/services/feed.service'
import featuresService from '../common/services/features.service';
import logService from '../common/services/log.service';
export default class NewsfeedService {
......@@ -50,8 +51,8 @@ export default class NewsfeedService {
/**
* Fetch top newsfeed
* @param {string} offset
* @param {int} limit
* @param {String} offset
* @param {Number} limit
*/
async getFeedSuggested(offset, limit = 12) {
return this._getFeed('api/v2/entities/suggested/activities' + (stores.hashtag.all ? '/all' : ''), offset, limit);
......@@ -59,9 +60,9 @@ export default class NewsfeedService {
/**
* Fetch channel feed
* @param {string} guid
* @param {string} offset
* @param {int} limit
* @param {String} guid
* @param {String} offset
* @param {Number} limit
*/
async getFeedChannel(guid, offset, limit = 12) {
return this._getFeed('api/v1/newsfeed/personal/' + guid, offset, limit);
......@@ -69,8 +70,8 @@ export default class NewsfeedService {
/**
* Fetch boosted content
* @param {string} offset
* @param {int} limit
* @param {String} offset
* @param {Number} limit
*/
async getBoosts(offset, limit = 15, rating) {
......@@ -90,74 +91,18 @@ export default class NewsfeedService {
}
}
// /**
// * Common function to fetch feeds
// * @param {string} endpoint
// * @param {string} offset
// * @param {int} limit
// */
// function _getFeed(endpoint, offset, limit) {
// return api.get(endpoint, { offset, limit })
// .then((data) => {
// return {
// entities: data.activity || data.entities,
// offset: data['load-next'],
// }
// })
// .catch(err => {
// if (!(typeof err === 'TypeError' && err.message === 'Network request failed')) {
// logService.exception('[NewsfeedService]', err);
// }
// throw "Oops, an error has occured updating your newsfeed";
// })
// }
// /**
// * Fetch channel feed
// * @param {string} guid
// * @param {string} offset
// * @param {int} limit
// */
// export function getFeedChannel(guid, offset, limit = 12) {
// return _getFeed('api/v1/newsfeed/personal/' + guid, offset, limit);
// }
// /**
// * Fetch boosted content
// * @param {string} offset
// * @param {int} limit
// */
// export function getBoosts(offset, limit = 15, rating) {
// return api.get('api/v1/boost/fetch/newsfeed', {
// limit: limit || '',
// offset: offset || '',
// rating: rating || 1,
// platform: Platform.OS === 'ios' ? 'ios' : 'other'
// })
// .then((data) => {
// return {
// entities: data.boosts||[],
// offset: data['load-next'],
// }
// });
// }
export function update(post) {
return api.post('api/v1/newsfeed/' + post.guid, post)
.then((data) => {
return {
entity: data.activity,
}
})
.catch(err => {
logService.exception('[NewsfeedService]', err);
throw "Oops, an error has occurred updating your newsfeed";
})
});
}
/**
* Toggle comments
* @param {string} guid
* @param {String} guid
* @param {boolean} value
*/
export function toggleComments(guid, value) {
......@@ -170,44 +115,23 @@ export function toggleComments(guid, value) {
/**
* Mark as viewed
* @param {object} entity
* @param {Object} entity
* @param {Object} data
*/
export async function setViewed(entity) {
export async function setViewed(entity, extra = {}) {
let data;
try {
if (entity.boosted) {
data = await api.post('api/v2/analytics/views/boost/' + entity.boosted_guid );
} else {
data = await api.post('api/v2/analytics/views/activity/' + entity.guid);
}
return data;
} catch (e) {
throw e;
}
}
/**
* Toggle user block
* @param {string} guid
* @param {boolean} value
*/
export function toggleUserBlock(guid, value) {
let result;
if (value) {
result = api.put('api/v1/block/' + guid);
blockListService.add(guid);
if (entity.boosted) {
data = await api.post('api/v2/analytics/views/boost/' + entity.boosted_guid, extra );
} else {
result = api.delete('api/v1/block/' + guid);
blockListService.remove(guid);
data = await api.post('api/v2/analytics/views/activity/' + entity.guid, extra);
}
return result;
return data;
}
/**
* Toggle muted
* @param {string} guid
* @param {String} guid
* @param {boolean} value
*/
......@@ -299,7 +223,7 @@ export async function getSingle(guid) {
/**
* Set Pinned to post
* @param {string} guid
* @param {String} guid
* @param {boolean} value
*/
export async function setPinPost(guid, value) {
......
import { Alert } from 'react-native';
import { observable, action, computed, extendObservable } from 'mobx'
import NewsfeedService, { setViewed } from './NewsfeedService';
import NewsfeedService from './NewsfeedService';
import OffsetFeedListStore from '../common/stores/OffsetFeedListStore';
import ActivityModel from './ActivityModel';
import logService from '../common/services/log.service';
......@@ -17,7 +17,6 @@ class NewsfeedStore {
service = new NewsfeedService;
viewed = [];
@observable filter = 'subscribed';
@observable.ref boosts = [];
......@@ -37,10 +36,10 @@ class NewsfeedStore {
buildStores() {
this.stores = {
'subscribed': {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
},
'boostfeed': {
list: new OffsetFeedListStore('shallow'),
list: new OffsetFeedListStore('shallow', true),
},
};
......@@ -50,6 +49,14 @@ class NewsfeedStore {
extendObservable(this.stores.boostfeed, {
loading: false
});
this.stores.subscribed.list.getMetadataService()
.setSource('feed/subscribed')
.setMedium('feed');
this.stores.boostfeed.list.getMetadataService()
.setSource('feed/boosts')
.setMedium('featured-content');
}
/**
......@@ -170,21 +177,6 @@ class NewsfeedStore {
this.loadFeed(true, false);
}
@action
async addViewed(entity) {
if (this.viewed.indexOf(entity.guid) < 0) {
let response;
try {
response = await setViewed(entity);
if (response) {
this.viewed.push(entity.guid);
}
} catch (e) {
throw new Error('There was an issue storing the view');
}
}
}
/**
* return service method based on filter
*/
......@@ -230,6 +222,7 @@ class NewsfeedStore {
@action
async refresh() {
// when refresh we report viewed again
await this.list.refresh();
await this.loadFeed(true);
this.list.refreshDone();
......@@ -240,7 +233,6 @@ class NewsfeedStore {
this.buildStores();
this.filter = 'subscribed';
this.boosts = [];
this.viewed = [];
this.loading = false;
this.loadingBoost = false;
}
......
......@@ -25,7 +25,7 @@ export default class TileElement extends PureComponent {
*/
_navToView = () => {
if (this.props.navigation) {
this.props.navigation.push('Activity', { entity: this.props.entity , store: this.props.newsfeed });
this.props.navigation.push('Activity', { entity: this.props.entity });
}
}
......
......@@ -46,7 +46,7 @@ export default class Activity extends Component {
* Nav to activity full screen
*/
navToActivity = () => {
const navOpts = { entity: this.props.entity, store: this.props.newsfeed };
const navOpts = { entity: this.props.entity };
if (this.props.entity.remind_object || this.props.hydrateOnNav) {
navOpts.hydrate = true;
......@@ -76,7 +76,7 @@ export default class Activity extends Component {
const message = this.state.editing ?
(
//Passing the store in newsfeed (could be channel also)
<ActivityEditor entity={entity} toggleEdit={this.toggleEdit} newsfeed={this.props.newsfeed} />
<ActivityEditor entity={entity} toggleEdit={this.toggleEdit}/>
):(
<View style={hasText ? styles.messageContainer : styles.emptyMessage}>
{hasText ? <ExplicitText entity={entity} navigation={this.props.navigation} style={styles.message} /> : null}
......@@ -104,7 +104,6 @@ export default class Activity extends Component {
entity={ entity }
navigation={this.props.navigation}
style={ styles.media }
newsfeed={this.props.newsfeed}
autoHeight={ this.props.autoHeight }
/>
{ overlay }
......@@ -161,7 +160,6 @@ export default class Activity extends Component {
const rightToolbar = (
<View style={styles.rightToolbar}>
<ActivityActionSheet
newsfeed={this.props.newsfeed}
toggleEdit={this.toggleEdit}
entity={this.props.entity}
navigation={this.props.navigation}
......@@ -198,12 +196,10 @@ export default class Activity extends Component {
<View>
<RemindOwnerBlock
entity={this.props.entity}
newsfeed={this.props.newsfeed}
navigation={this.props.navigation}
/>
<View style={styles.rightToolbar}>
{!this.props.hideTabs && <ActivityActionSheet
newsfeed={this.props.newsfeed}
toggleEdit={this.toggleEdit}
entity={this.props.entity}
navigation={this.props.navigation}
......@@ -243,9 +239,8 @@ export default class Activity extends Component {
return (
<View style={styles.remind}>
<Activity
ref={r => this.remind=r}
ref={r => this.remind = r}
hideTabs={true}
newsfeed={this.props.newsfeed}
entity={remind_object}
navigation={this.props.navigation}
isReminded={true}
......
......@@ -11,7 +11,6 @@ import {
StyleSheet,
Modal,
Alert,
} from 'react-native';
import {
......@@ -20,9 +19,8 @@ import {
} from 'mobx-react/native'
import translationService from '../../common/services/translation.service';
import { isFollowing } from '../../newsfeed/NewsfeedService';
import shareService from '../../share/ShareService';
import { toggleUserBlock } from '../NewsfeedService';
import { isFollowing } from '../NewsfeedService';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ActionSheet from 'react-native-actionsheet';
import { MINDS_URI } from '../../config/Config';
......@@ -123,11 +121,11 @@ export default class ActivityActions extends Component {
}
deleteEntity() {
this.props.newsfeed.list.deleteEntity(this.props.entity.guid).then( (result) => {
this.setState({
options: this.getOptions(),
});
async deleteEntity() {
try {
await this.props.entity.deleteEntity();
this.reloadOptions();
Alert.alert(
i18n.t('success'),
......@@ -136,15 +134,31 @@ export default class ActivityActions extends Component {
{text: i18n.t('ok'), onPress: () => {}},
],
{ cancelable: false }
)
);
if (this.props.navigation.state.routeName == 'Activity'){
this.props.navigation.goBack();
}
});
} catch (err) {
this.showError();
}
}
/**
* Show an error message
*/
showError() {
Alert.alert(
i18n.t('sorry'),
i18n.t('errorMessage') + '\n' + i18n.t('activity.tryAgain'),
[
{text: i18n.t('ok'), onPress: () => {}},
],
{ cancelable: false }
);
}
makeAction(option) {
async makeAction(option) {
switch (option) {
case i18n.t('translate.translate'):
if (this.props.onTranslate) this.props.onTranslate();
......@@ -165,44 +179,44 @@ export default class ActivityActions extends Component {
break;
case i18n.t('setExplicit'):
case i18n.t('removeExplicit'):
this.props.newsfeed.list.newsfeedToggleExplicit(this.props.entity.guid).then( (result) => {
this.setState({
options: this.getOptions(),
});
});
try {
await this.props.entity.toggleExplicit();
this.reloadOptions();
} catch (err) {
this.showError();
}
break;
case i18n.t('channel.block'):
toggleUserBlock(this.props.entity.ownerObj.guid, !this.state.userBlocked).then( (result) => {
try {
await this.props.entity.blockOwner();
this.setState({
userBlocked:true,
userBlocked: true,
options: this.getOptions(),
});
});
} catch (err) {
this.showError();
}
break;
case i18n.t('channel.unblock'):
toggleUserBlock(this.props.entity.ownerObj.guid, !this.state.userBlocked).then( (result) => {
try {
await this.props.entity.unblockOwner();
this.setState({
userBlocked:false,
userBlocked: false,
options: this.getOptions(),
});
});
} catch (err) {
this.showError();
}
break;
case i18n.t('follow'):
case i18n.t('unfollow'):
this.props.newsfeed.list.newsfeedToogleFollow(this.props.entity.guid).then( (result) => {
this.setState({
options: this.getOptions(),
});
});
try {
await this.props.entity.toogleFollow();
this.reloadOptions();
} catch (err) {
this.showError();
}
break;
// case 'Monetize':
// case 'Un-monetize':
// this.props.newsfeed.list.toggleMonetization(this.props.entity.guid).then( (result) => {
// this.setState({
// options: this.getOptions(),
// });
// });
// break;
case i18n.t('share'):
shareService.share(this.props.entity.text, MINDS_URI + 'newsfeed/' + this.props.entity.guid);
break;
......@@ -214,8 +228,12 @@ export default class ActivityActions extends Component {
this.props.navigation.navigate('Report', { entity: this.props.entity });
break;
}
}
reloadOptions() {
this.setState({
options: this.getOptions()
});
}
/**
......
......@@ -31,7 +31,8 @@ import i18n from '../../common/services/i18n.service';
export default class ActivityEditor extends Component {
state = {
text: ''
text: '',
saving: false
}
componentWillMount() {
......@@ -42,7 +43,7 @@ export default class ActivityEditor extends Component {
});
}
update = () => {
update = async() => {
if (HashtagService.slice(this.state.text).length > HashtagService.maxHashtags){ //if hashtag count greater than 5
Alert.alert(i18n.t('capture.maxHashtags', {maxHashtags: HashtagService.maxHashtags}));
......@@ -62,13 +63,23 @@ export default class ActivityEditor extends Component {
data.paywall = false;
}
this.props.newsfeed.list.updateActivity(this.props.entity, data)
.catch((err) => {
logService.exception('[ActivityEditor] update', err);
})
.finally(() => {
this.props.toggleEdit(false);
});
try {
this.setState({saving: true});
await this.props.entity.updateActivity(data);
this.props.toggleEdit(false);
} catch (err) {
logService.exception('[ActivityEditor] update', err);
Alert.alert(
i18n.t('sorry'),
i18n.t('errorMessage') + '\n' + i18n.t('activity.tryAgain'),
[
{text: i18n.t('ok'), onPress: () => {}},
],
{ cancelable: false }
);
} finally {
this.setState({saving: false});
}
}
cancel = () => {
......@@ -121,7 +132,7 @@ export default class ActivityEditor extends Component {
<View style={[CommonStyle.rowJustifyEnd, CommonStyle.paddingTop]}>
<Button text={i18n.t('cancel')} onPress={this.cancel} {...testID('Post editor cancel button')}/>
<Button text={i18n.t('save')} color={colors.primary} inverted={true} onPress={this.update} disabled={this.props.newsfeed.list.saving} {...testID('Post editor save button')}/>
<Button text={i18n.t('save')} color={colors.primary} inverted={true} onPress={this.update} disabled={this.state.saving} {...testID('Post editor save button')}/>
</View>
</View>
</View>
......
......@@ -6,6 +6,7 @@ import {
Text,
StyleSheet,
View,
Alert,
TouchableOpacity,
} from 'react-native';
......@@ -18,6 +19,8 @@ import { CommonStyle } from '../../../styles/Common';
import Counter from './Counter';
import withPreventDoubleTap from '../../../common/components/PreventDoubleTap';
import testID from '../../../common/helpers/testID';
import i18n from '../../../common/services/i18n.service';
import logService from '../../../common/services/log.service';
// prevent double tap in touchable
const TouchableOpacityCustom = withPreventDoubleTap(TouchableOpacity);
......@@ -72,7 +75,20 @@ export default class ThumbUpAction extends Component {
/**
* Toggle thumb
*/
toggleThumb = () => {
this.props.entity.toggleVote(this.direction);
toggleThumb = async () => {
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 }
);
}
}
}
{
"compilerOptions": {
"experimentalDecorators": true,
"allowJs": true,
"moduleResolution": "node",
"module": "commonjs",
"target": "es6",
"lib": [
"es2015"
],
"rootDir": "./",
"strictNullChecks": true,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"App.js",
"src/**/*.js",
]
}
This diff is collapsed.