diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 36433392c..eec5b535a 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -11185,6 +11185,3689 @@ exports[`Storyshots List with small font 1`] = `
`;
+exports[`Storyshots LoadMore basic 1`] = `
+Array [
+
+ Load More
+ ,
+
+ Load More
+ ,
+
+ Load Older
+ ,
+
+ Load Newer
+ ,
+]
+`;
+
+exports[`Storyshots LoadMore black theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots LoadMore dark theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Storyshots LoadMore light theme 1`] = `
+
+
+
+ Load Older
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Hey!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Older message
+
+
+
+
+
+
+
+
+
+ Load Newer
+
+
+ Load More
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the third message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the second message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diego.mello
+
+
+
+ 10:00 AM
+
+
+
+
+
+ This is the first message
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`Storyshots Markdown Block quote 1`] = `
{
const handlePress = () => {
if (!link) {
return;
}
- openLink(link, theme);
+ onLinkPress(link);
};
const childLength = React.Children.toArray(children).filter(o => o).length;
@@ -40,7 +39,8 @@ const Link = React.memo(({
Link.propTypes = {
children: PropTypes.node,
link: PropTypes.string,
- theme: PropTypes.string
+ theme: PropTypes.string,
+ onLinkPress: PropTypes.func
};
export default Link;
diff --git a/app/containers/markdown/index.js b/app/containers/markdown/index.js
index dfbae1841..bc2fdba73 100644
--- a/app/containers/markdown/index.js
+++ b/app/containers/markdown/index.js
@@ -82,7 +82,8 @@ class Markdown extends PureComponent {
preview: PropTypes.bool,
theme: PropTypes.string,
testID: PropTypes.string,
- style: PropTypes.array
+ style: PropTypes.array,
+ onLinkPress: PropTypes.func
};
constructor(props) {
@@ -218,11 +219,12 @@ class Markdown extends PureComponent {
};
renderLink = ({ children, href }) => {
- const { theme } = this.props;
+ const { theme, onLinkPress } = this.props;
return (
{children}
diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js
index 2f29bf4ac..90af48acd 100644
--- a/app/containers/message/Content.js
+++ b/app/containers/message/Content.js
@@ -45,7 +45,7 @@ const Content = React.memo((props) => {
} else if (props.isEncrypted) {
content = {I18n.t('Encrypted_message')};
} else {
- const { baseUrl, user } = useContext(MessageContext);
+ const { baseUrl, user, onLinkPress } = useContext(MessageContext);
content = (
{
tmid={props.tmid}
useRealName={props.useRealName}
theme={props.theme}
+ onLinkPress={onLinkPress}
/>
);
}
diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js
index 104244740..4bc03c000 100644
--- a/app/containers/message/Message.js
+++ b/app/containers/message/Message.js
@@ -19,6 +19,7 @@ import Discussion from './Discussion';
import Content from './Content';
import ReadReceipt from './ReadReceipt';
import CallButton from './CallButton';
+import { themes } from '../../constants/colors';
const MessageInner = React.memo((props) => {
if (props.type === 'discussion-created') {
@@ -120,6 +121,7 @@ const MessageTouchable = React.memo((props) => {
onLongPress={onLongPress}
onPress={onPress}
disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp}
+ style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }}
>
@@ -134,7 +136,9 @@ MessageTouchable.propTypes = {
isInfo: PropTypes.bool,
isThreadReply: PropTypes.bool,
isTemp: PropTypes.bool,
- archived: PropTypes.bool
+ archived: PropTypes.bool,
+ highlighted: PropTypes.bool,
+ theme: PropTypes.string
};
Message.propTypes = {
diff --git a/app/containers/message/RepliedThread.js b/app/containers/message/RepliedThread.js
index 733315485..46be5b1f6 100644
--- a/app/containers/message/RepliedThread.js
+++ b/app/containers/message/RepliedThread.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { memo, useEffect, useState } from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
@@ -8,24 +8,29 @@ import { themes } from '../../constants/colors';
import I18n from '../../i18n';
import Markdown from '../markdown';
-const RepliedThread = React.memo(({
+const RepliedThread = memo(({
tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme
}) => {
if (!tmid || !isHeader) {
return null;
}
- if (!tmsg) {
- fetchThreadName(tmid, id);
+ const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg);
+ const fetch = async() => {
+ const threadName = await fetchThreadName(tmid, id);
+ setMsg(threadName);
+ };
+
+ useEffect(() => {
+ if (!msg) {
+ fetch();
+ }
+ }, []);
+
+ if (!msg) {
return null;
}
- let msg = tmsg;
-
- if (isEncrypted) {
- msg = I18n.t('Encrypted_message');
- }
-
return (
@@ -45,23 +50,6 @@ const RepliedThread = React.memo(({
);
-}, (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 = {
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
index 4acecbc42..5dcf0447f 100644
--- a/app/containers/message/Reply.js
+++ b/app/containers/message/Reply.js
@@ -142,10 +142,13 @@ const Reply = React.memo(({
if (!attachment) {
return null;
}
- const { baseUrl, user } = useContext(MessageContext);
+ const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = () => {
let url = attachment.title_link || attachment.author_link;
+ if (attachment.message_link) {
+ return jumpToMessage(attachment.message_link);
+ }
if (!url) {
return;
}
diff --git a/app/containers/message/Urls.js b/app/containers/message/Urls.js
index 742b2f478..b82d029af 100644
--- a/app/containers/message/Urls.js
+++ b/app/containers/message/Urls.js
@@ -80,7 +80,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
});
const Url = React.memo(({ url, index, theme }) => {
- if (!url) {
+ if (!url || url?.ignoreParse) {
return null;
}
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index b4339ff0e..467c634a6 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -9,6 +9,7 @@ import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme';
+import openLink from '../../utils/openLink';
class MessageContainer extends React.Component {
static propTypes = {
@@ -33,6 +34,7 @@ class MessageContainer extends React.Component {
autoTranslateLanguage: PropTypes.string,
status: PropTypes.number,
isIgnored: PropTypes.bool,
+ highlighted: PropTypes.bool,
getCustomEmoji: PropTypes.func,
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
@@ -50,7 +52,9 @@ class MessageContainer extends React.Component {
blockAction: PropTypes.func,
theme: PropTypes.string,
threadBadgeColor: PropTypes.string,
- toggleFollowThread: PropTypes.func
+ toggleFollowThread: PropTypes.func,
+ jumpToMessage: PropTypes.func,
+ onPress: PropTypes.func
}
static defaultProps = {
@@ -89,10 +93,15 @@ class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { isManualUnignored } = this.state;
- const { theme, threadBadgeColor, isIgnored } = this.props;
+ const {
+ theme, threadBadgeColor, isIgnored, highlighted
+ } = this.props;
if (nextProps.theme !== theme) {
return true;
}
+ if (nextProps.highlighted !== highlighted) {
+ return true;
+ }
if (nextProps.threadBadgeColor !== threadBadgeColor) {
return true;
}
@@ -112,10 +121,15 @@ class MessageContainer extends React.Component {
}
onPress = debounce(() => {
+ const { onPress } = this.props;
if (this.isIgnored) {
return this.onIgnoredMessagePress();
}
+ if (onPress) {
+ return onPress();
+ }
+
const { item, isThreadRoom } = this.props;
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() {
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;
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;
let message = msg;
@@ -294,6 +365,8 @@ class MessageContainer extends React.Component {
onEncryptedPress: this.onEncryptedPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress,
+ onLinkPress: this.onLinkPress,
+ jumpToMessage,
threadBadgeColor,
toggleFollowThread,
replies
@@ -347,6 +420,7 @@ class MessageContainer extends React.Component {
callJitsi={callJitsi}
blockAction={blockAction}
theme={theme}
+ highlighted={highlighted}
/>
);
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index 2e8391956..1648d3bf2 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -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-can-not-be-removed": "Last owner cannot be removed",
"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"
}
diff --git a/app/lib/database/model/Message.js b/app/lib/database/model/Message.js
index bf776fc73..52cf63f0c 100644
--- a/app/lib/database/model/Message.js
+++ b/app/lib/database/model/Message.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'messages';
+
export default class Message extends Model {
- static table = 'messages';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js
index 5b1ebd141..275dae217 100644
--- a/app/lib/database/model/Subscription.js
+++ b/app/lib/database/model/Subscription.js
@@ -4,8 +4,10 @@ import {
} from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'subscriptions';
+
export default class Subscription extends Model {
- static table = 'subscriptions';
+ static table = TABLE_NAME;
static associations = {
messages: { type: 'has_many', foreignKey: 'rid' },
diff --git a/app/lib/database/model/Thread.js b/app/lib/database/model/Thread.js
index e0179fc35..04e658392 100644
--- a/app/lib/database/model/Thread.js
+++ b/app/lib/database/model/Thread.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'threads';
+
export default class Thread extends Model {
- static table = 'threads';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' }
diff --git a/app/lib/database/model/ThreadMessage.js b/app/lib/database/model/ThreadMessage.js
index b3b4216b5..687e09f96 100644
--- a/app/lib/database/model/ThreadMessage.js
+++ b/app/lib/database/model/ThreadMessage.js
@@ -5,8 +5,10 @@ import {
import { sanitizer } from '../utils';
+export const TABLE_NAME = 'thread_messages';
+
export default class ThreadMessage extends Model {
- static table = 'thread_messages';
+ static table = TABLE_NAME;
static associations = {
subscriptions: { type: 'belongs_to', key: 'subscription_id' }
diff --git a/app/lib/database/services/Message.js b/app/lib/database/services/Message.js
new file mode 100644
index 000000000..5999446ba
--- /dev/null
+++ b/app/lib/database/services/Message.js
@@ -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;
+ }
+};
diff --git a/app/lib/database/services/Subscription.js b/app/lib/database/services/Subscription.js
new file mode 100644
index 000000000..925bb97e4
--- /dev/null
+++ b/app/lib/database/services/Subscription.js
@@ -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;
+ }
+};
diff --git a/app/lib/database/services/Thread.js b/app/lib/database/services/Thread.js
new file mode 100644
index 000000000..4c4208609
--- /dev/null
+++ b/app/lib/database/services/Thread.js
@@ -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;
+ }
+};
diff --git a/app/lib/database/services/ThreadMessage.js b/app/lib/database/services/ThreadMessage.js
new file mode 100644
index 000000000..ca1e5fc83
--- /dev/null
+++ b/app/lib/database/services/ThreadMessage.js
@@ -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;
+ }
+};
diff --git a/app/lib/methods/getRoomInfo.js b/app/lib/methods/getRoomInfo.js
new file mode 100644
index 000000000..293d97c56
--- /dev/null
+++ b/app/lib/methods/getRoomInfo.js
@@ -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;
diff --git a/app/lib/methods/getSingleMessage.js b/app/lib/methods/getSingleMessage.js
new file mode 100644
index 000000000..56ecb3e63
--- /dev/null
+++ b/app/lib/methods/getSingleMessage.js
@@ -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;
diff --git a/app/lib/methods/getThreadName.js b/app/lib/methods/getThreadName.js
new file mode 100644
index 000000000..1eb1fbc78
--- /dev/null
+++ b/app/lib/methods/getThreadName.js
@@ -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;
diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js
index 012e1ea32..a8dc733ac 100644
--- a/app/lib/methods/loadMessagesForRoom.js
+++ b/app/lib/methods/loadMessagesForRoom.js
@@ -1,8 +1,15 @@
+import moment from 'moment';
+
+import { MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
import log from '../../utils/log';
+import { getMessageById } from '../database/services/Message';
import updateMessages from './updateMessages';
+import { generateLoadMoreId } from '../utils';
+
+const COUNT = 50;
async function load({ rid: roomId, latest, t }) {
- let params = { roomId, count: 50 };
+ let params = { roomId, count: COUNT };
if (latest) {
params = { ...params, latest: new Date(latest).toISOString() };
}
@@ -24,9 +31,20 @@ export default function loadMessagesForRoom(args) {
return new Promise(async(resolve, reject) => {
try {
const data = await load.call(this, args);
-
- if (data && data.length) {
- await updateMessages({ rid: args.rid, update: data });
+ if (data?.length) {
+ const lastMessage = data[data.length - 1];
+ 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);
} else {
return resolve([]);
diff --git a/app/lib/methods/loadNextMessages.js b/app/lib/methods/loadNextMessages.js
new file mode 100644
index 000000000..3a5e5e6ff
--- /dev/null
+++ b/app/lib/methods/loadNextMessages.js
@@ -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);
+ }
+ });
+}
diff --git a/app/lib/methods/loadSurroundingMessages.js b/app/lib/methods/loadSurroundingMessages.js
new file mode 100644
index 000000000..74c345c2f
--- /dev/null
+++ b/app/lib/methods/loadSurroundingMessages.js
@@ -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);
+ }
+ });
+}
diff --git a/app/lib/methods/loadThreadMessages.js b/app/lib/methods/loadThreadMessages.js
index de6f244c6..d170635e8 100644
--- a/app/lib/methods/loadThreadMessages.js
+++ b/app/lib/methods/loadThreadMessages.js
@@ -1,5 +1,6 @@
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
+import EJSON from 'ejson';
import buildMessage from './helpers/buildMessage';
import database from '../database';
@@ -7,30 +8,27 @@ import log from '../../utils/log';
import protectedFunction from './helpers/protectedFunction';
import { Encryption } from '../encryption';
-async function load({ tmid, offset }) {
+async function load({ tmid }) {
try {
// RC 1.0
- const result = await this.sdk.get('chat.getThreadMessages', {
- tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } }
- });
- if (!result || !result.success) {
+ const result = await this.methodCallWrapper('getThreadMessages', { tmid });
+ if (!result) {
return [];
}
- return result.messages;
+ return EJSON.fromJSONValue(result);
} catch (error) {
console.log(error);
return [];
}
}
-export default function loadThreadMessages({ tmid, rid, offset = 0 }) {
+export default function loadThreadMessages({ tmid, rid }) {
return new Promise(async(resolve, reject) => {
try {
- let data = await load.call(this, { tmid, offset });
-
+ let data = await load.call(this, { tmid });
if (data && data.length) {
try {
- data = data.map(m => buildMessage(m));
+ data = data.filter(m => m.tmid).map(m => buildMessage(m));
data = await Encryption.decryptMessages(data);
const db = database.active;
const threadMessagesCollection = db.get('thread_messages');
diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js
index 5f0db0f66..0b6b6c7c0 100644
--- a/app/lib/methods/updateMessages.js
+++ b/app/lib/methods/updateMessages.js
@@ -6,8 +6,12 @@ import log from '../../utils/log';
import database from '../database';
import protectedFunction from './helpers/protectedFunction';
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 {
if (!((update && update.length) || (remove && remove.length))) {
return;
@@ -30,7 +34,13 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
const threadCollection = db.get('threads');
const threadMessagesCollection = db.get('thread_messages');
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();
const allThreadsRecords = await threadCollection
.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 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
msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => {
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());
}
+ // Delete loaders
+ loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently());
+ if (loaderItem) {
+ loadersToDelete.push(loaderItem.prepareDestroyPermanently());
+ }
+
const allRecords = [
...msgsToCreate,
...msgsToUpdate,
@@ -130,7 +149,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) {
...threadsToDelete,
...threadMessagesToCreate,
...threadMessagesToUpdate,
- ...threadMessagesToDelete
+ ...threadMessagesToDelete,
+ ...loadersToDelete
];
try {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 3891d88cb..561e02c73 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1,4 +1,5 @@
import { InteractionManager } from 'react-native';
+import EJSON from 'ejson';
import {
Rocketchat as RocketchatClient,
settings as RocketChatSettings
@@ -41,6 +42,8 @@ import canOpenRoom from './methods/canOpenRoom';
import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
+import loadSurroundingMessages from './methods/loadSurroundingMessages';
+import loadNextMessages from './methods/loadNextMessages';
import loadMissedMessages from './methods/loadMissedMessages';
import loadThreadMessages from './methods/loadThreadMessages';
@@ -624,6 +627,8 @@ const RocketChat = {
},
loadMissedMessages,
loadMessagesForRoom,
+ loadSurroundingMessages,
+ loadNextMessages,
loadThreadMessages,
sendMessage,
getRooms,
@@ -938,7 +943,7 @@ const RocketChat = {
methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
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);
},
diff --git a/app/lib/utils.js b/app/lib/utils.js
index 769fd6d76..615b353ae 100644
--- a/app/lib/utils.js
+++ b/app/lib/utils.js
@@ -20,3 +20,5 @@ export const methods = {
};
export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare);
+
+export const generateLoadMoreId = id => `load-more-${ id }`;
diff --git a/app/notifications/push/index.js b/app/notifications/push/index.js
index df4ac152d..13e929164 100644
--- a/app/notifications/push/index.js
+++ b/app/notifications/push/index.js
@@ -10,7 +10,7 @@ export const onNotification = (notification) => {
if (data) {
try {
const {
- rid, name, sender, type, host, messageType
+ rid, name, sender, type, host, messageType, messageId
} = EJSON.parse(data.ejson);
const types = {
@@ -24,6 +24,7 @@ export const onNotification = (notification) => {
const params = {
host,
rid,
+ messageId,
path: `${ types[type] }/${ roomName }`,
isCall: messageType === 'jitsi_call_started'
};
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index 184a4d96e..985a28556 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -60,18 +60,19 @@ const navigate = function* navigate({ params }) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
const focusedRooms = yield select(state => state.room.rooms);
+ const jumpToMessageId = params.messageId;
if (focusedRooms.includes(room.rid)) {
// if there's one room on the list or last room is the one
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
} else {
popToRoot({ isMasterDetail });
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
}
} else {
popToRoot({ isMasterDetail });
- yield goRoom({ item, isMasterDetail });
+ yield goRoom({ item, isMasterDetail, jumpToMessageId });
}
if (params.isCall) {
diff --git a/app/utils/goRoom.js b/app/utils/goRoom.js
index 94adfde49..e9811e651 100644
--- a/app/utils/goRoom.js
+++ b/app/utils/goRoom.js
@@ -14,7 +14,6 @@ const navigate = ({ item, isMasterDetail, ...props }) => {
t: item.t,
prid: item.prid,
room: item,
- search: item.search,
visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item),
...props
diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js
index fc840b2d6..f6ea91942 100644
--- a/app/views/MessagesView/index.js
+++ b/app/views/MessagesView/index.js
@@ -16,6 +16,7 @@ import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { withActionSheet } from '../../containers/ActionSheet';
import SafeAreaView from '../../containers/SafeAreaView';
+import getThreadName from '../../lib/methods/getThreadName';
class MessagesView extends React.Component {
static propTypes = {
@@ -26,7 +27,8 @@ class MessagesView extends React.Component {
customEmojis: PropTypes.object,
theme: PropTypes.string,
showActionSheet: PropTypes.func,
- useRealName: PropTypes.bool
+ useRealName: PropTypes.bool,
+ isMasterDetail: PropTypes.bool
}
constructor(props) {
@@ -81,6 +83,32 @@ class MessagesView extends React.Component {
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) => {
const {
user, baseUrl, theme, useRealName
@@ -93,11 +121,13 @@ class MessagesView extends React.Component {
timeFormat: 'MMM Do YYYY, h:mm:ss a',
isEdited: !!item.editedAt,
isHeader: true,
+ isThreadRoom: true,
attachments: item.attachments || [],
useRealName,
showAttachment: this.showAttachment,
getCustomEmoji: this.getCustomEmoji,
- navToRoomInfo: this.navToRoomInfo
+ navToRoomInfo: this.navToRoomInfo,
+ onPress: () => this.jumpToMessage({ item })
});
return ({
@@ -315,7 +345,8 @@ const mapStateToProps = state => ({
baseUrl: state.server.server,
user: getUserSelector(state),
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)));
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 72ec2548b..3256dd0b9 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -636,7 +636,7 @@ class RoomActionsView extends React.Component {
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
} = this.state;
const {
- rid, t, encrypted
+ rid, t
} = room;
const isGroupChat = RocketChat.isGroupChat(room);
@@ -761,24 +761,6 @@ class RoomActionsView extends React.Component {
)
: null}
- {['c', 'p', 'd'].includes(t)
- ? (
- <>
- this.onPressTouchable({
- route: 'SearchMessagesView',
- params: { rid, encrypted }
- })}
- testID='room-actions-search'
- left={() => }
- showActionIndicator
- />
-
- >
- )
- : null}
-
{['c', 'p', 'd'].includes(t)
? (
<>
diff --git a/app/views/RoomView/List/List.js b/app/views/RoomView/List/List.js
new file mode 100644
index 000000000..407dcbf10
--- /dev/null
+++ b/app/views/RoomView/List/List.js
@@ -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 }) => (
+ 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;
diff --git a/app/views/RoomView/List/NavBottomFAB.js b/app/views/RoomView/List/NavBottomFAB.js
new file mode 100644
index 000000000..5c5aee746
--- /dev/null
+++ b/app/views/RoomView/List/NavBottomFAB.js
@@ -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 (
+
+
+
+
+
+
+
+ );
+};
+
+NavBottomFAB.propTypes = {
+ y: Animated.Value,
+ onPress: PropTypes.func,
+ isThread: PropTypes.bool
+};
+
+export default NavBottomFAB;
diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List/index.js
similarity index 59%
rename from app/views/RoomView/List.js
rename to app/views/RoomView/List/index.js
index 41d424fa3..19a8ccb90 100644
--- a/app/views/RoomView/List.js
+++ b/app/views/RoomView/List/index.js
@@ -1,30 +1,39 @@
import React from 'react';
-import { FlatList, RefreshControl } from 'react-native';
+import { RefreshControl } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb';
import moment from 'moment';
import { dequal } from 'dequal';
+import { Value, event } from 'react-native-reanimated';
-import styles from './styles';
-import database from '../../lib/database';
-import scrollPersistTaps from '../../utils/scrollPersistTaps';
-import RocketChat from '../../lib/rocketchat';
-import log from '../../utils/log';
-import EmptyRoom from './EmptyRoom';
-import { isIOS } from '../../utils/deviceInfo';
-import { animateNextTransition } from '../../utils/layoutAnimation';
-import ActivityIndicator from '../../containers/ActivityIndicator';
-import { themes } from '../../constants/colors';
+import database from '../../../lib/database';
+import RocketChat from '../../../lib/rocketchat';
+import log from '../../../utils/log';
+import EmptyRoom from '../EmptyRoom';
+import { animateNextTransition } from '../../../utils/layoutAnimation';
+import ActivityIndicator from '../../../containers/ActivityIndicator';
+import { themes } from '../../../constants/colors';
+import List from './List';
+import NavBottomFAB from './NavBottomFAB';
+import debounce from '../../../utils/debounce';
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 = {
- onEndReached: PropTypes.func,
- renderFooter: PropTypes.func,
renderRow: PropTypes.func,
rid: PropTypes.string,
- t: PropTypes.string,
tmid: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool,
@@ -36,34 +45,28 @@ class List extends React.Component {
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) {
super(props);
console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`);
this.count = 0;
- this.needsFetch = false;
this.mounted = false;
this.animated = false;
+ this.jumping = false;
this.state = {
- loading: true,
- end: false,
messages: [],
- refreshing: false
+ refreshing: false,
+ highlightedMessage: null
};
+ this.y = new Value(0);
+ this.onScroll = onScroll({ y: this.y });
this.query();
this.unsubscribeFocus = props.navigation.addListener('focus', () => {
this.animated = true;
});
+ this.viewabilityConfig = {
+ itemVisiblePercentThreshold: 10
+ };
console.timeEnd(`${ this.constructor.name } init`);
}
@@ -73,17 +76,17 @@ class List extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
- const { loading, end, refreshing } = this.state;
+ const { refreshing, highlightedMessage } = this.state;
const {
- hideSystemMessages, theme, tunread, ignored
+ hideSystemMessages, theme, tunread, ignored, loading
} = this.props;
if (theme !== nextProps.theme) {
return true;
}
- if (loading !== nextState.loading) {
+ if (loading !== nextProps.loading) {
return true;
}
- if (end !== nextState.end) {
+ if (highlightedMessage !== nextState.highlightedMessage) {
return true;
}
if (refreshing !== nextState.refreshing) {
@@ -116,32 +119,14 @@ class List extends React.Component {
if (this.unsubscribeFocus) {
this.unsubscribeFocus();
}
+ this.clearHighlightedMessageTimeout();
console.countReset(`${ this.constructor.name }.render calls`);
}
- fetchData = async() => {
- const {
- loading, end, messages, latest = messages[messages.length - 1]?.ts
- } = this.state;
- 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);
+ clearHighlightedMessageTimeout = () => {
+ if (this.highlightedMessageTimeout) {
+ clearTimeout(this.highlightedMessageTimeout);
+ this.highlightedMessageTimeout = false;
}
}
@@ -198,9 +183,6 @@ class List extends React.Component {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((messages) => {
- if (messages.length <= this.count) {
- this.needsFetch = true;
- }
if (tmid && this.thread) {
messages = [...messages, this.thread];
}
@@ -211,6 +193,7 @@ class List extends React.Component {
} else {
this.state.messages = messages;
}
+ // TODO: move it away from here
this.readThreads();
});
}
@@ -221,7 +204,7 @@ class List extends React.Component {
this.query();
}
- readThreads = async() => {
+ readThreads = debounce(async() => {
const { tmid } = this.props;
if (tmid) {
@@ -231,39 +214,9 @@ class List extends React.Component {
// Do nothing
}
}
- }
+ }, 300)
- onEndReached = async() => {
- 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();
- }
- }
+ onEndReached = () => this.query()
onRefresh = () => this.setState({ refreshing: true }, async() => {
const { messages } = this.state;
@@ -272,7 +225,7 @@ class List extends React.Component {
if (messages.length) {
try {
if (tmid) {
- await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
+ await RocketChat.loadThreadMessages({ tmid, rid });
} else {
await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
}
@@ -284,7 +237,6 @@ class List extends React.Component {
this.setState({ refreshing: false });
})
- // eslint-disable-next-line react/sort-comp
update = () => {
if (this.animated) {
animateNextTransition();
@@ -306,9 +258,53 @@ class List extends React.Component {
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 = () => {
- const { loading } = this.state;
- const { rid, theme } = this.props;
+ const { rid, theme, loading } = this.props;
if (loading && rid) {
return ;
}
@@ -316,36 +312,34 @@ class List extends React.Component {
}
renderItem = ({ item, index }) => {
- const { messages } = this.state;
+ const { messages, highlightedMessage } = this.state;
const { renderRow } = this.props;
- return renderRow(item, messages[index + 1]);
+ return renderRow(item, messages[index + 1], highlightedMessage);
+ }
+
+ onViewableItemsChanged = ({ viewableItems }) => {
+ this.viewableItems = viewableItems;
}
render() {
console.count(`${ this.constructor.name }.render calls`);
- const { rid, listRef } = this.props;
+ const { rid, tmid, listRef } = this.props;
const { messages, refreshing } = this.state;
const { theme } = this.props;
return (
<>
- item.id}
+
)}
- {...scrollPersistTaps}
/>
+
>
);
}
}
-export default List;
+export default ListContainer;
diff --git a/app/views/RoomView/LoadMore/LoadMore.stories.js b/app/views/RoomView/LoadMore/LoadMore.stories.js
new file mode 100644
index 000000000..1f110a9cf
--- /dev/null
+++ b/app/views/RoomView/LoadMore/LoadMore.stories.js
@@ -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', () => (
+ <>
+
+
+
+
+ >
+));
+
+const ThemeStory = ({ theme }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('light theme', () => );
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('dark theme', () => );
+
+stories
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageDecorator)
+ .add('black theme', () => );
+
diff --git a/app/views/RoomView/LoadMore/index.js b/app/views/RoomView/LoadMore/index.js
new file mode 100644
index 000000000..04b922835
--- /dev/null
+++ b/app/views/RoomView/LoadMore/index.js
@@ -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 (
+
+ {
+ loading
+ ?
+ : {I18n.t(text)}
+ }
+
+ );
+};
+
+LoadMore.propTypes = {
+ load: PropTypes.func,
+ type: PropTypes.string,
+ runOnRender: PropTypes.bool
+};
+
+export default LoadMore;
diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js
index 81b8f153b..f61488b5b 100644
--- a/app/views/RoomView/RightButtons.js
+++ b/app/views/RoomView/RightButtons.js
@@ -142,12 +142,12 @@ class RightButtonsContainer extends Component {
goSearchView = () => {
logEvent(events.ROOM_GO_SEARCH);
const {
- rid, navigation, isMasterDetail
+ rid, t, navigation, isMasterDetail
} = this.props;
if (isMasterDetail) {
navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } });
} else {
- navigation.navigate('SearchMessagesView', { rid });
+ navigation.navigate('SearchMessagesView', { rid, t });
}
}
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index b6b700db3..18d123ec4 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, InteractionManager } from 'react-native';
import { connect } from 'react-redux';
+import parse from 'url-parse';
-import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment';
import * as Haptics from 'expo-haptics';
import { Q } from '@nozbe/watermelondb';
@@ -17,7 +17,6 @@ import {
import List from './List';
import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat';
-import { Encryption } from '../../lib/encryption';
import Message from '../../containers/message';
import MessageActions from '../../containers/MessageActions';
import MessageErrorActions from '../../containers/MessageErrorActions';
@@ -35,6 +34,7 @@ import RightButtons from './RightButtons';
import StatusBar from '../../containers/StatusBar';
import Separator from './Separator';
import { themes } from '../../constants/colors';
+import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad';
import debounce from '../../utils/debounce';
import ReactionsModal from '../../containers/ReactionsModal';
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 { 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 = [
'joined',
@@ -76,7 +82,8 @@ const stateAttrsUpdate = [
'replying',
'reacting',
'readOnly',
- 'member'
+ 'member',
+ 'showingBlockingLoader'
];
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 name = props.route.params?.name;
const fname = props.route.params?.fname;
- const search = props.route.params?.search;
const prid = props.route.params?.prid;
const room = props.route.params?.room ?? {
rid: this.rid, t: this.t, name, fname, prid
};
+ this.jumpToMessageId = props.route.params?.jumpToMessageId;
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
this.state = {
joined: true,
@@ -133,6 +140,7 @@ class RoomView extends React.Component {
selectedMessage: selectedMessage || {},
canAutoTranslate: false,
loading: true,
+ showingBlockingLoader: false,
editing: false,
replying: !!selectedMessage,
replyWithMention: false,
@@ -151,13 +159,10 @@ class RoomView extends React.Component {
this.setReadOnly();
- if (search) {
- this.updateRoom();
- }
-
this.messagebox = React.createRef();
this.list = React.createRef();
this.joinCode = React.createRef();
+ this.flatList = React.createRef();
this.mounted = false;
// we don't need to subscribe to threads
@@ -181,6 +186,9 @@ class RoomView extends React.Component {
EventEmitter.addEventListener('connected', this.handleConnected);
}
}
+ if (this.jumpToMessageId) {
+ this.jumpToMessage(this.jumpToMessageId);
+ }
if (isIOS && this.rid) {
this.updateUnreadCount();
}
@@ -195,7 +203,9 @@ class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { state } = this;
const { roomUpdate, member } = state;
- const { appState, theme, insets } = this.props;
+ const {
+ appState, theme, insets, route
+ } = this.props;
if (theme !== nextProps.theme) {
return true;
}
@@ -212,12 +222,19 @@ class RoomView extends React.Component {
if (!dequal(nextProps.insets, insets)) {
return true;
}
+ if (!dequal(nextProps.route?.params, route?.params)) {
+ return true;
+ }
return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
}
componentDidUpdate(prevProps, prevState) {
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) {
// Fire List.query() just to keep observables working
@@ -417,34 +434,15 @@ class RoomView extends React.Component {
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() => {
try {
this.setState({ loading: true });
const { room, joined } = this.state;
if (this.tmid) {
- await this.getThreadMessages();
+ await RoomServices.getThreadMessages(this.tmid, this.rid);
} else {
const newLastOpen = new Date();
- await this.getMessages(room);
+ await RoomServices.getMessages(room);
// if room is joined
if (joined) {
@@ -453,7 +451,7 @@ class RoomView extends React.Component {
} else {
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) => {
- const { roomUserId } = this.state;
- const { navigation } = this.props;
- if (item.tmid) {
- if (!item.tmsg) {
- 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
- });
+ onThreadPress = debounce(item => this.navToThread(item), 1000, true)
+
+ shouldNavigateToRoom = (message) => {
+ if (message.tmid && message.tmid === this.tmid) {
+ return false;
}
- }, 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) => {
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) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
@@ -767,45 +797,7 @@ class RoomView extends React.Component {
}
}
- // eslint-disable-next-line react/sort-comp
- 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);
- }
- }
+ getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId)
toggleFollowThread = async(isFollowingThread, tmid) => {
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 = () => {
const { room } = this.state;
const { jitsiTimeout } = room;
@@ -900,7 +924,11 @@ class RoomView extends React.Component {
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 {
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;
+ if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) {
+ content = this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />;
+ } else {
+ content = (
+
+ );
+ }
if (showUnreadSeparator || dateSeparator) {
return (
<>
- {message}
+ {content}
{
@@ -1057,12 +1092,10 @@ class RoomView extends React.Component {
);
}
- setListRef = ref => this.flatList = ref;
-
render() {
console.count(`${ this.constructor.name }.render calls`);
const {
- room, reactionsModalVisible, selectedMessage, loading, reacting
+ room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader
} = this.state;
const {
user, baseUrl, theme, navigation, Hide_System_Messages, width, height
@@ -1087,7 +1120,7 @@ class RoomView extends React.Component {
/>
+
);
}
diff --git a/app/views/RoomView/services/getMessageInfo.js b/app/views/RoomView/services/getMessageInfo.js
new file mode 100644
index 000000000..f7f008c46
--- /dev/null
+++ b/app/views/RoomView/services/getMessageInfo.js
@@ -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;
diff --git a/app/views/RoomView/services/getMessages.js b/app/views/RoomView/services/getMessages.js
new file mode 100644
index 000000000..7e9c03de0
--- /dev/null
+++ b/app/views/RoomView/services/getMessages.js
@@ -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;
diff --git a/app/views/RoomView/services/getMoreMessages.js b/app/views/RoomView/services/getMoreMessages.js
new file mode 100644
index 000000000..6d16f69c2
--- /dev/null
+++ b/app/views/RoomView/services/getMoreMessages.js
@@ -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;
diff --git a/app/views/RoomView/services/getThreadMessages.js b/app/views/RoomView/services/getThreadMessages.js
new file mode 100644
index 000000000..0f9529cfc
--- /dev/null
+++ b/app/views/RoomView/services/getThreadMessages.js
@@ -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;
diff --git a/app/views/RoomView/services/index.js b/app/views/RoomView/services/index.js
new file mode 100644
index 000000000..f8799cda8
--- /dev/null
+++ b/app/views/RoomView/services/index.js
@@ -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
+};
diff --git a/app/views/RoomView/services/readMessages.js b/app/views/RoomView/services/readMessages.js
new file mode 100644
index 000000000..060d9aa7e
--- /dev/null
+++ b/app/views/RoomView/services/readMessages.js
@@ -0,0 +1,5 @@
+import RocketChat from '../../../lib/rocketchat';
+
+const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
+
+export default readMessages;
diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js
index fdbb61a7b..4f84d8f7a 100644
--- a/app/views/RoomView/styles.js
+++ b/app/views/RoomView/styles.js
@@ -9,12 +9,6 @@ export default StyleSheet.create({
safeAreaView: {
flex: 1
},
- list: {
- flex: 1
- },
- contentContainer: {
- paddingTop: 10
- },
readOnly: {
justifyContent: 'flex-end',
alignItems: 'center',
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index e11a5b83d..09c9e6c15 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -23,6 +23,8 @@ import SafeAreaView from '../../containers/SafeAreaView';
import * as HeaderButton from '../../containers/HeaderButton';
import database from '../../lib/database';
import { sanitizeLikeString } from '../../lib/database/utils';
+import getThreadName from '../../lib/methods/getThreadName';
+import getRoomInfo from '../../lib/methods/getRoomInfo';
class SearchMessagesView extends React.Component {
static navigationOptions = ({ navigation, route }) => {
@@ -54,9 +56,14 @@ class SearchMessagesView extends React.Component {
searchText: ''
};
this.rid = props.route.params?.rid;
+ this.t = props.route.params?.t;
this.encrypted = props.route.params?.encrypted;
}
+ async componentDidMount() {
+ this.room = await getRoomInfo(this.rid);
+ }
+
shouldComponentUpdate(nextProps, nextState) {
const { loading, searchText, messages } = this.state;
const { theme } = this.props;
@@ -126,6 +133,11 @@ class SearchMessagesView extends React.Component {
return null;
}
+ showAttachment = (attachment) => {
+ const { navigation } = this.props;
+ navigation.navigate('AttachmentView', { attachment });
+ }
+
navToRoomInfo = (navParam) => {
const { navigation, user } = this.props;
if (navParam.rid === user.id) {
@@ -134,6 +146,28 @@ class SearchMessagesView extends React.Component {
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 = () => {
const { theme } = this.props;
return (
@@ -152,13 +186,16 @@ class SearchMessagesView extends React.Component {
item={item}
baseUrl={baseUrl}
user={user}
- timeFormat='LLL'
+ timeFormat='MMM Do YYYY, h:mm:ss a'
isHeader
- showAttachment={() => {}}
+ isThreadRoom
+ showAttachment={this.showAttachment}
getCustomEmoji={this.getCustomEmoji}
navToRoomInfo={this.navToRoomInfo}
useRealName={useRealName}
theme={theme}
+ onPress={() => this.jumpToMessage({ item })}
+ jumpToMessage={() => this.jumpToMessage({ item })}
/>
);
}
diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js
index 82cfa1b03..6d6d2b4f1 100644
--- a/storybook/stories/Message.js
+++ b/storybook/stories/Message.js
@@ -40,7 +40,7 @@ const getCustomEmoji = (content) => {
return customEmoji;
};
-const messageDecorator = story => (
+export const MessageDecorator = story => (
(
);
-const Message = props => (
+export const Message = props => (
(
/>
);
+export const StoryProvider = story => {story()};
+const MessageScrollView = story => {story()};
const stories = storiesOf('Message', module)
- .addDecorator(story => {story()})
- .addDecorator(story => {story()})
- .addDecorator(messageDecorator);
+ .addDecorator(StoryProvider)
+ .addDecorator(MessageScrollView)
+ .addDecorator(MessageDecorator);
stories.add('Basic', () => (
<>
diff --git a/storybook/stories/index.js b/storybook/stories/index.js
index 20bfc4f25..b695d49d6 100644
--- a/storybook/stories/index.js
+++ b/storybook/stories/index.js
@@ -14,6 +14,7 @@ import '../../app/views/ThreadMessagesView/Item.stories.js';
import './Avatar';
import '../../app/containers/BackgroundContainer/index.stories.js';
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
+import '../../app/views/RoomView/LoadMore/LoadMore.stories';
// Change here to see themed storybook
export const theme = 'light';