[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:
parent
81bb89da6c
commit
6271b885ee
|
@ -1,5 +1,5 @@
|
|||
export const RectButton = () => 'View';
|
||||
export const RectButton = ({ children }) => children;
|
||||
export const State = () => 'View';
|
||||
export const LongPressGestureHandler = () => 'View';
|
||||
export const BorderlessButton = () => 'View';
|
||||
export const PanGestureHandler = () => 'View';
|
||||
export const LongPressGestureHandler = ({ children }) => children;
|
||||
export const BorderlessButton = ({ children }) => children;
|
||||
export const PanGestureHandler = ({ children }) => children;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -144,7 +144,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode VERSIONCODE as Integer
|
||||
versionName "4.11.0"
|
||||
versionName "4.12.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
if (isPlay) {
|
||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||
|
|
Binary file not shown.
|
@ -37,7 +37,7 @@ export const themes = {
|
|||
auxiliaryText: '#9ca2a8',
|
||||
infoText: '#6d6d72',
|
||||
tintColor: '#1d74f5',
|
||||
auxiliaryTintColor: '#caced1',
|
||||
auxiliaryTintColor: '#6C727A',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#cbcbcc',
|
||||
navbarBackground: '#ffffff',
|
||||
|
@ -82,7 +82,7 @@ export const themes = {
|
|||
auxiliaryText: '#9297a2',
|
||||
infoText: '#6D6D72',
|
||||
tintColor: '#1d74f5',
|
||||
auxiliaryTintColor: '#cdcdcd',
|
||||
auxiliaryTintColor: '#f9f9f9',
|
||||
actionTintColor: '#1d74f5',
|
||||
separatorColor: '#2b2b2d',
|
||||
navbarBackground: '#0b182c',
|
||||
|
@ -127,7 +127,7 @@ export const themes = {
|
|||
auxiliaryText: '#b2b8c6',
|
||||
infoText: '#6d6d72',
|
||||
tintColor: '#1e9bfe',
|
||||
auxiliaryTintColor: '#cdcdcd',
|
||||
auxiliaryTintColor: '#f9f9f9',
|
||||
actionTintColor: '#1e9bfe',
|
||||
separatorColor: '#272728',
|
||||
navbarBackground: '#0d0d0d',
|
||||
|
|
|
@ -186,7 +186,7 @@ export default class RecordAudio extends React.PureComponent {
|
|||
accessibilityLabel={I18n.t('Send_audio_message')}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name='microphone' size={23} color={themes[theme].tintColor} />
|
||||
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
|
||||
</BorderlessButton>
|
||||
);
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ export default class RecordAudio extends React.PureComponent {
|
|||
style={styles.actionButton}
|
||||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
size={24}
|
||||
color={themes[theme].dangerColor}
|
||||
name='close'
|
||||
/>
|
||||
|
@ -219,7 +219,7 @@ export default class RecordAudio extends React.PureComponent {
|
|||
style={styles.actionButton}
|
||||
>
|
||||
<CustomIcon
|
||||
size={22}
|
||||
size={24}
|
||||
color={themes[theme].successColor}
|
||||
name='check'
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,7 @@ import styles from '../styles';
|
|||
import I18n from '../../../i18n';
|
||||
|
||||
const BaseButton = React.memo(({
|
||||
onPress, testID, accessibilityLabel, icon, theme
|
||||
onPress, testID, accessibilityLabel, icon, theme, color
|
||||
}) => (
|
||||
<BorderlessButton
|
||||
onPress={onPress}
|
||||
|
@ -17,7 +17,7 @@ const BaseButton = React.memo(({
|
|||
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name={icon} size={25} color={themes[theme].tintColor} />
|
||||
<CustomIcon name={icon} size={24} color={color ?? themes[theme].auxiliaryTintColor} />
|
||||
</BorderlessButton>
|
||||
));
|
||||
|
||||
|
@ -26,7 +26,8 @@ BaseButton.propTypes = {
|
|||
onPress: PropTypes.func.isRequired,
|
||||
testID: PropTypes.string.isRequired,
|
||||
accessibilityLabel: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired
|
||||
icon: PropTypes.string.isRequired,
|
||||
color: PropTypes.string
|
||||
};
|
||||
|
||||
export default BaseButton;
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import BaseButton from './BaseButton';
|
||||
import { themes } from '../../../constants/colors';
|
||||
|
||||
const SendButton = React.memo(({ theme, onPress }) => (
|
||||
<BaseButton
|
||||
|
@ -10,6 +11,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
|
|||
accessibilityLabel='Send_message'
|
||||
icon='send-filled'
|
||||
theme={theme}
|
||||
color={themes[theme].tintColor}
|
||||
/>
|
||||
));
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Alert, Keyboard, NativeModules
|
||||
View, Alert, Keyboard, NativeModules, Text
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
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 DocumentPicker from 'react-native-document-picker';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
|
||||
|
||||
import { generateTriggerId } from '../../lib/methods/actions';
|
||||
import TextInput from '../../presentation/TextInput';
|
||||
|
@ -47,6 +48,7 @@ import { getUserSelector } from '../../selectors/login';
|
|||
import Navigation from '../../lib/Navigation';
|
||||
import { withActionSheet } from '../ActionSheet';
|
||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
|
||||
const imagePickerConfig = {
|
||||
cropping: true,
|
||||
|
@ -121,7 +123,8 @@ class MessageBox extends Component {
|
|||
trackingType: '',
|
||||
commandPreview: [],
|
||||
showCommandPreview: false,
|
||||
command: {}
|
||||
command: {},
|
||||
tshow: false
|
||||
};
|
||||
this.text = '';
|
||||
this.selection = { start: 0, end: 0 };
|
||||
|
@ -254,7 +257,7 @@ class MessageBox extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
||||
showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
|
@ -284,6 +287,9 @@ class MessageBox extends Component {
|
|||
if (nextState.recording !== recording) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.tshow !== tshow) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.mentions, mentions)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -576,6 +582,7 @@ class MessageBox extends Component {
|
|||
clearInput = () => {
|
||||
this.setInput('');
|
||||
this.setShowSend(false);
|
||||
this.setState({ tshow: false });
|
||||
}
|
||||
|
||||
canUploadFile = (file) => {
|
||||
|
@ -709,6 +716,7 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
submit = async() => {
|
||||
const { tshow } = this.state;
|
||||
const {
|
||||
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||
} = this.props;
|
||||
|
@ -772,7 +780,7 @@ class MessageBox extends Component {
|
|||
|
||||
// Thread
|
||||
if (threadsEnabled && replyWithMention) {
|
||||
onSubmit(message, replyingMessage.id);
|
||||
onSubmit(message, replyingMessage.id, tshow);
|
||||
|
||||
// Legacy reply or quote (quote is a reply without mention)
|
||||
} else {
|
||||
|
@ -792,7 +800,7 @@ class MessageBox extends Component {
|
|||
|
||||
// Normal message
|
||||
} 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 = () => {
|
||||
const {
|
||||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||
|
@ -903,6 +932,7 @@ class MessageBox extends Component {
|
|||
keyboardType='twitter'
|
||||
blurOnSubmit={false}
|
||||
placeholder={I18n.t('New_Message')}
|
||||
placeholderTextColor={themes[theme].auxiliaryTintColor}
|
||||
onChangeText={this.onChangeText}
|
||||
onSelectionChange={this.onSelectionChange}
|
||||
underlineColorAndroid='transparent'
|
||||
|
@ -938,6 +968,7 @@ class MessageBox extends Component {
|
|||
{textInputAndButtons}
|
||||
{recordAudio}
|
||||
</View>
|
||||
{this.renderSendToChannel()}
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
|
|
|
@ -7,12 +7,6 @@ const MENTION_HEIGHT = 50;
|
|||
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
|
||||
|
||||
export default StyleSheet.create({
|
||||
textBox: {
|
||||
flex: 0,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
zIndex: 2
|
||||
},
|
||||
composer: {
|
||||
flexDirection: 'column',
|
||||
borderTopWidth: 1
|
||||
|
@ -24,7 +18,7 @@ export default StyleSheet.create({
|
|||
},
|
||||
textBoxInput: {
|
||||
textAlignVertical: 'center',
|
||||
maxHeight: 242,
|
||||
maxHeight: 240,
|
||||
flexGrow: 1,
|
||||
width: 1,
|
||||
// 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,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
fontSize: 17,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
|
@ -40,7 +34,7 @@ export default StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 60,
|
||||
height: 56
|
||||
height: 48
|
||||
},
|
||||
mentionList: {
|
||||
maxHeight: MENTION_HEIGHT * 4
|
||||
|
@ -110,10 +104,21 @@ export default StyleSheet.create({
|
|||
justifyContent: 'space-between'
|
||||
},
|
||||
recordingCancelText: {
|
||||
fontSize: 17,
|
||||
fontSize: 16,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
buttonsWhitespace: {
|
||||
width: 15
|
||||
},
|
||||
sendToChannelButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 18
|
||||
},
|
||||
sendToChannelText: {
|
||||
fontSize: 12,
|
||||
marginLeft: 4,
|
||||
...sharedStyles.textRegular
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,36 +3,31 @@ import { View, Text } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
||||
const CallButton = React.memo(({
|
||||
dlm, theme, callJitsi
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={callJitsi}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<>
|
||||
<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>
|
||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
theme, callJitsi
|
||||
}) => (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={callJitsi}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
style={[styles.button, { 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>
|
||||
</>
|
||||
</Touchable>
|
||||
</View>
|
||||
));
|
||||
|
||||
CallButton.propTypes = {
|
||||
dlm: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
callJitsi: PropTypes.func
|
||||
};
|
||||
|
|
|
@ -3,18 +3,22 @@ import { View, Text } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import Touchable from './Touchable';
|
||||
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||
import { formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { DISCUSSION } from './constants';
|
||||
import { themes } from '../../constants/colors';
|
||||
import MessageContext from './Context';
|
||||
import { formatDateThreads } from '../../utils/room';
|
||||
|
||||
const Discussion = React.memo(({
|
||||
msg, dcount, dlm, theme
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
let time;
|
||||
if (dlm) {
|
||||
time = formatDateThreads(dlm);
|
||||
}
|
||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||
const { onDiscussionPress } = useContext(MessageContext);
|
||||
return (
|
||||
|
@ -25,11 +29,11 @@ const Discussion = React.memo(({
|
|||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
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}
|
||||
>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
</Touchable>
|
||||
|
|
|
@ -1,32 +1,49 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
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 { CustomIcon } from '../../lib/Icons';
|
||||
import { THREAD } from './constants';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { formatDateThreads } from '../../utils/room';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const Thread = React.memo(({
|
||||
msg, tcount, tlm, customThreadTimeFormat, isThreadRoom, theme
|
||||
msg, tcount, tlm, isThreadRoom, theme, id
|
||||
}) => {
|
||||
if (!tlm || isThreadRoom || tcount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = formatLastMessage(tlm, customThreadTimeFormat);
|
||||
const {
|
||||
getBadgeColor, toggleFollowThread, user, replies
|
||||
} = useContext(MessageContext);
|
||||
const time = formatDateThreads(tlm);
|
||||
const buttonText = formatMessageCount(tcount, THREAD);
|
||||
const badgeColor = getBadgeColor(id);
|
||||
const isFollowing = replies?.find(u => u === user.id);
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View
|
||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
||||
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||
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>
|
||||
</View>
|
||||
<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>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
|
@ -44,8 +61,8 @@ Thread.propTypes = {
|
|||
tcount: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
tlm: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
isThreadRoom: PropTypes.bool
|
||||
isThreadRoom: PropTypes.bool,
|
||||
id: PropTypes.string
|
||||
};
|
||||
Thread.displayName = 'MessageThread';
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ class MessageContainer extends React.Component {
|
|||
}),
|
||||
rid: PropTypes.string,
|
||||
timeFormat: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
style: PropTypes.any,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
|
@ -49,7 +48,9 @@ class MessageContainer extends React.Component {
|
|||
navToRoomInfo: PropTypes.func,
|
||||
callJitsi: PropTypes.func,
|
||||
blockAction: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
getBadgeColor: PropTypes.func,
|
||||
toggleFollowThread: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -265,10 +266,10 @@ class MessageContainer extends React.Component {
|
|||
render() {
|
||||
const { author } = this.state;
|
||||
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;
|
||||
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;
|
||||
|
||||
let message = msg;
|
||||
|
@ -291,7 +292,10 @@ class MessageContainer extends React.Component {
|
|||
onReactionPress: this.onReactionPress,
|
||||
onEncryptedPress: this.onEncryptedPress,
|
||||
onDiscussionPress: this.onDiscussionPress,
|
||||
onReactionLongPress: this.onReactionLongPress
|
||||
onReactionLongPress: this.onReactionLongPress,
|
||||
getBadgeColor,
|
||||
toggleFollowThread,
|
||||
replies
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
|
@ -309,7 +313,6 @@ class MessageContainer extends React.Component {
|
|||
avatar={avatar}
|
||||
emoji={emoji}
|
||||
timeFormat={timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
|
|
|
@ -40,11 +40,11 @@ export default StyleSheet.create({
|
|||
reactionsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 6
|
||||
marginTop: 8
|
||||
},
|
||||
reactionButton: {
|
||||
marginRight: 6,
|
||||
marginBottom: 6,
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
borderRadius: 2
|
||||
},
|
||||
reactionContainer: {
|
||||
|
@ -83,27 +83,24 @@ export default StyleSheet.create({
|
|||
paddingVertical: 5
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 6,
|
||||
marginTop: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
button: {
|
||||
paddingHorizontal: 15,
|
||||
height: 44,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2
|
||||
},
|
||||
smallButton: {
|
||||
height: 30
|
||||
},
|
||||
buttonIcon: {
|
||||
marginRight: 6
|
||||
marginRight: 8
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
fontSize: 12,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
imageContainer: {
|
||||
// flex: 1,
|
||||
|
@ -143,10 +140,8 @@ export default StyleSheet.create({
|
|||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
paddingLeft: 10,
|
||||
lineHeight: 22,
|
||||
...sharedStyles.textRegular,
|
||||
fontWeight: '300'
|
||||
paddingLeft: 8,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
repliedThread: {
|
||||
flexDirection: 'row',
|
||||
|
@ -170,6 +165,15 @@ export default StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
threadBadge: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginLeft: 8
|
||||
},
|
||||
threadBell: {
|
||||
marginLeft: 8
|
||||
},
|
||||
readReceipt: {
|
||||
lineHeight: 20
|
||||
},
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
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) => {
|
||||
const discussion = type === DISCUSSION;
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
|
|
|
@ -54,7 +54,6 @@ const OmnichannelStatus = memo(({
|
|||
<UnreadBadge
|
||||
style={styles.queueIcon}
|
||||
unread={queueSize}
|
||||
theme={theme}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
|
|
@ -658,5 +658,13 @@ export default {
|
|||
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',
|
||||
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'
|
||||
};
|
||||
|
|
|
@ -602,5 +602,13 @@ export default {
|
|||
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',
|
||||
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'
|
||||
};
|
||||
|
|
|
@ -79,4 +79,6 @@ export default class Message extends Model {
|
|||
@json('blocks', sanitizer) blocks;
|
||||
|
||||
@field('e2e') e2e;
|
||||
|
||||
@field('tshow') tshow;
|
||||
}
|
||||
|
|
|
@ -42,6 +42,12 @@ export default class Subscription extends Model {
|
|||
|
||||
@field('group_mentions') groupMentions;
|
||||
|
||||
@json('tunread', sanitizer) tunread;
|
||||
|
||||
@json('tunread_user', sanitizer) tunreadUser;
|
||||
|
||||
@json('tunread_group', sanitizer) tunreadGroup;
|
||||
|
||||
@date('room_updated_at') roomUpdatedAt;
|
||||
|
||||
@field('ro') ro;
|
||||
|
|
|
@ -16,5 +16,7 @@ export default class User extends Model {
|
|||
|
||||
@field('login_email_password') loginEmailPassword;
|
||||
|
||||
@field('show_message_in_main_thread') showMessageInMainThread;
|
||||
|
||||
@json('roles', sanitizer) roles;
|
||||
}
|
||||
|
|
|
@ -170,6 +170,12 @@ export default schemaMigrations({
|
|||
{
|
||||
toVersion: 11,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: 'messages',
|
||||
columns: [
|
||||
{ name: 'tshow', type: 'boolean', isOptional: true }
|
||||
]
|
||||
}),
|
||||
createTable({
|
||||
name: 'users',
|
||||
columns: [
|
||||
|
@ -182,6 +188,9 @@ export default schemaMigrations({
|
|||
addColumns({
|
||||
table: 'subscriptions',
|
||||
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 }
|
||||
]
|
||||
}),
|
||||
|
|
|
@ -90,6 +90,7 @@ export default schemaMigrations({
|
|||
addColumns({
|
||||
table: 'users',
|
||||
columns: [
|
||||
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
||||
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
||||
]
|
||||
})
|
||||
|
|
|
@ -20,6 +20,9 @@ export default appSchema({
|
|||
{ name: 'unread', type: 'number' },
|
||||
{ name: 'user_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: 'ro', type: 'boolean' },
|
||||
{ name: 'last_open', type: 'number', isOptional: true },
|
||||
|
@ -108,7 +111,8 @@ export default appSchema({
|
|||
{ name: 'translations', type: 'string', isOptional: true },
|
||||
{ name: 'tmsg', 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({
|
||||
|
|
|
@ -14,6 +14,7 @@ export default appSchema({
|
|||
{ name: 'statusText', type: 'string', isOptional: true },
|
||||
{ name: 'roles', type: 'string', 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 }
|
||||
]
|
||||
}),
|
||||
|
|
|
@ -14,8 +14,12 @@ export const merge = (subscription, room) => {
|
|||
}
|
||||
if (room) {
|
||||
if (room._updatedAt) {
|
||||
subscription.roomUpdatedAt = room._updatedAt;
|
||||
subscription.lastMessage = normalizeMessage(room.lastMessage);
|
||||
if (subscription.lastMessage) {
|
||||
subscription.roomUpdatedAt = subscription.lastMessage.ts;
|
||||
} else {
|
||||
subscription.roomUpdatedAt = room._updatedAt;
|
||||
}
|
||||
subscription.description = room.description;
|
||||
subscription.topic = room.topic;
|
||||
subscription.announcement = room.announcement;
|
||||
|
|
|
@ -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 {
|
||||
const db = database.active;
|
||||
const subsCollection = db.collections.get('subscriptions');
|
||||
|
@ -97,7 +97,7 @@ export default async function(rid, msg, tmid, user) {
|
|||
const batch = [];
|
||||
|
||||
let message = {
|
||||
_id: messageId, rid, msg, tmid
|
||||
_id: messageId, rid, msg, tmid, tshow
|
||||
};
|
||||
message = await Encryption.encryptMessage(message);
|
||||
|
||||
|
@ -179,8 +179,9 @@ export default async function(rid, msg, tmid, user) {
|
|||
};
|
||||
if (tmid && tMessageRecord) {
|
||||
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.tshow = tshow;
|
||||
}
|
||||
m.t = message.t;
|
||||
if (message.t === E2E_MESSAGE_TYPE) {
|
||||
|
|
|
@ -273,6 +273,9 @@ export default function subscribeRooms() {
|
|||
if (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 (type === 'removed') {
|
||||
|
|
|
@ -491,7 +491,8 @@ const RocketChat = {
|
|||
emails: result.me.emails,
|
||||
roles: result.me.roles,
|
||||
avatarETag: result.me.avatarETag,
|
||||
loginEmailPassword
|
||||
loginEmailPassword,
|
||||
showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true
|
||||
};
|
||||
return user;
|
||||
},
|
||||
|
@ -920,8 +921,12 @@ const RocketChat = {
|
|||
getUidDirectMessage(room) {
|
||||
const { id: userId } = reduxStore.getState().login.user;
|
||||
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// legacy method
|
||||
if (!room.uids && room.rid && room.t === 'd') {
|
||||
if (!room?.uids && room.rid && room.t === 'd') {
|
||||
return room.rid.replace(userId, '').trim();
|
||||
}
|
||||
|
||||
|
@ -929,8 +934,8 @@ const RocketChat = {
|
|||
return false;
|
||||
}
|
||||
|
||||
const me = room && room.uids && room.uids.find(uid => uid === userId);
|
||||
const other = room && room.uids && room.uids.filter(uid => uid !== userId);
|
||||
const me = room.uids?.find(uid => uid === userId);
|
||||
const other = room.uids?.filter(uid => uid !== userId);
|
||||
|
||||
return other && other.length ? other[0] : me;
|
||||
},
|
||||
|
@ -1242,11 +1247,18 @@ const RocketChat = {
|
|||
}
|
||||
return this.post('chat.unfollowMessage', { mid });
|
||||
},
|
||||
getThreadsList({ rid, count, offset }) {
|
||||
// RC 1.0
|
||||
return this.sdk.get('chat.getThreadsList', {
|
||||
getThreadsList({
|
||||
rid, count, offset, text
|
||||
}) {
|
||||
const params = {
|
||||
rid, count, offset, sort: { ts: -1 }
|
||||
});
|
||||
};
|
||||
if (text) {
|
||||
params.text = text;
|
||||
}
|
||||
|
||||
// RC 1.0
|
||||
return this.sdk.get('chat.getThreadsList', params);
|
||||
},
|
||||
getSyncThreadsList({ rid, updatedSince }) {
|
||||
// RC 1.0
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -37,9 +37,11 @@ const RoomItem = ({
|
|||
alert,
|
||||
hideUnreadStatus,
|
||||
unread,
|
||||
tunread,
|
||||
userMentions,
|
||||
groupMentions,
|
||||
tunread,
|
||||
tunreadUser,
|
||||
tunreadGroup,
|
||||
roomUpdatedAt,
|
||||
testID,
|
||||
swipeEnabled,
|
||||
|
@ -113,10 +115,11 @@ const RoomItem = ({
|
|||
/>
|
||||
<UnreadBadge
|
||||
unread={unread}
|
||||
tunread={tunread}
|
||||
userMentions={userMentions}
|
||||
groupMentions={groupMentions}
|
||||
theme={theme}
|
||||
tunread={tunread}
|
||||
tunreadUser={tunreadUser}
|
||||
tunreadGroup={tunreadGroup}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
|
@ -138,10 +141,11 @@ const RoomItem = ({
|
|||
/>
|
||||
<UnreadBadge
|
||||
unread={unread}
|
||||
tunread={tunread}
|
||||
userMentions={userMentions}
|
||||
groupMentions={groupMentions}
|
||||
theme={theme}
|
||||
tunread={tunread}
|
||||
tunreadUser={tunreadUser}
|
||||
tunreadGroup={tunreadGroup}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
@ -177,9 +181,11 @@ RoomItem.propTypes = {
|
|||
alert: PropTypes.bool,
|
||||
hideUnreadStatus: PropTypes.bool,
|
||||
unread: PropTypes.number,
|
||||
tunread: PropTypes.array,
|
||||
userMentions: PropTypes.number,
|
||||
groupMentions: PropTypes.number,
|
||||
tunread: PropTypes.array,
|
||||
tunreadUser: PropTypes.array,
|
||||
tunreadGroup: PropTypes.array,
|
||||
roomUpdatedAt: PropTypes.instanceOf(Date),
|
||||
swipeEnabled: PropTypes.bool,
|
||||
toggleFav: PropTypes.func,
|
||||
|
|
|
@ -178,6 +178,7 @@ class RoomItemContainer extends React.Component {
|
|||
const avatar = getRoomAvatar(item);
|
||||
const isRead = getIsRead(item);
|
||||
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
|
||||
const alert = (item.alert || item.tunread?.length);
|
||||
|
||||
let accessibilityLabel = name;
|
||||
if (item.unread === 1) {
|
||||
|
@ -203,7 +204,6 @@ class RoomItemContainer extends React.Component {
|
|||
onPress={this.onPress}
|
||||
date={date}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
userMentions={item.userMentions}
|
||||
width={width}
|
||||
favorite={item.f}
|
||||
toggleFav={toggleFav}
|
||||
|
@ -221,15 +221,18 @@ class RoomItemContainer extends React.Component {
|
|||
prid={item.prid}
|
||||
status={status}
|
||||
hideUnreadStatus={item.hideUnreadStatus}
|
||||
alert={item.alert}
|
||||
alert={alert}
|
||||
roomUpdatedAt={item.roomUpdatedAt}
|
||||
lastMessage={item.lastMessage}
|
||||
showLastMessage={showLastMessage}
|
||||
username={username}
|
||||
useRealName={useRealName}
|
||||
unread={item.unread}
|
||||
tunread={item.tunread}
|
||||
userMentions={item.userMentions}
|
||||
groupMentions={item.groupMentions}
|
||||
tunread={item.tunread}
|
||||
tunreadUser={item.tunreadUser}
|
||||
tunreadGroup={item.tunreadGroup}
|
||||
avatarETag={avatarETag || item.avatarETag}
|
||||
swipeEnabled={swipeEnabled}
|
||||
/>
|
||||
|
|
|
@ -10,9 +10,9 @@ export const getUnreadStyle = ({
|
|||
let backgroundColor = themes[theme].unreadBackground;
|
||||
const color = themes[theme].buttonText;
|
||||
if (userMentions > 0 || tunreadUser?.length) {
|
||||
backgroundColor = themes[theme].mentionMeColor;
|
||||
backgroundColor = themes[theme].mentionMeBackground;
|
||||
} else if (groupMentions > 0 || tunreadGroup?.length) {
|
||||
backgroundColor = themes[theme].mentionGroupColor;
|
||||
backgroundColor = themes[theme].mentionGroupBackground;
|
||||
} else if (tunread?.length > 0) {
|
||||
backgroundColor = themes[theme].tunreadBackground;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import { getUnreadStyle } from './getUnreadStyle';
|
|||
const testsForTheme = (theme) => {
|
||||
const getUnreadStyleUtil = ({ ...props }) => getUnreadStyle({ theme, ...props });
|
||||
|
||||
test('render empty', () => {
|
||||
expect(getUnreadStyleUtil({})).toEqual({});
|
||||
});
|
||||
|
||||
test('render unread', () => {
|
||||
expect(getUnreadStyleUtil({
|
||||
unread: 1
|
||||
|
@ -28,7 +32,7 @@ const testsForTheme = (theme) => {
|
|||
unread: 1,
|
||||
userMentions: 1
|
||||
})).toEqual({
|
||||
backgroundColor: themes[theme].mentionMeColor,
|
||||
backgroundColor: themes[theme].mentionMeBackground,
|
||||
color: themes[theme].buttonText
|
||||
});
|
||||
});
|
||||
|
@ -38,7 +42,7 @@ const testsForTheme = (theme) => {
|
|||
unread: 1,
|
||||
groupMentions: 1
|
||||
})).toEqual({
|
||||
backgroundColor: themes[theme].mentionGroupColor,
|
||||
backgroundColor: themes[theme].mentionGroupBackground,
|
||||
color: themes[theme].buttonText
|
||||
});
|
||||
});
|
||||
|
@ -50,7 +54,7 @@ const testsForTheme = (theme) => {
|
|||
groupMentions: 1,
|
||||
tunread: [1]
|
||||
})).toEqual({
|
||||
backgroundColor: themes[theme].mentionMeColor,
|
||||
backgroundColor: themes[theme].mentionMeBackground,
|
||||
color: themes[theme].buttonText
|
||||
});
|
||||
expect(getUnreadStyleUtil({
|
||||
|
@ -58,7 +62,7 @@ const testsForTheme = (theme) => {
|
|||
groupMentions: 1,
|
||||
tunread: [1]
|
||||
})).toEqual({
|
||||
backgroundColor: themes[theme].mentionGroupColor,
|
||||
backgroundColor: themes[theme].mentionGroupBackground,
|
||||
color: themes[theme].buttonText
|
||||
});
|
||||
expect(getUnreadStyleUtil({
|
||||
|
|
|
@ -22,10 +22,8 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center'
|
||||
},
|
||||
unreadText: {
|
||||
// overflow: 'hidden',
|
||||
fontSize: 13,
|
||||
...sharedStyles.textSemibold,
|
||||
fontWeight: '600'
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
textSmall: {
|
||||
fontSize: 10
|
||||
|
|
|
@ -179,6 +179,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
|||
statusText: user.statusText,
|
||||
roles: user.roles,
|
||||
loginEmailPassword: user.loginEmailPassword,
|
||||
showMessageInMainThread: user.showMessageInMainThread,
|
||||
avatarETag: user.avatarETag
|
||||
};
|
||||
yield serversDB.action(async() => {
|
||||
|
|
|
@ -190,7 +190,6 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
|||
<ModalStack.Screen
|
||||
name='ThreadMessagesView'
|
||||
component={ThreadMessagesView}
|
||||
options={props => ThreadMessagesView.navigationOptions({ ...props, isMasterDetail: true })}
|
||||
/>
|
||||
<ModalStack.Screen
|
||||
name='MarkdownTableView'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import moment from 'moment';
|
||||
import { themes } from '../constants/colors';
|
||||
|
||||
import I18n from '../i18n';
|
||||
|
||||
|
@ -21,5 +22,26 @@ export const formatDate = date => moment(date).calendar(null, {
|
|||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'LT',
|
||||
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;
|
||||
|
|
|
@ -24,9 +24,6 @@ const styles = StyleSheet.create({
|
|||
...sharedStyles.textSemibold,
|
||||
fontSize: TITLE_SIZE
|
||||
},
|
||||
scroll: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
subtitle: {
|
||||
...sharedStyles.textRegular,
|
||||
fontSize: 12
|
||||
|
@ -36,11 +33,9 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
||||
if (!subtitle && !usersTyping.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const SubTitle = React.memo(({
|
||||
usersTyping, subtitle, renderFunc, theme
|
||||
}) => {
|
||||
// typing
|
||||
if (usersTyping.length) {
|
||||
let usersText;
|
||||
|
@ -57,6 +52,11 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
|||
);
|
||||
}
|
||||
|
||||
// renderFunc
|
||||
if (renderFunc) {
|
||||
return renderFunc();
|
||||
}
|
||||
|
||||
// subtitle
|
||||
if (subtitle) {
|
||||
return (
|
||||
|
@ -69,12 +69,15 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
SubTitle.propTypes = {
|
||||
usersTyping: PropTypes.array,
|
||||
theme: PropTypes.string,
|
||||
subtitle: PropTypes.string
|
||||
subtitle: PropTypes.string,
|
||||
renderFunc: PropTypes.func
|
||||
};
|
||||
|
||||
const HeaderTitle = React.memo(({
|
||||
|
@ -113,7 +116,7 @@ HeaderTitle.propTypes = {
|
|||
};
|
||||
|
||||
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;
|
||||
let scale = 1;
|
||||
|
@ -126,6 +129,22 @@ const Header = React.memo(({
|
|||
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
testID='room-view-header-actions'
|
||||
|
@ -135,7 +154,7 @@ const Header = React.memo(({
|
|||
disabled={tmid}
|
||||
>
|
||||
<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
|
||||
title={title}
|
||||
tmid={tmid}
|
||||
|
@ -145,7 +164,7 @@ const Header = React.memo(({
|
|||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
{tmid ? null : <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} />}
|
||||
<SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} renderFunc={renderFunc} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
@ -163,6 +182,7 @@ Header.propTypes = {
|
|||
usersTyping: PropTypes.array,
|
||||
connecting: PropTypes.bool,
|
||||
roomUserId: PropTypes.string,
|
||||
parentTitle: PropTypes.string,
|
||||
goRoomActionsView: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,6 @@ const Icon = React.memo(({
|
|||
let icon;
|
||||
if (type === 'discussion') {
|
||||
icon = 'discussions';
|
||||
} else if (type === 'thread') {
|
||||
icon = 'threads';
|
||||
} else if (type === 'c') {
|
||||
icon = 'channel-public';
|
||||
} else if (type === 'l') {
|
||||
|
@ -68,6 +66,7 @@ Icon.propTypes = {
|
|||
roomUserId: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
export default Icon;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
import * as HeaderButton from '../../../containers/HeaderButton';
|
||||
import database from '../../../lib/database';
|
||||
import { getUserSelector } from '../../../selectors/login';
|
||||
import { logEvent, events } from '../../../utils/log';
|
||||
|
||||
|
||||
class RightButtonsContainer extends React.PureComponent {
|
||||
class RightButtonsContainer extends Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string,
|
||||
threadsEnabled: PropTypes.bool,
|
||||
|
@ -23,30 +23,63 @@ class RightButtonsContainer extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFollowingThread: true
|
||||
isFollowingThread: true,
|
||||
tunread: [],
|
||||
tunreadUser: [],
|
||||
tunreadGroup: []
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { tmid } = this.props;
|
||||
const { tmid, rid } = this.props;
|
||||
const db = database.active;
|
||||
if (tmid) {
|
||||
const db = database.active;
|
||||
try {
|
||||
const threadRecord = await db.collections.get('messages').find(tmid);
|
||||
this.observeThead(threadRecord);
|
||||
this.observeThread(threadRecord);
|
||||
} catch (e) {
|
||||
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() {
|
||||
if (this.threadSubscription && this.threadSubscription.unsubscribe) {
|
||||
this.threadSubscription.unsubscribe();
|
||||
}
|
||||
if (this.subSubscription && this.subSubscription.unsubscribe) {
|
||||
this.subSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
observeThead = (threadRecord) => {
|
||||
observeThread = (threadRecord) => {
|
||||
const threadObservable = threadRecord.observe();
|
||||
this.threadSubscription = threadObservable
|
||||
.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 = () => {
|
||||
logEvent(events.ROOM_GO_THREADS);
|
||||
const {
|
||||
|
@ -93,7 +142,9 @@ class RightButtonsContainer extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isFollowingThread } = this.state;
|
||||
const {
|
||||
isFollowingThread, tunread, tunreadUser, tunreadGroup
|
||||
} = this.state;
|
||||
const { t, tmid, threadsEnabled } = this.props;
|
||||
if (t === 'l') {
|
||||
return null;
|
||||
|
@ -116,6 +167,13 @@ class RightButtonsContainer extends React.PureComponent {
|
|||
iconName='threads'
|
||||
onPress={this.goThreadsView}
|
||||
testID='room-view-header-threads'
|
||||
badge={() => (
|
||||
<HeaderButton.Badge
|
||||
tunread={tunread}
|
||||
tunreadUser={tunreadUser}
|
||||
tunreadGroup={tunreadGroup}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderButton.Item
|
||||
|
|
|
@ -27,7 +27,8 @@ class RoomHeaderView extends Component {
|
|||
widthOffset: PropTypes.number,
|
||||
goRoomActionsView: PropTypes.func,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number
|
||||
height: PropTypes.number,
|
||||
parentTitle: PropTypes.string
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
|
@ -75,7 +76,7 @@ class RoomHeaderView extends Component {
|
|||
|
||||
render() {
|
||||
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;
|
||||
|
||||
let subtitle;
|
||||
|
@ -103,6 +104,7 @@ class RoomHeaderView extends Component {
|
|||
roomUserId={roomUserId}
|
||||
goRoomActionsView={goRoomActionsView}
|
||||
connecting={connecting}
|
||||
parentTitle={parentTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -111,10 +113,12 @@ class RoomHeaderView extends Component {
|
|||
const mapStateToProps = (state, ownProps) => {
|
||||
let statusText;
|
||||
let status = 'offline';
|
||||
const { roomUserId, type, visitor = {} } = ownProps;
|
||||
const {
|
||||
roomUserId, type, visitor = {}, tmid
|
||||
} = ownProps;
|
||||
|
||||
if (state.meteor.connected) {
|
||||
if (type === 'd' && state.activeUsers[roomUserId]) {
|
||||
if ((type === 'd' || (tmid && roomUserId)) && state.activeUsers[roomUserId]) {
|
||||
({ status, statusText } = state.activeUsers[roomUserId]);
|
||||
} else if (type === 'l' && visitor?.status) {
|
||||
({ status } = visitor);
|
||||
|
|
|
@ -30,7 +30,8 @@ class List extends React.Component {
|
|||
loading: PropTypes.bool,
|
||||
listRef: PropTypes.func,
|
||||
hideSystemMessages: PropTypes.array,
|
||||
navigation: PropTypes.object
|
||||
navigation: PropTypes.object,
|
||||
showMessageInMainThread: PropTypes.bool
|
||||
};
|
||||
|
||||
// this.state.loading works for this.onEndReached and RoomView.init
|
||||
|
@ -140,7 +141,7 @@ class List extends React.Component {
|
|||
|
||||
query = async() => {
|
||||
this.count += QUERY_SIZE;
|
||||
const { rid, tmid } = this.props;
|
||||
const { rid, tmid, showMessageInMainThread } = this.props;
|
||||
const db = database.active;
|
||||
|
||||
// handle servers with version < 3.0.0
|
||||
|
@ -167,14 +168,23 @@ class List extends React.Component {
|
|||
)
|
||||
.observe();
|
||||
} 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
|
||||
.get('messages')
|
||||
.query(
|
||||
Q.where('rid', rid),
|
||||
Q.experimentalSortBy('ts', Q.desc),
|
||||
Q.experimentalSkip(0),
|
||||
Q.experimentalTake(this.count)
|
||||
)
|
||||
.query(...whereClause)
|
||||
.observe();
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
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 unreadText = { color: themes[theme].dangerColor };
|
||||
if (ts && unread) {
|
||||
|
|
|
@ -35,7 +35,7 @@ import { themes } from '../../constants/colors';
|
|||
import debounce from '../../utils/debounce';
|
||||
import ReactionsModal from '../../containers/ReactionsModal';
|
||||
import { LISTENER } from '../../containers/Toast';
|
||||
import { isBlocked } from '../../utils/room';
|
||||
import { getBadgeColor, isBlocked, makeThreadName } from '../../utils/room';
|
||||
import { isReadOnly } from '../../utils/isReadOnly';
|
||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||
import { showErrorAlert } from '../../utils/info';
|
||||
|
@ -82,7 +82,8 @@ class RoomView extends React.Component {
|
|||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
token: PropTypes.string.isRequired,
|
||||
showMessageInMainThread: PropTypes.bool
|
||||
}),
|
||||
appState: PropTypes.string,
|
||||
useRealName: PropTypes.bool,
|
||||
|
@ -108,17 +109,18 @@ class RoomView extends React.Component {
|
|||
this.rid = props.route.params?.rid;
|
||||
this.t = props.route.params?.t;
|
||||
this.tmid = props.route.params?.tmid;
|
||||
const room = props.route.params?.room;
|
||||
const selectedMessage = props.route.params?.message;
|
||||
const name = props.route.params?.name;
|
||||
const fname = props.route.params?.fname;
|
||||
const search = props.route.params?.search;
|
||||
const prid = props.route.params?.prid;
|
||||
const room = props.route.params?.room ?? {
|
||||
rid: this.rid, t: this.t, name, fname, prid
|
||||
};
|
||||
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
||||
this.state = {
|
||||
joined: true,
|
||||
room: room || {
|
||||
rid: this.rid, t: this.t, name, fname, prid
|
||||
},
|
||||
room,
|
||||
roomUpdate: {},
|
||||
member: {},
|
||||
lastOpen: null,
|
||||
|
@ -132,7 +134,7 @@ class RoomView extends React.Component {
|
|||
reacting: false,
|
||||
readOnly: false,
|
||||
unreadsCount: null,
|
||||
roomUserId: null
|
||||
roomUserId
|
||||
};
|
||||
this.setHeader();
|
||||
|
||||
|
@ -292,24 +294,28 @@ class RoomView extends React.Component {
|
|||
}
|
||||
|
||||
setHeader = () => {
|
||||
const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state;
|
||||
const {
|
||||
navigation, route, isMasterDetail, theme, baseUrl, user, insets
|
||||
room, unreadsCount, roomUserId
|
||||
} = this.state;
|
||||
const {
|
||||
navigation, isMasterDetail, theme, baseUrl, user, insets, route
|
||||
} = this.props;
|
||||
const rid = route.params?.rid;
|
||||
const prid = route.params?.prid;
|
||||
const { rid, tmid } = this;
|
||||
const prid = room?.prid;
|
||||
let title = route.params?.name;
|
||||
if ((room.id || room.rid) && !this.tmid) {
|
||||
let parentTitle;
|
||||
if ((room.id || room.rid) && !tmid) {
|
||||
title = RocketChat.getRoomTitle(room);
|
||||
}
|
||||
if (tmid) {
|
||||
parentTitle = RocketChat.getRoomTitle(room);
|
||||
}
|
||||
const subtitle = room?.topic;
|
||||
const t = route.params?.t || room?.t;
|
||||
const tmid = route.params?.tmid;
|
||||
const t = room?.t;
|
||||
const { id: userId, token } = user;
|
||||
const avatar = room?.name;
|
||||
const roomUserId = route.params?.roomUserId || stateRoomUserId;
|
||||
const visitor = room?.visitor;
|
||||
if (!rid) {
|
||||
if (!room?.rid) {
|
||||
return;
|
||||
}
|
||||
const headerTitlePosition = getHeaderTitlePosition(insets);
|
||||
|
@ -341,6 +347,7 @@ class RoomView extends React.Component {
|
|||
prid={prid}
|
||||
tmid={tmid}
|
||||
title={title}
|
||||
parentTitle={parentTitle}
|
||||
subtitle={subtitle}
|
||||
type={t}
|
||||
roomUserId={roomUserId}
|
||||
|
@ -625,6 +632,7 @@ class RoomView extends React.Component {
|
|||
};
|
||||
|
||||
onThreadPress = debounce(async(item) => {
|
||||
const { roomUserId } = this.state;
|
||||
const { navigation } = this.props;
|
||||
if (item.tmid) {
|
||||
if (!item.tmsg) {
|
||||
|
@ -635,11 +643,11 @@ class RoomView extends React.Component {
|
|||
name = I18n.t('Encrypted_message');
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
|
@ -669,10 +677,10 @@ class RoomView extends React.Component {
|
|||
this.setState(...args);
|
||||
}
|
||||
|
||||
sendMessage = (message, tmid) => {
|
||||
sendMessage = (message, tmid, tshow) => {
|
||||
logEvent(events.ROOM_SEND_MESSAGE);
|
||||
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) {
|
||||
this.list.current.update();
|
||||
}
|
||||
|
@ -761,15 +769,21 @@ class RoomView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleFollowThread = async(isFollowingThread) => {
|
||||
toggleFollowThread = async(isFollowingThread, tmid) => {
|
||||
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') });
|
||||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
getBadgeColor = (messageId) => {
|
||||
const { room } = this.state;
|
||||
const { theme } = this.props;
|
||||
return getBadgeColor({ subscription: room, theme, messageId });
|
||||
}
|
||||
|
||||
navToRoomInfo = (navParam) => {
|
||||
const { navigation, user, isMasterDetail } = this.props;
|
||||
logEvent(events[`ROOM_GO_${ navParam.t === 'd' ? 'USER' : 'ROOM' }_INFO`]);
|
||||
|
@ -895,6 +909,8 @@ class RoomView extends React.Component {
|
|||
getCustomEmoji={this.getCustomEmoji}
|
||||
callJitsi={this.callJitsi}
|
||||
blockAction={this.blockAction}
|
||||
getBadgeColor={this.getBadgeColor}
|
||||
toggleFollowThread={this.toggleFollowThread}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -1042,6 +1058,7 @@ class RoomView extends React.Component {
|
|||
loading={loading}
|
||||
navigation={navigation}
|
||||
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
|
||||
showMessageInMainThread={user.showMessageInMainThread}
|
||||
/>
|
||||
{this.renderFooter()}
|
||||
{this.renderActions()}
|
||||
|
|
|
@ -75,7 +75,7 @@ const GROUPS_HEADER = 'Private_Groups';
|
|||
const OMNICHANNEL_HEADER = 'Open_Livechats';
|
||||
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 filterIsOmnichannel = s => s.t === 'l';
|
||||
|
||||
|
|
|
@ -63,8 +63,6 @@ export default StyleSheet.create({
|
|||
marginHorizontal: 12
|
||||
},
|
||||
queueIcon: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
marginHorizontal: 12
|
||||
},
|
||||
groupTitleContainer: {
|
||||
|
|
|
@ -149,7 +149,7 @@ class SearchMessagesView extends React.Component {
|
|||
item={item}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
timeFormat='MMM Do YYYY, h:mm:ss a'
|
||||
timeFormat='LLL'
|
||||
isHeader
|
||||
showAttachment={() => {}}
|
||||
getCustomEmoji={this.getCustomEmoji}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { themes } from '../../constants/colors';
|
|||
import { withTheme } from '../../theme';
|
||||
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
||||
import sharedStyles from '../Styles';
|
||||
import { makeThreadName } from '../../utils/room';
|
||||
|
||||
const androidMarginLeft = isTablet ? 0 : 4;
|
||||
|
||||
|
@ -64,6 +65,13 @@ const Header = React.memo(({ room, thread, theme }) => {
|
|||
|
||||
const textColor = themes[theme].previewTintColor;
|
||||
|
||||
let title;
|
||||
if (thread?.id) {
|
||||
title = makeThreadName(thread);
|
||||
} else {
|
||||
title = RocketChat.getRoomTitle(room);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.inner}>
|
||||
|
@ -78,7 +86,7 @@ const Header = React.memo(({ room, thread, theme }) => {
|
|||
style={[styles.name, { color: textColor }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{thread?.msg ?? RocketChat.getRoomTitle(room)}
|
||||
{title}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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));
|
|
@ -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);
|
|
@ -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' />
|
||||
</>
|
||||
));
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,5 @@
|
|||
export const FILTER = {
|
||||
ALL: 'All',
|
||||
FOLLOWING: 'Following',
|
||||
UNREAD: 'Unread'
|
||||
};
|
|
@ -1,20 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList, View, Text, InteractionManager
|
||||
} from 'react-native';
|
||||
import { FlatList, InteractionManager } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
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 Message from '../../containers/message';
|
||||
import Item from './Item';
|
||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||
import I18n from '../../i18n';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import database from '../../lib/database';
|
||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
||||
import log from '../../utils/log';
|
||||
|
@ -25,25 +24,19 @@ import { withTheme } from '../../theme';
|
|||
import { getUserSelector } from '../../selectors/login';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
|
||||
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />);
|
||||
Separator.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
import * as List from '../../containers/List';
|
||||
import Dropdown from './Dropdown';
|
||||
import DropdownItemHeader from './Dropdown/DropdownItemHeader';
|
||||
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;
|
||||
|
||||
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 = {
|
||||
user: PropTypes.object,
|
||||
navigation: PropTypes.object,
|
||||
|
@ -51,8 +44,8 @@ class ThreadMessagesView extends React.Component {
|
|||
baseUrl: PropTypes.string,
|
||||
useRealName: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
customEmojis: PropTypes.object,
|
||||
isMasterDetail: PropTypes.bool
|
||||
isMasterDetail: PropTypes.bool,
|
||||
insets: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -63,9 +56,17 @@ class ThreadMessagesView extends React.Component {
|
|||
this.state = {
|
||||
loading: false,
|
||||
end: false,
|
||||
messages: []
|
||||
messages: [],
|
||||
displayingThreads: [],
|
||||
subscription: {},
|
||||
showFilterDropdown: false,
|
||||
currentFilter: FILTER.ALL,
|
||||
isSearching: false,
|
||||
searchText: ''
|
||||
};
|
||||
this.subscribeData();
|
||||
this.setHeader();
|
||||
this.initSubscription();
|
||||
this.subscribeMessages();
|
||||
}
|
||||
|
||||
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() {
|
||||
console.countReset(`${ this.constructor.name }.render calls`);
|
||||
if (this.mountInteraction && this.mountInteraction.cancel) {
|
||||
|
@ -91,48 +101,133 @@ class ThreadMessagesView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
subscribeData = async() => {
|
||||
getHeader = () => {
|
||||
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 {
|
||||
const db = database.active;
|
||||
|
||||
// subscription query
|
||||
const subscription = await db.collections
|
||||
.get('subscriptions')
|
||||
.find(this.rid);
|
||||
const observable = subscription.observe();
|
||||
this.subSubscription = observable
|
||||
.subscribe((data) => {
|
||||
this.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.setState({ subscription: data });
|
||||
});
|
||||
|
||||
this.subscribeMessages(subscription);
|
||||
} 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 = () => {
|
||||
if (!this.subscription) {
|
||||
const { subscription } = this.state;
|
||||
if (!subscription) {
|
||||
return this.load();
|
||||
}
|
||||
try {
|
||||
const lastThreadSync = new Date();
|
||||
if (this.subscription.lastThreadSync) {
|
||||
this.sync(this.subscription.lastThreadSync);
|
||||
if (subscription.lastThreadSync) {
|
||||
this.sync(subscription.lastThreadSync);
|
||||
} else {
|
||||
this.load(lastThreadSync);
|
||||
}
|
||||
|
@ -142,9 +237,10 @@ class ThreadMessagesView extends React.Component {
|
|||
}
|
||||
|
||||
updateThreads = async({ update, remove, lastThreadSync }) => {
|
||||
const { subscription } = this.state;
|
||||
// if there's no subscription, manage data on this.state.messages
|
||||
// note: sync will never be called without subscription
|
||||
if (!this.subscription) {
|
||||
if (!subscription) {
|
||||
this.setState(({ messages }) => ({ messages: [...messages, ...update] }));
|
||||
return;
|
||||
}
|
||||
|
@ -152,7 +248,7 @@ class ThreadMessagesView extends React.Component {
|
|||
try {
|
||||
const db = database.active;
|
||||
const threadsCollection = db.collections.get('threads');
|
||||
const allThreadsRecords = await this.subscription.threads.fetch();
|
||||
const allThreadsRecords = await subscription.threads.fetch();
|
||||
let threadsToCreate = [];
|
||||
let threadsToUpdate = [];
|
||||
let threadsToDelete = [];
|
||||
|
@ -164,7 +260,7 @@ class ThreadMessagesView extends React.Component {
|
|||
threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
|
||||
threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => {
|
||||
t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema);
|
||||
t.subscription.set(this.subscription);
|
||||
t.subscription.set(subscription);
|
||||
Object.assign(t, thread);
|
||||
})));
|
||||
threadsToUpdate = threadsToUpdate.map((thread) => {
|
||||
|
@ -185,7 +281,7 @@ class ThreadMessagesView extends React.Component {
|
|||
...threadsToCreate,
|
||||
...threadsToUpdate,
|
||||
...threadsToDelete,
|
||||
this.subscription.prepareUpdate((s) => {
|
||||
subscription.prepareUpdate((s) => {
|
||||
s.lastThreadSync = lastThreadSync;
|
||||
})
|
||||
);
|
||||
|
@ -197,7 +293,9 @@ class ThreadMessagesView extends React.Component {
|
|||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
load = debounce(async(lastThreadSync) => {
|
||||
const { loading, end, messages } = this.state;
|
||||
const {
|
||||
loading, end, messages, searchText
|
||||
} = this.state;
|
||||
if (end || loading || !this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -206,7 +304,7 @@ class ThreadMessagesView extends React.Component {
|
|||
|
||||
try {
|
||||
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) {
|
||||
this.updateThreads({ update: result.threads, lastThreadSync });
|
||||
|
@ -244,112 +342,168 @@ class ThreadMessagesView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
formatMessage = lm => (
|
||||
lm ? moment(lm).calendar(null, {
|
||||
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;
|
||||
onSearchPress = () => {
|
||||
this.setState({ isSearching: true }, () => this.setHeader());
|
||||
}
|
||||
|
||||
showAttachment = (attachment) => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('AttachmentView', { attachment });
|
||||
onCancelSearchPress = () => {
|
||||
this.setState({ isSearching: false, searchText: '' }, () => {
|
||||
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) => {
|
||||
const { subscription } = this.state;
|
||||
const { navigation, isMasterDetail } = this.props;
|
||||
if (isMasterDetail) {
|
||||
navigation.pop();
|
||||
}
|
||||
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)
|
||||
|
||||
renderSeparator = () => {
|
||||
getBadgeColor = (item) => {
|
||||
const { subscription } = this.state;
|
||||
const { theme } = this.props;
|
||||
return <Separator theme={theme} />;
|
||||
return getBadgeColor({ subscription, theme, messageId: item?.id });
|
||||
}
|
||||
|
||||
renderEmpty = () => {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]} testID='thread-messages-view'>
|
||||
<Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_thread_messages')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
navToRoomInfo = (navParam) => {
|
||||
const { navigation, user } = this.props;
|
||||
if (navParam.rid === user.id) {
|
||||
return;
|
||||
// helper to query threads
|
||||
getFilteredThreads = (messages, subscription, currentFilter) => {
|
||||
// const { currentFilter } = this.state;
|
||||
const { user } = this.props;
|
||||
if (currentFilter === FILTER.FOLLOWING) {
|
||||
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));
|
||||
}
|
||||
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 }) => {
|
||||
const {
|
||||
user, navigation, baseUrl, useRealName
|
||||
} = this.props;
|
||||
const badgeColor = this.getBadgeColor(item);
|
||||
return (
|
||||
<Message
|
||||
key={item.id}
|
||||
item={item}
|
||||
user={user}
|
||||
archived={false}
|
||||
broadcast={false}
|
||||
status={item.status}
|
||||
navigation={navigation}
|
||||
timeFormat='MMM D'
|
||||
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
|
||||
onThreadPress={this.onThreadPress}
|
||||
baseUrl={baseUrl}
|
||||
useRealName={useRealName}
|
||||
getCustomEmoji={this.getCustomEmoji}
|
||||
navToRoomInfo={this.navToRoomInfo}
|
||||
showAttachment={this.showAttachment}
|
||||
<Item
|
||||
{...{
|
||||
item,
|
||||
user,
|
||||
navigation,
|
||||
baseUrl,
|
||||
useRealName,
|
||||
badgeColor
|
||||
}}
|
||||
onPress={this.onThreadPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
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() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { loading, messages } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
if (!loading && messages.length === 0) {
|
||||
return this.renderEmpty();
|
||||
}
|
||||
const { showFilterDropdown, currentFilter } = this.state;
|
||||
|
||||
return (
|
||||
<SafeAreaView testID='thread-messages-view'>
|
||||
<StatusBar />
|
||||
<FlatList
|
||||
data={messages}
|
||||
extraData={this.state}
|
||||
renderItem={this.renderItem}
|
||||
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
keyExtractor={item => item._id}
|
||||
onEndReached={this.load}
|
||||
onEndReachedThreshold={0.5}
|
||||
maxToRenderPerBatch={5}
|
||||
initialNumToRender={1}
|
||||
ItemSeparatorComponent={this.renderSeparator}
|
||||
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
|
||||
/>
|
||||
{this.renderContent()}
|
||||
{showFilterDropdown
|
||||
? (
|
||||
<Dropdown
|
||||
currentFilter={currentFilter}
|
||||
onFilterSelected={this.onFilterSelected}
|
||||
onClose={this.closeFilterDropdown}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
@ -359,8 +513,7 @@ const mapStateToProps = state => ({
|
|||
baseUrl: state.server.server,
|
||||
user: getUserSelector(state),
|
||||
useRealName: state.settings.UI_Use_Real_Name,
|
||||
customEmojis: state.customEmojis,
|
||||
isMasterDetail: state.app.isMasterDetail
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(ThreadMessagesView));
|
||||
export default connect(mapStateToProps)(withTheme(withSafeAreaInsets(ThreadMessagesView)));
|
||||
|
|
|
@ -18,10 +18,13 @@ export default StyleSheet.create({
|
|||
contentContainer: {
|
||||
paddingBottom: 30
|
||||
},
|
||||
separator: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
dropdownContainer: {
|
||||
width: '100%',
|
||||
marginLeft: 60,
|
||||
marginTop: 10
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFill
|
||||
}
|
||||
});
|
||||
|
|
|
@ -54,9 +54,9 @@ async function logout() {
|
|||
}
|
||||
|
||||
async function mockMessage(message) {
|
||||
await element(by.id('messagebox-input')).tap();
|
||||
await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`);
|
||||
await element(by.id('messagebox-send-message')).tap();
|
||||
await element(by.id('messagebox-input')).atIndex(0).tap();
|
||||
await element(by.id('messagebox-input')).atIndex(0).typeText(`${ data.random }${ message }`);
|
||||
await element(by.id('messagebox-send-message')).atIndex(0).tap();
|
||||
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 element(by.label(`${ data.random }${ message }`)).atIndex(0).tap();
|
||||
|
|
|
@ -294,24 +294,42 @@ describe('Room screen', () => {
|
|||
await element(by.id('room-view-header-follow')).tap();
|
||||
await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000);
|
||||
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(`${ 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(`${ data.random }${ messageText }`)).atIndex(0)).toNotExist().withTimeout(2000);
|
||||
});
|
||||
|
||||
await mockMessage('dummymessagebetweenthethread');
|
||||
await element(by.label(thread)).atIndex(0).longPress();
|
||||
await expect(element(by.id('action-sheet'))).toExist();
|
||||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||
await element(by.label('Reply in Thread')).tap();
|
||||
await element(by.id('messagebox-input')).typeText('repliedagain');
|
||||
it('should mark send to channel and show on main channel', async() => {
|
||||
const messageText = 'sendToChannel';
|
||||
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 waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
|
||||
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toExist();
|
||||
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 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 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 waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000);
|
||||
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-title-${ thread }`))).toExist().withTimeout(5000);
|
||||
await expect(element(by.id(`room-view-title-${ thread }`))).toExist();
|
||||
|
|
|
@ -14,7 +14,7 @@ async function navigateToRoomInfo(type) {
|
|||
room = privateRoomName;
|
||||
}
|
||||
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 waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
|
||||
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.type('UIScrollView')).atIndex(1).swipe('up');
|
||||
await element(by.id('room-info-edit-view-submit')).tap();
|
||||
await waitForToast();
|
||||
await tapBack();
|
||||
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
|
||||
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`);
|
||||
|
|
|
@ -1396,7 +1396,7 @@
|
|||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
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_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
||||
|
@ -1433,7 +1433,7 @@
|
|||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
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;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.11.0</string>
|
||||
<string>4.12.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.11.0</string>
|
||||
<string>4.12.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
|
BIN
ios/custom.ttf
BIN
ios/custom.ttf
Binary file not shown.
|
@ -129,12 +129,8 @@
|
|||
"@babel/core": "^7.8.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/runtime": "^7.8.4",
|
||||
"@storybook/addon-actions": "5.3.19",
|
||||
"@storybook/addon-links": "5.3.19",
|
||||
"@storybook/addon-storyshots": "5.3.19",
|
||||
"@storybook/addons": "5.3.19",
|
||||
"@storybook/react-native": "5.3.19",
|
||||
"@storybook/theming": "5.3.19",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"axios": "^0.19.2",
|
||||
"babel-core": "^6.26.3",
|
||||
|
|
|
@ -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';
|
|
@ -5,7 +5,7 @@ import RNBootSplash from 'react-native-bootsplash';
|
|||
import 'react-native-gesture-handler';
|
||||
|
||||
// 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();
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ const ThemeStory = ({ theme }) => (
|
|||
right={() => (
|
||||
<HeaderButton.Container>
|
||||
<HeaderButton.Item title='Threads' />
|
||||
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={999} />} />
|
||||
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
|
||||
</HeaderButton.Container>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -75,7 +75,11 @@ export default ({ theme }) => {
|
|||
<RoomItem alert name='user mentions' unread={1} userMentions={1} />
|
||||
<RoomItem alert name='group mentions' unread={1} groupMentions={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' />
|
||||
<RoomItem
|
||||
|
@ -119,7 +123,7 @@ export default ({ theme }) => {
|
|||
<RoomItem
|
||||
showLastMessage
|
||||
alert
|
||||
unread={1000}
|
||||
tunread={[1]}
|
||||
lastMessage={lastMessage}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
|
|
@ -12,10 +12,12 @@ import UiKitModal from './UiKitModal';
|
|||
import Markdown from './Markdown';
|
||||
import './HeaderButtons';
|
||||
import './UnreadBadge';
|
||||
import '../../app/views/ThreadMessagesView/Item.stories.js';
|
||||
import Avatar from './Avatar';
|
||||
// import RoomViewHeader from './RoomViewHeader';
|
||||
|
||||
import MessageContext from '../../app/containers/message/Context';
|
||||
import { themes } from '../../app/constants/colors';
|
||||
|
||||
// MessageProvider
|
||||
const baseUrl = 'https://open.rocket.chat';
|
||||
|
@ -53,7 +55,8 @@ const messageDecorator = story => (
|
|||
replyBroadcast: () => {},
|
||||
onReactionPress: () => {},
|
||||
onDiscussionPress: () => {},
|
||||
onReactionLongPress: () => {}
|
||||
onReactionLongPress: () => {},
|
||||
getBadgeColor: () => themes.light.tunreadBackground
|
||||
}}
|
||||
>
|
||||
{story()}
|
||||
|
|
63
yarn.lock
63
yarn.lock
|
@ -2248,42 +2248,6 @@
|
|||
dependencies:
|
||||
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":
|
||||
version "5.3.19"
|
||||
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"
|
||||
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:
|
||||
version "1.0.3"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
@ -12701,15 +12647,6 @@ react-hotkeys@2.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
|
Loading…
Reference in New Issue