[NEW] Threads (#2567)

* [IMPROVEMENT] Mentions layout without background

* Fix RoomItem

* Fix tests

* Smaller messagebox

* Messagebox colors tweak

* Beginning header buttons refactor

* Add HeaderButtons

* item with title

* Refactor

* Remove lib

* Refactor

* Update snapshot

* Send to channel on messagebox

* Add tshow

* Add showMessageInMainThread to login.user reducer

* Filter threads on main channel based on user setting

* Send tshow

* Add tunread

* Move unread colors logic away from UnreadBadge component so it can be used on other components

* Export UnreadBadge on index

* Add empty test

* Refactor

* Update tests

* Lint

* Thread unread user and group on RoomItem

* Thread badge working

* Started ThreadMessagesView.Item

* Fix separator

* Reactivity working

* Lint

* custom emojis aren't necessary

* Basic filter layout

* Filtering layout

* Refactor

* apply filter

* DropdownItemHeader

* default all

* few fixes

* No data found

* Fixes list performance issues

* Use locale on date formats

* Fixed minor styles

* Thread badge

* Refactor getBadgeColor

* Fix send to channel background color

* starting search threads

* Fix lint and tests

* Bump to 4.12.0 just for testing :)

* Search input layout

* query

* starting threads header

* fix unnecessary tlm on tmid messages

* Fix thread header

* lint

* Fix thread header on ShareView

* Add e2e tests

* Fix subscriptions sort

* Update stories and minor fixes

* Fix button sizes on Messagebox

* Remove comment

* Unnecessary conditional

* Add showMessageInMainThread to user collection

* Fix thread header

* Fix thread messages not working on tablet

* Reset Messagebox.tshow after sending a message

* Allow to send to channel when replying to a thread from main channel

* Unnecessary theme prop

* Address comments

* Remove re-render

* Fix scroll indicator bug

* Fix style

* Minor i18n fix

* Fix dropdown height

* I18n ptbr

* I18n
This commit is contained in:
Diego Mello 2020-10-30 14:35:07 -03:00 committed by GitHub
parent 81bb89da6c
commit 6271b885ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 21134 additions and 919 deletions

View File

@ -1,5 +1,5 @@
export const RectButton = () => 'View'; export const RectButton = ({ children }) => children;
export const State = () => 'View'; export const State = () => 'View';
export const LongPressGestureHandler = () => 'View'; export const LongPressGestureHandler = ({ children }) => children;
export const BorderlessButton = () => 'View'; export const BorderlessButton = ({ children }) => children;
export const PanGestureHandler = () => 'View'; export const PanGestureHandler = ({ children }) => children;

File diff suppressed because it is too large Load Diff

View File

@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.11.0" versionName "4.12.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (isPlay) { if (isPlay) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -37,7 +37,7 @@ export const themes = {
auxiliaryText: '#9ca2a8', auxiliaryText: '#9ca2a8',
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
auxiliaryTintColor: '#caced1', auxiliaryTintColor: '#6C727A',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#cbcbcc', separatorColor: '#cbcbcc',
navbarBackground: '#ffffff', navbarBackground: '#ffffff',
@ -82,7 +82,7 @@ export const themes = {
auxiliaryText: '#9297a2', auxiliaryText: '#9297a2',
infoText: '#6D6D72', infoText: '#6D6D72',
tintColor: '#1d74f5', tintColor: '#1d74f5',
auxiliaryTintColor: '#cdcdcd', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1d74f5', actionTintColor: '#1d74f5',
separatorColor: '#2b2b2d', separatorColor: '#2b2b2d',
navbarBackground: '#0b182c', navbarBackground: '#0b182c',
@ -127,7 +127,7 @@ export const themes = {
auxiliaryText: '#b2b8c6', auxiliaryText: '#b2b8c6',
infoText: '#6d6d72', infoText: '#6d6d72',
tintColor: '#1e9bfe', tintColor: '#1e9bfe',
auxiliaryTintColor: '#cdcdcd', auxiliaryTintColor: '#f9f9f9',
actionTintColor: '#1e9bfe', actionTintColor: '#1e9bfe',
separatorColor: '#272728', separatorColor: '#272728',
navbarBackground: '#0d0d0d', navbarBackground: '#0d0d0d',

View File

@ -186,7 +186,7 @@ export default class RecordAudio extends React.PureComponent {
accessibilityLabel={I18n.t('Send_audio_message')} accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button' accessibilityTraits='button'
> >
<CustomIcon name='microphone' size={23} color={themes[theme].tintColor} /> <CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
</BorderlessButton> </BorderlessButton>
); );
} }
@ -201,7 +201,7 @@ export default class RecordAudio extends React.PureComponent {
style={styles.actionButton} style={styles.actionButton}
> >
<CustomIcon <CustomIcon
size={22} size={24}
color={themes[theme].dangerColor} color={themes[theme].dangerColor}
name='close' name='close'
/> />
@ -219,7 +219,7 @@ export default class RecordAudio extends React.PureComponent {
style={styles.actionButton} style={styles.actionButton}
> >
<CustomIcon <CustomIcon
size={22} size={24}
color={themes[theme].successColor} color={themes[theme].successColor}
name='check' name='check'
/> />

View File

@ -8,7 +8,7 @@ import styles from '../styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
const BaseButton = React.memo(({ const BaseButton = React.memo(({
onPress, testID, accessibilityLabel, icon, theme onPress, testID, accessibilityLabel, icon, theme, color
}) => ( }) => (
<BorderlessButton <BorderlessButton
onPress={onPress} onPress={onPress}
@ -17,7 +17,7 @@ const BaseButton = React.memo(({
accessibilityLabel={I18n.t(accessibilityLabel)} accessibilityLabel={I18n.t(accessibilityLabel)}
accessibilityTraits='button' accessibilityTraits='button'
> >
<CustomIcon name={icon} size={25} color={themes[theme].tintColor} /> <CustomIcon name={icon} size={24} color={color ?? themes[theme].auxiliaryTintColor} />
</BorderlessButton> </BorderlessButton>
)); ));
@ -26,7 +26,8 @@ BaseButton.propTypes = {
onPress: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired, testID: PropTypes.string.isRequired,
accessibilityLabel: PropTypes.string.isRequired, accessibilityLabel: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired icon: PropTypes.string.isRequired,
color: PropTypes.string
}; };
export default BaseButton; export default BaseButton;

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import BaseButton from './BaseButton'; import BaseButton from './BaseButton';
import { themes } from '../../../constants/colors';
const SendButton = React.memo(({ theme, onPress }) => ( const SendButton = React.memo(({ theme, onPress }) => (
<BaseButton <BaseButton
@ -10,6 +11,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
accessibilityLabel='Send_message' accessibilityLabel='Send_message'
icon='send-filled' icon='send-filled'
theme={theme} theme={theme}
color={themes[theme].tintColor}
/> />
)); ));

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, Alert, Keyboard, NativeModules View, Alert, Keyboard, NativeModules, Text
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-keyboard-input'; import { KeyboardAccessoryView } from 'react-native-keyboard-input';
@ -9,6 +9,7 @@ import ImagePicker from 'react-native-image-crop-picker';
import equal from 'deep-equal'; import equal from 'deep-equal';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
import { generateTriggerId } from '../../lib/methods/actions'; import { generateTriggerId } from '../../lib/methods/actions';
import TextInput from '../../presentation/TextInput'; import TextInput from '../../presentation/TextInput';
@ -47,6 +48,7 @@ import { getUserSelector } from '../../selectors/login';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { withActionSheet } from '../ActionSheet'; import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils'; import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons';
const imagePickerConfig = { const imagePickerConfig = {
cropping: true, cropping: true,
@ -121,7 +123,8 @@ class MessageBox extends Component {
trackingType: '', trackingType: '',
commandPreview: [], commandPreview: [],
showCommandPreview: false, showCommandPreview: false,
command: {} command: {},
tshow: false
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
@ -254,7 +257,7 @@ class MessageBox extends Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
showEmojiKeyboard, showSend, recording, mentions, commandPreview showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow
} = this.state; } = this.state;
const { const {
@ -284,6 +287,9 @@ class MessageBox extends Component {
if (nextState.recording !== recording) { if (nextState.recording !== recording) {
return true; return true;
} }
if (nextState.tshow !== tshow) {
return true;
}
if (!equal(nextState.mentions, mentions)) { if (!equal(nextState.mentions, mentions)) {
return true; return true;
} }
@ -576,6 +582,7 @@ class MessageBox extends Component {
clearInput = () => { clearInput = () => {
this.setInput(''); this.setInput('');
this.setShowSend(false); this.setShowSend(false);
this.setState({ tshow: false });
} }
canUploadFile = (file) => { canUploadFile = (file) => {
@ -709,6 +716,7 @@ class MessageBox extends Component {
} }
submit = async() => { submit = async() => {
const { tshow } = this.state;
const { const {
onSubmit, rid: roomId, tmid, showSend, sharing onSubmit, rid: roomId, tmid, showSend, sharing
} = this.props; } = this.props;
@ -772,7 +780,7 @@ class MessageBox extends Component {
// Thread // Thread
if (threadsEnabled && replyWithMention) { if (threadsEnabled && replyWithMention) {
onSubmit(message, replyingMessage.id); onSubmit(message, replyingMessage.id, tshow);
// Legacy reply or quote (quote is a reply without mention) // Legacy reply or quote (quote is a reply without mention)
} else { } else {
@ -792,7 +800,7 @@ class MessageBox extends Component {
// Normal message // Normal message
} else { } else {
onSubmit(message); onSubmit(message, undefined, tshow);
} }
} }
@ -844,6 +852,27 @@ class MessageBox extends Component {
} }
} }
onPressSendToChannel = () => this.setState(({ tshow }) => ({ tshow: !tshow }))
renderSendToChannel = () => {
const { tshow } = this.state;
const { theme, tmid, replying } = this.props;
if (!tmid && !replying) {
return null;
}
return (
<TouchableWithoutFeedback
style={[styles.sendToChannelButton, { backgroundColor: themes[theme].messageboxBackground }]}
onPress={this.onPressSendToChannel}
testID='messagebox-send-to-channel'
>
<CustomIcon name={tshow ? 'checkbox-checked' : 'checkbox-unchecked'} size={24} color={themes[theme].auxiliaryText} />
<Text style={[styles.sendToChannelText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Messagebox_Send_to_channel')}</Text>
</TouchableWithoutFeedback>
);
}
renderContent = () => { renderContent = () => {
const { const {
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
@ -903,6 +932,7 @@ class MessageBox extends Component {
keyboardType='twitter' keyboardType='twitter'
blurOnSubmit={false} blurOnSubmit={false}
placeholder={I18n.t('New_Message')} placeholder={I18n.t('New_Message')}
placeholderTextColor={themes[theme].auxiliaryTintColor}
onChangeText={this.onChangeText} onChangeText={this.onChangeText}
onSelectionChange={this.onSelectionChange} onSelectionChange={this.onSelectionChange}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
@ -938,6 +968,7 @@ class MessageBox extends Component {
{textInputAndButtons} {textInputAndButtons}
{recordAudio} {recordAudio}
</View> </View>
{this.renderSendToChannel()}
</View> </View>
{children} {children}
</> </>

View File

@ -7,12 +7,6 @@ const MENTION_HEIGHT = 50;
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT; const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
export default StyleSheet.create({ export default StyleSheet.create({
textBox: {
flex: 0,
alignItems: 'center',
borderTopWidth: StyleSheet.hairlineWidth,
zIndex: 2
},
composer: { composer: {
flexDirection: 'column', flexDirection: 'column',
borderTopWidth: 1 borderTopWidth: 1
@ -24,7 +18,7 @@ export default StyleSheet.create({
}, },
textBoxInput: { textBoxInput: {
textAlignVertical: 'center', textAlignVertical: 'center',
maxHeight: 242, maxHeight: 240,
flexGrow: 1, flexGrow: 1,
width: 1, width: 1,
// paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering // paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering
@ -32,7 +26,7 @@ export default StyleSheet.create({
paddingBottom: 12, paddingBottom: 12,
paddingLeft: 0, paddingLeft: 0,
paddingRight: 0, paddingRight: 0,
fontSize: 17, fontSize: 16,
letterSpacing: 0, letterSpacing: 0,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
@ -40,7 +34,7 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 60, width: 60,
height: 56 height: 48
}, },
mentionList: { mentionList: {
maxHeight: MENTION_HEIGHT * 4 maxHeight: MENTION_HEIGHT * 4
@ -110,10 +104,21 @@ export default StyleSheet.create({
justifyContent: 'space-between' justifyContent: 'space-between'
}, },
recordingCancelText: { recordingCancelText: {
fontSize: 17, fontSize: 16,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
buttonsWhitespace: { buttonsWhitespace: {
width: 15 width: 15
},
sendToChannelButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 18
},
sendToChannelText: {
fontSize: 12,
marginLeft: 4,
...sharedStyles.textRegular
} }
}); });

View File

@ -3,36 +3,31 @@ import { View, Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable'; import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
const CallButton = React.memo(({ const CallButton = React.memo(({
dlm, theme, callJitsi theme, callJitsi
}) => { }) => (
const time = formatLastMessage(dlm); <View style={styles.buttonContainer}>
return ( <Touchable
<View style={styles.buttonContainer}> onPress={callJitsi}
<Touchable background={Touchable.Ripple(themes[theme].bannerBackground)}
onPress={callJitsi} style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
background={Touchable.Ripple(themes[theme].bannerBackground)} hitSlop={BUTTON_HIT_SLOP}
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]} >
hitSlop={BUTTON_HIT_SLOP} <>
> <CustomIcon name='camera' size={16} style={styles.buttonIcon} color={themes[theme].buttonText} />
<> <Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
<CustomIcon name='camera' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} /> </>
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text> </Touchable>
</> </View>
</Touchable> ));
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View>
);
});
CallButton.propTypes = { CallButton.propTypes = {
dlm: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
callJitsi: PropTypes.func callJitsi: PropTypes.func
}; };

View File

@ -3,18 +3,22 @@ import { View, Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable'; import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils'; import { formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { formatDateThreads } from '../../utils/room';
const Discussion = React.memo(({ const Discussion = React.memo(({
msg, dcount, dlm, theme msg, dcount, dlm, theme
}) => { }) => {
const time = formatLastMessage(dlm); let time;
if (dlm) {
time = formatDateThreads(dlm);
}
const buttonText = formatMessageCount(dcount, DISCUSSION); const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext); const { onDiscussionPress } = useContext(MessageContext);
return ( return (
@ -25,11 +29,11 @@ const Discussion = React.memo(({
<Touchable <Touchable
onPress={onDiscussionPress} onPress={onDiscussionPress}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]} style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
> >
<> <>
<CustomIcon name='discussions' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} /> <CustomIcon name='discussions' size={16} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text> <Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
</> </>
</Touchable> </Touchable>

View File

@ -1,32 +1,49 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import { formatLastMessage, formatMessageCount } from './utils'; import { formatMessageCount } from './utils';
import styles from './styles'; import styles from './styles';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { THREAD } from './constants'; import { THREAD } from './constants';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { formatDateThreads } from '../../utils/room';
import MessageContext from './Context';
const Thread = React.memo(({ const Thread = React.memo(({
msg, tcount, tlm, customThreadTimeFormat, isThreadRoom, theme msg, tcount, tlm, isThreadRoom, theme, id
}) => { }) => {
if (!tlm || isThreadRoom || tcount === 0) { if (!tlm || isThreadRoom || tcount === 0) {
return null; return null;
} }
const time = formatLastMessage(tlm, customThreadTimeFormat); const {
getBadgeColor, toggleFollowThread, user, replies
} = useContext(MessageContext);
const time = formatDateThreads(tlm);
const buttonText = formatMessageCount(tcount, THREAD); const buttonText = formatMessageCount(tcount, THREAD);
const badgeColor = getBadgeColor(id);
const isFollowing = replies?.find(u => u === user.id);
return ( return (
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<View <View
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]} style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
testID={`message-thread-button-${ msg }`} testID={`message-thread-button-${ msg }`}
> >
<CustomIcon name='threads' size={20} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} /> <CustomIcon name='threads' size={16} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text> <Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
</View> </View>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
{badgeColor ? <View style={[styles.threadBadge, { backgroundColor: badgeColor }]} /> : null}
<Touchable onPress={() => toggleFollowThread(isFollowing, id)}>
<CustomIcon
name={isFollowing ? 'notification' : 'notification-disabled'}
size={24}
color={themes[theme].auxiliaryText}
style={styles.threadBell}
/>
</Touchable>
</View> </View>
); );
}, (prevProps, nextProps) => { }, (prevProps, nextProps) => {
@ -44,8 +61,8 @@ Thread.propTypes = {
tcount: PropTypes.string, tcount: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
tlm: PropTypes.string, tlm: PropTypes.string,
customThreadTimeFormat: PropTypes.string, isThreadRoom: PropTypes.bool,
isThreadRoom: PropTypes.bool id: PropTypes.string
}; };
Thread.displayName = 'MessageThread'; Thread.displayName = 'MessageThread';

View File

@ -21,7 +21,6 @@ class MessageContainer extends React.Component {
}), }),
rid: PropTypes.string, rid: PropTypes.string,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: PropTypes.any, style: PropTypes.any,
archived: PropTypes.bool, archived: PropTypes.bool,
broadcast: PropTypes.bool, broadcast: PropTypes.bool,
@ -49,7 +48,9 @@ class MessageContainer extends React.Component {
navToRoomInfo: PropTypes.func, navToRoomInfo: PropTypes.func,
callJitsi: PropTypes.func, callJitsi: PropTypes.func,
blockAction: PropTypes.func, blockAction: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string,
getBadgeColor: PropTypes.func,
toggleFollowThread: PropTypes.func
} }
static defaultProps = { static defaultProps = {
@ -265,10 +266,10 @@ class MessageContainer extends React.Component {
render() { render() {
const { author } = this.state; const { author } = this.state;
const { const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, getBadgeColor, toggleFollowThread
} = 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 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;
@ -291,7 +292,10 @@ class MessageContainer extends React.Component {
onReactionPress: this.onReactionPress, onReactionPress: this.onReactionPress,
onEncryptedPress: this.onEncryptedPress, onEncryptedPress: this.onEncryptedPress,
onDiscussionPress: this.onDiscussionPress, onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress onReactionLongPress: this.onReactionLongPress,
getBadgeColor,
toggleFollowThread,
replies
}} }}
> >
<Message <Message
@ -309,7 +313,6 @@ class MessageContainer extends React.Component {
avatar={avatar} avatar={avatar}
emoji={emoji} emoji={emoji}
timeFormat={timeFormat} timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style} style={style}
archived={archived} archived={archived}
broadcast={broadcast} broadcast={broadcast}

View File

@ -40,11 +40,11 @@ export default StyleSheet.create({
reactionsContainer: { reactionsContainer: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
marginTop: 6 marginTop: 8
}, },
reactionButton: { reactionButton: {
marginRight: 6, marginRight: 8,
marginBottom: 6, marginBottom: 8,
borderRadius: 2 borderRadius: 2
}, },
reactionContainer: { reactionContainer: {
@ -83,27 +83,24 @@ export default StyleSheet.create({
paddingVertical: 5 paddingVertical: 5
}, },
buttonContainer: { buttonContainer: {
marginTop: 6, marginTop: 8,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
button: { button: {
paddingHorizontal: 15, paddingHorizontal: 12,
height: 44, paddingVertical: 8,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
borderRadius: 2 borderRadius: 2
}, },
smallButton: {
height: 30
},
buttonIcon: { buttonIcon: {
marginRight: 6 marginRight: 8
}, },
buttonText: { buttonText: {
fontSize: 14, fontSize: 12,
...sharedStyles.textMedium ...sharedStyles.textSemibold
}, },
imageContainer: { imageContainer: {
// flex: 1, // flex: 1,
@ -143,10 +140,8 @@ export default StyleSheet.create({
}, },
time: { time: {
fontSize: 12, fontSize: 12,
paddingLeft: 10, paddingLeft: 8,
lineHeight: 22, ...sharedStyles.textRegular
...sharedStyles.textRegular,
fontWeight: '300'
}, },
repliedThread: { repliedThread: {
flexDirection: 'row', flexDirection: 'row',
@ -170,6 +165,15 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
}, },
threadBadge: {
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 8
},
threadBell: {
marginLeft: 8
},
readReceipt: { readReceipt: {
lineHeight: 20 lineHeight: 20
}, },

View File

@ -1,20 +1,6 @@
import moment from 'moment';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
export const formatLastMessage = (lm, customFormat) => {
if (customFormat) {
return moment(lm).format(customFormat);
}
return lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
};
export const formatMessageCount = (count, type) => { export const formatMessageCount = (count, type) => {
const discussion = type === DISCUSSION; const discussion = type === DISCUSSION;
let text = discussion ? I18n.t('No_messages_yet') : null; let text = discussion ? I18n.t('No_messages_yet') : null;

View File

@ -54,7 +54,6 @@ const OmnichannelStatus = memo(({
<UnreadBadge <UnreadBadge
style={styles.queueIcon} style={styles.queueIcon}
unread={queueSize} unread={queueSize}
theme={theme}
/> />
) )
: null} : null}

View File

@ -658,5 +658,13 @@ export default {
You_will_be_logged_out_from_other_locations: 'You\'ll be logged out from other locations.', You_will_be_logged_out_from_other_locations: 'You\'ll be logged out from other locations.',
Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully', Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully',
Logout_failed: 'Logout failed!', Logout_failed: 'Logout failed!',
Log_analytics_events: 'Log analytics events' Log_analytics_events: 'Log analytics events',
Following: 'Following',
Threads_displaying_all: 'Displaying All',
Threads_displaying_following: 'Displaying Following',
Threads_displaying_unread: 'Displaying Unread',
No_threads: 'There are no threads',
No_threads_following: 'You are not following any threads',
No_threads_unread: 'There are no unread threads',
Messagebox_Send_to_channel: 'Send to channel'
}; };

View File

@ -602,5 +602,13 @@ export default {
You_will_be_logged_out_from_other_locations: 'Você perderá a sessão de outros clientes', You_will_be_logged_out_from_other_locations: 'Você perderá a sessão de outros clientes',
Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso', Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso',
Logout_failed: 'Falha ao desconectar!', Logout_failed: 'Falha ao desconectar!',
Log_analytics_events: 'Logar eventos no analytics' Log_analytics_events: 'Logar eventos no analytics',
Following: 'Seguindo',
Threads_displaying_all: 'Mostrando Tudo',
Threads_displaying_following: 'Mostrando Seguindo',
Threads_displaying_unread: 'Mostrando Não Lidos',
No_threads: 'Não há tópicos',
No_threads_following: 'Você não está seguindo tópicos',
No_threads_unread: 'Não há tópicos não lidos',
Messagebox_Send_to_channel: 'Mostrar no canal'
}; };

View File

@ -79,4 +79,6 @@ export default class Message extends Model {
@json('blocks', sanitizer) blocks; @json('blocks', sanitizer) blocks;
@field('e2e') e2e; @field('e2e') e2e;
@field('tshow') tshow;
} }

View File

@ -42,6 +42,12 @@ export default class Subscription extends Model {
@field('group_mentions') groupMentions; @field('group_mentions') groupMentions;
@json('tunread', sanitizer) tunread;
@json('tunread_user', sanitizer) tunreadUser;
@json('tunread_group', sanitizer) tunreadGroup;
@date('room_updated_at') roomUpdatedAt; @date('room_updated_at') roomUpdatedAt;
@field('ro') ro; @field('ro') ro;

View File

@ -16,5 +16,7 @@ export default class User extends Model {
@field('login_email_password') loginEmailPassword; @field('login_email_password') loginEmailPassword;
@field('show_message_in_main_thread') showMessageInMainThread;
@json('roles', sanitizer) roles; @json('roles', sanitizer) roles;
} }

View File

@ -170,6 +170,12 @@ export default schemaMigrations({
{ {
toVersion: 11, toVersion: 11,
steps: [ steps: [
addColumns({
table: 'messages',
columns: [
{ name: 'tshow', type: 'boolean', isOptional: true }
]
}),
createTable({ createTable({
name: 'users', name: 'users',
columns: [ columns: [
@ -182,6 +188,9 @@ export default schemaMigrations({
addColumns({ addColumns({
table: 'subscriptions', table: 'subscriptions',
columns: [ columns: [
{ name: 'tunread', type: 'string', isOptional: true },
{ name: 'tunread_user', type: 'string', isOptional: true },
{ name: 'tunread_group', type: 'string', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true } { name: 'avatar_etag', type: 'string', isOptional: true }
] ]
}), }),

View File

@ -90,6 +90,7 @@ export default schemaMigrations({
addColumns({ addColumns({
table: 'users', table: 'users',
columns: [ columns: [
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true } { name: 'avatar_etag', type: 'string', isOptional: true }
] ]
}) })

View File

@ -20,6 +20,9 @@ export default appSchema({
{ name: 'unread', type: 'number' }, { name: 'unread', type: 'number' },
{ name: 'user_mentions', type: 'number' }, { name: 'user_mentions', type: 'number' },
{ name: 'group_mentions', type: 'number' }, { name: 'group_mentions', type: 'number' },
{ name: 'tunread', type: 'string', isOptional: true },
{ name: 'tunread_user', type: 'string', isOptional: true },
{ name: 'tunread_group', type: 'string', isOptional: true },
{ name: 'room_updated_at', type: 'number' }, { name: 'room_updated_at', type: 'number' },
{ name: 'ro', type: 'boolean' }, { name: 'ro', type: 'boolean' },
{ name: 'last_open', type: 'number', isOptional: true }, { name: 'last_open', type: 'number', isOptional: true },
@ -108,7 +111,8 @@ export default appSchema({
{ name: 'translations', type: 'string', isOptional: true }, { name: 'translations', type: 'string', isOptional: true },
{ name: 'tmsg', type: 'string', isOptional: true }, { name: 'tmsg', type: 'string', isOptional: true },
{ name: 'blocks', type: 'string', isOptional: true }, { name: 'blocks', type: 'string', isOptional: true },
{ name: 'e2e', type: 'string', isOptional: true } { name: 'e2e', type: 'string', isOptional: true },
{ name: 'tshow', type: 'boolean', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -14,6 +14,7 @@ export default appSchema({
{ name: 'statusText', type: 'string', isOptional: true }, { name: 'statusText', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true }, { name: 'roles', type: 'string', isOptional: true },
{ name: 'login_email_password', type: 'boolean', isOptional: true }, { name: 'login_email_password', type: 'boolean', isOptional: true },
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true } { name: 'avatar_etag', type: 'string', isOptional: true }
] ]
}), }),

View File

@ -14,8 +14,12 @@ export const merge = (subscription, room) => {
} }
if (room) { if (room) {
if (room._updatedAt) { if (room._updatedAt) {
subscription.roomUpdatedAt = room._updatedAt;
subscription.lastMessage = normalizeMessage(room.lastMessage); subscription.lastMessage = normalizeMessage(room.lastMessage);
if (subscription.lastMessage) {
subscription.roomUpdatedAt = subscription.lastMessage.ts;
} else {
subscription.roomUpdatedAt = room._updatedAt;
}
subscription.description = room.description; subscription.description = room.description;
subscription.topic = room.topic; subscription.topic = room.topic;
subscription.announcement = room.announcement; subscription.announcement = room.announcement;

View File

@ -86,7 +86,7 @@ export async function resendMessage(message, tmid) {
} }
} }
export default async function(rid, msg, tmid, user) { export default async function(rid, msg, tmid, user, tshow) {
try { try {
const db = database.active; const db = database.active;
const subsCollection = db.collections.get('subscriptions'); const subsCollection = db.collections.get('subscriptions');
@ -97,7 +97,7 @@ export default async function(rid, msg, tmid, user) {
const batch = []; const batch = [];
let message = { let message = {
_id: messageId, rid, msg, tmid _id: messageId, rid, msg, tmid, tshow
}; };
message = await Encryption.encryptMessage(message); message = await Encryption.encryptMessage(message);
@ -179,8 +179,9 @@ export default async function(rid, msg, tmid, user) {
}; };
if (tmid && tMessageRecord) { if (tmid && tMessageRecord) {
m.tmid = tmid; m.tmid = tmid;
m.tlm = messageDate; // m.tlm = messageDate; // I don't think this is necessary... leaving it commented just in case...
m.tmsg = tMessageRecord.msg; m.tmsg = tMessageRecord.msg;
m.tshow = tshow;
} }
m.t = message.t; m.t = message.t;
if (message.t === E2E_MESSAGE_TYPE) { if (message.t === E2E_MESSAGE_TYPE) {

View File

@ -273,6 +273,9 @@ export default function subscribeRooms() {
if (diff?.statusLivechat) { if (diff?.statusLivechat) {
store.dispatch(setUser({ statusLivechat: diff.statusLivechat })); store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
} }
if (['settings.preferences.showMessageInMainThread'] in diff) {
store.dispatch(setUser({ showMessageInMainThread: diff['settings.preferences.showMessageInMainThread'] }));
}
} }
if (/subscriptions/.test(ev)) { if (/subscriptions/.test(ev)) {
if (type === 'removed') { if (type === 'removed') {

View File

@ -491,7 +491,8 @@ const RocketChat = {
emails: result.me.emails, emails: result.me.emails,
roles: result.me.roles, roles: result.me.roles,
avatarETag: result.me.avatarETag, avatarETag: result.me.avatarETag,
loginEmailPassword loginEmailPassword,
showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true
}; };
return user; return user;
}, },
@ -920,8 +921,12 @@ const RocketChat = {
getUidDirectMessage(room) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user; const { id: userId } = reduxStore.getState().login.user;
if (!room) {
return false;
}
// legacy method // legacy method
if (!room.uids && room.rid && room.t === 'd') { if (!room?.uids && room.rid && room.t === 'd') {
return room.rid.replace(userId, '').trim(); return room.rid.replace(userId, '').trim();
} }
@ -929,8 +934,8 @@ const RocketChat = {
return false; return false;
} }
const me = room && room.uids && room.uids.find(uid => uid === userId); const me = room.uids?.find(uid => uid === userId);
const other = room && room.uids && room.uids.filter(uid => uid !== userId); const other = room.uids?.filter(uid => uid !== userId);
return other && other.length ? other[0] : me; return other && other.length ? other[0] : me;
}, },
@ -1242,11 +1247,18 @@ const RocketChat = {
} }
return this.post('chat.unfollowMessage', { mid }); return this.post('chat.unfollowMessage', { mid });
}, },
getThreadsList({ rid, count, offset }) { getThreadsList({
// RC 1.0 rid, count, offset, text
return this.sdk.get('chat.getThreadsList', { }) {
const params = {
rid, count, offset, sort: { ts: -1 } rid, count, offset, sort: { ts: -1 }
}); };
if (text) {
params.text = text;
}
// RC 1.0
return this.sdk.get('chat.getThreadsList', params);
}, },
getSyncThreadsList({ rid, updatedSince }) { getSyncThreadsList({ rid, updatedSince }) {
// RC 1.0 // RC 1.0

File diff suppressed because one or more lines are too long

View File

@ -37,9 +37,11 @@ const RoomItem = ({
alert, alert,
hideUnreadStatus, hideUnreadStatus,
unread, unread,
tunread,
userMentions, userMentions,
groupMentions, groupMentions,
tunread,
tunreadUser,
tunreadGroup,
roomUpdatedAt, roomUpdatedAt,
testID, testID,
swipeEnabled, swipeEnabled,
@ -113,10 +115,11 @@ const RoomItem = ({
/> />
<UnreadBadge <UnreadBadge
unread={unread} unread={unread}
tunread={tunread}
userMentions={userMentions} userMentions={userMentions}
groupMentions={groupMentions} groupMentions={groupMentions}
theme={theme} tunread={tunread}
tunreadUser={tunreadUser}
tunreadGroup={tunreadGroup}
/> />
</View> </View>
</> </>
@ -138,10 +141,11 @@ const RoomItem = ({
/> />
<UnreadBadge <UnreadBadge
unread={unread} unread={unread}
tunread={tunread}
userMentions={userMentions} userMentions={userMentions}
groupMentions={groupMentions} groupMentions={groupMentions}
theme={theme} tunread={tunread}
tunreadUser={tunreadUser}
tunreadGroup={tunreadGroup}
/> />
</View> </View>
) )
@ -177,9 +181,11 @@ RoomItem.propTypes = {
alert: PropTypes.bool, alert: PropTypes.bool,
hideUnreadStatus: PropTypes.bool, hideUnreadStatus: PropTypes.bool,
unread: PropTypes.number, unread: PropTypes.number,
tunread: PropTypes.array,
userMentions: PropTypes.number, userMentions: PropTypes.number,
groupMentions: PropTypes.number, groupMentions: PropTypes.number,
tunread: PropTypes.array,
tunreadUser: PropTypes.array,
tunreadGroup: PropTypes.array,
roomUpdatedAt: PropTypes.instanceOf(Date), roomUpdatedAt: PropTypes.instanceOf(Date),
swipeEnabled: PropTypes.bool, swipeEnabled: PropTypes.bool,
toggleFav: PropTypes.func, toggleFav: PropTypes.func,

View File

@ -178,6 +178,7 @@ class RoomItemContainer extends React.Component {
const avatar = getRoomAvatar(item); const avatar = getRoomAvatar(item);
const isRead = getIsRead(item); const isRead = getIsRead(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts); const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
const alert = (item.alert || item.tunread?.length);
let accessibilityLabel = name; let accessibilityLabel = name;
if (item.unread === 1) { if (item.unread === 1) {
@ -203,7 +204,6 @@ class RoomItemContainer extends React.Component {
onPress={this.onPress} onPress={this.onPress}
date={date} date={date}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
userMentions={item.userMentions}
width={width} width={width}
favorite={item.f} favorite={item.f}
toggleFav={toggleFav} toggleFav={toggleFav}
@ -221,15 +221,18 @@ class RoomItemContainer extends React.Component {
prid={item.prid} prid={item.prid}
status={status} status={status}
hideUnreadStatus={item.hideUnreadStatus} hideUnreadStatus={item.hideUnreadStatus}
alert={item.alert} alert={alert}
roomUpdatedAt={item.roomUpdatedAt} roomUpdatedAt={item.roomUpdatedAt}
lastMessage={item.lastMessage} lastMessage={item.lastMessage}
showLastMessage={showLastMessage} showLastMessage={showLastMessage}
username={username} username={username}
useRealName={useRealName} useRealName={useRealName}
unread={item.unread} unread={item.unread}
tunread={item.tunread} userMentions={item.userMentions}
groupMentions={item.groupMentions} groupMentions={item.groupMentions}
tunread={item.tunread}
tunreadUser={item.tunreadUser}
tunreadGroup={item.tunreadGroup}
avatarETag={avatarETag || item.avatarETag} avatarETag={avatarETag || item.avatarETag}
swipeEnabled={swipeEnabled} swipeEnabled={swipeEnabled}
/> />

View File

@ -10,9 +10,9 @@ export const getUnreadStyle = ({
let backgroundColor = themes[theme].unreadBackground; let backgroundColor = themes[theme].unreadBackground;
const color = themes[theme].buttonText; const color = themes[theme].buttonText;
if (userMentions > 0 || tunreadUser?.length) { if (userMentions > 0 || tunreadUser?.length) {
backgroundColor = themes[theme].mentionMeColor; backgroundColor = themes[theme].mentionMeBackground;
} else if (groupMentions > 0 || tunreadGroup?.length) { } else if (groupMentions > 0 || tunreadGroup?.length) {
backgroundColor = themes[theme].mentionGroupColor; backgroundColor = themes[theme].mentionGroupBackground;
} else if (tunread?.length > 0) { } else if (tunread?.length > 0) {
backgroundColor = themes[theme].tunreadBackground; backgroundColor = themes[theme].tunreadBackground;
} }

View File

@ -5,6 +5,10 @@ import { getUnreadStyle } from './getUnreadStyle';
const testsForTheme = (theme) => { const testsForTheme = (theme) => {
const getUnreadStyleUtil = ({ ...props }) => getUnreadStyle({ theme, ...props }); const getUnreadStyleUtil = ({ ...props }) => getUnreadStyle({ theme, ...props });
test('render empty', () => {
expect(getUnreadStyleUtil({})).toEqual({});
});
test('render unread', () => { test('render unread', () => {
expect(getUnreadStyleUtil({ expect(getUnreadStyleUtil({
unread: 1 unread: 1
@ -28,7 +32,7 @@ const testsForTheme = (theme) => {
unread: 1, unread: 1,
userMentions: 1 userMentions: 1
})).toEqual({ })).toEqual({
backgroundColor: themes[theme].mentionMeColor, backgroundColor: themes[theme].mentionMeBackground,
color: themes[theme].buttonText color: themes[theme].buttonText
}); });
}); });
@ -38,7 +42,7 @@ const testsForTheme = (theme) => {
unread: 1, unread: 1,
groupMentions: 1 groupMentions: 1
})).toEqual({ })).toEqual({
backgroundColor: themes[theme].mentionGroupColor, backgroundColor: themes[theme].mentionGroupBackground,
color: themes[theme].buttonText color: themes[theme].buttonText
}); });
}); });
@ -50,7 +54,7 @@ const testsForTheme = (theme) => {
groupMentions: 1, groupMentions: 1,
tunread: [1] tunread: [1]
})).toEqual({ })).toEqual({
backgroundColor: themes[theme].mentionMeColor, backgroundColor: themes[theme].mentionMeBackground,
color: themes[theme].buttonText color: themes[theme].buttonText
}); });
expect(getUnreadStyleUtil({ expect(getUnreadStyleUtil({
@ -58,7 +62,7 @@ const testsForTheme = (theme) => {
groupMentions: 1, groupMentions: 1,
tunread: [1] tunread: [1]
})).toEqual({ })).toEqual({
backgroundColor: themes[theme].mentionGroupColor, backgroundColor: themes[theme].mentionGroupBackground,
color: themes[theme].buttonText color: themes[theme].buttonText
}); });
expect(getUnreadStyleUtil({ expect(getUnreadStyleUtil({

View File

@ -22,10 +22,8 @@ const styles = StyleSheet.create({
justifyContent: 'center' justifyContent: 'center'
}, },
unreadText: { unreadText: {
// overflow: 'hidden',
fontSize: 13, fontSize: 13,
...sharedStyles.textSemibold, ...sharedStyles.textSemibold
fontWeight: '600'
}, },
textSmall: { textSmall: {
fontSize: 10 fontSize: 10

View File

@ -179,6 +179,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
statusText: user.statusText, statusText: user.statusText,
roles: user.roles, roles: user.roles,
loginEmailPassword: user.loginEmailPassword, loginEmailPassword: user.loginEmailPassword,
showMessageInMainThread: user.showMessageInMainThread,
avatarETag: user.avatarETag avatarETag: user.avatarETag
}; };
yield serversDB.action(async() => { yield serversDB.action(async() => {

View File

@ -190,7 +190,6 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
<ModalStack.Screen <ModalStack.Screen
name='ThreadMessagesView' name='ThreadMessagesView'
component={ThreadMessagesView} component={ThreadMessagesView}
options={props => ThreadMessagesView.navigationOptions({ ...props, isMasterDetail: true })}
/> />
<ModalStack.Screen <ModalStack.Screen
name='MarkdownTableView' name='MarkdownTableView'

View File

@ -1,4 +1,5 @@
import moment from 'moment'; import moment from 'moment';
import { themes } from '../constants/colors';
import I18n from '../i18n'; import I18n from '../i18n';
@ -21,5 +22,26 @@ export const formatDate = date => moment(date).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`, lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'LT', sameDay: 'LT',
lastWeek: 'dddd', lastWeek: 'dddd',
sameElse: 'MMM D' sameElse: 'L'
}); });
export const formatDateThreads = date => moment(date).calendar(null, {
sameDay: 'LT',
lastDay: `[${ I18n.t('Yesterday') }] LT`,
lastWeek: 'dddd LT',
sameElse: 'LL'
});
export const getBadgeColor = ({ subscription, messageId, theme }) => {
if (subscription?.tunreadUser?.includes(messageId)) {
return themes[theme].mentionMeBackground;
}
if (subscription?.tunreadGroup?.includes(messageId)) {
return themes[theme].mentionGroupBackground;
}
if (subscription?.tunread?.includes(messageId)) {
return themes[theme].tunreadBackground;
}
};
export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title;

View File

@ -24,9 +24,6 @@ const styles = StyleSheet.create({
...sharedStyles.textSemibold, ...sharedStyles.textSemibold,
fontSize: TITLE_SIZE fontSize: TITLE_SIZE
}, },
scroll: {
alignItems: 'center'
},
subtitle: { subtitle: {
...sharedStyles.textRegular, ...sharedStyles.textRegular,
fontSize: 12 fontSize: 12
@ -36,11 +33,9 @@ const styles = StyleSheet.create({
} }
}); });
const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => { const SubTitle = React.memo(({
if (!subtitle && !usersTyping.length) { usersTyping, subtitle, renderFunc, theme
return null; }) => {
}
// typing // typing
if (usersTyping.length) { if (usersTyping.length) {
let usersText; let usersText;
@ -57,6 +52,11 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
); );
} }
// renderFunc
if (renderFunc) {
return renderFunc();
}
// subtitle // subtitle
if (subtitle) { if (subtitle) {
return ( return (
@ -69,12 +69,15 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
/> />
); );
} }
return null;
}); });
SubTitle.propTypes = { SubTitle.propTypes = {
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
theme: PropTypes.string, theme: PropTypes.string,
subtitle: PropTypes.string subtitle: PropTypes.string,
renderFunc: PropTypes.func
}; };
const HeaderTitle = React.memo(({ const HeaderTitle = React.memo(({
@ -113,7 +116,7 @@ HeaderTitle.propTypes = {
}; };
const Header = React.memo(({ const Header = React.memo(({
title, subtitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme
}) => { }) => {
const portrait = height > width; const portrait = height > width;
let scale = 1; let scale = 1;
@ -126,6 +129,22 @@ const Header = React.memo(({
const onPress = () => goRoomActionsView(); const onPress = () => goRoomActionsView();
let renderFunc;
if (tmid) {
renderFunc = () => (
<View style={styles.titleContainer}>
<Icon
type={prid ? 'discussion' : type}
tmid={tmid}
status={status}
roomUserId={roomUserId}
theme={theme}
/>
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}>{parentTitle}</Text>
</View>
);
}
return ( return (
<TouchableOpacity <TouchableOpacity
testID='room-view-header-actions' testID='room-view-header-actions'
@ -135,7 +154,7 @@ const Header = React.memo(({
disabled={tmid} disabled={tmid}
> >
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} /> {tmid ? null : <Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />}
<HeaderTitle <HeaderTitle
title={title} title={title}
tmid={tmid} tmid={tmid}
@ -145,7 +164,7 @@ const Header = React.memo(({
theme={theme} theme={theme}
/> />
</View> </View>
{tmid ? null : <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} />} <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} renderFunc={renderFunc} />
</TouchableOpacity> </TouchableOpacity>
); );
}); });
@ -163,6 +182,7 @@ Header.propTypes = {
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
connecting: PropTypes.bool, connecting: PropTypes.bool,
roomUserId: PropTypes.string, roomUserId: PropTypes.string,
parentTitle: PropTypes.string,
goRoomActionsView: PropTypes.func goRoomActionsView: PropTypes.func
}; };

View File

@ -21,9 +21,9 @@ const styles = StyleSheet.create({
}); });
const Icon = React.memo(({ const Icon = React.memo(({
roomUserId, type, status, theme roomUserId, type, status, theme, tmid
}) => { }) => {
if (type === 'd' && roomUserId) { if ((type === 'd' || tmid) && roomUserId) {
return <Status size={10} style={styles.status} status={status} />; return <Status size={10} style={styles.status} status={status} />;
} }
@ -37,8 +37,6 @@ const Icon = React.memo(({
let icon; let icon;
if (type === 'discussion') { if (type === 'discussion') {
icon = 'discussions'; icon = 'discussions';
} else if (type === 'thread') {
icon = 'threads';
} else if (type === 'c') { } else if (type === 'c') {
icon = 'channel-public'; icon = 'channel-public';
} else if (type === 'l') { } else if (type === 'l') {
@ -68,6 +66,7 @@ Icon.propTypes = {
roomUserId: PropTypes.string, roomUserId: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string,
tmid: PropTypes.string
}; };
export default Icon; export default Icon;

View File

@ -1,14 +1,14 @@
import React from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import isEqual from 'react-fast-compare';
import * as HeaderButton from '../../../containers/HeaderButton'; import * as HeaderButton from '../../../containers/HeaderButton';
import database from '../../../lib/database'; import database from '../../../lib/database';
import { getUserSelector } from '../../../selectors/login'; import { getUserSelector } from '../../../selectors/login';
import { logEvent, events } from '../../../utils/log'; import { logEvent, events } from '../../../utils/log';
class RightButtonsContainer extends Component {
class RightButtonsContainer extends React.PureComponent {
static propTypes = { static propTypes = {
userId: PropTypes.string, userId: PropTypes.string,
threadsEnabled: PropTypes.bool, threadsEnabled: PropTypes.bool,
@ -23,30 +23,63 @@ class RightButtonsContainer extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
isFollowingThread: true isFollowingThread: true,
tunread: [],
tunreadUser: [],
tunreadGroup: []
}; };
} }
async componentDidMount() { async componentDidMount() {
const { tmid } = this.props; const { tmid, rid } = this.props;
const db = database.active;
if (tmid) { if (tmid) {
const db = database.active;
try { try {
const threadRecord = await db.collections.get('messages').find(tmid); const threadRecord = await db.collections.get('messages').find(tmid);
this.observeThead(threadRecord); this.observeThread(threadRecord);
} catch (e) { } catch (e) {
console.log('Can\'t find message to observe.'); console.log('Can\'t find message to observe.');
} }
} }
if (rid) {
try {
const subCollection = db.collections.get('subscriptions');
const subRecord = await subCollection.find(rid);
this.observeSubscription(subRecord);
} catch (e) {
console.log('Can\'t find subscription to observe.');
}
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
isFollowingThread, tunread, tunreadUser, tunreadGroup
} = this.state;
if (nextState.isFollowingThread !== isFollowingThread) {
return true;
}
if (!isEqual(nextState.tunread, tunread)) {
return true;
}
if (!isEqual(nextState.tunreadUser, tunreadUser)) {
return true;
}
if (!isEqual(nextState.tunreadGroup, tunreadGroup)) {
return true;
}
} }
componentWillUnmount() { componentWillUnmount() {
if (this.threadSubscription && this.threadSubscription.unsubscribe) { if (this.threadSubscription && this.threadSubscription.unsubscribe) {
this.threadSubscription.unsubscribe(); this.threadSubscription.unsubscribe();
} }
if (this.subSubscription && this.subSubscription.unsubscribe) {
this.subSubscription.unsubscribe();
}
} }
observeThead = (threadRecord) => { observeThread = (threadRecord) => {
const threadObservable = threadRecord.observe(); const threadObservable = threadRecord.observe();
this.threadSubscription = threadObservable this.threadSubscription = threadObservable
.subscribe(thread => this.updateThread(thread)); .subscribe(thread => this.updateThread(thread));
@ -59,6 +92,22 @@ class RightButtonsContainer extends React.PureComponent {
}); });
} }
observeSubscription = (subRecord) => {
const subObservable = subRecord.observe();
this.subSubscription = subObservable
.subscribe((sub) => {
this.updateSubscription(sub);
});
}
updateSubscription = (sub) => {
this.setState({
tunread: sub?.tunread,
tunreadUser: sub?.tunreadUser,
tunreadGroup: sub?.tunreadGroup
});
}
goThreadsView = () => { goThreadsView = () => {
logEvent(events.ROOM_GO_THREADS); logEvent(events.ROOM_GO_THREADS);
const { const {
@ -93,7 +142,9 @@ class RightButtonsContainer extends React.PureComponent {
} }
render() { render() {
const { isFollowingThread } = this.state; const {
isFollowingThread, tunread, tunreadUser, tunreadGroup
} = this.state;
const { t, tmid, threadsEnabled } = this.props; const { t, tmid, threadsEnabled } = this.props;
if (t === 'l') { if (t === 'l') {
return null; return null;
@ -116,6 +167,13 @@ class RightButtonsContainer extends React.PureComponent {
iconName='threads' iconName='threads'
onPress={this.goThreadsView} onPress={this.goThreadsView}
testID='room-view-header-threads' testID='room-view-header-threads'
badge={() => (
<HeaderButton.Badge
tunread={tunread}
tunreadUser={tunreadUser}
tunreadGroup={tunreadGroup}
/>
)}
/> />
) : null} ) : null}
<HeaderButton.Item <HeaderButton.Item

View File

@ -27,7 +27,8 @@ class RoomHeaderView extends Component {
widthOffset: PropTypes.number, widthOffset: PropTypes.number,
goRoomActionsView: PropTypes.func, goRoomActionsView: PropTypes.func,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number height: PropTypes.number,
parentTitle: PropTypes.string
}; };
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -75,7 +76,7 @@ class RoomHeaderView extends Component {
render() { render() {
const { const {
title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height, parentTitle
} = this.props; } = this.props;
let subtitle; let subtitle;
@ -103,6 +104,7 @@ class RoomHeaderView extends Component {
roomUserId={roomUserId} roomUserId={roomUserId}
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
connecting={connecting} connecting={connecting}
parentTitle={parentTitle}
/> />
); );
} }
@ -111,10 +113,12 @@ class RoomHeaderView extends Component {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let statusText; let statusText;
let status = 'offline'; let status = 'offline';
const { roomUserId, type, visitor = {} } = ownProps; const {
roomUserId, type, visitor = {}, tmid
} = ownProps;
if (state.meteor.connected) { if (state.meteor.connected) {
if (type === 'd' && state.activeUsers[roomUserId]) { if ((type === 'd' || (tmid && roomUserId)) && state.activeUsers[roomUserId]) {
({ status, statusText } = state.activeUsers[roomUserId]); ({ status, statusText } = state.activeUsers[roomUserId]);
} else if (type === 'l' && visitor?.status) { } else if (type === 'l' && visitor?.status) {
({ status } = visitor); ({ status } = visitor);

View File

@ -30,7 +30,8 @@ class List extends React.Component {
loading: PropTypes.bool, loading: PropTypes.bool,
listRef: PropTypes.func, listRef: PropTypes.func,
hideSystemMessages: PropTypes.array, hideSystemMessages: PropTypes.array,
navigation: PropTypes.object navigation: PropTypes.object,
showMessageInMainThread: PropTypes.bool
}; };
// this.state.loading works for this.onEndReached and RoomView.init // this.state.loading works for this.onEndReached and RoomView.init
@ -140,7 +141,7 @@ class List extends React.Component {
query = async() => { query = async() => {
this.count += QUERY_SIZE; this.count += QUERY_SIZE;
const { rid, tmid } = this.props; const { rid, tmid, showMessageInMainThread } = this.props;
const db = database.active; const db = database.active;
// handle servers with version < 3.0.0 // handle servers with version < 3.0.0
@ -167,14 +168,23 @@ class List extends React.Component {
) )
.observe(); .observe();
} else if (rid) { } else if (rid) {
const whereClause = [
Q.where('rid', rid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
];
if (!showMessageInMainThread) {
whereClause.push(
Q.or(
Q.where('tmid', null),
Q.where('tshow', Q.eq(true))
)
);
}
this.messagesObservable = db.collections this.messagesObservable = db.collections
.get('messages') .get('messages')
.query( .query(...whereClause)
Q.where('rid', rid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
)
.observe(); .observe();
} }

View File

@ -35,7 +35,7 @@ const styles = StyleSheet.create({
}); });
const DateSeparator = React.memo(({ ts, unread, theme }) => { const DateSeparator = React.memo(({ ts, unread, theme }) => {
const date = ts ? moment(ts).format('MMM DD, YYYY') : null; const date = ts ? moment(ts).format('LL') : null;
const unreadLine = { backgroundColor: themes[theme].dangerColor }; const unreadLine = { backgroundColor: themes[theme].dangerColor };
const unreadText = { color: themes[theme].dangerColor }; const unreadText = { color: themes[theme].dangerColor };
if (ts && unread) { if (ts && unread) {

View File

@ -35,7 +35,7 @@ import { themes } from '../../constants/colors';
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';
import { isBlocked } from '../../utils/room'; import { getBadgeColor, isBlocked, makeThreadName } from '../../utils/room';
import { isReadOnly } from '../../utils/isReadOnly'; import { isReadOnly } from '../../utils/isReadOnly';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
import { showErrorAlert } from '../../utils/info'; import { showErrorAlert } from '../../utils/info';
@ -82,7 +82,8 @@ class RoomView extends React.Component {
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired token: PropTypes.string.isRequired,
showMessageInMainThread: PropTypes.bool
}), }),
appState: PropTypes.string, appState: PropTypes.string,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
@ -108,17 +109,18 @@ class RoomView extends React.Component {
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
this.t = props.route.params?.t; this.t = props.route.params?.t;
this.tmid = props.route.params?.tmid; this.tmid = props.route.params?.tmid;
const room = props.route.params?.room;
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 search = props.route.params?.search;
const prid = props.route.params?.prid; const prid = props.route.params?.prid;
const room = props.route.params?.room ?? {
rid: this.rid, t: this.t, name, fname, prid
};
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
this.state = { this.state = {
joined: true, joined: true,
room: room || { room,
rid: this.rid, t: this.t, name, fname, prid
},
roomUpdate: {}, roomUpdate: {},
member: {}, member: {},
lastOpen: null, lastOpen: null,
@ -132,7 +134,7 @@ class RoomView extends React.Component {
reacting: false, reacting: false,
readOnly: false, readOnly: false,
unreadsCount: null, unreadsCount: null,
roomUserId: null roomUserId
}; };
this.setHeader(); this.setHeader();
@ -292,24 +294,28 @@ class RoomView extends React.Component {
} }
setHeader = () => { setHeader = () => {
const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state;
const { const {
navigation, route, isMasterDetail, theme, baseUrl, user, insets room, unreadsCount, roomUserId
} = this.state;
const {
navigation, isMasterDetail, theme, baseUrl, user, insets, route
} = this.props; } = this.props;
const rid = route.params?.rid; const { rid, tmid } = this;
const prid = route.params?.prid; const prid = room?.prid;
let title = route.params?.name; let title = route.params?.name;
if ((room.id || room.rid) && !this.tmid) { let parentTitle;
if ((room.id || room.rid) && !tmid) {
title = RocketChat.getRoomTitle(room); title = RocketChat.getRoomTitle(room);
} }
if (tmid) {
parentTitle = RocketChat.getRoomTitle(room);
}
const subtitle = room?.topic; const subtitle = room?.topic;
const t = route.params?.t || room?.t; const t = room?.t;
const tmid = route.params?.tmid;
const { id: userId, token } = user; const { id: userId, token } = user;
const avatar = room?.name; const avatar = room?.name;
const roomUserId = route.params?.roomUserId || stateRoomUserId;
const visitor = room?.visitor; const visitor = room?.visitor;
if (!rid) { if (!room?.rid) {
return; return;
} }
const headerTitlePosition = getHeaderTitlePosition(insets); const headerTitlePosition = getHeaderTitlePosition(insets);
@ -341,6 +347,7 @@ class RoomView extends React.Component {
prid={prid} prid={prid}
tmid={tmid} tmid={tmid}
title={title} title={title}
parentTitle={parentTitle}
subtitle={subtitle} subtitle={subtitle}
type={t} type={t}
roomUserId={roomUserId} roomUserId={roomUserId}
@ -625,6 +632,7 @@ class RoomView extends React.Component {
}; };
onThreadPress = debounce(async(item) => { onThreadPress = debounce(async(item) => {
const { roomUserId } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
if (item.tmid) { if (item.tmid) {
if (!item.tmsg) { if (!item.tmsg) {
@ -635,11 +643,11 @@ class RoomView extends React.Component {
name = I18n.t('Encrypted_message'); name = I18n.t('Encrypted_message');
} }
navigation.push('RoomView', { navigation.push('RoomView', {
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread' rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId
}); });
} else if (item.tlm) { } else if (item.tlm) {
navigation.push('RoomView', { navigation.push('RoomView', {
rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread' rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
}); });
} }
}, 1000, true) }, 1000, true)
@ -669,10 +677,10 @@ class RoomView extends React.Component {
this.setState(...args); this.setState(...args);
} }
sendMessage = (message, tmid) => { sendMessage = (message, tmid, tshow) => {
logEvent(events.ROOM_SEND_MESSAGE); logEvent(events.ROOM_SEND_MESSAGE);
const { user } = this.props; const { user } = this.props;
RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user).then(() => { RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user, tshow).then(() => {
if (this.list && this.list.current) { if (this.list && this.list.current) {
this.list.current.update(); this.list.current.update();
} }
@ -761,15 +769,21 @@ class RoomView extends React.Component {
} }
} }
toggleFollowThread = async(isFollowingThread) => { toggleFollowThread = async(isFollowingThread, tmid) => {
try { try {
await RocketChat.toggleFollowMessage(this.tmid, !isFollowingThread); await RocketChat.toggleFollowMessage(tmid ?? this.tmid, !isFollowingThread);
EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') }); EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') });
} catch (e) { } catch (e) {
log(e); log(e);
} }
} }
getBadgeColor = (messageId) => {
const { room } = this.state;
const { theme } = this.props;
return getBadgeColor({ subscription: room, theme, messageId });
}
navToRoomInfo = (navParam) => { navToRoomInfo = (navParam) => {
const { navigation, user, isMasterDetail } = this.props; const { navigation, user, isMasterDetail } = this.props;
logEvent(events[`ROOM_GO_${ navParam.t === 'd' ? 'USER' : 'ROOM' }_INFO`]); logEvent(events[`ROOM_GO_${ navParam.t === 'd' ? 'USER' : 'ROOM' }_INFO`]);
@ -895,6 +909,8 @@ class RoomView extends React.Component {
getCustomEmoji={this.getCustomEmoji} getCustomEmoji={this.getCustomEmoji}
callJitsi={this.callJitsi} callJitsi={this.callJitsi}
blockAction={this.blockAction} blockAction={this.blockAction}
getBadgeColor={this.getBadgeColor}
toggleFollowThread={this.toggleFollowThread}
/> />
); );
@ -1042,6 +1058,7 @@ class RoomView extends React.Component {
loading={loading} loading={loading}
navigation={navigation} navigation={navigation}
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages} hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
showMessageInMainThread={user.showMessageInMainThread}
/> />
{this.renderFooter()} {this.renderFooter()}
{this.renderActions()} {this.renderActions()}

View File

@ -75,7 +75,7 @@ const GROUPS_HEADER = 'Private_Groups';
const OMNICHANNEL_HEADER = 'Open_Livechats'; const OMNICHANNEL_HEADER = 'Open_Livechats';
const QUERY_SIZE = 20; const QUERY_SIZE = 20;
const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus; const filterIsUnread = s => (s.unread > 0 || s.tunread?.length > 0 || s.alert) && !s.hideUnreadStatus;
const filterIsFavorite = s => s.f; const filterIsFavorite = s => s.f;
const filterIsOmnichannel = s => s.t === 'l'; const filterIsOmnichannel = s => s.t === 'l';

View File

@ -63,8 +63,6 @@ export default StyleSheet.create({
marginHorizontal: 12 marginHorizontal: 12
}, },
queueIcon: { queueIcon: {
width: 22,
height: 22,
marginHorizontal: 12 marginHorizontal: 12
}, },
groupTitleContainer: { groupTitleContainer: {

View File

@ -149,7 +149,7 @@ class SearchMessagesView extends React.Component {
item={item} item={item}
baseUrl={baseUrl} baseUrl={baseUrl}
user={user} user={user}
timeFormat='MMM Do YYYY, h:mm:ss a' timeFormat='LLL'
isHeader isHeader
showAttachment={() => {}} showAttachment={() => {}}
getCustomEmoji={this.getCustomEmoji} getCustomEmoji={this.getCustomEmoji}

View File

@ -9,6 +9,7 @@ import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { isAndroid, isTablet } from '../../utils/deviceInfo'; import { isAndroid, isTablet } from '../../utils/deviceInfo';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { makeThreadName } from '../../utils/room';
const androidMarginLeft = isTablet ? 0 : 4; const androidMarginLeft = isTablet ? 0 : 4;
@ -64,6 +65,13 @@ const Header = React.memo(({ room, thread, theme }) => {
const textColor = themes[theme].previewTintColor; const textColor = themes[theme].previewTintColor;
let title;
if (thread?.id) {
title = makeThreadName(thread);
} else {
title = RocketChat.getRoomTitle(room);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.inner}> <View style={styles.inner}>
@ -78,7 +86,7 @@ const Header = React.memo(({ room, thread, theme }) => {
style={[styles.name, { color: textColor }]} style={[styles.name, { color: textColor }]}
numberOfLines={1} numberOfLines={1}
> >
{thread?.msg ?? RocketChat.getRoomTitle(room)} {title}
</Text> </Text>
</Text> </Text>
</View> </View>

View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles';
const styles = StyleSheet.create({
container: {
paddingVertical: 8,
minHeight: 40,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center'
},
text: {
flex: 1,
fontSize: 16,
...sharedStyles.textRegular
}
});
const DropdownItem = React.memo(({
theme, onPress, iconName, text
}) => (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
{iconName ? <CustomIcon name={iconName} size={22} color={themes[theme].auxiliaryText} /> : null}
</View>
</Touch>
));
DropdownItem.propTypes = {
text: PropTypes.string,
iconName: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
};
export default withTheme(DropdownItem);

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import DropdownItem from './DropdownItem';
import I18n from '../../../i18n';
const DropdownItemFilter = ({ currentFilter, value, onPress }) => (
<DropdownItem
text={I18n.t(value)}
iconName={currentFilter === value ? 'check' : null}
onPress={() => onPress(value)}
/>
);
DropdownItemFilter.propTypes = {
currentFilter: PropTypes.string,
value: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemFilter;

View File

@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import DropdownItem from './DropdownItem';
import { FILTER } from '../filters';
import I18n from '../../../i18n';
const DropdownItemHeader = ({ currentFilter, onPress }) => {
let text;
switch (currentFilter) {
case FILTER.FOLLOWING:
text = I18n.t('Threads_displaying_following');
break;
case FILTER.UNREAD:
text = I18n.t('Threads_displaying_unread');
break;
default:
text = I18n.t('Threads_displaying_all');
break;
}
return <DropdownItem text={text} iconName='filter' onPress={onPress} />;
};
DropdownItemHeader.propTypes = {
currentFilter: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemHeader;

View File

@ -0,0 +1,105 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Animated, Easing, TouchableWithoutFeedback
} from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import { headerHeight } from '../../../containers/Header';
import * as List from '../../../containers/List';
import { FILTER } from '../filters';
import DropdownItemFilter from './DropdownItemFilter';
import DropdownItemHeader from './DropdownItemHeader';
const ANIMATION_DURATION = 200;
class Dropdown extends React.Component {
static propTypes = {
isMasterDetail: PropTypes.bool,
theme: PropTypes.string,
insets: PropTypes.object,
currentFilter: PropTypes.string,
onClose: PropTypes.func,
onFilterSelected: PropTypes.func
}
constructor(props) {
super(props);
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: ANIMATION_DURATION,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true
}
).start();
}
close = () => {
const { onClose } = this.props;
Animated.timing(
this.animatedValue,
{
toValue: 0,
duration: ANIMATION_DURATION,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true
}
).start(() => onClose());
}
render() {
const {
isMasterDetail, insets, theme, currentFilter, onFilterSelected
} = this.props;
const statusBarHeight = insets?.top ?? 0;
const heightDestination = isMasterDetail ? headerHeight + statusBarHeight : 0;
const translateY = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-300, heightDestination] // approximated height of the component when closed/open
});
const backdropOpacity = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.3]
});
return (
<>
<TouchableWithoutFeedback onPress={this.close}>
<Animated.View style={[styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity: backdropOpacity,
top: heightDestination
}]}
/>
</TouchableWithoutFeedback>
<Animated.View
style={[
styles.dropdownContainer,
{
transform: [{ translateY }],
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor
}
]}
>
<DropdownItemHeader currentFilter={currentFilter} onPress={this.close} />
<List.Separator />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.ALL} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.FOLLOWING} onPress={onFilterSelected} />
<DropdownItemFilter currentFilter={currentFilter} value={FILTER.UNREAD} onPress={onFilterSelected} />
</Animated.View>
</>
);
}
}
export default withTheme(withSafeAreaInsets(Dropdown));

View File

@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import { withTheme } from '../../theme';
import Avatar from '../../containers/Avatar';
import Touch from '../../utils/touch';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
import Markdown from '../../containers/markdown';
import { CustomIcon } from '../../lib/Icons';
import { formatDateThreads, makeThreadName } from '../../utils/room';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 16
},
contentContainer: {
flexDirection: 'column',
flex: 1
},
titleContainer: {
flexDirection: 'row',
marginBottom: 2,
alignItems: 'center'
},
title: {
flexShrink: 1,
fontSize: 18,
...sharedStyles.textMedium
},
time: {
fontSize: 14,
marginLeft: 4,
...sharedStyles.textRegular
},
avatar: {
marginRight: 8
},
detailsContainer: {
marginTop: 8,
flexDirection: 'row'
},
detailContainer: {
marginRight: 8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
detailText: {
fontSize: 10,
marginLeft: 2,
...sharedStyles.textSemibold
},
badgeContainer: {
marginLeft: 8,
justifyContent: 'center'
},
badge: {
width: 12,
height: 12,
borderRadius: 6
}
});
const Item = ({
item, baseUrl, theme, useRealName, user, badgeColor, onPress
}) => {
const username = (useRealName && item?.u?.name) || item?.u?.username;
let time;
if (item?.ts) {
time = formatDateThreads(item.ts);
}
let tlm;
if (item?.tlm) {
tlm = formatDateThreads(item.tlm);
}
return (
<Touch theme={theme} onPress={() => onPress(item)} testID={`thread-messages-view-${ item.msg }`} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Avatar
style={styles.avatar}
text={item?.u?.username}
size={36}
borderRadius={4}
baseUrl={baseUrl}
userId={user?.id}
token={user?.token}
theme={theme}
/>
<View style={styles.contentContainer}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>{username}</Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View>
<Markdown msg={makeThreadName(item)} baseUrl={baseUrl} username={username} theme={theme} numberOfLines={2} preview />
<View style={styles.detailsContainer}>
<View style={styles.detailContainer}>
<CustomIcon name='threads' size={20} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{item?.tcount}</Text>
</View>
<View style={styles.detailContainer}>
<CustomIcon name='user' size={20} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{item?.replies?.length}</Text>
</View>
<View style={styles.detailContainer}>
<CustomIcon name='clock' size={20} color={themes[theme].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme].auxiliaryText }]}>{tlm}</Text>
</View>
</View>
</View>
{badgeColor
? (
<View style={styles.badgeContainer}>
<View style={[styles.badge, { backgroundColor: badgeColor }]} />
</View>
)
: null}
</View>
</Touch>
);
};
Item.propTypes = {
item: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string,
useRealName: PropTypes.bool,
user: PropTypes.object,
badgeColor: PropTypes.string,
onPress: PropTypes.func
};
export default withTheme(Item);

View File

@ -0,0 +1,138 @@
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types */
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { ScrollView } from 'react-native';
import { combineReducers, createStore } from 'redux';
import { Provider } from 'react-redux';
import Item from './Item';
import * as List from '../../containers/List';
import { themes } from '../../constants/colors';
import { ThemeContext } from '../../theme';
const author = {
_id: 'userid',
username: 'rocket.cat',
name: 'Rocket Cat'
};
const baseUrl = 'https://open.rocket.chat';
const date = new Date(2020, 10, 10, 10);
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const defaultItem = {
msg: 'Message content',
tcount: 1,
replies: [1],
ts: date,
tlm: date,
u: author,
attachments: []
};
const BaseItem = ({ item, ...props }) => (
<Item
baseUrl={baseUrl}
item={{
...defaultItem,
...item
}}
onPress={() => alert('pressed')}
{...props}
/>
);
const listDecorator = story => (
<ScrollView>
<List.Separator />
{story()}
<List.Separator />
</ScrollView>
);
const reducers = combineReducers({
login: () => ({
user: {
id: 'abc',
username: 'rocket.cat',
name: 'Rocket Cat'
}
}),
share: () => ({
server: 'https://open.rocket.chat'
}),
settings: () => ({
blockUnauthenticatedAccess: false
})
});
const store = createStore(reducers);
const stories = storiesOf('Thread Messages.Item', module)
.addDecorator(listDecorator)
.addDecorator(story => <Provider store={store}>{story()}</Provider>);
stories.add('content', () => (
<>
<BaseItem />
<List.Separator />
<BaseItem
item={{
msg: longText
}}
/>
<List.Separator />
<BaseItem
item={{
tcount: 1000,
replies: [...new Array(1000)]
}}
/>
<List.Separator />
<BaseItem
item={{
msg: '',
attachments: [{ title: 'Attachment title' }]
}}
/>
<List.Separator />
<BaseItem useRealName />
</>
));
stories.add('badge', () => (
<>
<BaseItem
badgeColor={themes.light.mentionMeBackground}
/>
<List.Separator />
<BaseItem
badgeColor={themes.light.mentionGroupBackground}
/>
<List.Separator />
<BaseItem
badgeColor={themes.light.tunreadBackground}
/>
<BaseItem
item={{
msg: longText
}}
badgeColor={themes.light.tunreadBackground}
/>
</>
));
const ThemeStory = ({ theme }) => (
<ThemeContext.Provider
value={{ theme }}
>
<BaseItem
badgeColor={themes[theme].mentionMeBackground}
/>
</ThemeContext.Provider>
);
stories.add('themes', () => (
<>
<ThemeStory theme='light' />
<ThemeStory theme='dark' />
<ThemeStory theme='black' />
</>
));

View File

@ -0,0 +1,43 @@
import React from 'react';
import {
ImageBackground, StyleSheet, Text, View
} from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
container: {
flex: 1
},
image: {
width: '100%',
height: '100%',
position: 'absolute'
},
text: {
position: 'absolute',
top: 60,
left: 0,
right: 0,
textAlign: 'center',
fontSize: 16,
paddingHorizontal: 24,
...sharedStyles.textRegular
}
});
const EmptyRoom = ({ theme, text }) => (
<View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${ theme }` }} style={styles.image} />
<Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text>
</View>
);
EmptyRoom.propTypes = {
text: PropTypes.string,
theme: PropTypes.string
};
export default withTheme(EmptyRoom);

View File

@ -0,0 +1,49 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
import TextInput from '../../presentation/TextInput';
import { isTablet, isIOS } from '../../utils/deviceInfo';
import { useOrientation } from '../../dimensions';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
marginLeft: 0
},
title: {
...sharedStyles.textSemibold
}
});
// TODO: it might be useful to refactor this component for reusage
const SearchHeader = ({ theme, onSearchChangeText }) => {
const titleColorStyle = { color: themes[theme].headerTitleColor };
const isLight = theme === 'light';
const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
const titleFontSize = 16 * scale;
return (
<View style={styles.container}>
<TextInput
autoFocus
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]}
placeholder='Search'
onChangeText={onSearchChangeText}
theme={theme}
testID='thread-messages-view-search-header'
/>
</View>
);
};
SearchHeader.propTypes = {
theme: PropTypes.string,
onSearchChangeText: PropTypes.func
};
export default withTheme(SearchHeader);

View File

@ -0,0 +1,5 @@
export const FILTER = {
ALL: 'All',
FOLLOWING: 'Following',
UNREAD: 'Unread'
};

View File

@ -1,20 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { FlatList, InteractionManager } from 'react-native';
FlatList, View, Text, InteractionManager
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import moment from 'moment';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton } from '@react-navigation/stack';
import styles from './styles'; import styles from './styles';
import Message from '../../containers/message'; import Item from './Item';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database'; import database from '../../lib/database';
import { sanitizeLikeString } from '../../lib/database/utils';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import buildMessage from '../../lib/methods/helpers/buildMessage'; import buildMessage from '../../lib/methods/helpers/buildMessage';
import log from '../../utils/log'; import log from '../../utils/log';
@ -25,25 +24,19 @@ import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List';
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />); import Dropdown from './Dropdown';
Separator.propTypes = { import DropdownItemHeader from './Dropdown/DropdownItemHeader';
theme: PropTypes.string import { FILTER } from './filters';
}; import NoDataFound from './NoDataFound';
import { isIOS } from '../../utils/deviceInfo';
import { getBadgeColor, makeThreadName } from '../../utils/room';
import { getHeaderTitlePosition } from '../../containers/Header';
import SearchHeader from './SearchHeader';
const API_FETCH_COUNT = 50; const API_FETCH_COUNT = 50;
class ThreadMessagesView extends React.Component { class ThreadMessagesView extends React.Component {
static navigationOptions = ({ navigation, isMasterDetail }) => {
const options = {
title: I18n.t('Threads')
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
return options;
}
static propTypes = { static propTypes = {
user: PropTypes.object, user: PropTypes.object,
navigation: PropTypes.object, navigation: PropTypes.object,
@ -51,8 +44,8 @@ class ThreadMessagesView extends React.Component {
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
customEmojis: PropTypes.object, isMasterDetail: PropTypes.bool,
isMasterDetail: PropTypes.bool insets: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -63,9 +56,17 @@ class ThreadMessagesView extends React.Component {
this.state = { this.state = {
loading: false, loading: false,
end: false, end: false,
messages: [] messages: [],
displayingThreads: [],
subscription: {},
showFilterDropdown: false,
currentFilter: FILTER.ALL,
isSearching: false,
searchText: ''
}; };
this.subscribeData(); this.setHeader();
this.initSubscription();
this.subscribeMessages();
} }
componentDidMount() { componentDidMount() {
@ -75,6 +76,15 @@ class ThreadMessagesView extends React.Component {
}); });
} }
componentDidUpdate(prevProps) {
const {
insets
} = this.props;
if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) {
this.setHeader();
}
}
componentWillUnmount() { componentWillUnmount() {
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
if (this.mountInteraction && this.mountInteraction.cancel) { if (this.mountInteraction && this.mountInteraction.cancel) {
@ -91,48 +101,133 @@ class ThreadMessagesView extends React.Component {
} }
} }
// eslint-disable-next-line react/sort-comp getHeader = () => {
subscribeData = async() => { const { isSearching } = this.state;
const {
navigation, isMasterDetail, insets, theme
} = this.props;
const headerTitlePosition = getHeaderTitlePosition(insets);
if (isSearching) {
return {
headerTitleAlign: 'left',
headerLeft: () => (
<HeaderButton.Container left>
<HeaderButton.Item
iconName='close'
onPress={this.onCancelSearchPress}
/>
</HeaderButton.Container>
),
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />,
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerRight: () => null
};
}
const options = {
headerLeft: () => (
<HeaderBackButton
labelVisible={false}
onPress={() => navigation.pop()}
tintColor={themes[theme].headerTintColor}
/>
),
headerTitleAlign: 'center',
headerTitle: I18n.t('Threads'),
headerTitleContainerStyle: {
left: null,
right: null
}
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
options.headerRight = () => (
<HeaderButton.Container>
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
</HeaderButton.Container>
);
return options;
}
setHeader = () => {
const { navigation } = this.props;
const options = this.getHeader();
navigation.setOptions(options);
}
initSubscription = async() => {
try { try {
const db = database.active; const db = database.active;
// subscription query
const subscription = await db.collections const subscription = await db.collections
.get('subscriptions') .get('subscriptions')
.find(this.rid); .find(this.rid);
const observable = subscription.observe(); const observable = subscription.observe();
this.subSubscription = observable this.subSubscription = observable
.subscribe((data) => { .subscribe((data) => {
this.subscription = data; this.setState({ subscription: data });
});
this.messagesObservable = db.collections
.get('threads')
.query(
Q.where('rid', this.rid),
Q.where('t', Q.notEq('rm'))
)
.observeWithColumns(['updated_at']);
this.messagesSubscription = this.messagesObservable
.subscribe((data) => {
const messages = orderBy(data, ['ts'], ['desc']);
if (this.mounted) {
this.setState({ messages });
} else {
this.state.messages = messages;
}
}); });
this.subscribeMessages(subscription);
} catch (e) { } catch (e) {
// Do nothing log(e);
}
}
subscribeMessages = (subscription, searchText) => {
try {
const db = database.active;
if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
this.messagesSubscription.unsubscribe();
}
const whereClause = [
Q.where('rid', this.rid),
Q.experimentalSortBy('tlm', Q.desc)
];
if (searchText?.trim()) {
whereClause.push(Q.where('msg', Q.like(`%${ sanitizeLikeString(searchText.trim()) }%`)));
}
this.messagesObservable = db.collections
.get('threads')
.query(...whereClause)
.observeWithColumns(['updated_at']);
this.messagesSubscription = this.messagesObservable
.subscribe((messages) => {
const { currentFilter } = this.state;
const displayingThreads = this.getFilteredThreads(messages, subscription, currentFilter);
if (this.mounted) {
this.setState({ messages, displayingThreads });
} else {
this.state.messages = messages;
this.state.displayingThreads = displayingThreads;
}
});
} catch (e) {
log(e);
} }
} }
// eslint-disable-next-line react/sort-comp
init = () => { init = () => {
if (!this.subscription) { const { subscription } = this.state;
if (!subscription) {
return this.load(); return this.load();
} }
try { try {
const lastThreadSync = new Date(); const lastThreadSync = new Date();
if (this.subscription.lastThreadSync) { if (subscription.lastThreadSync) {
this.sync(this.subscription.lastThreadSync); this.sync(subscription.lastThreadSync);
} else { } else {
this.load(lastThreadSync); this.load(lastThreadSync);
} }
@ -142,9 +237,10 @@ class ThreadMessagesView extends React.Component {
} }
updateThreads = async({ update, remove, lastThreadSync }) => { updateThreads = async({ update, remove, lastThreadSync }) => {
const { subscription } = this.state;
// if there's no subscription, manage data on this.state.messages // if there's no subscription, manage data on this.state.messages
// note: sync will never be called without subscription // note: sync will never be called without subscription
if (!this.subscription) { if (!subscription) {
this.setState(({ messages }) => ({ messages: [...messages, ...update] })); this.setState(({ messages }) => ({ messages: [...messages, ...update] }));
return; return;
} }
@ -152,7 +248,7 @@ class ThreadMessagesView extends React.Component {
try { try {
const db = database.active; const db = database.active;
const threadsCollection = db.collections.get('threads'); const threadsCollection = db.collections.get('threads');
const allThreadsRecords = await this.subscription.threads.fetch(); const allThreadsRecords = await subscription.threads.fetch();
let threadsToCreate = []; let threadsToCreate = [];
let threadsToUpdate = []; let threadsToUpdate = [];
let threadsToDelete = []; let threadsToDelete = [];
@ -164,7 +260,7 @@ class ThreadMessagesView extends React.Component {
threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id)); threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => { threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => {
t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema); t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema);
t.subscription.set(this.subscription); t.subscription.set(subscription);
Object.assign(t, thread); Object.assign(t, thread);
}))); })));
threadsToUpdate = threadsToUpdate.map((thread) => { threadsToUpdate = threadsToUpdate.map((thread) => {
@ -185,7 +281,7 @@ class ThreadMessagesView extends React.Component {
...threadsToCreate, ...threadsToCreate,
...threadsToUpdate, ...threadsToUpdate,
...threadsToDelete, ...threadsToDelete,
this.subscription.prepareUpdate((s) => { subscription.prepareUpdate((s) => {
s.lastThreadSync = lastThreadSync; s.lastThreadSync = lastThreadSync;
}) })
); );
@ -197,7 +293,9 @@ class ThreadMessagesView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
load = debounce(async(lastThreadSync) => { load = debounce(async(lastThreadSync) => {
const { loading, end, messages } = this.state; const {
loading, end, messages, searchText
} = this.state;
if (end || loading || !this.mounted) { if (end || loading || !this.mounted) {
return; return;
} }
@ -206,7 +304,7 @@ class ThreadMessagesView extends React.Component {
try { try {
const result = await RocketChat.getThreadsList({ const result = await RocketChat.getThreadsList({
rid: this.rid, count: API_FETCH_COUNT, offset: messages.length rid: this.rid, count: API_FETCH_COUNT, offset: messages.length, text: searchText
}); });
if (result.success) { if (result.success) {
this.updateThreads({ update: result.threads, lastThreadSync }); this.updateThreads({ update: result.threads, lastThreadSync });
@ -244,112 +342,168 @@ class ThreadMessagesView extends React.Component {
} }
} }
formatMessage = lm => ( onSearchPress = () => {
lm ? moment(lm).calendar(null, { this.setState({ isSearching: true }, () => this.setHeader());
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null
)
getCustomEmoji = (name) => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {
return emoji;
}
return null;
} }
showAttachment = (attachment) => { onCancelSearchPress = () => {
const { navigation } = this.props; this.setState({ isSearching: false, searchText: '' }, () => {
navigation.navigate('AttachmentView', { attachment }); const { subscription } = this.state;
this.setHeader();
this.subscribeMessages(subscription);
});
} }
onSearchChangeText = debounce((searchText) => {
const { subscription } = this.state;
this.setState({ searchText }, () => this.subscribeMessages(subscription, searchText));
}, 300)
onThreadPress = debounce((item) => { onThreadPress = debounce((item) => {
const { subscription } = this.state;
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) { if (isMasterDetail) {
navigation.pop(); navigation.pop();
} }
navigation.push('RoomView', { navigation.push('RoomView', {
rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread' rid: item.subscription.id,
tmid: item.id,
name: makeThreadName(item),
t: 'thread',
roomUserId: RocketChat.getUidDirectMessage(subscription)
}); });
}, 1000, true) }, 1000, true)
renderSeparator = () => { getBadgeColor = (item) => {
const { subscription } = this.state;
const { theme } = this.props; const { theme } = this.props;
return <Separator theme={theme} />; return getBadgeColor({ subscription, theme, messageId: item?.id });
} }
renderEmpty = () => { // helper to query threads
const { theme } = this.props; getFilteredThreads = (messages, subscription, currentFilter) => {
return ( // const { currentFilter } = this.state;
<View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]} testID='thread-messages-view'> const { user } = this.props;
<Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_thread_messages')}</Text> if (currentFilter === FILTER.FOLLOWING) {
</View> return messages?.filter(item => item?.replies?.find(u => u === user.id));
); } else if (currentFilter === FILTER.UNREAD) {
} return messages?.filter(item => subscription?.tunread?.includes(item?.id));
navToRoomInfo = (navParam) => {
const { navigation, user } = this.props;
if (navParam.rid === user.id) {
return;
} }
navigation.navigate('RoomInfoView', navParam); return messages;
}
// method to update state with filtered threads
filterThreads = () => {
const { messages, subscription } = this.state;
const displayingThreads = this.getFilteredThreads(messages, subscription);
this.setState({ displayingThreads });
}
showFilterDropdown = () => this.setState({ showFilterDropdown: true })
closeFilterDropdown = () => this.setState({ showFilterDropdown: false })
onFilterSelected = (filter) => {
const { messages, subscription } = this.state;
const displayingThreads = this.getFilteredThreads(messages, subscription, filter);
this.setState({ currentFilter: filter, displayingThreads });
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { const {
user, navigation, baseUrl, useRealName user, navigation, baseUrl, useRealName
} = this.props; } = this.props;
const badgeColor = this.getBadgeColor(item);
return ( return (
<Message <Item
key={item.id} {...{
item={item} item,
user={user} user,
archived={false} navigation,
broadcast={false} baseUrl,
status={item.status} useRealName,
navigation={navigation} badgeColor
timeFormat='MMM D' }}
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a' onPress={this.onThreadPress}
onThreadPress={this.onThreadPress} />
baseUrl={baseUrl} );
useRealName={useRealName} }
getCustomEmoji={this.getCustomEmoji}
navToRoomInfo={this.navToRoomInfo} renderHeader = () => {
showAttachment={this.showAttachment} const { messages, currentFilter } = this.state;
if (!messages.length) {
return null;
}
return (
<>
<DropdownItemHeader currentFilter={currentFilter} onPress={this.showFilterDropdown} />
<List.Separator />
</>
);
}
renderContent = () => {
const {
loading, messages, displayingThreads, currentFilter
} = this.state;
const { theme } = this.props;
if (!messages?.length || !displayingThreads?.length) {
let text;
if (currentFilter === FILTER.FOLLOWING) {
text = I18n.t('No_threads_following');
} else if (currentFilter === FILTER.UNREAD) {
text = I18n.t('No_threads_unread');
} else {
text = I18n.t('No_threads');
}
return (
<>
{this.renderHeader()}
<NoDataFound text={text} />
</>
);
}
return (
<FlatList
data={displayingThreads}
extraData={this.state}
renderItem={this.renderItem}
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
contentContainerStyle={styles.contentContainer}
onEndReached={this.load}
onEndReachedThreshold={0.5}
maxToRenderPerBatch={5}
windowSize={10}
initialNumToRender={7}
removeClippedSubviews={isIOS}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={this.renderHeader}
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
/> />
); );
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { loading, messages } = this.state; const { showFilterDropdown, currentFilter } = this.state;
const { theme } = this.props;
if (!loading && messages.length === 0) {
return this.renderEmpty();
}
return ( return (
<SafeAreaView testID='thread-messages-view'> <SafeAreaView testID='thread-messages-view'>
<StatusBar /> <StatusBar />
<FlatList {this.renderContent()}
data={messages} {showFilterDropdown
extraData={this.state} ? (
renderItem={this.renderItem} <Dropdown
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} currentFilter={currentFilter}
contentContainerStyle={styles.contentContainer} onFilterSelected={this.onFilterSelected}
keyExtractor={item => item._id} onClose={this.closeFilterDropdown}
onEndReached={this.load} />
onEndReachedThreshold={0.5} )
maxToRenderPerBatch={5} : null}
initialNumToRender={1}
ItemSeparatorComponent={this.renderSeparator}
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
/>
</SafeAreaView> </SafeAreaView>
); );
} }
@ -359,8 +513,7 @@ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name, useRealName: state.settings.UI_Use_Real_Name,
customEmojis: state.customEmojis,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(withTheme(ThreadMessagesView)); export default connect(mapStateToProps)(withTheme(withSafeAreaInsets(ThreadMessagesView)));

View File

@ -18,10 +18,13 @@ export default StyleSheet.create({
contentContainer: { contentContainer: {
paddingBottom: 30 paddingBottom: 30
}, },
separator: { dropdownContainer: {
height: StyleSheet.hairlineWidth,
width: '100%', width: '100%',
marginLeft: 60, position: 'absolute',
marginTop: 10 top: 0,
borderBottomWidth: StyleSheet.hairlineWidth
},
backdrop: {
...StyleSheet.absoluteFill
} }
}); });

View File

@ -54,9 +54,9 @@ async function logout() {
} }
async function mockMessage(message) { async function mockMessage(message) {
await element(by.id('messagebox-input')).tap(); await element(by.id('messagebox-input')).atIndex(0).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`); await element(by.id('messagebox-input')).atIndex(0).typeText(`${ data.random }${ message }`);
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).atIndex(0).tap();
await waitFor(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist().withTimeout(60000); await waitFor(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist().withTimeout(60000);
await expect(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist(); await expect(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist();
await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap(); await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap();

View File

@ -294,24 +294,42 @@ describe('Room screen', () => {
await element(by.id('room-view-header-follow')).tap(); await element(by.id('room-view-header-follow')).tap();
await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000); await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view-header-unfollow'))).toExist(); await expect(element(by.id('room-view-header-unfollow'))).toExist();
await tapBack();
}); });
it('should navigate to thread from thread name', async() => { it('should send message in thread only', async() => {
const messageText = 'threadonly';
await mockMessage(messageText);
await tapBack();
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000); await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :( await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await waitFor(element(by.label(`${ data.random }${ messageText }`)).atIndex(0)).toNotExist().withTimeout(2000);
});
await mockMessage('dummymessagebetweenthethread'); it('should mark send to channel and show on main channel', async() => {
await element(by.label(thread)).atIndex(0).longPress(); const messageText = 'sendToChannel';
await expect(element(by.id('action-sheet'))).toExist(); await element(by.id(`message-thread-button-${ thread }`)).tap();
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('messagebox-input')).atIndex(0).typeText(messageText);
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('messagebox-send-to-channel')).tap();
await element(by.label('Reply in Thread')).tap();
await element(by.id('messagebox-input')).typeText('repliedagain');
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000); await tapBack();
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toExist(); await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await waitFor(element(by.label(messageText)).atIndex(0)).toExist().withTimeout(2000);
});
it('should navigate to thread from thread name', async() => {
const messageText = 'navthreadname';
await mockMessage('dummymessagebetweenthethread');
await element(by.id(`message-thread-button-${ thread }`)).tap();
await element(by.id('messagebox-input')).atIndex(0).typeText(messageText);
await element(by.id('messagebox-send-to-channel')).tap();
await element(by.id('messagebox-send-message')).tap();
await tapBack();
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await element(by.id(`message-thread-replied-on-${ thread }`)).tap(); await element(by.id(`message-thread-replied-on-${ thread }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
@ -325,7 +343,7 @@ describe('Room screen', () => {
await element(by.id('room-view-header-threads')).tap(); await element(by.id('room-view-header-threads')).tap();
await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000);
await expect(element(by.id('thread-messages-view'))).toExist(); await expect(element(by.id('thread-messages-view'))).toExist();
await element(by.id(`message-thread-button-${ thread }`)).atIndex(0).tap(); await element(by.id(`thread-messages-view-${ thread }`)).atIndex(0).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toExist(); await expect(element(by.id(`room-view-title-${ thread }`))).toExist();

View File

@ -14,7 +14,7 @@ async function navigateToRoomInfo(type) {
room = privateRoomName; room = privateRoomName;
} }
await searchRoom(room); await searchRoom(room);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toExist().withTimeout(60000);
await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap(); await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
await element(by.id('room-view-header-actions')).tap(); await element(by.id('room-view-header-actions')).tap();
@ -167,6 +167,7 @@ describe('Room info screen', () => {
await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }new`); await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }new`);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await tapBack(); await tapBack();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`); await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`);

View File

@ -1396,7 +1396,7 @@
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.11.0; MARKETING_VERSION = 4.12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
@ -1433,7 +1433,7 @@
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.11.0; MARKETING_VERSION = 4.12.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.11.0</string> <string>4.12.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@ -19,7 +19,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.11.0</string> <string>4.12.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>

Binary file not shown.

View File

@ -129,12 +129,8 @@
"@babel/core": "^7.8.4", "@babel/core": "^7.8.4",
"@babel/plugin-proposal-decorators": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.8.4",
"@storybook/addon-actions": "5.3.19",
"@storybook/addon-links": "5.3.19",
"@storybook/addon-storyshots": "5.3.19", "@storybook/addon-storyshots": "5.3.19",
"@storybook/addons": "5.3.19",
"@storybook/react-native": "5.3.19", "@storybook/react-native": "5.3.19",
"@storybook/theming": "5.3.19",
"@types/react-native": "^0.62.7", "@types/react-native": "^0.62.7",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",

View File

@ -1,4 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';

View File

@ -5,7 +5,7 @@ import RNBootSplash from 'react-native-bootsplash';
import 'react-native-gesture-handler'; import 'react-native-gesture-handler';
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
// jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => jest.fn(() => null)); jest.mock('../app/lib/database', () => jest.fn(() => null)); // comment this line to make storybook work
RNBootSplash.hide(); RNBootSplash.hide();

View File

@ -106,7 +106,7 @@ const ThemeStory = ({ theme }) => (
right={() => ( right={() => (
<HeaderButton.Container> <HeaderButton.Container>
<HeaderButton.Item title='Threads' /> <HeaderButton.Item title='Threads' />
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={999} />} /> <HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
</HeaderButton.Container> </HeaderButton.Container>
)} )}
/> />

View File

@ -75,7 +75,11 @@ export default ({ theme }) => {
<RoomItem alert name='user mentions' unread={1} userMentions={1} /> <RoomItem alert name='user mentions' unread={1} userMentions={1} />
<RoomItem alert name='group mentions' unread={1} groupMentions={1} /> <RoomItem alert name='group mentions' unread={1} groupMentions={1} />
<RoomItem alert name='thread unread' tunread={[1]} /> <RoomItem alert name='thread unread' tunread={[1]} />
<RoomItem name='user mentions > group mentions' alert unread={1} userMentions={1} groupMentions={1} /> <RoomItem alert name='thread unread user' tunread={[1]} tunreadUser={[1]} />
<RoomItem alert name='thread unread group' tunread={[1]} tunreadGroup={[1]} />
<RoomItem name='user mentions priority 1' alert unread={1} userMentions={1} groupMentions={1} tunread={[1]} />
<RoomItem name='group mentions priority 2' alert unread={1} groupMentions={1} tunread={[1]} />
<RoomItem name='thread unread priority 3' alert unread={1} tunread={[1]} />
<Separator title='Last Message' /> <Separator title='Last Message' />
<RoomItem <RoomItem
@ -119,7 +123,7 @@ export default ({ theme }) => {
<RoomItem <RoomItem
showLastMessage showLastMessage
alert alert
unread={1000} tunread={[1]}
lastMessage={lastMessage} lastMessage={lastMessage}
/> />
</ScrollView> </ScrollView>

View File

@ -12,10 +12,12 @@ import UiKitModal from './UiKitModal';
import Markdown from './Markdown'; import Markdown from './Markdown';
import './HeaderButtons'; import './HeaderButtons';
import './UnreadBadge'; import './UnreadBadge';
import '../../app/views/ThreadMessagesView/Item.stories.js';
import Avatar from './Avatar'; import Avatar from './Avatar';
// import RoomViewHeader from './RoomViewHeader'; // import RoomViewHeader from './RoomViewHeader';
import MessageContext from '../../app/containers/message/Context'; import MessageContext from '../../app/containers/message/Context';
import { themes } from '../../app/constants/colors';
// MessageProvider // MessageProvider
const baseUrl = 'https://open.rocket.chat'; const baseUrl = 'https://open.rocket.chat';
@ -53,7 +55,8 @@ const messageDecorator = story => (
replyBroadcast: () => {}, replyBroadcast: () => {},
onReactionPress: () => {}, onReactionPress: () => {},
onDiscussionPress: () => {}, onDiscussionPress: () => {},
onReactionLongPress: () => {} onReactionLongPress: () => {},
getBadgeColor: () => themes.light.tunreadBackground
}} }}
> >
{story()} {story()}

View File

@ -2248,42 +2248,6 @@
dependencies: dependencies:
type-detect "4.0.8" type-detect "4.0.8"
"@storybook/addon-actions@5.3.19":
version "5.3.19"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.3.19.tgz#50548fa6e84bc79ad95233ce23ade4878fc7cfac"
integrity sha512-gXF29FFUgYlUoFf1DcVCmH1chg2ElaHWMmCi5h7aZe+g6fXBQw0UtEdJnYLMOqZCIiWoZyuf1ETD0RbNHPhRIw==
dependencies:
"@storybook/addons" "5.3.19"
"@storybook/api" "5.3.19"
"@storybook/client-api" "5.3.19"
"@storybook/components" "5.3.19"
"@storybook/core-events" "5.3.19"
"@storybook/theming" "5.3.19"
core-js "^3.0.1"
fast-deep-equal "^2.0.1"
global "^4.3.2"
polished "^3.3.1"
prop-types "^15.7.2"
react "^16.8.3"
react-inspector "^4.0.0"
uuid "^3.3.2"
"@storybook/addon-links@5.3.19":
version "5.3.19"
resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-5.3.19.tgz#3c23e886d44b56978ae254fed3bf8be54c877178"
integrity sha512-gn9u8lebREfRsyzxoDPG0O+kOf5aJ0BhzcCJGZZdqha0F6OWHhh8vJYZZvjJ/Qwze+Qt2zjrgWm+Q6+JLD8ugQ==
dependencies:
"@storybook/addons" "5.3.19"
"@storybook/client-logger" "5.3.19"
"@storybook/core-events" "5.3.19"
"@storybook/csf" "0.0.1"
"@storybook/router" "5.3.19"
core-js "^3.0.1"
global "^4.3.2"
prop-types "^15.7.2"
qs "^6.6.0"
ts-dedent "^1.1.0"
"@storybook/addon-storyshots@5.3.19": "@storybook/addon-storyshots@5.3.19":
version "5.3.19" version "5.3.19"
resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-5.3.19.tgz#cb07ac3cc20d3a399ed4b6758008e10f910691d0" resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-5.3.19.tgz#cb07ac3cc20d3a399ed4b6758008e10f910691d0"
@ -8464,14 +8428,6 @@ is-docker@^2.0.0:
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
is-dom@^1.0.9:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a"
integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==
dependencies:
is-object "^1.0.1"
is-window "^1.0.2"
is-dotfile@^1.0.0: is-dotfile@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@ -8608,11 +8564,6 @@ is-obj@^1.0.0:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
is-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
is-plain-obj@^1.0.0: is-plain-obj@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@ -8711,11 +8662,6 @@ is-weakset@^2.0.1:
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-window@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
is-windows@^1.0.2: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -12701,15 +12647,6 @@ react-hotkeys@2.0.0:
dependencies: dependencies:
prop-types "^15.6.1" prop-types "^15.6.1"
react-inspector@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8"
integrity sha512-xSiM6CE79JBqSj8Fzd9dWBHv57tLTH7OM57GP3VrE5crzVF3D5Khce9w1Xcw75OAbvrA0Mi2vBneR1OajKmXFg==
dependencies:
"@babel/runtime" "^7.6.3"
is-dom "^1.0.9"
prop-types "^15.6.1"
react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"