...
 
Commits (4)
......@@ -7,7 +7,9 @@ import BlogModel from '../../src/blogs/BlogModel';
import UserStore from '../../src/auth/UserStore';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import NavigationService from '../../src/navigation/NavigationService';
jest.mock('../../src/navigation/NavigationService');
jest.mock('../../src/blogs/BlogsViewStore');
jest.mock('../../src/auth/UserStore');
jest.mock('../../src/blogs/BlogViewHTML', () => 'BlogViewHTML');
......@@ -21,6 +23,8 @@ describe('blog view screen component', () => {
let store, user;
beforeEach(() => {
NavigationService.getCurrentState.mockClear();
NavigationService.getCurrentState.mockReturnValue({});
store = new BlogsViewStore();
user = new UserStore();
user.me = {
......
......@@ -899,7 +899,9 @@ exports[`blog view screen component should renders correctly 1`] = `
"setRichEmbedPromise": null,
},
"entity": null,
"focusedUrn": null,
"guid": "",
"level": 0,
"loadNext": "",
"loadPrevious": "",
"onAttachedMedia": [Function],
......
......@@ -31,7 +31,7 @@
"jwt-simple": "^0.5.5",
"lodash": "^4.13.1",
"mobx": "^5.9.4",
"mobx-react": "^5.4.3",
"mobx-react": "^5.4.4",
"mobx-utils": "3.2.2",
"moment": "^2.20.1",
"moment-timezone": "^0.5.16",
......
......@@ -72,7 +72,6 @@ export default class BlogsViewScreen extends Component {
constructor(props) {
super(props);
const params = props.navigation.state.params;
this.comments = commentsStoreProvider.get();
}
......@@ -155,15 +154,6 @@ export default class BlogsViewScreen extends Component {
<SafeAreaView style={styles.header}>
<Icon raised color={colors.primary} size={22} name='arrow-back' onPress={() => this.props.navigation.goBack()}/>
</SafeAreaView>
{ this.comments.loadPrevious && !this.comments.loading ?
<TouchableHighlight
onPress={() => { this.loadComments()}}
underlayColor = 'transparent'
style = {styles.loadCommentsContainer}
>
<Text style={styles.loadCommentsText}> LOAD EARLIER </Text>
</TouchableHighlight> : null
}
</View>
)
}
......
......@@ -21,10 +21,8 @@ import {
} from 'react-native';
import ActionSheet from 'react-native-actionsheet';
import { thumbActivity } from '../newsfeed/activity/ActionsService';
import CommentEditor from './CommentEditor';
import { CommonStyle } from '../styles/Common';
import OwnerBlock from '../newsfeed/activity/OwnerBlock';
import formatDate from '../common/helpers/date';
import ThumbUpAction from '../newsfeed/activity/actions/ThumbUpAction';
import ThumbDownAction from '../newsfeed/activity/actions/ThumbDownAction';
......@@ -36,9 +34,9 @@ import {
} from '../config/Config';
import CommentList from './CommentList';
import commentsStoreProvider from '../comments/CommentsStoreProvider';
import DoubleTap from '../common/components/DoubleTap';
import ExplicitOverlay from '../common/components/explicit/ExplicitOverlay';
import colors from '../styles/Colors';
const DoubleTapText = DoubleTap(Text);
......@@ -84,7 +82,7 @@ export default class Comment extends Component {
)
return (
<View style={styles.container}>
<View style={[styles.container, comment.focused ? styles.focused : null]}>
<TouchableOpacity onPress={this._navToChannel} style={styles.avatarContainer}>
<Image source={avatarSrc} style={styles.avatar}/>
</TouchableOpacity>
......@@ -114,7 +112,7 @@ export default class Comment extends Component {
<CommentList
entity={this.props.entity}
parent={comment}
store={this.comments}
store={comment.comments}
onInputFocus={this.onInputFocus}
navigation={this.props.navigation}
/>
......@@ -151,10 +149,6 @@ export default class Comment extends Component {
* Toggle expand
*/
toggleExpand = () => {
if (!this.props.comment.expanded && !this.comments) {
this.comments = commentsStoreProvider.get();
this.comments.setParent(this.props.comment);
}
this.props.comment.toggleExpanded();
}
......@@ -296,6 +290,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'stretch',
},
focused: {
borderLeftColor: colors.primary,
borderLeftWidth: 4
},
media: {
flex: 1,
margin: 8,
......
......@@ -177,7 +177,7 @@ export default class CommentList extends React.Component<Props, State> {
onLayout = (e) => {
if (!this.props.parent) {
this.height = e.nativeEvent.layout.height;
this.height = e.nativeEvent.layout.height || 0;
}
}
......@@ -385,7 +385,7 @@ export default class CommentList extends React.Component<Props, State> {
return (
<View>
{ header }
{ this.props.store.loadPrevious && !this.props.store.loading ?
{ this.props.store.loadPrevious && !this.props.store.loadingPrevious ?
<TouchableHighlight
onPress={() => { this.loadComments(true)}}
underlayColor = 'transparent'
......@@ -394,8 +394,8 @@ export default class CommentList extends React.Component<Props, State> {
<Text style={[CS.fontM, CS.colorPrimary]}><IconMC name="update" size={16} /> LOAD EARLIER </Text>
</TouchableHighlight> : null
}
{this.props.store.loading && this.props.store.loaded && <ActivityIndicator size="small" style={CS.paddingTop2x}/>}
{this.getErrorLoading()}
{this.props.store.loadingPrevious && this.props.store.loaded && <ActivityIndicator size="small" style={CS.paddingTop2x}/>}
{this.getErrorLoading(this.props.store.errorLoadingPrevious, true)}
</View>
)
}
......@@ -409,8 +409,8 @@ export default class CommentList extends React.Component<Props, State> {
this.props.store.refreshDone();
}
getErrorLoading() {
if (this.props.store.errorLoading) {
getErrorLoading(errorLoading, descending) {
if (errorLoading) {
const message = this.props.store.comments.length ?
"Can't load more\nTry again" :
"Can't load the comments\nTry again";
......@@ -420,6 +420,28 @@ export default class CommentList extends React.Component<Props, State> {
return null;
}
/**
* Get list footer
*/
getFooter() {
return (
<View>
{ this.props.store.loadNext && !this.props.store.loadingNext ?
<TouchableHighlight
onPress={() => this.loadComments(true, false)}
underlayColor = 'transparent'
style = {[CS.rowJustifyCenter, CS.padding2x]}
>
<Text style={[CS.fontM, CS.colorPrimary]}><IconMC name="update" size={16} /> LOAD LATER </Text>
</TouchableHighlight> : null
}
{this.props.store.loadingNext && this.props.store.loaded && <ActivityIndicator size="small" style={CS.paddingTop2x}/>}
{this.getErrorLoading(this.props.store.errorLoadingNext, false)}
</View>
)
}
getComments() {
if (!this.props.store.comments) {
return [];
......@@ -435,11 +457,37 @@ export default class CommentList extends React.Component<Props, State> {
this.props.store.comments.length !== this.getComments().length;
}
/**
* @param {object} ref
*/
setListRef = ref => this.listRef = ref;
/**
* @param {object} ref
*/
setActionSheetRef = o => this.actionAttachmentSheet = o;
componentWillReact(){
if (this.props.store.getLevel() == 1) console.log('will react',
{comments : this.props.store.comments.length,
refreshing : this.props.store.refreshing,
loaded : this.props.store.loaded,
saving : this.props.store.saving,
text : this.props.store.text,
loadingPrevious : this.props.store.loadingPrevious,
loadingNext : this.props.store.loadingNext})
}
/**
* Render
*/
render() {
if (this.props.store.getLevel() == 1) console.log('RENDERING');
const header = this.getHeader();
let actionsheet = null;
......@@ -454,6 +502,7 @@ export default class CommentList extends React.Component<Props, State> {
}
const comments = this.getComments();
const footer = this.getFooter();
const emptyThread = (<View style={[CS.textCenter]}>
{this.isWholeThreadBlocked() && <Text style={[CS.textCenter, CS.marginBottom2x, CS.marginTop2x, CS.fontLight]}>
......@@ -467,8 +516,9 @@ export default class CommentList extends React.Component<Props, State> {
keyboardVerticalOffset={vPadding} enabled={this.state.focused && !this.props.parent}>
<View style={CS.flexContainer}>
<FlatList
ref={ref => this.listRef = ref}
ref={this.setListRef}
ListHeaderComponent={header}
ListFooterComponent={footer}
data={comments}
keyboardShouldPersistTaps={'handled'}
renderItem={this.renderComment}
......@@ -489,7 +539,7 @@ export default class CommentList extends React.Component<Props, State> {
</View>
{actionsheet}
<ActionSheet
ref={o => this.actionAttachmentSheet = o}
ref={this.setActionSheetRef}
options={['Cancel', 'Gallery', 'Photo', 'Video']}
onPress={this.selectMediaSource}
cancelButtonIndex={0}
......
import { computed, action, observable, decorate } from 'mobx';
import ActivityModel from '../newsfeed/ActivityModel';
import commentsStoreProvider from '../comments/CommentsStoreProvider';
import {
MINDS_CDN_URI
} from '../config/Config';
......@@ -12,9 +13,29 @@ export default class CommentModel extends ActivityModel {
@observable expanded = false;
/**
* Store for child comments
*/
comments = null;
/**
* The parent comment
*/
parent = null;
@action
toggleExpanded() {
this.expanded = !this.expanded;
this.buildCommentsStore();
}
buildCommentsStore(parent) {
if (this.expanded && !this.comments) {
console.log('BUILD STORE FOR '+this.description)
this.comments = commentsStoreProvider.get();
this.comments.setParent(this);
this.parent = parent;
}
}
/**
......
import api from './../common/services/api.service';
const decodeUrn = (urn) => {
let parts = urn.split(':');
const obj = {
entity_guid: parts[2],
parent_guid_l1: parts[3],
parent_guid_l2: parts[4],
parent_guid_l3: parts[5],
guid: parts[6],
parent_path: parts[5] ? `${parts[3]}:${parts[4]}:0` : `${parts[3]}:0:0`,
};
return obj;
}
/**
* Get comments
* @param {string} guid
......@@ -7,13 +22,57 @@ import api from './../common/services/api.service';
* @param {string} offset
* @param {integer} limit
*/
export async function getComments(guid, parent_path, descending, token, include_offset, limit = 12, comment_guid = 0) {
const params = { limit, descending, reversed: false};
if (token) params.token = token;
if (include_offset) params.include_offset = include_offset;
export async function getComments(focusedUrn, entity_guid, parent_path, level, limit, loadNext, loadPrevious, descending ) {
let focusedUrnObject = focusedUrn ? decodeUrn(focusedUrn) : null;
if (focusedUrn) {
if (entity_guid != focusedUrnObject.entity_guid)
focusedUrn = null; //wrong comment thread to focus on
if (loadNext || loadPrevious)
focusedUrn = null; //can not focus and have pagination
if (focusedUrn && parent_path === '0:0:0') {
loadNext = focusedUrnObject.parent_guid_l1;
}
if (focusedUrn && parent_path === `${focusedUrnObject.parent_guid_l1}:0:0`) {
loadNext = focusedUrnObject.parent_guid_l2;
}
if (focusedUrn && parent_path === `${focusedUrnObject.parent_guid_l1}:${focusedUrnObject.parent_guid_l2}:0`) {
loadNext = focusedUrnObject.guid;
}
}
const opts = {
entity_guid,
parent_path,
focused_urn: focusedUrn,
limit: limit,
'load-previous': loadPrevious || null,
'load-next': loadNext || null,
};
let uri = `api/v2/comments/${opts.entity_guid}/0/${opts.parent_path}`;
let response = await api.get(uri, opts);
if (focusedUrn && focusedUrnObject) {
for (let comment of response.comments) {
switch (level) {
case 0:
comment.expanded = (comment.child_path === `${focusedUrnObject.parent_guid_l1}:0:0`);
break;
case 1:
comment.expanded = comment.child_path === `${focusedUrnObject.parent_guid_l1}:${focusedUrnObject.parent_guid_l2}:0`;
break;
default:
console.log('Level out of scope', level);
}
comment.focused = (comment._guid === focusedUrnObject.guid);
}
}
const data = await api.get(`api/v1/comments/${guid}/${comment_guid}/${parent_path}`, params);
return data;
//only use once
focusedUrn = null;
return response;
}
/**
......
import {
observable,
action
action,
runInAction
} from 'mobx'
import { Platform } from 'react-native';
......@@ -9,11 +10,7 @@ import {
getComments,
postComment,
updateComment,
deleteComment,
getCommentsReply,
postReplyComment,
updateReplyComment,
deleteReplyComment
deleteComment
} from './CommentsService';
import Comment from './Comment';
......@@ -25,8 +22,9 @@ import attachmentService from '../common/services/attachment.service';
import {toggleExplicit} from '../newsfeed/NewsfeedService';
import RichEmbedStore from '../common/stores/RichEmbedStore';
import logService from '../common/services/log.service';
import NavigationService from '../navigation/NavigationService';
const COMMENTS_PAGE_SIZE = 6;
const COMMENTS_PAGE_SIZE = 3;
/**
* Comments Store
......@@ -38,8 +36,14 @@ export default class CommentsStore {
@observable loaded = false;
@observable saving = false;
@observable text = '';
@observable loading = false;
@observable errorLoading = false;
@observable loadingPrevious = false;
@observable loadingNext = false;
@observable errorLoadingPrevious = false;
@observable errorLoadingNext = false;
level = 0;
focusedUrn = null;
// attachment store
attachment = new AttachmentStore();
......@@ -56,6 +60,10 @@ export default class CommentsStore {
// parent for reply
parent = null;
constructor() {
this.focusedUrn = this.getFocuedUrn();
}
getParentPath() {
return (this.parent && this.parent.child_path) ? this.parent.child_path : '0:0:0';
}
......@@ -68,42 +76,110 @@ export default class CommentsStore {
this.entity = entity;
}
/**
* Set focused urn
* @param {String|null} value
*/
setFocusedUrn(value) {
this.focusedUrn = value;
}
/**
* Get level
*/
getLevel() {
if (this.parent) {
if (this.parent.parent) {
return 2;
}
return 1;
}
return 0
}
/**
* Get focused urn
*/
getFocuedUrn() {
const params = NavigationService.getCurrentState().params;
let value = null;
if (params && params.focusedUrn) value = params.focusedUrn;
return value;
}
/**
* Load Comments
*/
@action
async loadComments(guid, descending = true, comment_guid = 0) {
if (this.cantLoadMore(guid)) {
async loadComments(guid, descending = true) {
if (this.cantLoadMore(guid, descending)) {
return;
}
this.guid = guid;
this.loading = true;
this.setErrorLoading(false);
if (descending) {
this.setErrorLoadingPrevious(false);
this.loadingPrevious = true;
} else {
this.setErrorLoadingNext(false);
this.loadingNext = true;
}
this.include_offset = '';
const parent_path = this.getParentPath();
try {
const response = await getComments(this.guid, parent_path, descending, this.loadPrevious, this.include_offset, COMMENTS_PAGE_SIZE, comment_guid);
const response = await getComments(
this.focusedUrn,
this.guid,
parent_path,
this.getLevel(),
COMMENTS_PAGE_SIZE,
descending ? null : this.loadNext,
descending ? this.loadPrevious : null,
descending,
);
runInAction(() => {
this.loaded = true;
this.setComments(response, descending);
});
this.loaded = true;
this.setComments(response, descending);
this.checkListen(response);
} catch (err) {
this.setErrorLoading(true);
if (descending) {
this.setErrorLoadingPrevious(true);
} else {
this.setErrorLoadingNext(true);
}
if (!(typeof err === 'TypeError' && err.message === 'Network request failed')) {
logService.exception('[CommentsStore] loadComments', err);
}
} finally {
this.loading = false;
runInAction(() => {
if (descending) {
this.loadingPrevious = false;
} else {
this.loadingNext = false;
}
})
// use only once
this.focusedUrn = null;
}
}
@action
setErrorLoading(value) {
this.errorLoading = value;
setErrorLoadingNext(value) {
this.errorLoadingNext = value;
}
@action
setErrorLoadingPrevious(value) {
this.errorLoadingPrevious = value;
}
/**
......@@ -209,20 +285,31 @@ export default class CommentsStore {
*/
@action
setComments(response, descending) {
if (this.comments.length) {
if (descending) {
this.loadPrevious = response['load-previous'];
} else {
this.loadNext = response['load-next'];
}
} else {
this.loadPrevious = response['load-previous'];
this.loadNext = response['load-next'];
}
if (response.comments) {
const comments = CommentModel.createMany(response.comments)
comments.reverse().forEach(c => this.comments.unshift(c));
if (response.comments.length < COMMENTS_PAGE_SIZE) { //nothing more to load
response['load-previous'] = '';
// check and build child comments store if necessary
comments.forEach(c => c.buildCommentsStore(this.parent))
if (descending) {
comments.reverse().forEach(c => this.comments.unshift(c));
} else {
comments.forEach(c => this.comments.push(c));
}
}
this.reversed = response.reversed;
if (descending) {
this.loadPrevious = response['load-previous'];
} else {
this.loadNext = response['load-previous'];
}
}
/**
......@@ -341,9 +428,10 @@ export default class CommentsStore {
/**
* Cant load more
* @param {string} guid
* @param {boolean} descending
*/
cantLoadMore(guid) {
return this.loaded && !(this.loadPrevious) && !this.refreshing && this.guid === guid;
cantLoadMore(guid, descending) {
return this.loaded && !(descending ? this.loadPrevious : this.loadNext) && !this.refreshing && this.guid === guid;
}
/**
......@@ -505,5 +593,4 @@ export default class CommentsStore {
this.comments.splice(index, 1);
}
}
}
\ No newline at end of file
import api from './../common/services/api.service';
import { abort } from '../common/helpers/abortableFetch';
import logService from '../common/services/log.service';
export default class NotificationsService {
......
......@@ -41,21 +41,21 @@ export default class CommentView extends Component {
navTo = () => {
switch (this.props.entity.entityObj.type ) {
case 'activity':
this.props.navigation.push('Activity', { entity: this.props.entity.entityObj, hydrate: true });
this.props.navigation.push('Activity', { entity: this.props.entity.entityObj, hydrate: true, focusedUrn: this.props.entity.params.focusedCommentUrn });
break;
case 'object':
switch(this.props.entity.entityObj.subtype) {
case 'blog':
this.props.navigation.push('BlogView', { blog: this.props.entity.entityObj, hydrate: true });
this.props.navigation.push('BlogView', { blog: this.props.entity.entityObj, hydrate: true, focusedUrn: this.props.entity.params.focusedCommentUrn });
break;
case 'image':
case 'video':
this.props.navigation.push('Activity', { entity: this.props.entity.entityObj, hydrate: true });
this.props.navigation.push('Activity', { entity: this.props.entity.entityObj, hydrate: true, focusedUrn: this.props.entity.params.focusedCommentUrn });
break;
}
break;
case 'group':
this.props.navigation.push('GroupView', { group: this.props.entity.entityObj, hydrate: true, tab: 'conversation' });
this.props.navigation.push('GroupView', { group: this.props.entity.entityObj, hydrate: true, tab: 'conversation', focusedUrn: this.props.entity.params.focusedCommentUrn });
break;
}
}
......
......@@ -5841,10 +5841,10 @@ mkdirp@*, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"
mobx-react@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.4.3.tgz#6709b7dd89670c40e9815914ac2ca49cc02bfb47"
integrity sha512-WC8yFlwvJ91hy8j6CrydAuFteUafcuvdITFQeHl3LRIf5ayfT/4W3M/byhEYD2BcJWejeXr8y4Rh2H26RunCRQ==
mobx-react@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.4.4.tgz#b3de9c6eabcd0ed8a40036888cb0221ab9568b80"
integrity sha512-2mTzpyEjVB/RGk2i6KbcmP4HWcAUFox5ZRCrGvSyz49w20I4C4qql63grPpYrS9E9GKwgydBHQlA4y665LuRCQ==
dependencies:
hoist-non-react-statics "^3.0.0"
react-lifecycles-compat "^3.0.2"
......