Skip to content
Commits on Source (2)
......@@ -294,7 +294,8 @@
"subscribed":"SUBSCRIBED",
"boostfeed":"BOOSTFEED",
"designYourChannel":"Design your channel",
"empty":"Your newsfeed is empty"
"empty":"Your newsfeed is empty",
"olderThan":"Older than {{period}}"
},
"comments": {
"successRemoving":"Comment removed succesfully",
......
// @flow
import logService from './log.service';
import apiService from './api.service';
import { abort, isNetworkFail } from '../helpers/abortableFetch';
import {abort, isNetworkFail} from '../helpers/abortableFetch';
import entitiesService from './entities.service';
import feedsStorage from './sql/feeds.storage';
import { showMessage } from 'react-native-flash-message';
import {showMessage} from 'react-native-flash-message';
import i18n from './i18n.service';
import connectivityService from './connectivity.service';
import Colors from '../../styles/Colors';
......@@ -15,14 +15,13 @@ export type FeedRecordType = {
owner_guid: string,
timestamp: string,
urn: string,
entity?: Object
entity?: Object,
};
/**
* Feed store
*/
export default class FeedsService {
/**
* @var {boolean}
*/
......@@ -51,7 +50,7 @@ export default class FeedsService {
/**
* @var {Object}
*/
params: Object = {sync: 1}
params: Object = {sync: 1};
/**
* @var {Array}
......@@ -73,6 +72,16 @@ export default class FeedsService {
*/
paginated = true;
/**
* @var {number|null}
*/
fallbackAt = null;
/**
* @var {number}
*/
fallbackIndex = -1;
/**
* Get entities from the current page
*/
......@@ -91,9 +100,15 @@ export default class FeedsService {
const feedPage = this.feed.slice(this.offset, end);
const result: Array<any> = await entitiesService.getFromFeed(feedPage, this, this.asActivities);
const result: Array<any> = await entitiesService.getFromFeed(
feedPage,
this,
this.asActivities,
);
if (!this.injectBoost) return result;
if (!this.injectBoost) {
return result;
}
this.injectBoosted(3, result, end);
this.injectBoosted(8, result, end);
......@@ -114,8 +129,10 @@ export default class FeedsService {
*/
injectBoosted(position: number, entities: Array<BaseModel>, end: number) {
if (this.offset <= position && end >= position) {
const boost = boostedContentService.fetch();
if (boost) entities.splice( position + this.offset, 0, boost );
const boost = boostedContentService.fetch();
if (boost) {
entities.splice(position + this.offset, 0, boost);
}
}
}
......@@ -127,10 +144,11 @@ export default class FeedsService {
this.feed.unshift({
owner_guid: entity.owner_guid,
timestamp: Date.now().toString(),
urn: entity.urn
urn: entity.urn,
});
this.offset++;
this.fallbackIndex++;
const plainEntity = entity.toPlainObject();
......@@ -146,6 +164,14 @@ export default class FeedsService {
return this.feed.length > this.limit + this.offset;
}
/**
* Set fallback index
* @param {number} value
*/
setFallbackIndex(value: number) {
this.fallbackIndex = value;
}
/**
* Set feed
* @param {Array<FeedRecordType>} feed
......@@ -235,6 +261,28 @@ export default class FeedsService {
abort(this);
}
/**
* Calculate the index of the fallback
*/
calculateFallbackIndex = () => {
let index = -1;
if (this.fallbackAt) {
index = this.feed.findIndex(
r =>
r.entity &&
r.entity.time_created &&
parseInt(r.entity.time_created, 10) < this.fallbackAt,
);
}
if (index !== -1) {
this.fallbackIndex = index;
} else {
this.fallbackIndex = -1;
}
};
/**
* Fetch
* @param {boolean} more
......@@ -242,17 +290,30 @@ export default class FeedsService {
async fetch(more: boolean = false): Promise<void> {
abort(this);
const params = {...this.params, ...{ limit: 150, as_activities: this.asActivities ? 1 : 0 }};
const params = {
...this.params,
...{limit: 150, as_activities: this.asActivities ? 1 : 0},
};
if (this.paginated && more) params.from_timestamp = this.pagingToken;
if (this.paginated && more) {
params.from_timestamp = this.pagingToken;
}
const response = await apiService.get(this.endpoint, params, this);
if (response.entities && response.entities.length) {
if (more) {
this.feed = this.feed.concat(response.entities);
} else {
this.feed = response.entities;
}
if (response.fallback_at) {
this.fallbackAt = response.fallback_at;
this.calculateFallbackIndex();
} else {
this.fallbackAt = null;
this.fallbackIndex = -1;
}
this.pagingToken = response['load-next'];
} else {
this.endReached = true;
......@@ -281,6 +342,8 @@ export default class FeedsService {
this.pagingToken = (this.feed[this.feed.length - 1].timestamp - 1).toString();
} else {
this.feed = feed.feed;
this.fallbackAt = feed.fallbackAt;
this.fallbackIndex = feed.fallbackIndex;
this.pagingToken = feed.next;
}
return true;
......@@ -299,9 +362,13 @@ export default class FeedsService {
const status = await this.fetchLocal();
try {
if (!status) await this.fetch();
if (!status) {
await this.fetch();
}
} catch (err) {
if (err.code === 'Abort') return;
if (err.code === 'Abort') {
return;
}
if (!isNetworkFail(err)) {
logService.exception('[FeedService]', err);
......@@ -318,13 +385,15 @@ export default class FeedsService {
try {
await this.fetch();
} catch (err) {
if (err.code === 'Abort') return;
if (err.code === 'Abort') {
return;
}
if (!isNetworkFail(err)) {
logService.exception('[FeedService]', err);
}
if (!await this.fetchLocal()) {
if (!(await this.fetchLocal())) {
// if there is no local data rethrow the exception
throw err;
}
......@@ -332,12 +401,14 @@ export default class FeedsService {
showMessage({
floating: true,
position: 'top',
message: (connectivityService.isConnected ? i18n.t('cantReachServer') : i18n.t('noInternet')),
message: connectivityService.isConnected
? i18n.t('cantReachServer')
: i18n.t('noInternet'),
description: i18n.t('showingStored'),
duration: 1300,
backgroundColor: '#FFDD63DD',
color: Colors.dark,
type: "info",
type: 'info',
});
}
}
......@@ -356,8 +427,10 @@ export default class FeedsService {
clear(): FeedsService {
this.offset = 0;
this.limit = 12;
this.fallbackAt = null;
this.fallbackIndex = -1;
this.pagingToken = '';
this.params = {sync: 1};
this.params = {sync: 1};
this.feed = [];
return this;
}
......
......@@ -25,8 +25,21 @@ export class FeedsStorage {
try {
await this.getDb();
const params = [key, 0, JSON.stringify({feed: this.map(feed.feed), next: feed.pagingToken}), Math.floor(Date.now() / 1000)];
await this.db.executeSql('REPLACE INTO feeds (key, offset, data, updated) values (?,?,?,?)', params);
const params = [
key,
0,
JSON.stringify({
feed: this.map(feed.feed),
next: feed.pagingToken,
fallbackAt: feed.fallbackAt,
fallbackIndex: feed.fallbackIndex,
}),
Math.floor(Date.now() / 1000),
];
await this.db.executeSql(
'REPLACE INTO feeds (key, offset, data, updated) values (?,?,?,?)',
params,
);
} catch (err) {
logService.exception('[FeedsStorage]', err);
}
......@@ -41,7 +54,10 @@ export class FeedsStorage {
try {
const key = this.getKey(feed);
const [result] = await this.db.executeSql('SELECT * FROM feeds WHERE key=? AND offset=?;', [key, 0]);
const [result] = await this.db.executeSql(
'SELECT * FROM feeds WHERE key=? AND offset=?;',
[key, 0],
);
const rows = result.rows.raw();
......
......@@ -47,7 +47,7 @@ export default class FeedStore {
/**
* Viewed store
*/
viewed = new Viewed;
viewed = new Viewed();
/**
* Metadata service
......@@ -57,20 +57,27 @@ export default class FeedStore {
/**
* @var {FeedsService}
*/
feedsService = new FeedsService;
feedsService = new FeedsService();
/**
* The offset of the list
*/
scrollOffset = 0;
/**
* Getter fallback index
*/
get fallbackIndex() {
return this.feedsService.fallbackIndex;
}
/**
* Class constructor
* @param {boolean} includeMetadata include a metadata service
*/
constructor(includeMetadata = false) {
if (includeMetadata) {
this.metadataService = new MetadataService;
this.metadataService = new MetadataService();
}
}
......@@ -260,6 +267,15 @@ export default class FeedStore {
return this;
}
/**
* Set fallback index
* @param {number} value
*/
setFallbackIndex(value: number): FeedStore {
this.feedsService.setFallbackIndex(value);
return this;
}
/**
* Fetch from the endpoint
*/
......
......@@ -31,6 +31,7 @@ import GroupsListItem from '../groups/GroupsListItem'
import ErrorBoundary from '../common/components/ErrorBoundary';
import i18n from '../common/services/i18n.service';
import FeedList from '../common/components/FeedList';
import FallbackBoundary from './FallbackBoundary';
/**
* Discovery Feed Screen
......@@ -45,6 +46,34 @@ export default class DiscoveryFeedScreen extends Component {
}
}
/**
* Render activity
*/
renderActivity = row => {
let isLast = this.props.discovery.feedStore.list.entities.length == row.index + 1;
const entity = row.item;
const boundaryText =
this.props.discovery.feedStore.list.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary message={this.cantShowActivity} containerStyle={CS.hairLineBottom}>
{boundaryText && <FallbackBoundary title={boundaryText}/>}
<Activity
entity={entity}
newsfeed={this.props.feedStore}
navigation={this.props.navigation}
autoHeight={false}
isLast={isLast}
/>
</ErrorBoundary>
)
}
/**
* Render
*/
......@@ -53,6 +82,7 @@ export default class DiscoveryFeedScreen extends Component {
return (
<FeedList
renderActivity={this.renderActivity}
feedStore={store}
ListFooterComponent={this.getFooter}
keyExtractor={this.keyExtractor}
......
......@@ -29,7 +29,7 @@ class DiscoveryFeedStore {
this.buildListStores();
}
setFeed(feed) {
setFeed(feed, fallbackIndex) {
this.list.clear();
this.list.viewed.clearViewed();
......@@ -38,6 +38,8 @@ class DiscoveryFeedStore {
.setFeed(feed)
.setOffset(0)
.hydratePage();
this.list.setFallbackIndex(fallbackIndex);
}
/**
......
......@@ -7,7 +7,6 @@ import {
StyleSheet,
Platform,
Text,
FlatList,
Dimensions,
RefreshControl,
View,
......@@ -18,7 +17,6 @@ import {
import { ListItem, Avatar } from 'react-native-elements';
import IonIcon from 'react-native-vector-icons/Ionicons';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Modal from 'react-native-modal'
import {
observer,
......@@ -47,14 +45,15 @@ import ErrorBoundary from '../common/components/ErrorBoundary';
import testID from '../common/helpers/testID';
import i18n from '../common/services/i18n.service';
import { FLAG_VIEW } from '../common/Permissions';
import FallbackBoundary from './FallbackBoundary';
/**
* Discovery screen
*/
export default
@inject('discovery', 'channel')
@observer
export default class DiscoveryScreen extends Component {
class DiscoveryScreen extends Component {
cols = 3;
iconSize = 28;
......@@ -62,13 +61,13 @@ export default class DiscoveryScreen extends Component {
active: false,
showFeed: false,
itemHeight: 0,
q: ''
}
q: '',
};
viewOptsFeed = {
viewAreaCoveragePercentThreshold: 50,
minimumViewTime: 300
}
minimumViewTime: 300,
};
static navigationOptions = {
tabBarIcon: ({ tintColor }) => (
......@@ -524,14 +523,22 @@ export default class DiscoveryScreen extends Component {
*/
navigateToFeed = ({urn}) => {
const index = this.props.discovery.listStore.feedsService.feed.findIndex(e => e.urn === urn);
let fallbackIndex = this.props.discovery.listStore.fallbackIndex;
this.props.discovery.feedStore.setFeed(this.props.discovery.listStore.feedsService.feed.slice(index));
if (fallbackIndex !== -1 && fallbackIndex > index) {
fallbackIndex -= index;
}
this.props.discovery.feedStore.setFeed(
this.props.discovery.listStore.feedsService.feed.slice(index),
fallbackIndex,
);
this.props.navigation.push('DiscoveryFeed', {
'showFeed': index,
title: _.capitalize(this.props.discovery.filters.filter) + ' ' + _.capitalize(this.props.discovery.filters.type)
})
}
});
};
/**
* Render a tile
......@@ -540,12 +547,20 @@ export default class DiscoveryScreen extends Component {
if (!this.state.active && row.item.isGif()) {
return <View style={{ height: this.state.itemHeight, width: this.state.itemHeight }}/>;
}
const boundaryText =
this.props.discovery.listStore.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary message={this.tileError} containerStyle={[CS.centered, {width: this.state.itemHeight, height: this.state.itemHeight}]} textSmall={true}>
<DiscoveryTile
entity={row.item}
size={this.state.itemHeight}
onPress={this.navigateToFeed}
boundaryText={boundaryText}
/>
</ErrorBoundary>
);
......@@ -567,8 +582,16 @@ export default class DiscoveryScreen extends Component {
* Render activity item
*/
renderActivity = (row) => {
const boundaryText =
this.props.discovery.listStore.fallbackIndex === row.index
? i18n.t('newsfeed.olderThan', {
period: this.props.discovery.filters.period,
})
: undefined;
return (
<ErrorBoundary containerStyle={CS.hairLineBottom}>
{boundaryText && <FallbackBoundary title={boundaryText}/>}
<Activity entity={row.item} navigation={this.props.navigation} autoHeight={false} />
</ErrorBoundary>
);
......
......@@ -75,14 +75,14 @@ class DiscoveryStore {
if (this.filters.type !== 'lastchannels') {
this.fetch();
}
}
};
/**
* On search change
*/
onSearchChange = (searchtext) => {
this.fetch(true);
}
};
fetch(refresh = false) {
const hashtags = appStores.hashtag.hashtag ? encodeURIComponent(appStores.hashtag.hashtag) : '';
......@@ -99,6 +99,7 @@ class DiscoveryStore {
all,
query: this.filters.searchtext,
nsfw: this.filters.nsfw.concat([]),
period_fallback: 1,
})
.fetchRemoteOrLocal(refresh);
}
......
......@@ -104,11 +104,24 @@ class DiscoveryTile extends Component {
<ExplicitOverlay entity={entity} iconSize={45} hideText={true} />
) : null;
const boundary = this.props.boundaryText ? (
<View
style={[
CS.positionAbsoluteTop,
CS.backgroundGreyed,
CS.centered,
styles.boundary,
]}>
<Text>{this.props.boundaryText}</Text>
</View>
) : null;
return (
<TouchableOpacity
onPress={this._onPress}
style={[this.state.style, styles.tile]}>
<View style={[CS.flexContainer, CS.backgroundGreyed]}>
{boundary}
<FastImage
source={url}
style={CS.positionAbsolute}
......@@ -123,6 +136,11 @@ class DiscoveryTile extends Component {
}
const styles = StyleSheet.create({
boundary: {
height: 20,
width: '100%',
zIndex: 1000,
},
tile: {
paddingTop: 1,
paddingBottom: 1,
......
import React from 'react';
import {View, Text} from 'react-native';
import {CommonStyle as CS} from '../styles/Common';
/**
* Fallback boundary for feed views
* @param {object} props
*/
export default function FallbackBoundary(props) {
return (
<View
style={[
CS.flexContainer,
CS.centered,
CS.paddingTop,
CS.paddingBottom,
CS.borderBottomHair,
CS.fullWidth,
CS.borderGreyed,
CS.backgroundLight,
]}>
<Text style={[CS.colorDarkGreyed, CS.fontXL]}>{props.title}</Text>
</View>
);
}