[IMPROVEMENT] Native sort and limit queries (#2249)

* Update WatermelonDB to 0.18.0

* Low onEndReachedThreshold

* Query experiment

* QUERY_SIZE

* Query or fetch data

* Reorder class functions

* Reset variables

* Hide system messages

* Change this.count behaviour

* Starting on RoomsListView

* unsubscribeQuery

* onEndReached

* Separate queries

* Reusable where clause

* Refactoring

* Refactor RoomItem to accept item as prop

* Comment RoomItem tests just so jest passes

* Fix alert and status

* onPress

* Unnecessary diff

* react-fast-compare

* Native limit on ShareListView

* Tweak item description

* Lint

* Fix on foreground crash

* Suggested changes
This commit is contained in:
Diego Mello 2020-07-20 13:44:54 -03:00 committed by GitHub
parent 9882ace694
commit 9dbe10bcf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 339 additions and 430 deletions

View File

@ -4994,137 +4994,6 @@ exports[`Storyshots Message list message 1`] = `
</RCTScrollView> </RCTScrollView>
`; `;
exports[`Storyshots RoomItem list roomitem 1`] = `
<RCTScrollView
style={
Object {
"backgroundColor": "#efeff4",
}
}
>
<View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Basic
</Text>
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
User
</Text>
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Type
</Text>
View
View
View
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Alerts
</Text>
View
View
View
View
View
View
View
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Last Message
</Text>
View
View
View
View
View
View
</View>
</RCTScrollView>
`;
exports[`Storyshots UiKitMessage list uikitmessage 1`] = ` exports[`Storyshots UiKitMessage list uikitmessage 1`] = `
<RCTSafeAreaView <RCTSafeAreaView
emulateUnlessSupported={true} emulateUnlessSupported={true}

View File

@ -845,6 +845,12 @@ const RocketChat = {
return other && other.length ? other[0] : me; return other && other.length ? other[0] : me;
}, },
isRead(item) {
let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread;
},
isGroupChat(room) { isGroupChat(room) {
return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2); return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2);
}, },

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -16,82 +16,79 @@ import { themes } from '../../constants/colors';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
const attrs = [ const attrs = [
'name',
'unread',
'userMentions',
'showLastMessage',
'useRealName',
'alert',
'type',
'width', 'width',
'isRead',
'favorite',
'status', 'status',
'connected', 'connected',
'theme', 'theme',
'isFocused' 'isFocused',
'forceUpdate',
'showLastMessage'
]; ];
const arePropsEqual = (oldProps, newProps) => { const arePropsEqual = (oldProps, newProps) => attrs.every(key => oldProps[key] === newProps[key]);
const { _updatedAt: _updatedAtOld } = oldProps;
const { _updatedAt: _updatedAtNew } = newProps;
if (_updatedAtOld && _updatedAtNew && _updatedAtOld.toISOString() !== _updatedAtNew.toISOString()) {
return false;
}
return attrs.every(key => oldProps[key] === newProps[key]);
};
const RoomItem = React.memo(({ const RoomItem = React.memo(({
item,
onPress, onPress,
width, width,
favorite,
toggleFav, toggleFav,
isRead,
rid,
toggleRead, toggleRead,
hideChannel, hideChannel,
testID, testID,
unread,
userMentions,
name,
_updatedAt,
alert,
type,
avatarSize, avatarSize,
baseUrl, baseUrl,
userId, userId,
username, username,
token, token,
id, id,
prid,
showLastMessage, showLastMessage,
hideUnreadStatus,
lastMessage,
status, status,
avatar,
useRealName, useRealName,
getUserPresence, getUserPresence,
isGroupChat,
connected, connected,
theme, theme,
isFocused isFocused,
getRoomTitle,
getRoomAvatar,
getIsGroupChat,
getIsRead
}) => { }) => {
const [, setForceUpdate] = useState(1);
useEffect(() => { useEffect(() => {
if (connected && type === 'd' && id) { if (connected && item.t === 'd' && id) {
getUserPresence(id); getUserPresence(id);
} }
}, [connected]); }, [connected]);
const date = lastMessage && formatDate(lastMessage.ts); useEffect(() => {
if (item?.observe) {
const observable = item.observe();
const subscription = observable?.subscribe?.(() => {
setForceUpdate(prevForceUpdate => prevForceUpdate + 1);
});
return () => {
subscription?.unsubscribe?.();
};
}
}, []);
const name = getRoomTitle(item);
const avatar = getRoomAvatar(item);
const isGroupChat = getIsGroupChat(item);
const isRead = getIsRead(item);
const _onPress = () => onPress(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
let accessibilityLabel = name; let accessibilityLabel = name;
if (unread === 1) { if (item.unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`; accessibilityLabel += `, ${ item.unread } ${ I18n.t('alert') }`;
} else if (unread > 1) { } else if (item.unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`; accessibilityLabel += `, ${ item.unread } ${ I18n.t('alerts') }`;
} }
if (userMentions > 0) { if (item.userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`; accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
} }
@ -101,16 +98,16 @@ const RoomItem = React.memo(({
return ( return (
<Touchable <Touchable
onPress={onPress} onPress={_onPress}
width={width} width={width}
favorite={favorite} favorite={item.f}
toggleFav={toggleFav} toggleFav={toggleFav}
isRead={isRead} isRead={isRead}
rid={rid} rid={item.rid}
toggleRead={toggleRead} toggleRead={toggleRead}
hideChannel={hideChannel} hideChannel={hideChannel}
testID={testID} testID={testID}
type={type} type={item.t}
theme={theme} theme={theme}
isFocused={isFocused} isFocused={isFocused}
> >
@ -121,7 +118,7 @@ const RoomItem = React.memo(({
<Avatar <Avatar
text={avatar} text={avatar}
size={avatarSize} size={avatarSize}
type={type} type={item.t}
baseUrl={baseUrl} baseUrl={baseUrl}
style={styles.avatar} style={styles.avatar}
userId={userId} userId={userId}
@ -137,8 +134,8 @@ const RoomItem = React.memo(({
> >
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<TypeIcon <TypeIcon
type={type} type={item.t}
prid={prid} prid={item.prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme} theme={theme}
@ -146,7 +143,7 @@ const RoomItem = React.memo(({
<Text <Text
style={[ style={[
styles.title, styles.title,
alert && !hideUnreadStatus && styles.alert, item.alert && !item.hideUnreadStatus && styles.alert,
{ color: themes[theme].titleText } { color: themes[theme].titleText }
]} ]}
ellipsizeMode='tail' ellipsizeMode='tail'
@ -154,7 +151,7 @@ const RoomItem = React.memo(({
> >
{name} {name}
</Text> </Text>
{_updatedAt ? ( {item.roomUpdatedAt ? (
<Text <Text
style={[ style={[
styles.date, styles.date,
@ -163,7 +160,7 @@ const RoomItem = React.memo(({
themes[theme] themes[theme]
.auxiliaryText .auxiliaryText
}, },
alert && !hideUnreadStatus && [ item.alert && !item.hideUnreadStatus && [
styles.updateAlert, styles.updateAlert,
{ {
color: color:
@ -181,18 +178,18 @@ const RoomItem = React.memo(({
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<LastMessage <LastMessage
lastMessage={lastMessage} lastMessage={item.lastMessage}
type={type} type={item.t}
showLastMessage={showLastMessage} showLastMessage={showLastMessage}
username={username} username={username}
alert={alert && !hideUnreadStatus} alert={item.alert && !item.hideUnreadStatus}
useRealName={useRealName} useRealName={useRealName}
theme={theme} theme={theme}
/> />
<UnreadBadge <UnreadBadge
unread={unread} unread={item.unread}
userMentions={userMentions} userMentions={item.userMentions}
type={type} type={item.t}
theme={theme} theme={theme}
/> />
</View> </View>
@ -203,17 +200,10 @@ const RoomItem = React.memo(({
}, arePropsEqual); }, arePropsEqual);
RoomItem.propTypes = { RoomItem.propTypes = {
type: PropTypes.string.isRequired, item: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool, showLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string, id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
userId: PropTypes.string, userId: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
@ -221,27 +211,29 @@ RoomItem.propTypes = {
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
testID: PropTypes.string, testID: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
favorite: PropTypes.bool,
isRead: PropTypes.bool,
rid: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
toggleFav: PropTypes.func, toggleFav: PropTypes.func,
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
hideChannel: PropTypes.func, hideChannel: PropTypes.func,
avatar: PropTypes.bool,
hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
getUserPresence: PropTypes.func, getUserPresence: PropTypes.func,
connected: PropTypes.bool, connected: PropTypes.bool,
isGroupChat: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
isFocused: PropTypes.bool isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func
}; };
RoomItem.defaultProps = { RoomItem.defaultProps = {
avatarSize: 48, avatarSize: 48,
status: 'offline', status: 'offline',
getUserPresence: () => {} getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false
}; };
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { FlatList, RefreshControl } from 'react-native'; import { FlatList, RefreshControl } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import moment from 'moment'; import moment from 'moment';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -15,9 +14,10 @@ import EmptyRoom from './EmptyRoom';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { animateNextTransition } from '../../utils/layoutAnimation'; import { animateNextTransition } from '../../utils/layoutAnimation';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import debounce from '../../utils/debounce';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
const QUERY_SIZE = 50;
class List extends React.Component { class List extends React.Component {
static propTypes = { static propTypes = {
onEndReached: PropTypes.func, onEndReached: PropTypes.func,
@ -47,7 +47,8 @@ class List extends React.Component {
super(props); super(props);
console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
this.count = 0;
this.needsFetch = false;
this.mounted = false; this.mounted = false;
this.state = { this.state = {
loading: true, loading: true,
@ -56,7 +57,7 @@ class List extends React.Component {
refreshing: false, refreshing: false,
animated: false animated: false
}; };
this.init(); this.query();
this.unsubscribeFocus = props.navigation.addListener('focus', () => { this.unsubscribeFocus = props.navigation.addListener('focus', () => {
if (this.mounted) { if (this.mounted) {
this.setState({ animated: true }); this.setState({ animated: true });
@ -72,72 +73,6 @@ class List extends React.Component {
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
} }
// eslint-disable-next-line react/sort-comp
async init() {
const { rid, tmid } = this.props;
const db = database.active;
// handle servers with version < 3.0.0
let { hideSystemMessages = [] } = this.props;
if (!Array.isArray(hideSystemMessages)) {
hideSystemMessages = [];
}
if (tmid) {
try {
this.thread = await db.collections
.get('threads')
.find(tmid);
} catch (e) {
console.log(e);
}
this.messagesObservable = db.collections
.get('thread_messages')
.query(Q.where('rid', tmid), Q.or(Q.where('t', Q.notIn(hideSystemMessages)), Q.where('t', Q.eq(null))))
.observe();
} else if (rid) {
this.messagesObservable = db.collections
.get('messages')
.query(Q.where('rid', rid), Q.or(Q.where('t', Q.notIn(hideSystemMessages)), Q.where('t', Q.eq(null))))
.observe();
}
if (rid) {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((data) => {
if (tmid && this.thread) {
data = [this.thread, ...data];
}
const messages = orderBy(data, ['ts'], ['desc']);
if (this.mounted) {
this.setState({ messages }, () => this.update());
} else {
this.state.messages = messages;
}
this.readThreads();
});
}
}
// eslint-disable-next-line react/sort-comp
reload = () => {
this.unsubscribeMessages();
this.init();
}
readThreads = async() => {
const { tmid } = this.props;
if (tmid) {
try {
await RocketChat.readThreads(tmid);
} catch {
// Do nothing
}
}
}
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { loading, end, refreshing } = this.state; const { loading, end, refreshing } = this.state;
const { hideSystemMessages, theme } = this.props; const { hideSystemMessages, theme } = this.props;
@ -177,7 +112,7 @@ class List extends React.Component {
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
onEndReached = debounce(async() => { fetchData = async() => {
const { const {
loading, end, messages, latest = messages[messages.length - 1]?.ts loading, end, messages, latest = messages[messages.length - 1]?.ts
} = this.state; } = this.state;
@ -196,12 +131,99 @@ class List extends React.Component {
result = await RocketChat.loadMessagesForRoom({ rid, t, latest }); result = await RocketChat.loadMessagesForRoom({ rid, t, latest });
} }
this.setState({ end: result.length < 50, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result)); this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result));
} catch (e) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
log(e); log(e);
} }
}, 300) }
query = async() => {
this.count += QUERY_SIZE;
const { rid, tmid } = this.props;
const db = database.active;
// handle servers with version < 3.0.0
let { hideSystemMessages = [] } = this.props;
if (!Array.isArray(hideSystemMessages)) {
hideSystemMessages = [];
}
if (tmid) {
try {
this.thread = await db.collections
.get('threads')
.find(tmid);
} catch (e) {
console.log(e);
}
this.messagesObservable = db.collections
.get('thread_messages')
.query(
Q.where('rid', tmid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
)
.observe();
} else if (rid) {
this.messagesObservable = db.collections
.get('messages')
.query(
Q.where('rid', rid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
)
.observe();
}
if (rid) {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((messages) => {
if (messages.length <= this.count) {
this.needsFetch = true;
}
if (tmid && this.thread) {
messages = [...messages, this.thread];
}
messages = messages.filter(m => !m.t || !hideSystemMessages?.includes(m.t));
if (this.mounted) {
this.setState({ messages }, () => this.update());
} else {
this.state.messages = messages;
}
this.readThreads();
});
}
}
reload = () => {
this.count = 0;
this.query();
}
readThreads = async() => {
const { tmid } = this.props;
if (tmid) {
try {
await RocketChat.readThreads(tmid);
} catch {
// Do nothing
}
}
}
onEndReached = async() => {
if (this.needsFetch) {
this.needsFetch = false;
await this.fetchData();
}
this.query();
}
loadMoreMessages = (result) => { loadMoreMessages = (result) => {
const { end } = this.state; const { end } = this.state;
@ -305,7 +327,7 @@ class List extends React.Component {
removeClippedSubviews={isIOS} removeClippedSubviews={isIOS}
initialNumToRender={7} initialNumToRender={7}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
onEndReachedThreshold={5} onEndReachedThreshold={0.5}
maxToRenderPerBatch={5} maxToRenderPerBatch={5}
windowSize={10} windowSize={10}
ListFooterComponent={this.renderFooter} ListFooterComponent={this.renderFooter}

View File

@ -206,12 +206,10 @@ class RoomView extends React.Component {
const { appState, insets } = this.props; const { appState, insets } = this.props;
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { // Fire List.query() just to keep observables working
// Fire List.init() just to keep observables working
if (this.list && this.list.current) { if (this.list && this.list.current) {
this.list.current.init(); this.list.current?.query?.();
} }
});
} }
// If it's not direct message // If it's not direct message
if (this.t !== 'd') { if (this.t !== 'd') {
@ -267,9 +265,6 @@ class RoomView extends React.Component {
if (this.didMountInteraction && this.didMountInteraction.cancel) { if (this.didMountInteraction && this.didMountInteraction.cancel) {
this.didMountInteraction.cancel(); this.didMountInteraction.cancel();
} }
if (this.onForegroundInteraction && this.onForegroundInteraction.cancel) {
this.onForegroundInteraction.cancel();
}
if (this.willBlurListener && this.willBlurListener.remove) { if (this.willBlurListener && this.willBlurListener.remove) {
this.willBlurListener.remove(); this.willBlurListener.remove();
} }

View File

@ -9,7 +9,7 @@ import {
RefreshControl RefreshControl
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual, orderBy } from 'lodash'; import isEqual from 'react-fast-compare';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
@ -71,6 +71,7 @@ const DISCUSSIONS_HEADER = 'Discussions';
const CHANNELS_HEADER = 'Channels'; const CHANNELS_HEADER = 'Channels';
const DM_HEADER = 'Direct_Messages'; const DM_HEADER = 'Direct_Messages';
const GROUPS_HEADER = 'Private_Groups'; const GROUPS_HEADER = 'Private_Groups';
const QUERY_SIZE = 20;
const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus; const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus;
const filterIsFavorite = s => s.f; const filterIsFavorite = s => s.f;
@ -140,11 +141,12 @@ class RoomsListView extends React.Component {
this.gotSubscriptions = false; this.gotSubscriptions = false;
this.animated = false; this.animated = false;
this.count = 0;
this.state = { this.state = {
searching: false, searching: false,
search: [], search: [],
loading: true, loading: true,
allChats: [], chatsOrder: [],
chats: [], chats: [],
item: {} item: {}
}; };
@ -211,7 +213,7 @@ class RoomsListView extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { allChats, searching, item } = this.state; const { chatsOrder, searching, item } = this.state;
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]); const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]);
if (propsUpdated) { if (propsUpdated) {
@ -219,7 +221,7 @@ class RoomsListView extends React.Component {
} }
// Compare changes only once // Compare changes only once
const chatsNotEqual = !isEqual(nextState.allChats, allChats); const chatsNotEqual = !isEqual(nextState.chatsOrder, chatsOrder);
// If they aren't equal, set to update if focused // If they aren't equal, set to update if focused
if (chatsNotEqual) { if (chatsNotEqual) {
@ -290,7 +292,7 @@ class RoomsListView extends React.Component {
&& prevProps.showUnread === showUnread && prevProps.showUnread === showUnread
) )
) { ) {
this.getSubscriptions(true); this.getSubscriptions();
} else if ( } else if (
appState === 'foreground' appState === 'foreground'
&& appState !== prevProps.appState && appState !== prevProps.appState
@ -309,9 +311,7 @@ class RoomsListView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.querySubscription && this.querySubscription.unsubscribe) { this.unsubscribeQuery();
this.querySubscription.unsubscribe();
}
if (this.unsubscribeFocus) { if (this.unsubscribeFocus) {
this.unsubscribeFocus(); this.unsubscribeFocus();
} }
@ -396,17 +396,8 @@ class RoomsListView extends React.Component {
return allData; return allData;
} }
getSubscriptions = async(force = false) => { getSubscriptions = async() => {
if (this.gotSubscriptions && !force) { this.unsubscribeQuery();
return;
}
this.gotSubscriptions = true;
if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
this.setState({ loading: true });
const { const {
sortBy, sortBy,
@ -416,41 +407,49 @@ class RoomsListView extends React.Component {
} = this.props; } = this.props;
const db = database.active; const db = database.active;
const observable = await db.collections let observable;
.get('subscriptions')
.query( const defaultWhereClause = [
Q.where('archived', false), Q.where('archived', false),
Q.where('open', true) Q.where('open', true)
];
if (sortBy === 'alphabetical') {
defaultWhereClause.push(Q.experimentalSortBy(`${ this.useRealName ? 'fname' : 'name' }`, Q.asc));
} else {
defaultWhereClause.push(Q.experimentalSortBy('room_updated_at', Q.desc));
}
// When we're grouping by something
if (this.isGrouping) {
observable = await db.collections
.get('subscriptions')
.query(...defaultWhereClause)
.observe();
// When we're NOT grouping
} else {
this.count += QUERY_SIZE;
observable = await db.collections
.get('subscriptions')
.query(
...defaultWhereClause,
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
) )
.observeWithColumns(['room_updated_at', 'unread', 'alert', 'user_mentions', 'f', 't']); .observe();
}
this.querySubscription = observable.subscribe((data) => { this.querySubscription = observable.subscribe((data) => {
let tempChats = []; let tempChats = [];
let chats = []; let chats = data;
if (sortBy === 'alphabetical') {
chats = orderBy(data, [`${ this.useRealName ? 'fname' : 'name' }`], ['asc']);
} else {
chats = orderBy(data, ['roomUpdatedAt'], ['desc']);
}
// it's better to map and test all subs altogether then testing them individually /**
const allChats = data.map(item => ({ * We trigger re-render only when chats order changes
alert: item.alert, * RoomItem handles its own re-render
unread: item.unread, */
userMentions: item.userMentions, const chatsOrder = data.map(item => item.rid);
isRead: this.getIsRead(item),
favorite: item.f,
lastMessage: item.lastMessage,
name: this.getRoomTitle(item),
_updatedAt: item.roomUpdatedAt,
key: item._id,
rid: item.rid,
type: item.t,
prid: item.prid,
uids: item.uids,
usernames: item.usernames,
visitor: item.visitor
}));
// unread // unread
if (showUnread) { if (showUnread) {
@ -484,12 +483,18 @@ class RoomsListView extends React.Component {
this.internalSetState({ this.internalSetState({
chats: tempChats, chats: tempChats,
allChats, chatsOrder,
loading: false loading: false
}); });
}); });
} }
unsubscribeQuery = () => {
if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
}
initSearching = () => { initSearching = () => {
const { openSearchHeader } = this.props; const { openSearchHeader } = this.props;
this.internalSetState({ searching: true }, () => { this.internalSetState({ searching: true }, () => {
@ -548,10 +553,19 @@ class RoomsListView extends React.Component {
getRoomAvatar = item => RocketChat.getRoomAvatar(item) getRoomAvatar = item => RocketChat.getRoomAvatar(item)
isGroupChat = item => RocketChat.isGroupChat(item)
isRead = item => RocketChat.isRead(item)
getUserPresence = uid => RocketChat.getUserPresence(uid) getUserPresence = uid => RocketChat.getUserPresence(uid)
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room); getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
get isGrouping() {
const { showUnread, showFavorites, groupByType } = this.props;
return showUnread || showFavorites || groupByType;
}
onPressItem = (item = {}) => { onPressItem = (item = {}) => {
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
if (!navigation.isFocused()) { if (!navigation.isFocused()) {
@ -743,6 +757,13 @@ class RoomsListView extends React.Component {
roomsRequest({ allData: true }); roomsRequest({ allData: true });
} }
onEndReached = () => {
// Run only when we're not grouping by anything
if (!this.isGrouping) {
this.getSubscriptions();
}
}
getScrollRef = ref => (this.scroll = ref); getScrollRef = ref => (this.scroll = ref);
renderListHeader = () => { renderListHeader = () => {
@ -774,12 +795,6 @@ class RoomsListView extends React.Component {
); );
} }
getIsRead = (item) => {
let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread;
};
renderItem = ({ item }) => { renderItem = ({ item }) => {
if (item.separator) { if (item.separator) {
return this.renderSectionHeader(item.rid); return this.renderSectionHeader(item.rid);
@ -800,32 +815,19 @@ class RoomsListView extends React.Component {
width width
} = this.props; } = this.props;
const id = this.getUidDirectMessage(item); const id = this.getUidDirectMessage(item);
const isGroupChat = RocketChat.isGroupChat(item);
return ( return (
<RoomItem <RoomItem
item={item}
theme={theme} theme={theme}
alert={item.alert}
unread={item.unread}
hideUnreadStatus={item.hideUnreadStatus}
userMentions={item.userMentions}
isRead={this.getIsRead(item)}
favorite={item.f}
avatar={this.getRoomAvatar(item)}
lastMessage={item.lastMessage}
name={this.getRoomTitle(item)}
_updatedAt={item.roomUpdatedAt}
key={item._id}
id={id} id={id}
type={item.t}
userId={userId} userId={userId}
username={username} username={username}
token={token} token={token}
rid={item.rid}
type={item.t}
baseUrl={server} baseUrl={server}
prid={item.prid}
showLastMessage={StoreLastMessage} showLastMessage={StoreLastMessage}
onPress={() => this.onPressItem(item)} onPress={this.onPressItem}
testID={`rooms-list-view-item-${ item.name }`} testID={`rooms-list-view-item-${ item.name }`}
width={isMasterDetail ? MAX_SIDEBAR_WIDTH : width} width={isMasterDetail ? MAX_SIDEBAR_WIDTH : width}
toggleFav={this.toggleFav} toggleFav={this.toggleFav}
@ -833,7 +835,10 @@ class RoomsListView extends React.Component {
hideChannel={this.hideChannel} hideChannel={this.hideChannel}
useRealName={useRealName} useRealName={useRealName}
getUserPresence={this.getUserPresence} getUserPresence={this.getUserPresence}
isGroupChat={isGroupChat} getRoomTitle={this.getRoomTitle}
getRoomAvatar={this.getRoomAvatar}
getIsGroupChat={this.isGroupChat}
getIsRead={this.isRead}
visitor={item.visitor} visitor={item.visitor}
isFocused={currentItem?.rid === item.rid} isFocused={currentItem?.rid === item.rid}
/> />
@ -880,6 +885,8 @@ class RoomsListView extends React.Component {
/> />
)} )}
windowSize={9} windowSize={9}
onEndReached={this.onEndReached}
onEndReachedThreshold={0.5}
/> />
); );
}; };

View File

@ -7,7 +7,7 @@ import ShareExtension from 'rn-extensions-share';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as mime from 'react-native-mime-types'; import * as mime from 'react-native-mime-types';
import { isEqual, orderBy } from 'lodash'; import isEqual from 'react-fast-compare';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database'; import database from '../../lib/database';
@ -32,7 +32,6 @@ const permission = {
message: I18n.t('Read_External_Permission_Message') message: I18n.t('Read_External_Permission_Message')
}; };
const LIMIT = 50;
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
const keyExtractor = item => item.rid; const keyExtractor = item => item.rid;
@ -47,7 +46,7 @@ class ShareListView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.data = []; this.chats = [];
this.state = { this.state = {
searching: false, searching: false,
searchText: '', searchText: '',
@ -186,22 +185,36 @@ class ShareListView extends React.Component {
this.setState(...args); this.setState(...args);
} }
getSubscriptions = async(server) => { query = (text) => {
const db = database.active; const db = database.active;
const defaultWhereClause = [
Q.where('archived', false),
Q.where('open', true),
Q.experimentalSkip(0),
Q.experimentalTake(50),
Q.experimentalSortBy('room_updated_at', Q.desc)
];
if (text) {
return db.collections
.get('subscriptions')
.query(
...defaultWhereClause,
Q.or(
Q.where('name', Q.like(`%${ Q.sanitizeLikeString(text) }%`)),
Q.where('fname', Q.like(`%${ Q.sanitizeLikeString(text) }%`))
)
).fetch();
}
return db.collections.get('subscriptions').query(...defaultWhereClause).fetch();
}
getSubscriptions = async(server) => {
const serversDB = database.servers; const serversDB = database.servers;
if (server) { if (server) {
this.data = await db.collections this.chats = await this.query();
.get('subscriptions')
.query(
Q.where('archived', false),
Q.where('open', true)
).fetch();
this.data = orderBy(this.data, ['roomUpdatedAt'], ['desc']);
const serversCollection = serversDB.collections.get('servers'); const serversCollection = serversDB.collections.get('servers');
this.servers = await serversCollection.query().fetch(); this.servers = await serversCollection.query().fetch();
this.chats = this.data.slice(0, LIMIT);
let serverInfo = {}; let serverInfo = {};
try { try {
serverInfo = await serversCollection.find(server); serverInfo = await serversCollection.find(server);
@ -210,8 +223,8 @@ class ShareListView extends React.Component {
} }
this.internalSetState({ this.internalSetState({
chats: this.chats ? this.chats.slice() : [], chats: this.chats ?? [],
servers: this.servers ? this.servers.slice() : [], servers: this.servers ?? [],
loading: false, loading: false,
serverInfo serverInfo
}); });
@ -253,10 +266,10 @@ class ShareListView extends React.Component {
}); });
} }
search = (text) => { search = async(text) => {
const result = this.data.filter(item => item.name.includes(text)) || []; const result = await this.query(text);
this.internalSetState({ this.internalSetState({
searchResults: result.slice(0, LIMIT), searchResults: result,
searchText: text searchText: text
}); });
} }
@ -297,9 +310,26 @@ class ShareListView extends React.Component {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { serverInfo } = this.state;
const { useRealName } = serverInfo;
const { const {
userId, token, server, theme userId, token, server, theme
} = this.props; } = this.props;
let description;
switch (item.t) {
case 'c':
description = item.topic || item.description;
break;
case 'p':
description = item.topic || item.description;
break;
case 'd':
description = useRealName ? item.name : item.fname;
break;
default:
description = item.fname;
break;
}
return ( return (
<DirectoryItem <DirectoryItem
user={{ user={{
@ -309,11 +339,7 @@ class ShareListView extends React.Component {
title={this.getRoomTitle(item)} title={this.getRoomTitle(item)}
baseUrl={server} baseUrl={server}
avatar={RocketChat.getRoomAvatar(item)} avatar={RocketChat.getRoomAvatar(item)}
description={ description={description}
item.t === 'c'
? (item.topic || item.description)
: item.fname
}
type={item.prid ? 'discussion' : item.t} type={item.prid ? 'discussion' : item.t}
onPress={() => this.shareMessage(item)} onPress={() => this.shareMessage(item)}
testID={`share-extension-item-${ item.name }`} testID={`share-extension-item-${ item.name }`}

View File

@ -24,7 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@codler/react-native-keyboard-aware-scroll-view": "^1.0.1", "@codler/react-native-keyboard-aware-scroll-view": "^1.0.1",
"@nozbe/watermelondb": "0.16.2", "@nozbe/watermelondb": "^0.18.0",
"@react-native-community/art": "^1.2.0", "@react-native-community/art": "^1.2.0",
"@react-native-community/async-storage": "1.11.0", "@react-native-community/async-storage": "1.11.0",
"@react-native-community/cameraroll": "4.0.0", "@react-native-community/cameraroll": "4.0.0",
@ -61,6 +61,7 @@
"pretty-bytes": "^5.3.0", "pretty-bytes": "^5.3.0",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react": "16.13.1", "react": "16.13.1",
"react-fast-compare": "^3.2.0",
"react-native": "^0.63.1", "react-native": "^0.63.1",
"react-native-animatable": "^1.3.3", "react-native-animatable": "^1.3.3",
"react-native-appearance": "0.3.4", "react-native-appearance": "0.3.4",

View File

@ -1,20 +0,0 @@
diff --git a/node_modules/@nozbe/watermelondb/native/ios/WatermelonDB.xcodeproj/project.pbxproj b/node_modules/@nozbe/watermelondb/native/ios/WatermelonDB.xcodeproj/project.pbxproj
index 63e6ef9..45c092b 100644
--- a/node_modules/@nozbe/watermelondb/native/ios/WatermelonDB.xcodeproj/project.pbxproj
+++ b/node_modules/@nozbe/watermelondb/native/ios/WatermelonDB.xcodeproj/project.pbxproj
@@ -374,6 +374,7 @@
"$(SRCROOT)/../../../../node_modules/react-native/Libraries/**",
"$(SRCROOT)/../../../../../ios/Pods/Headers/Public/React-Core/**",
"$(SRCROOT)/../../../../../../native/ios/Pods/Headers/Public/React-Core",
+ "$(SRCROOT)/../../../../../ios/Pods/Headers/Public/React-jsi/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -426,6 +427,7 @@
"$(SRCROOT)/../../../../node_modules/react-native/Libraries/**",
"$(SRCROOT)/../../../../../ios/Pods/Headers/Public/React-Core/**",
"$(SRCROOT)/../../../../../../native/ios/Pods/Headers/Public/React-Core",
+ "$(SRCROOT)/../../../../../ios/Pods/Headers/Public/React-jsi/**",
);
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = (

View File

@ -1,10 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; // import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux'; // import { createStore, combineReducers } from 'redux';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import RoomItem from './RoomItem'; // import RoomItem from './RoomItem';
import Message from './Message'; import Message from './Message';
import UiKitMessage from './UiKitMessage'; import UiKitMessage from './UiKitMessage';
import UiKitModal from './UiKitModal'; import UiKitModal from './UiKitModal';
@ -24,17 +24,17 @@ const user = {
// Change here to see themed storybook // Change here to see themed storybook
const theme = 'light'; const theme = 'light';
const reducers = combineReducers({ // const reducers = combineReducers({
settings: () => ({}), // settings: () => ({}),
login: () => ({ // login: () => ({
user: { // user: {
username: 'diego.mello' // username: 'diego.mello'
} // }
}), // }),
meteor: () => ({ connected: true }), // meteor: () => ({ connected: true }),
activeUsers: () => ({ abc: { status: 'online', statusText: 'dog' } }) // activeUsers: () => ({ abc: { status: 'online', statusText: 'dog' } })
}); // });
const store = createStore(reducers); // const store = createStore(reducers);
const messageDecorator = story => ( const messageDecorator = story => (
<MessageContext.Provider <MessageContext.Provider
@ -55,9 +55,9 @@ const messageDecorator = story => (
</MessageContext.Provider> </MessageContext.Provider>
); );
storiesOf('RoomItem', module) // storiesOf('RoomItem', module)
.addDecorator(story => <Provider store={store}>{story()}</Provider>) // .addDecorator(story => <Provider store={store}>{story()}</Provider>)
.add('list roomitem', () => <RoomItem theme={theme} />); // .add('list roomitem', () => <RoomItem theme={theme} />);
storiesOf('Message', module) storiesOf('Message', module)
.addDecorator(messageDecorator) .addDecorator(messageDecorator)
.add('list message', () => <Message theme={theme} />); .add('list message', () => <Message theme={theme} />);

View File

@ -1917,13 +1917,19 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@nozbe/watermelondb@0.16.2": "@nozbe/sqlite@3.31.1":
version "0.16.2" version "3.31.1"
resolved "https://registry.yarnpkg.com/@nozbe/watermelondb/-/watermelondb-0.16.2.tgz#9c29c426bedb3d3af5d38304344bb49d60ddd1af" resolved "https://registry.yarnpkg.com/@nozbe/sqlite/-/sqlite-3.31.1.tgz#ffd394ad7c188c6b73f89fd6e1ccb849a1b96dba"
integrity sha512-LLOEl13bVCiLsZf8QCHt5KUmuks7QnMtsXuhBB2rSb1vBDjc3mG2HmkH/eTzbgJnGIdFzH/XtoVUHlS/ll7Log== integrity sha512-z5+GdcHZl9OQ1g0pnygORAnwCYUlYw/gQxdW/8rS0HxD2Gnn/k3DBQOvqQIH4Z3Z3KWVMbGUYhcH1v4SqTAdwg==
"@nozbe/watermelondb@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@nozbe/watermelondb/-/watermelondb-0.18.0.tgz#f174d8a4b9e8665bb9e8a7825a682905a43cd92c"
integrity sha512-jybGScFR+b0wp8QDNJw9cfd7Qfu51SegivjIniX6dXV/KZxjjhQ+IJxUB6bp2f2tWJIQcxC5e9syctLsrh/PiQ==
dependencies: dependencies:
"@nozbe/sqlite" "3.31.1"
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
lokijs "git+https://github.com/Nozbe/LokiJS.git#d08f660" lokijs "git+https://github.com/Nozbe/LokiJS.git#fabe503"
rambdax "2.15.0" rambdax "2.15.0"
rxjs "^6.2.2" rxjs "^6.2.2"
rxjs-compat "^6.3.2" rxjs-compat "^6.3.2"
@ -10206,9 +10212,9 @@ logkitty@^0.7.1:
dayjs "^1.8.15" dayjs "^1.8.15"
yargs "^15.1.0" yargs "^15.1.0"
"lokijs@git+https://github.com/Nozbe/LokiJS.git#d08f660": "lokijs@git+https://github.com/Nozbe/LokiJS.git#fabe503":
version "1.5.7" version "1.5.9"
resolved "git+https://github.com/Nozbe/LokiJS.git#d08f660794be558529f5ba049fe2868d4f243ec4" resolved "git+https://github.com/Nozbe/LokiJS.git#fabe503e4fc7887b76b0e4892b28fbd0753da27d"
lolex@^5.0.0: lolex@^5.0.0:
version "5.1.2" version "5.1.2"
@ -12573,6 +12579,11 @@ react-fast-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.1.1.tgz#0becf31e3812fa70dc231e259f40d892d4767900" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.1.1.tgz#0becf31e3812fa70dc231e259f40d892d4767900"
integrity sha512-SCsAORWK59BvauR2L1BTdjQbJcSGJJz03U0awektk2hshLKrITDDFTlgGCqIZpTDlPC/NFlZee6xTMzXPVLiHw== integrity sha512-SCsAORWK59BvauR2L1BTdjQbJcSGJJz03U0awektk2hshLKrITDDFTlgGCqIZpTDlPC/NFlZee6xTMzXPVLiHw==
react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-focus-lock@^2.1.0: react-focus-lock@^2.1.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"