[NEW] Jump to message (#3099)
* Scrolling * Add loadMore button at the end of loadMessagesForRoom * Delete dummy item on tap * Only insert loadMore dummy if there's more data * load surrounding messages * fixes and load next * First dummy and dummy-next * Save load next messages * Check if message exists before fetching surroundings * Refactoring List * Jumping to message :) * Showing blocking loader while scrolling/fetching message * Check if message exists on local db before inserting dummy * Delete dummies automatically when the message sent to updateMessages again * Minor cleanup * Fix scroll * Highlight message * Jump to bottom * Load more on scroll * Adding stories to LoadMore * Refactoring * Add loading indicator to LoadMore * Small refactor * Add LoadMore to threads * getMoreMessages * chat.getThreadMessages -> getThreadMessages * Start jumping to threads * Add jumpToMessageId on RoomView * Nav to correct channel * Fix PK issue on thread_messages * Disable jump to thread from another room * Fix nav to thread params * Add navToRoom * Refactor styles * Test notch * Fix Android border * Fix thread message on title * Fix NavBottomFAB on threads * Minor cleanup * Workaround for readThreads being called too often * Lint * Update tests * Jump from search * Go to threads from search * Remove getItemLayout and rely on viewable items * Fix load older * stash working * Fix infinite loading * Lower itemVisiblePercentThreshhold to 10, so very long messages behave as viewable * Add generateLoadMoreId util * Minor cleanup * Jump to message from notification/deep linking * Add getMessageInfo * Nav to threads from other rooms * getThreadName * Unnecessary logic * getRoomInfo * Colocate getMessageInfo closer to RoomView * Minor cleanup * Remove search from RoomActionsView * Minor fix for search on not joined public channels * Jump to any link * Fix tablets * Jump to message from MessagesView and other bug fixes * Fix issue on Urls * Adds race condition to cancel jump to message if it's stuck or after 5 seconds * Jump from message search quote * lint * Stop onPress * Small refactor on load methods * Minor fixes for loadThreadMessages * Minor typo * LoadMore i18n * Minor cleanup
This commit is contained in:
parent
62336c6d3a
commit
3ef4ef5317
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
export const MESSAGE_TYPE_LOAD_MORE = 'load_more';
|
||||||
|
export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk';
|
||||||
|
export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk';
|
||||||
|
|
||||||
|
export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK];
|
|
@ -4,19 +4,18 @@ import { Text, Clipboard } from 'react-native';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import openLink from '../../utils/openLink';
|
|
||||||
import { LISTENER } from '../Toast';
|
import { LISTENER } from '../Toast';
|
||||||
import EventEmitter from '../../utils/events';
|
import EventEmitter from '../../utils/events';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
|
|
||||||
const Link = React.memo(({
|
const Link = React.memo(({
|
||||||
children, link, theme
|
children, link, theme, onLinkPress
|
||||||
}) => {
|
}) => {
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openLink(link, theme);
|
onLinkPress(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
const childLength = React.Children.toArray(children).filter(o => o).length;
|
const childLength = React.Children.toArray(children).filter(o => o).length;
|
||||||
|
@ -40,7 +39,8 @@ const Link = React.memo(({
|
||||||
Link.propTypes = {
|
Link.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
link: PropTypes.string,
|
link: PropTypes.string,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
onLinkPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
|
|
|
@ -82,7 +82,8 @@ class Markdown extends PureComponent {
|
||||||
preview: PropTypes.bool,
|
preview: PropTypes.bool,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
testID: PropTypes.string,
|
testID: PropTypes.string,
|
||||||
style: PropTypes.array
|
style: PropTypes.array,
|
||||||
|
onLinkPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -218,11 +219,12 @@ class Markdown extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLink = ({ children, href }) => {
|
renderLink = ({ children, href }) => {
|
||||||
const { theme } = this.props;
|
const { theme, onLinkPress } = this.props;
|
||||||
return (
|
return (
|
||||||
<MarkdownLink
|
<MarkdownLink
|
||||||
link={href}
|
link={href}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onLinkPress={onLinkPress}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MarkdownLink>
|
</MarkdownLink>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const Content = React.memo((props) => {
|
||||||
} else if (props.isEncrypted) {
|
} else if (props.isEncrypted) {
|
||||||
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>;
|
content = <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>;
|
||||||
} else {
|
} else {
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
|
||||||
content = (
|
content = (
|
||||||
<Markdown
|
<Markdown
|
||||||
msg={props.msg}
|
msg={props.msg}
|
||||||
|
@ -61,6 +61,7 @@ const Content = React.memo((props) => {
|
||||||
tmid={props.tmid}
|
tmid={props.tmid}
|
||||||
useRealName={props.useRealName}
|
useRealName={props.useRealName}
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
onLinkPress={onLinkPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import Discussion from './Discussion';
|
||||||
import Content from './Content';
|
import Content from './Content';
|
||||||
import ReadReceipt from './ReadReceipt';
|
import ReadReceipt from './ReadReceipt';
|
||||||
import CallButton from './CallButton';
|
import CallButton from './CallButton';
|
||||||
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
const MessageInner = React.memo((props) => {
|
const MessageInner = React.memo((props) => {
|
||||||
if (props.type === 'discussion-created') {
|
if (props.type === 'discussion-created') {
|
||||||
|
@ -120,6 +121,7 @@ const MessageTouchable = React.memo((props) => {
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
|
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
|
||||||
|
style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Message {...props} />
|
<Message {...props} />
|
||||||
|
@ -134,7 +136,9 @@ MessageTouchable.propTypes = {
|
||||||
isInfo: PropTypes.bool,
|
isInfo: PropTypes.bool,
|
||||||
isThreadReply: PropTypes.bool,
|
isThreadReply: PropTypes.bool,
|
||||||
isTemp: PropTypes.bool,
|
isTemp: PropTypes.bool,
|
||||||
archived: PropTypes.bool
|
archived: PropTypes.bool,
|
||||||
|
highlighted: PropTypes.bool,
|
||||||
|
theme: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
Message.propTypes = {
|
Message.propTypes = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
@ -8,24 +8,29 @@ import { themes } from '../../constants/colors';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
|
||||||
const RepliedThread = React.memo(({
|
const RepliedThread = memo(({
|
||||||
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
|
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
|
||||||
}) => {
|
}) => {
|
||||||
if (!tmid || !isHeader) {
|
if (!tmid || !isHeader) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tmsg) {
|
const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
|
||||||
fetchThreadName(tmid, id);
|
const fetch = async() => {
|
||||||
|
const threadName = await fetchThreadName(tmid, id);
|
||||||
|
setMsg(threadName);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!msg) {
|
||||||
|
fetch();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = tmsg;
|
|
||||||
|
|
||||||
if (isEncrypted) {
|
|
||||||
msg = I18n.t('Encrypted_message');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||||
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
|
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
|
||||||
|
@ -45,23 +50,6 @@ const RepliedThread = React.memo(({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
|
||||||
if (prevProps.tmid !== nextProps.tmid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.tmsg !== nextProps.tmsg) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.isEncrypted !== nextProps.isEncrypted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.isHeader !== nextProps.isHeader) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (prevProps.theme !== nextProps.theme) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
RepliedThread.propTypes = {
|
RepliedThread.propTypes = {
|
||||||
|
|
|
@ -142,10 +142,13 @@ const Reply = React.memo(({
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
let url = attachment.title_link || attachment.author_link;
|
let url = attachment.title_link || attachment.author_link;
|
||||||
|
if (attachment.message_link) {
|
||||||
|
return jumpToMessage(attachment.message_link);
|
||||||
|
}
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
|
||||||
});
|
});
|
||||||
|
|
||||||
const Url = React.memo(({ url, index, theme }) => {
|
const Url = React.memo(({ url, index, theme }) => {
|
||||||
if (!url) {
|
if (!url || url?.ignoreParse) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
|
||||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
import messagesStatus from '../../constants/messagesStatus';
|
import messagesStatus from '../../constants/messagesStatus';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
|
import openLink from '../../utils/openLink';
|
||||||
|
|
||||||
class MessageContainer extends React.Component {
|
class MessageContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -33,6 +34,7 @@ class MessageContainer extends React.Component {
|
||||||
autoTranslateLanguage: PropTypes.string,
|
autoTranslateLanguage: PropTypes.string,
|
||||||
status: PropTypes.number,
|
status: PropTypes.number,
|
||||||
isIgnored: PropTypes.bool,
|
isIgnored: PropTypes.bool,
|
||||||
|
highlighted: PropTypes.bool,
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func,
|
||||||
onLongPress: PropTypes.func,
|
onLongPress: PropTypes.func,
|
||||||
onReactionPress: PropTypes.func,
|
onReactionPress: PropTypes.func,
|
||||||
|
@ -50,7 +52,9 @@ class MessageContainer extends React.Component {
|
||||||
blockAction: PropTypes.func,
|
blockAction: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
threadBadgeColor: PropTypes.string,
|
threadBadgeColor: PropTypes.string,
|
||||||
toggleFollowThread: PropTypes.func
|
toggleFollowThread: PropTypes.func,
|
||||||
|
jumpToMessage: PropTypes.func,
|
||||||
|
onPress: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -89,10 +93,15 @@ class MessageContainer extends React.Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { isManualUnignored } = this.state;
|
const { isManualUnignored } = this.state;
|
||||||
const { theme, threadBadgeColor, isIgnored } = this.props;
|
const {
|
||||||
|
theme, threadBadgeColor, isIgnored, highlighted
|
||||||
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextProps.highlighted !== highlighted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (nextProps.threadBadgeColor !== threadBadgeColor) {
|
if (nextProps.threadBadgeColor !== threadBadgeColor) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -112,10 +121,15 @@ class MessageContainer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPress = debounce(() => {
|
onPress = debounce(() => {
|
||||||
|
const { onPress } = this.props;
|
||||||
if (this.isIgnored) {
|
if (this.isIgnored) {
|
||||||
return this.onIgnoredMessagePress();
|
return this.onIgnoredMessagePress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onPress) {
|
||||||
|
return onPress();
|
||||||
|
}
|
||||||
|
|
||||||
const { item, isThreadRoom } = this.props;
|
const { item, isThreadRoom } = this.props;
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
|
|
||||||
|
@ -265,12 +279,69 @@ class MessageContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLinkPress = (link) => {
|
||||||
|
const { item, theme, jumpToMessage } = this.props;
|
||||||
|
const isMessageLink = item?.attachments?.findIndex(att => att?.message_link === link) !== -1;
|
||||||
|
if (isMessageLink) {
|
||||||
|
return jumpToMessage(link);
|
||||||
|
}
|
||||||
|
openLink(link, theme);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, threadBadgeColor, toggleFollowThread
|
item,
|
||||||
|
user,
|
||||||
|
style,
|
||||||
|
archived,
|
||||||
|
baseUrl,
|
||||||
|
useRealName,
|
||||||
|
broadcast,
|
||||||
|
fetchThreadName,
|
||||||
|
showAttachment,
|
||||||
|
timeFormat,
|
||||||
|
isReadReceiptEnabled,
|
||||||
|
autoTranslateRoom,
|
||||||
|
autoTranslateLanguage,
|
||||||
|
navToRoomInfo,
|
||||||
|
getCustomEmoji,
|
||||||
|
isThreadRoom,
|
||||||
|
callJitsi,
|
||||||
|
blockAction,
|
||||||
|
rid,
|
||||||
|
theme,
|
||||||
|
threadBadgeColor,
|
||||||
|
toggleFollowThread,
|
||||||
|
jumpToMessage,
|
||||||
|
highlighted
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies
|
id,
|
||||||
|
msg,
|
||||||
|
ts,
|
||||||
|
attachments,
|
||||||
|
urls,
|
||||||
|
reactions,
|
||||||
|
t,
|
||||||
|
avatar,
|
||||||
|
emoji,
|
||||||
|
u,
|
||||||
|
alias,
|
||||||
|
editedBy,
|
||||||
|
role,
|
||||||
|
drid,
|
||||||
|
dcount,
|
||||||
|
dlm,
|
||||||
|
tmid,
|
||||||
|
tcount,
|
||||||
|
tlm,
|
||||||
|
tmsg,
|
||||||
|
mentions,
|
||||||
|
channels,
|
||||||
|
unread,
|
||||||
|
blocks,
|
||||||
|
autoTranslate: autoTranslateMessage,
|
||||||
|
replies
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
let message = msg;
|
let message = msg;
|
||||||
|
@ -294,6 +365,8 @@ class MessageContainer extends React.Component {
|
||||||
onEncryptedPress: this.onEncryptedPress,
|
onEncryptedPress: this.onEncryptedPress,
|
||||||
onDiscussionPress: this.onDiscussionPress,
|
onDiscussionPress: this.onDiscussionPress,
|
||||||
onReactionLongPress: this.onReactionLongPress,
|
onReactionLongPress: this.onReactionLongPress,
|
||||||
|
onLinkPress: this.onLinkPress,
|
||||||
|
jumpToMessage,
|
||||||
threadBadgeColor,
|
threadBadgeColor,
|
||||||
toggleFollowThread,
|
toggleFollowThread,
|
||||||
replies
|
replies
|
||||||
|
@ -347,6 +420,7 @@ class MessageContainer extends React.Component {
|
||||||
callJitsi={callJitsi}
|
callJitsi={callJitsi}
|
||||||
blockAction={blockAction}
|
blockAction={blockAction}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
highlighted={highlighted}
|
||||||
/>
|
/>
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -735,5 +735,8 @@
|
||||||
"Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
|
"Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
|
||||||
"last-owner-can-not-be-removed": "Last owner cannot be removed",
|
"last-owner-can-not-be-removed": "Last owner cannot be removed",
|
||||||
"leaving_team": "leaving team",
|
"leaving_team": "leaving team",
|
||||||
"member-does-not-exist": "Member does not exist"
|
"member-does-not-exist": "Member does not exist",
|
||||||
|
"Load_More": "Load More",
|
||||||
|
"Load_Newer": "Load Newer",
|
||||||
|
"Load_Older": "Load Older"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'messages';
|
||||||
|
|
||||||
export default class Message extends Model {
|
export default class Message extends Model {
|
||||||
static table = 'messages';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'rid' }
|
subscriptions: { type: 'belongs_to', key: 'rid' }
|
||||||
|
|
|
@ -4,8 +4,10 @@ import {
|
||||||
} from '@nozbe/watermelondb/decorators';
|
} from '@nozbe/watermelondb/decorators';
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'subscriptions';
|
||||||
|
|
||||||
export default class Subscription extends Model {
|
export default class Subscription extends Model {
|
||||||
static table = 'subscriptions';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
messages: { type: 'has_many', foreignKey: 'rid' },
|
messages: { type: 'has_many', foreignKey: 'rid' },
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'threads';
|
||||||
|
|
||||||
export default class Thread extends Model {
|
export default class Thread extends Model {
|
||||||
static table = 'threads';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'rid' }
|
subscriptions: { type: 'belongs_to', key: 'rid' }
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
|
|
||||||
import { sanitizer } from '../utils';
|
import { sanitizer } from '../utils';
|
||||||
|
|
||||||
|
export const TABLE_NAME = 'thread_messages';
|
||||||
|
|
||||||
export default class ThreadMessage extends Model {
|
export default class ThreadMessage extends Model {
|
||||||
static table = 'thread_messages';
|
static table = TABLE_NAME;
|
||||||
|
|
||||||
static associations = {
|
static associations = {
|
||||||
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
|
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Message';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getMessageById = async(messageId) => {
|
||||||
|
const db = database.active;
|
||||||
|
const messageCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await messageCollection.find(messageId);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Subscription';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getSubscriptionByRoomId = async(rid) => {
|
||||||
|
const db = database.active;
|
||||||
|
const subCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await subCollection.find(rid);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/Thread';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getThreadById = async(tmid) => {
|
||||||
|
const db = database.active;
|
||||||
|
const threadCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await threadCollection.find(tmid);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import database from '..';
|
||||||
|
import { TABLE_NAME } from '../model/ThreadMessage';
|
||||||
|
|
||||||
|
const getCollection = db => db.get(TABLE_NAME);
|
||||||
|
|
||||||
|
export const getThreadMessageById = async(messageId) => {
|
||||||
|
const db = database.active;
|
||||||
|
const threadMessageCollection = getCollection(db);
|
||||||
|
try {
|
||||||
|
const result = await threadMessageCollection.find(messageId);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { getSubscriptionByRoomId } from '../database/services/Subscription';
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
|
||||||
|
const getRoomInfo = async(rid) => {
|
||||||
|
let result;
|
||||||
|
result = await getSubscriptionByRoomId(rid);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
rid,
|
||||||
|
name: result.name,
|
||||||
|
fname: result.fname,
|
||||||
|
t: result.t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await RocketChat.getRoomInfo(rid);
|
||||||
|
if (result?.success) {
|
||||||
|
return {
|
||||||
|
rid,
|
||||||
|
name: result.room.name,
|
||||||
|
fname: result.room.fname,
|
||||||
|
t: result.room.t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getRoomInfo;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import RocketChat from '../rocketchat';
|
||||||
|
|
||||||
|
const getSingleMessage = messageId => new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const result = await RocketChat.getSingleMessage(messageId);
|
||||||
|
if (result.success) {
|
||||||
|
return resolve(result.message);
|
||||||
|
}
|
||||||
|
return reject();
|
||||||
|
} catch (e) {
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getSingleMessage;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
|
||||||
|
import database from '../database';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { getThreadById } from '../database/services/Thread';
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import getSingleMessage from './getSingleMessage';
|
||||||
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
|
const buildThreadName = thread => thread.msg || thread?.attachments?.[0]?.title;
|
||||||
|
|
||||||
|
const getThreadName = async(rid, tmid, messageId) => {
|
||||||
|
let tmsg;
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const threadCollection = db.get('threads');
|
||||||
|
const messageRecord = await getMessageById(messageId);
|
||||||
|
const threadRecord = await getThreadById(tmid);
|
||||||
|
if (threadRecord) {
|
||||||
|
tmsg = buildThreadName(threadRecord);
|
||||||
|
await db.action(async() => {
|
||||||
|
await messageRecord?.update((m) => {
|
||||||
|
m.tmsg = tmsg;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let thread = await getSingleMessage(tmid);
|
||||||
|
thread = await Encryption.decryptMessage(thread);
|
||||||
|
tmsg = buildThreadName(thread);
|
||||||
|
await db.action(async() => {
|
||||||
|
await db.batch(
|
||||||
|
threadCollection?.prepareCreate((t) => {
|
||||||
|
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
|
||||||
|
t.subscription.id = rid;
|
||||||
|
Object.assign(t, thread);
|
||||||
|
}),
|
||||||
|
messageRecord?.prepareUpdate((m) => {
|
||||||
|
m.tmsg = tmsg;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
return tmsg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getThreadName;
|
|
@ -1,8 +1,15 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
import updateMessages from './updateMessages';
|
import updateMessages from './updateMessages';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
async function load({ rid: roomId, latest, t }) {
|
async function load({ rid: roomId, latest, t }) {
|
||||||
let params = { roomId, count: 50 };
|
let params = { roomId, count: COUNT };
|
||||||
if (latest) {
|
if (latest) {
|
||||||
params = { ...params, latest: new Date(latest).toISOString() };
|
params = { ...params, latest: new Date(latest).toISOString() };
|
||||||
}
|
}
|
||||||
|
@ -24,9 +31,20 @@ export default function loadMessagesForRoom(args) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const data = await load.call(this, args);
|
const data = await load.call(this, args);
|
||||||
|
if (data?.length) {
|
||||||
if (data && data.length) {
|
const lastMessage = data[data.length - 1];
|
||||||
await updateMessages({ rid: args.rid, update: data });
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord && data.length === COUNT) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
ts: moment(lastMessage.ts).subtract(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_MORE,
|
||||||
|
msg: lastMessage.msg
|
||||||
|
};
|
||||||
|
data.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem });
|
||||||
return resolve(data);
|
return resolve(data);
|
||||||
} else {
|
} else {
|
||||||
return resolve([]);
|
return resolve([]);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import moment from 'moment';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import updateMessages from './updateMessages';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
|
export default function loadNextMessages(args) {
|
||||||
|
return new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = await this.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT);
|
||||||
|
let messages = EJSON.fromJSONValue(data?.messages);
|
||||||
|
messages = orderBy(messages, 'ts');
|
||||||
|
if (messages?.length) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord && messages.length === COUNT) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
tmid: args.tmid,
|
||||||
|
ts: moment(lastMessage.ts).add(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_NEXT_CHUNK
|
||||||
|
};
|
||||||
|
messages.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem });
|
||||||
|
return resolve(messages);
|
||||||
|
} else {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
import moment from 'moment';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import log from '../../utils/log';
|
||||||
|
import updateMessages from './updateMessages';
|
||||||
|
import { getMessageById } from '../database/services/Message';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
|
const COUNT = 50;
|
||||||
|
|
||||||
|
export default function loadSurroundingMessages({ messageId, rid }) {
|
||||||
|
return new Promise(async(resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const data = await this.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT);
|
||||||
|
let messages = EJSON.fromJSONValue(data?.messages);
|
||||||
|
messages = orderBy(messages, 'ts');
|
||||||
|
|
||||||
|
const message = messages.find(m => m._id === messageId);
|
||||||
|
const { tmid } = message;
|
||||||
|
|
||||||
|
if (messages?.length) {
|
||||||
|
if (data?.moreBefore) {
|
||||||
|
const firstMessage = messages[0];
|
||||||
|
const firstMessageRecord = await getMessageById(firstMessage._id);
|
||||||
|
if (!firstMessageRecord) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(firstMessage._id),
|
||||||
|
rid: firstMessage.rid,
|
||||||
|
tmid,
|
||||||
|
ts: moment(firstMessage.ts).subtract(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK,
|
||||||
|
msg: firstMessage.msg
|
||||||
|
};
|
||||||
|
messages.unshift(loadMoreItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.moreAfter) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
const lastMessageRecord = await getMessageById(lastMessage._id);
|
||||||
|
if (!lastMessageRecord) {
|
||||||
|
const loadMoreItem = {
|
||||||
|
_id: generateLoadMoreId(lastMessage._id),
|
||||||
|
rid: lastMessage.rid,
|
||||||
|
tmid,
|
||||||
|
ts: moment(lastMessage.ts).add(1, 'millisecond'),
|
||||||
|
t: MESSAGE_TYPE_LOAD_NEXT_CHUNK,
|
||||||
|
msg: lastMessage.msg
|
||||||
|
};
|
||||||
|
messages.push(loadMoreItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await updateMessages({ rid, update: messages });
|
||||||
|
return resolve(messages);
|
||||||
|
} else {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
import EJSON from 'ejson';
|
||||||
|
|
||||||
import buildMessage from './helpers/buildMessage';
|
import buildMessage from './helpers/buildMessage';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
|
@ -7,30 +8,27 @@ import log from '../../utils/log';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
import { Encryption } from '../encryption';
|
import { Encryption } from '../encryption';
|
||||||
|
|
||||||
async function load({ tmid, offset }) {
|
async function load({ tmid }) {
|
||||||
try {
|
try {
|
||||||
// RC 1.0
|
// RC 1.0
|
||||||
const result = await this.sdk.get('chat.getThreadMessages', {
|
const result = await this.methodCallWrapper('getThreadMessages', { tmid });
|
||||||
tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
|
if (!result) {
|
||||||
});
|
|
||||||
if (!result || !result.success) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return result.messages;
|
return EJSON.fromJSONValue(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
|
export default function loadThreadMessages({ tmid, rid }) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let data = await load.call(this, { tmid, offset });
|
let data = await load.call(this, { tmid });
|
||||||
|
|
||||||
if (data && data.length) {
|
if (data && data.length) {
|
||||||
try {
|
try {
|
||||||
data = data.map(m => buildMessage(m));
|
data = data.filter(m => m.tmid).map(m => buildMessage(m));
|
||||||
data = await Encryption.decryptMessages(data);
|
data = await Encryption.decryptMessages(data);
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const threadMessagesCollection = db.get('thread_messages');
|
const threadMessagesCollection = db.get('thread_messages');
|
||||||
|
|
|
@ -6,8 +6,12 @@ import log from '../../utils/log';
|
||||||
import database from '../database';
|
import database from '../database';
|
||||||
import protectedFunction from './helpers/protectedFunction';
|
import protectedFunction from './helpers/protectedFunction';
|
||||||
import { Encryption } from '../encryption';
|
import { Encryption } from '../encryption';
|
||||||
|
import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad';
|
||||||
|
import { generateLoadMoreId } from '../utils';
|
||||||
|
|
||||||
export default function updateMessages({ rid, update = [], remove = [] }) {
|
export default function updateMessages({
|
||||||
|
rid, update = [], remove = [], loaderItem
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
if (!((update && update.length) || (remove && remove.length))) {
|
if (!((update && update.length) || (remove && remove.length))) {
|
||||||
return;
|
return;
|
||||||
|
@ -30,7 +34,13 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
const threadCollection = db.get('threads');
|
const threadCollection = db.get('threads');
|
||||||
const threadMessagesCollection = db.get('thread_messages');
|
const threadMessagesCollection = db.get('thread_messages');
|
||||||
const allMessagesRecords = await msgCollection
|
const allMessagesRecords = await msgCollection
|
||||||
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
.query(
|
||||||
|
Q.where('rid', rid),
|
||||||
|
Q.or(
|
||||||
|
Q.where('id', Q.oneOf(messagesIds)),
|
||||||
|
Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD))
|
||||||
|
)
|
||||||
|
)
|
||||||
.fetch();
|
.fetch();
|
||||||
const allThreadsRecords = await threadCollection
|
const allThreadsRecords = await threadCollection
|
||||||
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds)))
|
||||||
|
@ -55,6 +65,9 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
|
let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
|
||||||
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
|
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id));
|
||||||
|
|
||||||
|
// filter loaders to delete
|
||||||
|
let loadersToDelete = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === generateLoadMoreId(i2._id)));
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
|
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
|
||||||
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema);
|
||||||
|
@ -121,6 +134,12 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
|
threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete loaders
|
||||||
|
loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently());
|
||||||
|
if (loaderItem) {
|
||||||
|
loadersToDelete.push(loaderItem.prepareDestroyPermanently());
|
||||||
|
}
|
||||||
|
|
||||||
const allRecords = [
|
const allRecords = [
|
||||||
...msgsToCreate,
|
...msgsToCreate,
|
||||||
...msgsToUpdate,
|
...msgsToUpdate,
|
||||||
|
@ -130,7 +149,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
|
||||||
...threadsToDelete,
|
...threadsToDelete,
|
||||||
...threadMessagesToCreate,
|
...threadMessagesToCreate,
|
||||||
...threadMessagesToUpdate,
|
...threadMessagesToUpdate,
|
||||||
...threadMessagesToDelete
|
...threadMessagesToDelete,
|
||||||
|
...loadersToDelete
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { InteractionManager } from 'react-native';
|
import { InteractionManager } from 'react-native';
|
||||||
|
import EJSON from 'ejson';
|
||||||
import {
|
import {
|
||||||
Rocketchat as RocketchatClient,
|
Rocketchat as RocketchatClient,
|
||||||
settings as RocketChatSettings
|
settings as RocketChatSettings
|
||||||
|
@ -41,6 +42,8 @@ import canOpenRoom from './methods/canOpenRoom';
|
||||||
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
|
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
|
||||||
|
|
||||||
import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
import loadMessagesForRoom from './methods/loadMessagesForRoom';
|
||||||
|
import loadSurroundingMessages from './methods/loadSurroundingMessages';
|
||||||
|
import loadNextMessages from './methods/loadNextMessages';
|
||||||
import loadMissedMessages from './methods/loadMissedMessages';
|
import loadMissedMessages from './methods/loadMissedMessages';
|
||||||
import loadThreadMessages from './methods/loadThreadMessages';
|
import loadThreadMessages from './methods/loadThreadMessages';
|
||||||
|
|
||||||
|
@ -624,6 +627,8 @@ const RocketChat = {
|
||||||
},
|
},
|
||||||
loadMissedMessages,
|
loadMissedMessages,
|
||||||
loadMessagesForRoom,
|
loadMessagesForRoom,
|
||||||
|
loadSurroundingMessages,
|
||||||
|
loadNextMessages,
|
||||||
loadThreadMessages,
|
loadThreadMessages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getRooms,
|
getRooms,
|
||||||
|
@ -938,7 +943,7 @@ const RocketChat = {
|
||||||
methodCallWrapper(method, ...params) {
|
methodCallWrapper(method, ...params) {
|
||||||
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
|
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
|
||||||
if (API_Use_REST_For_DDP_Calls) {
|
if (API_Use_REST_For_DDP_Calls) {
|
||||||
return this.post(`method.call/${ method }`, { message: JSON.stringify({ method, params }) });
|
return this.post(`method.call/${ method }`, { message: EJSON.stringify({ method, params }) });
|
||||||
}
|
}
|
||||||
return this.methodCall(method, ...params);
|
return this.methodCall(method, ...params);
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,3 +20,5 @@ export const methods = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
|
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
|
||||||
|
|
||||||
|
export const generateLoadMoreId = id => `load-more-${ id }`;
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const onNotification = (notification) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
rid, name, sender, type, host, messageType
|
rid, name, sender, type, host, messageType, messageId
|
||||||
} = EJSON.parse(data.ejson);
|
} = EJSON.parse(data.ejson);
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
|
@ -24,6 +24,7 @@ export const onNotification = (notification) => {
|
||||||
const params = {
|
const params = {
|
||||||
host,
|
host,
|
||||||
rid,
|
rid,
|
||||||
|
messageId,
|
||||||
path: `${ types[type] }/${ roomName }`,
|
path: `${ types[type] }/${ roomName }`,
|
||||||
isCall: messageType === 'jitsi_call_started'
|
isCall: messageType === 'jitsi_call_started'
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,18 +60,19 @@ const navigate = function* navigate({ params }) {
|
||||||
|
|
||||||
const isMasterDetail = yield select(state => state.app.isMasterDetail);
|
const isMasterDetail = yield select(state => state.app.isMasterDetail);
|
||||||
const focusedRooms = yield select(state => state.room.rooms);
|
const focusedRooms = yield select(state => state.room.rooms);
|
||||||
|
const jumpToMessageId = params.messageId;
|
||||||
|
|
||||||
if (focusedRooms.includes(room.rid)) {
|
if (focusedRooms.includes(room.rid)) {
|
||||||
// if there's one room on the list or last room is the one
|
// if there's one room on the list or last room is the one
|
||||||
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
|
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
} else {
|
} else {
|
||||||
popToRoot({ isMasterDetail });
|
popToRoot({ isMasterDetail });
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
popToRoot({ isMasterDetail });
|
popToRoot({ isMasterDetail });
|
||||||
yield goRoom({ item, isMasterDetail });
|
yield goRoom({ item, isMasterDetail, jumpToMessageId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.isCall) {
|
if (params.isCall) {
|
||||||
|
|
|
@ -14,7 +14,6 @@ const navigate = ({ item, isMasterDetail, ...props }) => {
|
||||||
t: item.t,
|
t: item.t,
|
||||||
prid: item.prid,
|
prid: item.prid,
|
||||||
room: item,
|
room: item,
|
||||||
search: item.search,
|
|
||||||
visitor: item.visitor,
|
visitor: item.visitor,
|
||||||
roomUserId: RocketChat.getUidDirectMessage(item),
|
roomUserId: RocketChat.getUidDirectMessage(item),
|
||||||
...props
|
...props
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { withTheme } from '../../theme';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import { withActionSheet } from '../../containers/ActionSheet';
|
import { withActionSheet } from '../../containers/ActionSheet';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
|
||||||
class MessagesView extends React.Component {
|
class MessagesView extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -26,7 +27,8 @@ class MessagesView extends React.Component {
|
||||||
customEmojis: PropTypes.object,
|
customEmojis: PropTypes.object,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
showActionSheet: PropTypes.func,
|
showActionSheet: PropTypes.func,
|
||||||
useRealName: PropTypes.bool
|
useRealName: PropTypes.bool,
|
||||||
|
isMasterDetail: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -81,6 +83,32 @@ class MessagesView extends React.Component {
|
||||||
navigation.navigate('RoomInfoView', navParam);
|
navigation.navigate('RoomInfoView', navParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async({ item }) => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
let params = {
|
||||||
|
rid: this.rid,
|
||||||
|
jumpToMessageId: item._id,
|
||||||
|
t: this.t,
|
||||||
|
room: this.room
|
||||||
|
};
|
||||||
|
if (item.tmid) {
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('DrawerNavigator');
|
||||||
|
} else {
|
||||||
|
navigation.pop(2);
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
tmid: item.tmid,
|
||||||
|
name: await getThreadName(this.rid, item.tmid, item._id),
|
||||||
|
t: 'thread'
|
||||||
|
};
|
||||||
|
navigation.push('RoomView', params);
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomView', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defineMessagesViewContent = (name) => {
|
defineMessagesViewContent = (name) => {
|
||||||
const {
|
const {
|
||||||
user, baseUrl, theme, useRealName
|
user, baseUrl, theme, useRealName
|
||||||
|
@ -93,11 +121,13 @@ class MessagesView extends React.Component {
|
||||||
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
||||||
isEdited: !!item.editedAt,
|
isEdited: !!item.editedAt,
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
|
isThreadRoom: true,
|
||||||
attachments: item.attachments || [],
|
attachments: item.attachments || [],
|
||||||
useRealName,
|
useRealName,
|
||||||
showAttachment: this.showAttachment,
|
showAttachment: this.showAttachment,
|
||||||
getCustomEmoji: this.getCustomEmoji,
|
getCustomEmoji: this.getCustomEmoji,
|
||||||
navToRoomInfo: this.navToRoomInfo
|
navToRoomInfo: this.navToRoomInfo,
|
||||||
|
onPress: () => this.jumpToMessage({ item })
|
||||||
});
|
});
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
|
@ -315,7 +345,8 @@ const mapStateToProps = state => ({
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
customEmojis: state.customEmojis,
|
customEmojis: state.customEmojis,
|
||||||
useRealName: state.settings.UI_Use_Real_Name
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));
|
||||||
|
|
|
@ -636,7 +636,7 @@ class RoomActionsView extends React.Component {
|
||||||
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
rid, t, encrypted
|
rid, t
|
||||||
} = room;
|
} = room;
|
||||||
const isGroupChat = RocketChat.isGroupChat(room);
|
const isGroupChat = RocketChat.isGroupChat(room);
|
||||||
|
|
||||||
|
@ -761,24 +761,6 @@ class RoomActionsView extends React.Component {
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{['c', 'p', 'd'].includes(t)
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
<List.Item
|
|
||||||
title='Search'
|
|
||||||
onPress={() => this.onPressTouchable({
|
|
||||||
route: 'SearchMessagesView',
|
|
||||||
params: { rid, encrypted }
|
|
||||||
})}
|
|
||||||
testID='room-actions-search'
|
|
||||||
left={() => <List.Icon name='search' />}
|
|
||||||
showActionIndicator
|
|
||||||
/>
|
|
||||||
<List.Separator />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{['c', 'p', 'd'].includes(t)
|
{['c', 'p', 'd'].includes(t)
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FlatList, StyleSheet } from 'react-native';
|
||||||
|
import Animated from 'react-native-reanimated';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { isIOS } from '../../../utils/deviceInfo';
|
||||||
|
import scrollPersistTaps from '../../../utils/scrollPersistTaps';
|
||||||
|
|
||||||
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
list: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
paddingTop: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const List = ({ listRef, ...props }) => (
|
||||||
|
<AnimatedFlatList
|
||||||
|
testID='room-view-messages'
|
||||||
|
ref={listRef}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
style={styles.list}
|
||||||
|
inverted
|
||||||
|
removeClippedSubviews={isIOS}
|
||||||
|
initialNumToRender={7}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
maxToRenderPerBatch={5}
|
||||||
|
windowSize={10}
|
||||||
|
{...props}
|
||||||
|
{...scrollPersistTaps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
List.propTypes = {
|
||||||
|
listRef: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default List;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Animated, {
|
||||||
|
call, cond, greaterOrEq, useCode
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { CustomIcon } from '../../../lib/Icons';
|
||||||
|
import { useTheme } from '../../../theme';
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
import { hasNotch } from '../../../utils/deviceInfo';
|
||||||
|
|
||||||
|
const SCROLL_LIMIT = 200;
|
||||||
|
const SEND_TO_CHANNEL_HEIGHT = 40;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 15
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
borderRadius: 25
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 25,
|
||||||
|
borderWidth: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const NavBottomFAB = ({ y, onPress, isThread }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const handleOnPress = useCallback(() => onPress());
|
||||||
|
const toggle = v => setShow(v);
|
||||||
|
|
||||||
|
useCode(() => cond(greaterOrEq(y, SCROLL_LIMIT),
|
||||||
|
call([y], () => toggle(true)),
|
||||||
|
call([y], () => toggle(false))),
|
||||||
|
[y]);
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bottom = hasNotch ? 100 : 60;
|
||||||
|
if (isThread) {
|
||||||
|
bottom += SEND_TO_CHANNEL_HEIGHT;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Animated.View style={[styles.container, { bottom }]}>
|
||||||
|
<Touch
|
||||||
|
onPress={handleOnPress}
|
||||||
|
theme={theme}
|
||||||
|
style={[styles.button, { backgroundColor: themes[theme].backgroundColor }]}
|
||||||
|
>
|
||||||
|
<View style={[styles.content, { borderColor: themes[theme].borderColor }]}>
|
||||||
|
<CustomIcon name='chevron-down' color={themes[theme].auxiliaryTintColor} size={36} />
|
||||||
|
</View>
|
||||||
|
</Touch>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NavBottomFAB.propTypes = {
|
||||||
|
y: Animated.Value,
|
||||||
|
onPress: PropTypes.func,
|
||||||
|
isThread: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBottomFAB;
|
|
@ -1,30 +1,39 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FlatList, RefreshControl } from 'react-native';
|
import { RefreshControl } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { dequal } from 'dequal';
|
import { dequal } from 'dequal';
|
||||||
|
import { Value, event } from 'react-native-reanimated';
|
||||||
|
|
||||||
import styles from './styles';
|
import database from '../../../lib/database';
|
||||||
import database from '../../lib/database';
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
import log from '../../../utils/log';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import EmptyRoom from '../EmptyRoom';
|
||||||
import log from '../../utils/log';
|
import { animateNextTransition } from '../../../utils/layoutAnimation';
|
||||||
import EmptyRoom from './EmptyRoom';
|
import ActivityIndicator from '../../../containers/ActivityIndicator';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { themes } from '../../../constants/colors';
|
||||||
import { animateNextTransition } from '../../utils/layoutAnimation';
|
import List from './List';
|
||||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
import NavBottomFAB from './NavBottomFAB';
|
||||||
import { themes } from '../../constants/colors';
|
import debounce from '../../../utils/debounce';
|
||||||
|
|
||||||
const QUERY_SIZE = 50;
|
const QUERY_SIZE = 50;
|
||||||
|
|
||||||
class List extends React.Component {
|
const onScroll = ({ y }) => event(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
nativeEvent: {
|
||||||
|
contentOffset: { y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{ useNativeDriver: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
class ListContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onEndReached: PropTypes.func,
|
|
||||||
renderFooter: PropTypes.func,
|
|
||||||
renderRow: PropTypes.func,
|
renderRow: PropTypes.func,
|
||||||
rid: PropTypes.string,
|
rid: PropTypes.string,
|
||||||
t: PropTypes.string,
|
|
||||||
tmid: PropTypes.string,
|
tmid: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
|
@ -36,34 +45,28 @@ class List extends React.Component {
|
||||||
showMessageInMainThread: PropTypes.bool
|
showMessageInMainThread: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
// this.state.loading works for this.onEndReached and RoomView.init
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.loading !== state.loading) {
|
|
||||||
return {
|
|
||||||
loading: props.loading
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
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.count = 0;
|
||||||
this.needsFetch = false;
|
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.animated = false;
|
this.animated = false;
|
||||||
|
this.jumping = false;
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
|
||||||
end: false,
|
|
||||||
messages: [],
|
messages: [],
|
||||||
refreshing: false
|
refreshing: false,
|
||||||
|
highlightedMessage: null
|
||||||
};
|
};
|
||||||
|
this.y = new Value(0);
|
||||||
|
this.onScroll = onScroll({ y: this.y });
|
||||||
this.query();
|
this.query();
|
||||||
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
|
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
|
||||||
this.animated = true;
|
this.animated = true;
|
||||||
});
|
});
|
||||||
|
this.viewabilityConfig = {
|
||||||
|
itemVisiblePercentThreshold: 10
|
||||||
|
};
|
||||||
console.timeEnd(`${ this.constructor.name } init`);
|
console.timeEnd(`${ this.constructor.name } init`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,17 +76,17 @@ class List extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { loading, end, refreshing } = this.state;
|
const { refreshing, highlightedMessage } = this.state;
|
||||||
const {
|
const {
|
||||||
hideSystemMessages, theme, tunread, ignored
|
hideSystemMessages, theme, tunread, ignored, loading
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (theme !== nextProps.theme) {
|
if (theme !== nextProps.theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (loading !== nextState.loading) {
|
if (loading !== nextProps.loading) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (end !== nextState.end) {
|
if (highlightedMessage !== nextState.highlightedMessage) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (refreshing !== nextState.refreshing) {
|
if (refreshing !== nextState.refreshing) {
|
||||||
|
@ -116,32 +119,14 @@ class List extends React.Component {
|
||||||
if (this.unsubscribeFocus) {
|
if (this.unsubscribeFocus) {
|
||||||
this.unsubscribeFocus();
|
this.unsubscribeFocus();
|
||||||
}
|
}
|
||||||
|
this.clearHighlightedMessageTimeout();
|
||||||
console.countReset(`${ this.constructor.name }.render calls`);
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData = async() => {
|
clearHighlightedMessageTimeout = () => {
|
||||||
const {
|
if (this.highlightedMessageTimeout) {
|
||||||
loading, end, messages, latest = messages[messages.length - 1]?.ts
|
clearTimeout(this.highlightedMessageTimeout);
|
||||||
} = this.state;
|
this.highlightedMessageTimeout = false;
|
||||||
if (loading || end) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
const { rid, t, tmid } = this.props;
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
if (tmid) {
|
|
||||||
// `offset` is `messages.length - 1` because we append thread start to `messages` obj
|
|
||||||
result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
|
|
||||||
} else {
|
|
||||||
result = await RocketChat.loadMessagesForRoom({ rid, t, latest });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result));
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
log(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,9 +183,6 @@ class List extends React.Component {
|
||||||
this.unsubscribeMessages();
|
this.unsubscribeMessages();
|
||||||
this.messagesSubscription = this.messagesObservable
|
this.messagesSubscription = this.messagesObservable
|
||||||
.subscribe((messages) => {
|
.subscribe((messages) => {
|
||||||
if (messages.length <= this.count) {
|
|
||||||
this.needsFetch = true;
|
|
||||||
}
|
|
||||||
if (tmid && this.thread) {
|
if (tmid && this.thread) {
|
||||||
messages = [...messages, this.thread];
|
messages = [...messages, this.thread];
|
||||||
}
|
}
|
||||||
|
@ -211,6 +193,7 @@ class List extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.state.messages = messages;
|
this.state.messages = messages;
|
||||||
}
|
}
|
||||||
|
// TODO: move it away from here
|
||||||
this.readThreads();
|
this.readThreads();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -221,7 +204,7 @@ class List extends React.Component {
|
||||||
this.query();
|
this.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
readThreads = async() => {
|
readThreads = debounce(async() => {
|
||||||
const { tmid } = this.props;
|
const { tmid } = this.props;
|
||||||
|
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
|
@ -231,39 +214,9 @@ class List extends React.Component {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, 300)
|
||||||
|
|
||||||
onEndReached = async() => {
|
onEndReached = () => this.query()
|
||||||
if (this.needsFetch) {
|
|
||||||
this.needsFetch = false;
|
|
||||||
await this.fetchData();
|
|
||||||
}
|
|
||||||
this.query();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreMessages = (result) => {
|
|
||||||
const { end } = this.state;
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle servers with version < 3.0.0
|
|
||||||
let { hideSystemMessages = [] } = this.props;
|
|
||||||
if (!Array.isArray(hideSystemMessages)) {
|
|
||||||
hideSystemMessages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hideSystemMessages.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasReadableMessages = result.filter(message => !message.t || (message.t && !hideSystemMessages.includes(message.t))).length > 0;
|
|
||||||
// if this batch doesn't contain any messages that will be displayed, we'll request a new batch
|
|
||||||
if (!hasReadableMessages) {
|
|
||||||
this.onEndReached();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefresh = () => this.setState({ refreshing: true }, async() => {
|
onRefresh = () => this.setState({ refreshing: true }, async() => {
|
||||||
const { messages } = this.state;
|
const { messages } = this.state;
|
||||||
|
@ -272,7 +225,7 @@ class List extends React.Component {
|
||||||
if (messages.length) {
|
if (messages.length) {
|
||||||
try {
|
try {
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
|
await RocketChat.loadThreadMessages({ tmid, rid });
|
||||||
} else {
|
} else {
|
||||||
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
|
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
|
||||||
}
|
}
|
||||||
|
@ -284,7 +237,6 @@ class List extends React.Component {
|
||||||
this.setState({ refreshing: false });
|
this.setState({ refreshing: false });
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
|
||||||
update = () => {
|
update = () => {
|
||||||
if (this.animated) {
|
if (this.animated) {
|
||||||
animateNextTransition();
|
animateNextTransition();
|
||||||
|
@ -306,9 +258,53 @@ class List extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleScrollToIndexFailed = (params) => {
|
||||||
|
const { listRef } = this.props;
|
||||||
|
listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessage = messageId => new Promise(async(resolve) => {
|
||||||
|
this.jumping = true;
|
||||||
|
const { messages } = this.state;
|
||||||
|
const { listRef } = this.props;
|
||||||
|
const index = messages.findIndex(item => item.id === messageId);
|
||||||
|
if (index > -1) {
|
||||||
|
listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5 });
|
||||||
|
await new Promise(res => setTimeout(res, 300));
|
||||||
|
if (!this.viewableItems.map(vi => vi.key).includes(messageId)) {
|
||||||
|
if (!this.jumping) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ highlightedMessage: messageId });
|
||||||
|
this.clearHighlightedMessageTimeout();
|
||||||
|
this.highlightedMessageTimeout = setTimeout(() => {
|
||||||
|
this.setState({ highlightedMessage: null });
|
||||||
|
}, 10000);
|
||||||
|
await setTimeout(() => resolve(), 300);
|
||||||
|
} else {
|
||||||
|
listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false });
|
||||||
|
if (!this.jumping) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// this.jumping is checked in between operations to make sure we're not stuck
|
||||||
|
cancelJumpToMessage = () => {
|
||||||
|
this.jumping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToBottom = () => {
|
||||||
|
const { listRef } = this.props;
|
||||||
|
listRef.current.getNode().scrollToOffset({ offset: -100 });
|
||||||
|
}
|
||||||
|
|
||||||
renderFooter = () => {
|
renderFooter = () => {
|
||||||
const { loading } = this.state;
|
const { rid, theme, loading } = this.props;
|
||||||
const { rid, theme } = this.props;
|
|
||||||
if (loading && rid) {
|
if (loading && rid) {
|
||||||
return <ActivityIndicator theme={theme} />;
|
return <ActivityIndicator theme={theme} />;
|
||||||
}
|
}
|
||||||
|
@ -316,36 +312,34 @@ class List extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ item, index }) => {
|
renderItem = ({ item, index }) => {
|
||||||
const { messages } = this.state;
|
const { messages, highlightedMessage } = this.state;
|
||||||
const { renderRow } = this.props;
|
const { renderRow } = this.props;
|
||||||
return renderRow(item, messages[index + 1]);
|
return renderRow(item, messages[index + 1], highlightedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewableItemsChanged = ({ viewableItems }) => {
|
||||||
|
this.viewableItems = viewableItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const { rid, listRef } = this.props;
|
const { rid, tmid, listRef } = this.props;
|
||||||
const { messages, refreshing } = this.state;
|
const { messages, refreshing } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} />
|
<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} />
|
||||||
<FlatList
|
<List
|
||||||
testID='room-view-messages'
|
onScroll={this.onScroll}
|
||||||
ref={listRef}
|
scrollEventThrottle={16}
|
||||||
keyExtractor={item => item.id}
|
listRef={listRef}
|
||||||
data={messages}
|
data={messages}
|
||||||
extraData={this.state}
|
|
||||||
renderItem={this.renderItem}
|
renderItem={this.renderItem}
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
style={styles.list}
|
|
||||||
inverted
|
|
||||||
removeClippedSubviews={isIOS}
|
|
||||||
initialNumToRender={7}
|
|
||||||
onEndReached={this.onEndReached}
|
onEndReached={this.onEndReached}
|
||||||
onEndReachedThreshold={0.5}
|
|
||||||
maxToRenderPerBatch={5}
|
|
||||||
windowSize={10}
|
|
||||||
ListFooterComponent={this.renderFooter}
|
ListFooterComponent={this.renderFooter}
|
||||||
|
onScrollToIndexFailed={this.handleScrollToIndexFailed}
|
||||||
|
onViewableItemsChanged={this.onViewableItemsChanged}
|
||||||
|
viewabilityConfig={this.viewabilityConfig}
|
||||||
refreshControl={(
|
refreshControl={(
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
|
@ -353,11 +347,11 @@ class List extends React.Component {
|
||||||
tintColor={themes[theme].auxiliaryText}
|
tintColor={themes[theme].auxiliaryText}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{...scrollPersistTaps}
|
|
||||||
/>
|
/>
|
||||||
|
<NavBottomFAB y={this.y} onPress={this.jumpToBottom} isThread={!!tmid} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default List;
|
export default ListContainer;
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */
|
||||||
|
import React from 'react';
|
||||||
|
import { ScrollView } from 'react-native';
|
||||||
|
import { storiesOf } from '@storybook/react-native';
|
||||||
|
|
||||||
|
import LoadMore from './index';
|
||||||
|
import { longText } from '../../../../storybook/utils';
|
||||||
|
import { ThemeContext } from '../../../theme';
|
||||||
|
import {
|
||||||
|
Message, StoryProvider, MessageDecorator
|
||||||
|
} from '../../../../storybook/stories/Message';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
|
||||||
|
const stories = storiesOf('LoadMore', module);
|
||||||
|
|
||||||
|
// FIXME: for some reason, this promise never resolves on Storybook (it works on the app, so maybe the issue isn't on the component)
|
||||||
|
const load = () => new Promise(res => setTimeout(res, 1000));
|
||||||
|
|
||||||
|
stories.add('basic', () => (
|
||||||
|
<>
|
||||||
|
<LoadMore load={load} />
|
||||||
|
<LoadMore load={load} runOnRender />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} />
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
|
||||||
|
const ThemeStory = ({ theme }) => (
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={{ theme }}
|
||||||
|
>
|
||||||
|
<ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} />
|
||||||
|
<Message msg='Hey!' theme={theme} />
|
||||||
|
<Message msg={longText} theme={theme} isHeader={false} />
|
||||||
|
<Message msg='Older message' theme={theme} isHeader={false} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} />
|
||||||
|
<LoadMore load={load} type={MESSAGE_TYPE_LOAD_MORE} />
|
||||||
|
<Message msg={longText} theme={theme} />
|
||||||
|
<Message msg='This is the third message' isHeader={false} theme={theme} />
|
||||||
|
<Message msg='This is the second message' isHeader={false} theme={theme} />
|
||||||
|
<Message msg='This is the first message' theme={theme} />
|
||||||
|
</ScrollView>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('light theme', () => <ThemeStory theme='light' />);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('dark theme', () => <ThemeStory theme='dark' />);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.addDecorator(StoryProvider)
|
||||||
|
.addDecorator(MessageDecorator)
|
||||||
|
.add('black theme', () => <ThemeStory theme='black' />);
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
|
import { Text, StyleSheet, ActivityIndicator } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
import { useTheme } from '../../../theme';
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
import sharedStyles from '../../Styles';
|
||||||
|
import I18n from '../../../i18n';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoadMore = ({ load, type, runOnRender }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLoad = useCallback(async() => {
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
await load();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runOnRender) {
|
||||||
|
handleLoad();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let text = 'Load_More';
|
||||||
|
if (type === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
|
||||||
|
text = 'Load_Newer';
|
||||||
|
}
|
||||||
|
if (type === MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK) {
|
||||||
|
text = 'Load_Older';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Touch
|
||||||
|
onPress={handleLoad}
|
||||||
|
style={styles.button}
|
||||||
|
theme={theme}
|
||||||
|
enabled={!loading}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
loading
|
||||||
|
? <ActivityIndicator color={themes[theme].auxiliaryText} />
|
||||||
|
: <Text style={[styles.text, { color: themes[theme].titleText }]}>{I18n.t(text)}</Text>
|
||||||
|
}
|
||||||
|
</Touch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadMore.propTypes = {
|
||||||
|
load: PropTypes.func,
|
||||||
|
type: PropTypes.string,
|
||||||
|
runOnRender: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadMore;
|
|
@ -142,12 +142,12 @@ class RightButtonsContainer extends Component {
|
||||||
goSearchView = () => {
|
goSearchView = () => {
|
||||||
logEvent(events.ROOM_GO_SEARCH);
|
logEvent(events.ROOM_GO_SEARCH);
|
||||||
const {
|
const {
|
||||||
rid, navigation, isMasterDetail
|
rid, t, navigation, isMasterDetail
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (isMasterDetail) {
|
if (isMasterDetail) {
|
||||||
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
|
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate('SearchMessagesView', { rid });
|
navigation.navigate('SearchMessagesView', { rid, t });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text, View, InteractionManager } from 'react-native';
|
import { Text, View, InteractionManager } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import parse from 'url-parse';
|
||||||
|
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
@ -17,7 +17,6 @@ import {
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import { Encryption } from '../../lib/encryption';
|
|
||||||
import Message from '../../containers/message';
|
import Message from '../../containers/message';
|
||||||
import MessageActions from '../../containers/MessageActions';
|
import MessageActions from '../../containers/MessageActions';
|
||||||
import MessageErrorActions from '../../containers/MessageErrorActions';
|
import MessageErrorActions from '../../containers/MessageErrorActions';
|
||||||
|
@ -35,6 +34,7 @@ import RightButtons from './RightButtons';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import Separator from './Separator';
|
import Separator from './Separator';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
|
||||||
import debounce from '../../utils/debounce';
|
import debounce from '../../utils/debounce';
|
||||||
import ReactionsModal from '../../containers/ReactionsModal';
|
import ReactionsModal from '../../containers/ReactionsModal';
|
||||||
import { LISTENER } from '../../containers/Toast';
|
import { LISTENER } from '../../containers/Toast';
|
||||||
|
@ -64,6 +64,12 @@ import { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
|
||||||
|
|
||||||
import { takeInquiry } from '../../ee/omnichannel/lib';
|
import { takeInquiry } from '../../ee/omnichannel/lib';
|
||||||
|
import Loading from '../../containers/Loading';
|
||||||
|
import LoadMore from './LoadMore';
|
||||||
|
import RoomServices from './services';
|
||||||
|
import { goRoom } from '../../utils/goRoom';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
import getRoomInfo from '../../lib/methods/getRoomInfo';
|
||||||
|
|
||||||
const stateAttrsUpdate = [
|
const stateAttrsUpdate = [
|
||||||
'joined',
|
'joined',
|
||||||
|
@ -76,7 +82,8 @@ const stateAttrsUpdate = [
|
||||||
'replying',
|
'replying',
|
||||||
'reacting',
|
'reacting',
|
||||||
'readOnly',
|
'readOnly',
|
||||||
'member'
|
'member',
|
||||||
|
'showingBlockingLoader'
|
||||||
];
|
];
|
||||||
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
|
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
|
||||||
|
|
||||||
|
@ -117,11 +124,11 @@ class RoomView extends React.Component {
|
||||||
const selectedMessage = props.route.params?.message;
|
const selectedMessage = props.route.params?.message;
|
||||||
const name = props.route.params?.name;
|
const name = props.route.params?.name;
|
||||||
const fname = props.route.params?.fname;
|
const fname = props.route.params?.fname;
|
||||||
const search = props.route.params?.search;
|
|
||||||
const prid = props.route.params?.prid;
|
const prid = props.route.params?.prid;
|
||||||
const room = props.route.params?.room ?? {
|
const room = props.route.params?.room ?? {
|
||||||
rid: this.rid, t: this.t, name, fname, prid
|
rid: this.rid, t: this.t, name, fname, prid
|
||||||
};
|
};
|
||||||
|
this.jumpToMessageId = props.route.params?.jumpToMessageId;
|
||||||
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
||||||
this.state = {
|
this.state = {
|
||||||
joined: true,
|
joined: true,
|
||||||
|
@ -133,6 +140,7 @@ class RoomView extends React.Component {
|
||||||
selectedMessage: selectedMessage || {},
|
selectedMessage: selectedMessage || {},
|
||||||
canAutoTranslate: false,
|
canAutoTranslate: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
showingBlockingLoader: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
replying: !!selectedMessage,
|
replying: !!selectedMessage,
|
||||||
replyWithMention: false,
|
replyWithMention: false,
|
||||||
|
@ -151,13 +159,10 @@ class RoomView extends React.Component {
|
||||||
|
|
||||||
this.setReadOnly();
|
this.setReadOnly();
|
||||||
|
|
||||||
if (search) {
|
|
||||||
this.updateRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messagebox = React.createRef();
|
this.messagebox = React.createRef();
|
||||||
this.list = React.createRef();
|
this.list = React.createRef();
|
||||||
this.joinCode = React.createRef();
|
this.joinCode = React.createRef();
|
||||||
|
this.flatList = React.createRef();
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
|
||||||
// we don't need to subscribe to threads
|
// we don't need to subscribe to threads
|
||||||
|
@ -181,6 +186,9 @@ class RoomView extends React.Component {
|
||||||
EventEmitter.addEventListener('connected', this.handleConnected);
|
EventEmitter.addEventListener('connected', this.handleConnected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.jumpToMessageId) {
|
||||||
|
this.jumpToMessage(this.jumpToMessageId);
|
||||||
|
}
|
||||||
if (isIOS && this.rid) {
|
if (isIOS && this.rid) {
|
||||||
this.updateUnreadCount();
|
this.updateUnreadCount();
|
||||||
}
|
}
|
||||||
|
@ -195,7 +203,9 @@ class RoomView extends React.Component {
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { state } = this;
|
const { state } = this;
|
||||||
const { roomUpdate, member } = state;
|
const { roomUpdate, member } = state;
|
||||||
const { appState, theme, insets } = this.props;
|
const {
|
||||||
|
appState, theme, insets, route
|
||||||
|
} = this.props;
|
||||||
if (theme !== nextProps.theme) {
|
if (theme !== nextProps.theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -212,12 +222,19 @@ class RoomView extends React.Component {
|
||||||
if (!dequal(nextProps.insets, insets)) {
|
if (!dequal(nextProps.insets, insets)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!dequal(nextProps.route?.params, route?.params)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
|
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { roomUpdate } = this.state;
|
const { roomUpdate } = this.state;
|
||||||
const { appState, insets } = this.props;
|
const { appState, insets, route } = this.props;
|
||||||
|
|
||||||
|
if (route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) {
|
||||||
|
this.jumpToMessage(route?.params?.jumpToMessageId);
|
||||||
|
}
|
||||||
|
|
||||||
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
|
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
|
||||||
// Fire List.query() just to keep observables working
|
// Fire List.query() just to keep observables working
|
||||||
|
@ -417,34 +434,15 @@ class RoomView extends React.Component {
|
||||||
this.setState({ readOnly });
|
this.setState({ readOnly });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoom = async() => {
|
|
||||||
const db = database.active;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subCollection = db.get('subscriptions');
|
|
||||||
const sub = await subCollection.find(this.rid);
|
|
||||||
|
|
||||||
const { room } = await RocketChat.getRoomInfo(this.rid);
|
|
||||||
|
|
||||||
await db.action(async() => {
|
|
||||||
await sub.update((s) => {
|
|
||||||
Object.assign(s, room);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init = async() => {
|
init = async() => {
|
||||||
try {
|
try {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
const { room, joined } = this.state;
|
const { room, joined } = this.state;
|
||||||
if (this.tmid) {
|
if (this.tmid) {
|
||||||
await this.getThreadMessages();
|
await RoomServices.getThreadMessages(this.tmid, this.rid);
|
||||||
} else {
|
} else {
|
||||||
const newLastOpen = new Date();
|
const newLastOpen = new Date();
|
||||||
await this.getMessages(room);
|
await RoomServices.getMessages(room);
|
||||||
|
|
||||||
// if room is joined
|
// if room is joined
|
||||||
if (joined) {
|
if (joined) {
|
||||||
|
@ -453,7 +451,7 @@ class RoomView extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.setLastOpen(null);
|
this.setLastOpen(null);
|
||||||
}
|
}
|
||||||
RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
|
RoomServices.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,26 +658,69 @@ class RoomView extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onThreadPress = debounce(async(item) => {
|
onThreadPress = debounce(item => this.navToThread(item), 1000, true)
|
||||||
const { roomUserId } = this.state;
|
|
||||||
const { navigation } = this.props;
|
shouldNavigateToRoom = (message) => {
|
||||||
if (item.tmid) {
|
if (message.tmid && message.tmid === this.tmid) {
|
||||||
if (!item.tmsg) {
|
return false;
|
||||||
await this.fetchThreadName(item.tmid, item.id);
|
|
||||||
}
|
|
||||||
let name = item.tmsg;
|
|
||||||
if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
|
|
||||||
name = I18n.t('Encrypted_message');
|
|
||||||
}
|
|
||||||
navigation.push('RoomView', {
|
|
||||||
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId
|
|
||||||
});
|
|
||||||
} else if (item.tlm) {
|
|
||||||
navigation.push('RoomView', {
|
|
||||||
rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, 1000, true)
|
if (!message.tmid && message.rid === this.rid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessageByUrl = async(messageUrl) => {
|
||||||
|
if (!messageUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.setState({ showingBlockingLoader: true });
|
||||||
|
const parsedUrl = parse(messageUrl, true);
|
||||||
|
const messageId = parsedUrl.query.msg;
|
||||||
|
await this.jumpToMessage(messageId);
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async(messageId) => {
|
||||||
|
try {
|
||||||
|
this.setState({ showingBlockingLoader: true });
|
||||||
|
const message = await RoomServices.getMessageInfo(messageId);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.shouldNavigateToRoom(message)) {
|
||||||
|
if (message.rid !== this.rid) {
|
||||||
|
this.navToRoom(message);
|
||||||
|
} else {
|
||||||
|
this.navToThread(message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* if it's from server, we don't have it saved locally and so we fetch surroundings
|
||||||
|
* we test if it's not from threads because we're fetching from threads currently with `getThreadMessages`
|
||||||
|
*/
|
||||||
|
if (message.fromServer && !message.tmid) {
|
||||||
|
await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid });
|
||||||
|
}
|
||||||
|
await Promise.race([
|
||||||
|
this.list.current.jumpToMessage(message.id),
|
||||||
|
new Promise(res => setTimeout(res, 5000))
|
||||||
|
]);
|
||||||
|
this.list.current.cancelJumpToMessage();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
} finally {
|
||||||
|
this.setState({ showingBlockingLoader: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
replyBroadcast = (message) => {
|
replyBroadcast = (message) => {
|
||||||
const { replyBroadcast } = this.props;
|
const { replyBroadcast } = this.props;
|
||||||
|
@ -718,17 +759,6 @@ class RoomView extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getMessages = () => {
|
|
||||||
const { room } = this.state;
|
|
||||||
if (room.lastOpen) {
|
|
||||||
return RocketChat.loadMissedMessages(room);
|
|
||||||
} else {
|
|
||||||
return RocketChat.loadMessagesForRoom(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getThreadMessages = () => RocketChat.loadThreadMessages({ tmid: this.tmid, rid: this.rid })
|
|
||||||
|
|
||||||
getCustomEmoji = (name) => {
|
getCustomEmoji = (name) => {
|
||||||
const { customEmojis } = this.props;
|
const { customEmojis } = this.props;
|
||||||
const emoji = customEmojis[name];
|
const emoji = customEmojis[name];
|
||||||
|
@ -767,45 +797,7 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId)
|
||||||
fetchThreadName = async(tmid, messageId) => {
|
|
||||||
try {
|
|
||||||
const db = database.active;
|
|
||||||
const threadCollection = db.get('threads');
|
|
||||||
const messageCollection = db.get('messages');
|
|
||||||
const messageRecord = await messageCollection.find(messageId);
|
|
||||||
let threadRecord;
|
|
||||||
try {
|
|
||||||
threadRecord = await threadCollection.find(tmid);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Thread not found. We have to search for it.');
|
|
||||||
}
|
|
||||||
if (threadRecord) {
|
|
||||||
await db.action(async() => {
|
|
||||||
await messageRecord.update((m) => {
|
|
||||||
m.tmsg = threadRecord.msg || (threadRecord.attachments && threadRecord.attachments.length && threadRecord.attachments[0].title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let { message: thread } = await RocketChat.getSingleMessage(tmid);
|
|
||||||
thread = await Encryption.decryptMessage(thread);
|
|
||||||
await db.action(async() => {
|
|
||||||
await db.batch(
|
|
||||||
threadCollection.prepareCreate((t) => {
|
|
||||||
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
|
|
||||||
t.subscription.id = this.rid;
|
|
||||||
Object.assign(t, thread);
|
|
||||||
}),
|
|
||||||
messageRecord.prepareUpdate((m) => {
|
|
||||||
m.tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleFollowThread = async(isFollowingThread, tmid) => {
|
toggleFollowThread = async(isFollowingThread, tmid) => {
|
||||||
try {
|
try {
|
||||||
|
@ -836,6 +828,38 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navToThread = async(item) => {
|
||||||
|
const { roomUserId } = this.state;
|
||||||
|
const { navigation } = this.props;
|
||||||
|
|
||||||
|
if (item.tmid) {
|
||||||
|
let name = item.tmsg;
|
||||||
|
if (!name) {
|
||||||
|
name = await this.getThreadName(item.tmid, item.id);
|
||||||
|
}
|
||||||
|
if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
|
||||||
|
name = I18n.t('Encrypted_message');
|
||||||
|
}
|
||||||
|
return navigation.push('RoomView', {
|
||||||
|
rid: this.rid, tmid: item.tmid, name, t: 'thread', roomUserId, jumpToMessageId: item.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.tlm) {
|
||||||
|
return navigation.push('RoomView', {
|
||||||
|
rid: this.rid, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navToRoom = async(message) => {
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
const roomInfo = await getRoomInfo(message.rid);
|
||||||
|
return goRoom({
|
||||||
|
item: roomInfo, isMasterDetail, navigationMethod: navigation.push, jumpToMessageId: message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
callJitsi = () => {
|
callJitsi = () => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { jitsiTimeout } = room;
|
const { jitsiTimeout } = room;
|
||||||
|
@ -900,7 +924,11 @@ class RoomView extends React.Component {
|
||||||
return room?.ignored?.includes?.(message?.u?._id) ?? false;
|
return room?.ignored?.includes?.(message?.u?._id) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = (item, previousItem) => {
|
onLoadMoreMessages = loaderItem => RoomServices.getMoreMessages({
|
||||||
|
rid: this.rid, tmid: this.tmid, t: this.t, loaderItem
|
||||||
|
})
|
||||||
|
|
||||||
|
renderItem = (item, previousItem, highlightedMessage) => {
|
||||||
const { room, lastOpen, canAutoTranslate } = this.state;
|
const { room, lastOpen, canAutoTranslate } = this.state;
|
||||||
const {
|
const {
|
||||||
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme
|
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme
|
||||||
|
@ -920,48 +948,55 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = (
|
let content = null;
|
||||||
<Message
|
if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) {
|
||||||
item={item}
|
content = <LoadMore load={() => this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />;
|
||||||
user={user}
|
} else {
|
||||||
rid={room.rid}
|
content = (
|
||||||
archived={room.archived}
|
<Message
|
||||||
broadcast={room.broadcast}
|
item={item}
|
||||||
status={item.status}
|
user={user}
|
||||||
isThreadRoom={!!this.tmid}
|
rid={room.rid}
|
||||||
isIgnored={this.isIgnored(item)}
|
archived={room.archived}
|
||||||
previousItem={previousItem}
|
broadcast={room.broadcast}
|
||||||
fetchThreadName={this.fetchThreadName}
|
status={item.status}
|
||||||
onReactionPress={this.onReactionPress}
|
isThreadRoom={!!this.tmid}
|
||||||
onReactionLongPress={this.onReactionLongPress}
|
isIgnored={this.isIgnored(item)}
|
||||||
onLongPress={this.onMessageLongPress}
|
previousItem={previousItem}
|
||||||
onEncryptedPress={this.onEncryptedPress}
|
fetchThreadName={this.getThreadName}
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
onReactionPress={this.onReactionPress}
|
||||||
onThreadPress={this.onThreadPress}
|
onReactionLongPress={this.onReactionLongPress}
|
||||||
showAttachment={this.showAttachment}
|
onLongPress={this.onMessageLongPress}
|
||||||
reactionInit={this.onReactionInit}
|
onEncryptedPress={this.onEncryptedPress}
|
||||||
replyBroadcast={this.replyBroadcast}
|
onDiscussionPress={this.onDiscussionPress}
|
||||||
errorActionsShow={this.errorActionsShow}
|
onThreadPress={this.onThreadPress}
|
||||||
baseUrl={baseUrl}
|
showAttachment={this.showAttachment}
|
||||||
Message_GroupingPeriod={Message_GroupingPeriod}
|
reactionInit={this.onReactionInit}
|
||||||
timeFormat={Message_TimeFormat}
|
replyBroadcast={this.replyBroadcast}
|
||||||
useRealName={useRealName}
|
errorActionsShow={this.errorActionsShow}
|
||||||
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
|
baseUrl={baseUrl}
|
||||||
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
|
Message_GroupingPeriod={Message_GroupingPeriod}
|
||||||
autoTranslateLanguage={room.autoTranslateLanguage}
|
timeFormat={Message_TimeFormat}
|
||||||
navToRoomInfo={this.navToRoomInfo}
|
useRealName={useRealName}
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
isReadReceiptEnabled={Message_Read_Receipt_Enabled}
|
||||||
callJitsi={this.callJitsi}
|
autoTranslateRoom={canAutoTranslate && room.autoTranslate}
|
||||||
blockAction={this.blockAction}
|
autoTranslateLanguage={room.autoTranslateLanguage}
|
||||||
threadBadgeColor={this.getBadgeColor(item?.id)}
|
navToRoomInfo={this.navToRoomInfo}
|
||||||
toggleFollowThread={this.toggleFollowThread}
|
getCustomEmoji={this.getCustomEmoji}
|
||||||
/>
|
callJitsi={this.callJitsi}
|
||||||
);
|
blockAction={this.blockAction}
|
||||||
|
threadBadgeColor={this.getBadgeColor(item?.id)}
|
||||||
|
toggleFollowThread={this.toggleFollowThread}
|
||||||
|
jumpToMessage={this.jumpToMessageByUrl}
|
||||||
|
highlighted={highlightedMessage === item.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (showUnreadSeparator || dateSeparator) {
|
if (showUnreadSeparator || dateSeparator) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message}
|
{content}
|
||||||
<Separator
|
<Separator
|
||||||
ts={dateSeparator}
|
ts={dateSeparator}
|
||||||
unread={showUnreadSeparator}
|
unread={showUnreadSeparator}
|
||||||
|
@ -971,7 +1006,7 @@ class RoomView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter = () => {
|
renderFooter = () => {
|
||||||
|
@ -1057,12 +1092,10 @@ class RoomView extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListRef = ref => this.flatList = ref;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const {
|
const {
|
||||||
room, reactionsModalVisible, selectedMessage, loading, reacting
|
room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
|
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
|
||||||
|
@ -1087,7 +1120,7 @@ class RoomView extends React.Component {
|
||||||
/>
|
/>
|
||||||
<List
|
<List
|
||||||
ref={this.list}
|
ref={this.list}
|
||||||
listRef={this.setListRef}
|
listRef={this.flatList}
|
||||||
rid={rid}
|
rid={rid}
|
||||||
t={t}
|
t={t}
|
||||||
tmid={this.tmid}
|
tmid={this.tmid}
|
||||||
|
@ -1127,6 +1160,7 @@ class RoomView extends React.Component {
|
||||||
t={t}
|
t={t}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
|
<Loading visible={showingBlockingLoader} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { getMessageById } from '../../../lib/database/services/Message';
|
||||||
|
import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage';
|
||||||
|
import getSingleMessage from '../../../lib/methods/getSingleMessage';
|
||||||
|
|
||||||
|
const getMessageInfo = async(messageId) => {
|
||||||
|
let result;
|
||||||
|
result = await getMessageById(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
rid: result.subscription.id,
|
||||||
|
tmid: result.tmid,
|
||||||
|
msg: result.msg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await getThreadMessageById(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
rid: result.subscription.id,
|
||||||
|
tmid: result.rid,
|
||||||
|
msg: result.msg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await getSingleMessage(messageId);
|
||||||
|
if (result) {
|
||||||
|
return {
|
||||||
|
id: result._id,
|
||||||
|
rid: result.rid,
|
||||||
|
tmid: result.tmid,
|
||||||
|
msg: result.msg,
|
||||||
|
fromServer: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getMessageInfo;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const getMessages = (room) => {
|
||||||
|
if (room.lastOpen) {
|
||||||
|
return RocketChat.loadMissedMessages(room);
|
||||||
|
} else {
|
||||||
|
return RocketChat.loadMessagesForRoom(room);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default getMessages;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const getMoreMessages = ({
|
||||||
|
rid, t, tmid, loaderItem
|
||||||
|
}) => {
|
||||||
|
if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) {
|
||||||
|
return RocketChat.loadMessagesForRoom({
|
||||||
|
rid, t, latest: loaderItem.ts, loaderItem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
|
||||||
|
return RocketChat.loadNextMessages({
|
||||||
|
rid, tmid, ts: loaderItem.ts, loaderItem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default getMoreMessages;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
// unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already
|
||||||
|
const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid });
|
||||||
|
|
||||||
|
export default getThreadMessages;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import getMessages from './getMessages';
|
||||||
|
import getMoreMessages from './getMoreMessages';
|
||||||
|
import getThreadMessages from './getThreadMessages';
|
||||||
|
import readMessages from './readMessages';
|
||||||
|
import getMessageInfo from './getMessageInfo';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getMessages,
|
||||||
|
getMoreMessages,
|
||||||
|
getThreadMessages,
|
||||||
|
readMessages,
|
||||||
|
getMessageInfo
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import RocketChat from '../../../lib/rocketchat';
|
||||||
|
|
||||||
|
const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
|
||||||
|
|
||||||
|
export default readMessages;
|
|
@ -9,12 +9,6 @@ export default StyleSheet.create({
|
||||||
safeAreaView: {
|
safeAreaView: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
list: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
contentContainer: {
|
|
||||||
paddingTop: 10
|
|
||||||
},
|
|
||||||
readOnly: {
|
readOnly: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
@ -23,6 +23,8 @@ import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import * as HeaderButton from '../../containers/HeaderButton';
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||||
|
import getThreadName from '../../lib/methods/getThreadName';
|
||||||
|
import getRoomInfo from '../../lib/methods/getRoomInfo';
|
||||||
|
|
||||||
class SearchMessagesView extends React.Component {
|
class SearchMessagesView extends React.Component {
|
||||||
static navigationOptions = ({ navigation, route }) => {
|
static navigationOptions = ({ navigation, route }) => {
|
||||||
|
@ -54,9 +56,14 @@ class SearchMessagesView extends React.Component {
|
||||||
searchText: ''
|
searchText: ''
|
||||||
};
|
};
|
||||||
this.rid = props.route.params?.rid;
|
this.rid = props.route.params?.rid;
|
||||||
|
this.t = props.route.params?.t;
|
||||||
this.encrypted = props.route.params?.encrypted;
|
this.encrypted = props.route.params?.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.room = await getRoomInfo(this.rid);
|
||||||
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { loading, searchText, messages } = this.state;
|
const { loading, searchText, messages } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
@ -126,6 +133,11 @@ class SearchMessagesView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAttachment = (attachment) => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.navigate('AttachmentView', { attachment });
|
||||||
|
}
|
||||||
|
|
||||||
navToRoomInfo = (navParam) => {
|
navToRoomInfo = (navParam) => {
|
||||||
const { navigation, user } = this.props;
|
const { navigation, user } = this.props;
|
||||||
if (navParam.rid === user.id) {
|
if (navParam.rid === user.id) {
|
||||||
|
@ -134,6 +146,28 @@ class SearchMessagesView extends React.Component {
|
||||||
navigation.navigate('RoomInfoView', navParam);
|
navigation.navigate('RoomInfoView', navParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jumpToMessage = async({ item }) => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
let params = {
|
||||||
|
rid: this.rid,
|
||||||
|
jumpToMessageId: item._id,
|
||||||
|
t: this.t,
|
||||||
|
room: this.room
|
||||||
|
};
|
||||||
|
if (item.tmid) {
|
||||||
|
navigation.pop();
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
tmid: item.tmid,
|
||||||
|
name: await getThreadName(this.rid, item.tmid, item._id),
|
||||||
|
t: 'thread'
|
||||||
|
};
|
||||||
|
navigation.push('RoomView', params);
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomView', params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderEmpty = () => {
|
renderEmpty = () => {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -152,13 +186,16 @@ class SearchMessagesView extends React.Component {
|
||||||
item={item}
|
item={item}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
timeFormat='LLL'
|
timeFormat='MMM Do YYYY, h:mm:ss a'
|
||||||
isHeader
|
isHeader
|
||||||
showAttachment={() => {}}
|
isThreadRoom
|
||||||
|
showAttachment={this.showAttachment}
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
getCustomEmoji={this.getCustomEmoji}
|
||||||
navToRoomInfo={this.navToRoomInfo}
|
navToRoomInfo={this.navToRoomInfo}
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onPress={() => this.jumpToMessage({ item })}
|
||||||
|
jumpToMessage={() => this.jumpToMessage({ item })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ const getCustomEmoji = (content) => {
|
||||||
return customEmoji;
|
return customEmoji;
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageDecorator = story => (
|
export const MessageDecorator = story => (
|
||||||
<MessageContext.Provider
|
<MessageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
|
@ -60,7 +60,7 @@ const messageDecorator = story => (
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Message = props => (
|
export const Message = props => (
|
||||||
<MessageComponent
|
<MessageComponent
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -74,12 +74,14 @@ const Message = props => (
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const StoryProvider = story => <Provider store={store}>{story()}</Provider>;
|
||||||
|
|
||||||
|
const MessageScrollView = story => <ScrollView style={{ backgroundColor: themes[_theme].backgroundColor }}>{story()}</ScrollView>;
|
||||||
|
|
||||||
const stories = storiesOf('Message', module)
|
const stories = storiesOf('Message', module)
|
||||||
.addDecorator(story => <Provider store={store}>{story()}</Provider>)
|
.addDecorator(StoryProvider)
|
||||||
.addDecorator(story => <ScrollView style={{ backgroundColor: themes[_theme].backgroundColor }}>{story()}</ScrollView>)
|
.addDecorator(MessageScrollView)
|
||||||
.addDecorator(messageDecorator);
|
.addDecorator(MessageDecorator);
|
||||||
|
|
||||||
stories.add('Basic', () => (
|
stories.add('Basic', () => (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import '../../app/views/ThreadMessagesView/Item.stories.js';
|
||||||
import './Avatar';
|
import './Avatar';
|
||||||
import '../../app/containers/BackgroundContainer/index.stories.js';
|
import '../../app/containers/BackgroundContainer/index.stories.js';
|
||||||
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
|
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
|
||||||
|
import '../../app/views/RoomView/LoadMore/LoadMore.stories';
|
||||||
|
|
||||||
// Change here to see themed storybook
|
// Change here to see themed storybook
|
||||||
export const theme = 'light';
|
export const theme = 'light';
|
||||||
|
|
Loading…
Reference in New Issue