[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 State = () => 'View';
|
||||||
export const LongPressGestureHandler = () => 'View';
|
export const LongPressGestureHandler = ({ children }) => children;
|
||||||
export const BorderlessButton = () => 'View';
|
export const BorderlessButton = ({ children }) => children;
|
||||||
export const PanGestureHandler = () => 'View';
|
export const PanGestureHandler = ({ children }) => children;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -144,7 +144,7 @@ android {
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode VERSIONCODE as Integer
|
versionCode VERSIONCODE as Integer
|
||||||
versionName "4.11.0"
|
versionName "4.12.0"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
if (isPlay) {
|
if (isPlay) {
|
||||||
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
|
||||||
|
|
Binary file not shown.
|
@ -37,7 +37,7 @@ export const themes = {
|
||||||
auxiliaryText: '#9ca2a8',
|
auxiliaryText: '#9ca2a8',
|
||||||
infoText: '#6d6d72',
|
infoText: '#6d6d72',
|
||||||
tintColor: '#1d74f5',
|
tintColor: '#1d74f5',
|
||||||
auxiliaryTintColor: '#caced1',
|
auxiliaryTintColor: '#6C727A',
|
||||||
actionTintColor: '#1d74f5',
|
actionTintColor: '#1d74f5',
|
||||||
separatorColor: '#cbcbcc',
|
separatorColor: '#cbcbcc',
|
||||||
navbarBackground: '#ffffff',
|
navbarBackground: '#ffffff',
|
||||||
|
@ -82,7 +82,7 @@ export const themes = {
|
||||||
auxiliaryText: '#9297a2',
|
auxiliaryText: '#9297a2',
|
||||||
infoText: '#6D6D72',
|
infoText: '#6D6D72',
|
||||||
tintColor: '#1d74f5',
|
tintColor: '#1d74f5',
|
||||||
auxiliaryTintColor: '#cdcdcd',
|
auxiliaryTintColor: '#f9f9f9',
|
||||||
actionTintColor: '#1d74f5',
|
actionTintColor: '#1d74f5',
|
||||||
separatorColor: '#2b2b2d',
|
separatorColor: '#2b2b2d',
|
||||||
navbarBackground: '#0b182c',
|
navbarBackground: '#0b182c',
|
||||||
|
@ -127,7 +127,7 @@ export const themes = {
|
||||||
auxiliaryText: '#b2b8c6',
|
auxiliaryText: '#b2b8c6',
|
||||||
infoText: '#6d6d72',
|
infoText: '#6d6d72',
|
||||||
tintColor: '#1e9bfe',
|
tintColor: '#1e9bfe',
|
||||||
auxiliaryTintColor: '#cdcdcd',
|
auxiliaryTintColor: '#f9f9f9',
|
||||||
actionTintColor: '#1e9bfe',
|
actionTintColor: '#1e9bfe',
|
||||||
separatorColor: '#272728',
|
separatorColor: '#272728',
|
||||||
navbarBackground: '#0d0d0d',
|
navbarBackground: '#0d0d0d',
|
||||||
|
|
|
@ -186,7 +186,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
accessibilityLabel={I18n.t('Send_audio_message')}
|
accessibilityLabel={I18n.t('Send_audio_message')}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
>
|
>
|
||||||
<CustomIcon name='microphone' size={23} color={themes[theme].tintColor} />
|
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}
|
||||||
>
|
>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
size={22}
|
size={24}
|
||||||
color={themes[theme].dangerColor}
|
color={themes[theme].dangerColor}
|
||||||
name='close'
|
name='close'
|
||||||
/>
|
/>
|
||||||
|
@ -219,7 +219,7 @@ export default class RecordAudio extends React.PureComponent {
|
||||||
style={styles.actionButton}
|
style={styles.actionButton}
|
||||||
>
|
>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
size={22}
|
size={24}
|
||||||
color={themes[theme].successColor}
|
color={themes[theme].successColor}
|
||||||
name='check'
|
name='check'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import styles from '../styles';
|
||||||
import I18n from '../../../i18n';
|
import I18n from '../../../i18n';
|
||||||
|
|
||||||
const BaseButton = React.memo(({
|
const BaseButton = React.memo(({
|
||||||
onPress, testID, accessibilityLabel, icon, theme
|
onPress, testID, accessibilityLabel, icon, theme, color
|
||||||
}) => (
|
}) => (
|
||||||
<BorderlessButton
|
<BorderlessButton
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
@ -17,7 +17,7 @@ const BaseButton = React.memo(({
|
||||||
accessibilityLabel={I18n.t(accessibilityLabel)}
|
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
>
|
>
|
||||||
<CustomIcon name={icon} size={25} color={themes[theme].tintColor} />
|
<CustomIcon name={icon} size={24} color={color ?? themes[theme].auxiliaryTintColor} />
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@ BaseButton.propTypes = {
|
||||||
onPress: PropTypes.func.isRequired,
|
onPress: PropTypes.func.isRequired,
|
||||||
testID: PropTypes.string.isRequired,
|
testID: PropTypes.string.isRequired,
|
||||||
accessibilityLabel: PropTypes.string.isRequired,
|
accessibilityLabel: PropTypes.string.isRequired,
|
||||||
icon: PropTypes.string.isRequired
|
icon: PropTypes.string.isRequired,
|
||||||
|
color: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BaseButton;
|
export default BaseButton;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import BaseButton from './BaseButton';
|
import BaseButton from './BaseButton';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
|
||||||
const SendButton = React.memo(({ theme, onPress }) => (
|
const SendButton = React.memo(({ theme, onPress }) => (
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
@ -10,6 +11,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
|
||||||
accessibilityLabel='Send_message'
|
accessibilityLabel='Send_message'
|
||||||
icon='send-filled'
|
icon='send-filled'
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
color={themes[theme].tintColor}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, Alert, Keyboard, NativeModules
|
View, Alert, Keyboard, NativeModules, Text
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||||
|
@ -9,6 +9,7 @@ import ImagePicker from 'react-native-image-crop-picker';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
import { generateTriggerId } from '../../lib/methods/actions';
|
import { generateTriggerId } from '../../lib/methods/actions';
|
||||||
import TextInput from '../../presentation/TextInput';
|
import TextInput from '../../presentation/TextInput';
|
||||||
|
@ -47,6 +48,7 @@ import { getUserSelector } from '../../selectors/login';
|
||||||
import Navigation from '../../lib/Navigation';
|
import Navigation from '../../lib/Navigation';
|
||||||
import { withActionSheet } from '../ActionSheet';
|
import { withActionSheet } from '../ActionSheet';
|
||||||
import { sanitizeLikeString } from '../../lib/database/utils';
|
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||||
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
|
||||||
const imagePickerConfig = {
|
const imagePickerConfig = {
|
||||||
cropping: true,
|
cropping: true,
|
||||||
|
@ -121,7 +123,8 @@ class MessageBox extends Component {
|
||||||
trackingType: '',
|
trackingType: '',
|
||||||
commandPreview: [],
|
commandPreview: [],
|
||||||
showCommandPreview: false,
|
showCommandPreview: false,
|
||||||
command: {}
|
command: {},
|
||||||
|
tshow: false
|
||||||
};
|
};
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.selection = { start: 0, end: 0 };
|
this.selection = { start: 0, end: 0 };
|
||||||
|
@ -254,7 +257,7 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const {
|
const {
|
||||||
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -284,6 +287,9 @@ class MessageBox extends Component {
|
||||||
if (nextState.recording !== recording) {
|
if (nextState.recording !== recording) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextState.tshow !== tshow) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!equal(nextState.mentions, mentions)) {
|
if (!equal(nextState.mentions, mentions)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -576,6 +582,7 @@ class MessageBox extends Component {
|
||||||
clearInput = () => {
|
clearInput = () => {
|
||||||
this.setInput('');
|
this.setInput('');
|
||||||
this.setShowSend(false);
|
this.setShowSend(false);
|
||||||
|
this.setState({ tshow: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
canUploadFile = (file) => {
|
canUploadFile = (file) => {
|
||||||
|
@ -709,6 +716,7 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async() => {
|
submit = async() => {
|
||||||
|
const { tshow } = this.state;
|
||||||
const {
|
const {
|
||||||
onSubmit, rid: roomId, tmid, showSend, sharing
|
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -772,7 +780,7 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
// Thread
|
// Thread
|
||||||
if (threadsEnabled && replyWithMention) {
|
if (threadsEnabled && replyWithMention) {
|
||||||
onSubmit(message, replyingMessage.id);
|
onSubmit(message, replyingMessage.id, tshow);
|
||||||
|
|
||||||
// Legacy reply or quote (quote is a reply without mention)
|
// Legacy reply or quote (quote is a reply without mention)
|
||||||
} else {
|
} else {
|
||||||
|
@ -792,7 +800,7 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
// Normal message
|
// Normal message
|
||||||
} else {
|
} else {
|
||||||
onSubmit(message);
|
onSubmit(message, undefined, tshow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -844,6 +852,27 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressSendToChannel = () => this.setState(({ tshow }) => ({ tshow: !tshow }))
|
||||||
|
|
||||||
|
renderSendToChannel = () => {
|
||||||
|
const { tshow } = this.state;
|
||||||
|
const { theme, tmid, replying } = this.props;
|
||||||
|
|
||||||
|
if (!tmid && !replying) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
style={[styles.sendToChannelButton, { backgroundColor: themes[theme].messageboxBackground }]}
|
||||||
|
onPress={this.onPressSendToChannel}
|
||||||
|
testID='messagebox-send-to-channel'
|
||||||
|
>
|
||||||
|
<CustomIcon name={tshow ? 'checkbox-checked' : 'checkbox-unchecked'} size={24} color={themes[theme].auxiliaryText} />
|
||||||
|
<Text style={[styles.sendToChannelText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Messagebox_Send_to_channel')}</Text>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderContent = () => {
|
renderContent = () => {
|
||||||
const {
|
const {
|
||||||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||||
|
@ -903,6 +932,7 @@ class MessageBox extends Component {
|
||||||
keyboardType='twitter'
|
keyboardType='twitter'
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
placeholder={I18n.t('New_Message')}
|
placeholder={I18n.t('New_Message')}
|
||||||
|
placeholderTextColor={themes[theme].auxiliaryTintColor}
|
||||||
onChangeText={this.onChangeText}
|
onChangeText={this.onChangeText}
|
||||||
onSelectionChange={this.onSelectionChange}
|
onSelectionChange={this.onSelectionChange}
|
||||||
underlineColorAndroid='transparent'
|
underlineColorAndroid='transparent'
|
||||||
|
@ -938,6 +968,7 @@ class MessageBox extends Component {
|
||||||
{textInputAndButtons}
|
{textInputAndButtons}
|
||||||
{recordAudio}
|
{recordAudio}
|
||||||
</View>
|
</View>
|
||||||
|
{this.renderSendToChannel()}
|
||||||
</View>
|
</View>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,12 +7,6 @@ const MENTION_HEIGHT = 50;
|
||||||
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
|
const SCROLLVIEW_MENTION_HEIGHT = 4 * MENTION_HEIGHT;
|
||||||
|
|
||||||
export default StyleSheet.create({
|
export default StyleSheet.create({
|
||||||
textBox: {
|
|
||||||
flex: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
|
||||||
zIndex: 2
|
|
||||||
},
|
|
||||||
composer: {
|
composer: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderTopWidth: 1
|
borderTopWidth: 1
|
||||||
|
@ -24,7 +18,7 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
textBoxInput: {
|
textBoxInput: {
|
||||||
textAlignVertical: 'center',
|
textAlignVertical: 'center',
|
||||||
maxHeight: 242,
|
maxHeight: 240,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
// paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering
|
// paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering
|
||||||
|
@ -32,7 +26,7 @@ export default StyleSheet.create({
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
fontSize: 17,
|
fontSize: 16,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
...sharedStyles.textRegular
|
...sharedStyles.textRegular
|
||||||
},
|
},
|
||||||
|
@ -40,7 +34,7 @@ export default StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 56
|
height: 48
|
||||||
},
|
},
|
||||||
mentionList: {
|
mentionList: {
|
||||||
maxHeight: MENTION_HEIGHT * 4
|
maxHeight: MENTION_HEIGHT * 4
|
||||||
|
@ -110,10 +104,21 @@ export default StyleSheet.create({
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between'
|
||||||
},
|
},
|
||||||
recordingCancelText: {
|
recordingCancelText: {
|
||||||
fontSize: 17,
|
fontSize: 16,
|
||||||
...sharedStyles.textRegular
|
...sharedStyles.textRegular
|
||||||
},
|
},
|
||||||
buttonsWhitespace: {
|
buttonsWhitespace: {
|
||||||
width: 15
|
width: 15
|
||||||
|
},
|
||||||
|
sendToChannelButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 18
|
||||||
|
},
|
||||||
|
sendToChannelText: {
|
||||||
|
fontSize: 12,
|
||||||
|
marginLeft: 4,
|
||||||
|
...sharedStyles.textRegular
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,36 +3,31 @@ import { View, Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Touchable from './Touchable';
|
import Touchable from './Touchable';
|
||||||
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
|
import { BUTTON_HIT_SLOP } from './utils';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
|
||||||
const CallButton = React.memo(({
|
const CallButton = React.memo(({
|
||||||
dlm, theme, callJitsi
|
theme, callJitsi
|
||||||
}) => {
|
}) => (
|
||||||
const time = formatLastMessage(dlm);
|
<View style={styles.buttonContainer}>
|
||||||
return (
|
<Touchable
|
||||||
<View style={styles.buttonContainer}>
|
onPress={callJitsi}
|
||||||
<Touchable
|
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||||
onPress={callJitsi}
|
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
>
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
<>
|
||||||
>
|
<CustomIcon name='camera' size={16} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||||
<>
|
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
|
||||||
<CustomIcon name='camera' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
</>
|
||||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
|
</Touchable>
|
||||||
</>
|
</View>
|
||||||
</Touchable>
|
));
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CallButton.propTypes = {
|
CallButton.propTypes = {
|
||||||
dlm: PropTypes.string,
|
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
callJitsi: PropTypes.func
|
callJitsi: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,18 +3,22 @@ import { View, Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Touchable from './Touchable';
|
import Touchable from './Touchable';
|
||||||
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
import { formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { DISCUSSION } from './constants';
|
import { DISCUSSION } from './constants';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import MessageContext from './Context';
|
import MessageContext from './Context';
|
||||||
|
import { formatDateThreads } from '../../utils/room';
|
||||||
|
|
||||||
const Discussion = React.memo(({
|
const Discussion = React.memo(({
|
||||||
msg, dcount, dlm, theme
|
msg, dcount, dlm, theme
|
||||||
}) => {
|
}) => {
|
||||||
const time = formatLastMessage(dlm);
|
let time;
|
||||||
|
if (dlm) {
|
||||||
|
time = formatDateThreads(dlm);
|
||||||
|
}
|
||||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||||
const { onDiscussionPress } = useContext(MessageContext);
|
const { onDiscussionPress } = useContext(MessageContext);
|
||||||
return (
|
return (
|
||||||
|
@ -25,11 +29,11 @@ const Discussion = React.memo(({
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={onDiscussionPress}
|
onPress={onDiscussionPress}
|
||||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<CustomIcon name='discussions' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
<CustomIcon name='discussions' size={16} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
|
|
|
@ -1,32 +1,49 @@
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
|
||||||
import { formatLastMessage, formatMessageCount } from './utils';
|
import { formatMessageCount } from './utils';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { THREAD } from './constants';
|
import { THREAD } from './constants';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import { formatDateThreads } from '../../utils/room';
|
||||||
|
import MessageContext from './Context';
|
||||||
|
|
||||||
const Thread = React.memo(({
|
const Thread = React.memo(({
|
||||||
msg, tcount, tlm, customThreadTimeFormat, isThreadRoom, theme
|
msg, tcount, tlm, isThreadRoom, theme, id
|
||||||
}) => {
|
}) => {
|
||||||
if (!tlm || isThreadRoom || tcount === 0) {
|
if (!tlm || isThreadRoom || tcount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = formatLastMessage(tlm, customThreadTimeFormat);
|
const {
|
||||||
|
getBadgeColor, toggleFollowThread, user, replies
|
||||||
|
} = useContext(MessageContext);
|
||||||
|
const time = formatDateThreads(tlm);
|
||||||
const buttonText = formatMessageCount(tcount, THREAD);
|
const buttonText = formatMessageCount(tcount, THREAD);
|
||||||
|
const badgeColor = getBadgeColor(id);
|
||||||
|
const isFollowing = replies?.find(u => u === user.id);
|
||||||
return (
|
return (
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<View
|
<View
|
||||||
style={[styles.button, styles.smallButton, { backgroundColor: themes[theme].tintColor }]}
|
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
|
||||||
testID={`message-thread-button-${ msg }`}
|
testID={`message-thread-button-${ msg }`}
|
||||||
>
|
>
|
||||||
<CustomIcon name='threads' size={20} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} />
|
<CustomIcon name='threads' size={16} style={[styles.buttonIcon, { color: themes[theme].buttonText }]} />
|
||||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{buttonText}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||||
|
{badgeColor ? <View style={[styles.threadBadge, { backgroundColor: badgeColor }]} /> : null}
|
||||||
|
<Touchable onPress={() => toggleFollowThread(isFollowing, id)}>
|
||||||
|
<CustomIcon
|
||||||
|
name={isFollowing ? 'notification' : 'notification-disabled'}
|
||||||
|
size={24}
|
||||||
|
color={themes[theme].auxiliaryText}
|
||||||
|
style={styles.threadBell}
|
||||||
|
/>
|
||||||
|
</Touchable>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
|
@ -44,8 +61,8 @@ Thread.propTypes = {
|
||||||
tcount: PropTypes.string,
|
tcount: PropTypes.string,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
tlm: PropTypes.string,
|
tlm: PropTypes.string,
|
||||||
customThreadTimeFormat: PropTypes.string,
|
isThreadRoom: PropTypes.bool,
|
||||||
isThreadRoom: PropTypes.bool
|
id: PropTypes.string
|
||||||
};
|
};
|
||||||
Thread.displayName = 'MessageThread';
|
Thread.displayName = 'MessageThread';
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ class MessageContainer extends React.Component {
|
||||||
}),
|
}),
|
||||||
rid: PropTypes.string,
|
rid: PropTypes.string,
|
||||||
timeFormat: PropTypes.string,
|
timeFormat: PropTypes.string,
|
||||||
customThreadTimeFormat: PropTypes.string,
|
|
||||||
style: PropTypes.any,
|
style: PropTypes.any,
|
||||||
archived: PropTypes.bool,
|
archived: PropTypes.bool,
|
||||||
broadcast: PropTypes.bool,
|
broadcast: PropTypes.bool,
|
||||||
|
@ -49,7 +48,9 @@ class MessageContainer extends React.Component {
|
||||||
navToRoomInfo: PropTypes.func,
|
navToRoomInfo: PropTypes.func,
|
||||||
callJitsi: PropTypes.func,
|
callJitsi: PropTypes.func,
|
||||||
blockAction: PropTypes.func,
|
blockAction: PropTypes.func,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
getBadgeColor: PropTypes.func,
|
||||||
|
toggleFollowThread: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -265,10 +266,10 @@ class MessageContainer extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { author } = this.state;
|
const { author } = this.state;
|
||||||
const {
|
const {
|
||||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
|
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, getBadgeColor, toggleFollowThread
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
|
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
let message = msg;
|
let message = msg;
|
||||||
|
@ -291,7 +292,10 @@ class MessageContainer extends React.Component {
|
||||||
onReactionPress: this.onReactionPress,
|
onReactionPress: this.onReactionPress,
|
||||||
onEncryptedPress: this.onEncryptedPress,
|
onEncryptedPress: this.onEncryptedPress,
|
||||||
onDiscussionPress: this.onDiscussionPress,
|
onDiscussionPress: this.onDiscussionPress,
|
||||||
onReactionLongPress: this.onReactionLongPress
|
onReactionLongPress: this.onReactionLongPress,
|
||||||
|
getBadgeColor,
|
||||||
|
toggleFollowThread,
|
||||||
|
replies
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Message
|
<Message
|
||||||
|
@ -309,7 +313,6 @@ class MessageContainer extends React.Component {
|
||||||
avatar={avatar}
|
avatar={avatar}
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
customThreadTimeFormat={customThreadTimeFormat}
|
|
||||||
style={style}
|
style={style}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
broadcast={broadcast}
|
broadcast={broadcast}
|
||||||
|
|
|
@ -40,11 +40,11 @@ export default StyleSheet.create({
|
||||||
reactionsContainer: {
|
reactionsContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
marginTop: 6
|
marginTop: 8
|
||||||
},
|
},
|
||||||
reactionButton: {
|
reactionButton: {
|
||||||
marginRight: 6,
|
marginRight: 8,
|
||||||
marginBottom: 6,
|
marginBottom: 8,
|
||||||
borderRadius: 2
|
borderRadius: 2
|
||||||
},
|
},
|
||||||
reactionContainer: {
|
reactionContainer: {
|
||||||
|
@ -83,27 +83,24 @@ export default StyleSheet.create({
|
||||||
paddingVertical: 5
|
paddingVertical: 5
|
||||||
},
|
},
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
marginTop: 6,
|
marginTop: 8,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 12,
|
||||||
height: 44,
|
paddingVertical: 8,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: 2
|
borderRadius: 2
|
||||||
},
|
},
|
||||||
smallButton: {
|
|
||||||
height: 30
|
|
||||||
},
|
|
||||||
buttonIcon: {
|
buttonIcon: {
|
||||||
marginRight: 6
|
marginRight: 8
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
...sharedStyles.textMedium
|
...sharedStyles.textSemibold
|
||||||
},
|
},
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
// flex: 1,
|
// flex: 1,
|
||||||
|
@ -143,10 +140,8 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
paddingLeft: 10,
|
paddingLeft: 8,
|
||||||
lineHeight: 22,
|
...sharedStyles.textRegular
|
||||||
...sharedStyles.textRegular,
|
|
||||||
fontWeight: '300'
|
|
||||||
},
|
},
|
||||||
repliedThread: {
|
repliedThread: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -170,6 +165,15 @@ export default StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
|
threadBadge: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginLeft: 8
|
||||||
|
},
|
||||||
|
threadBell: {
|
||||||
|
marginLeft: 8
|
||||||
|
},
|
||||||
readReceipt: {
|
readReceipt: {
|
||||||
lineHeight: 20
|
lineHeight: 20
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,20 +1,6 @@
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { DISCUSSION } from './constants';
|
import { DISCUSSION } from './constants';
|
||||||
|
|
||||||
export const formatLastMessage = (lm, customFormat) => {
|
|
||||||
if (customFormat) {
|
|
||||||
return moment(lm).format(customFormat);
|
|
||||||
}
|
|
||||||
return lm ? moment(lm).calendar(null, {
|
|
||||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
|
||||||
sameDay: 'h:mm A',
|
|
||||||
lastWeek: 'dddd',
|
|
||||||
sameElse: 'MMM D'
|
|
||||||
}) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatMessageCount = (count, type) => {
|
export const formatMessageCount = (count, type) => {
|
||||||
const discussion = type === DISCUSSION;
|
const discussion = type === DISCUSSION;
|
||||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||||
|
|
|
@ -54,7 +54,6 @@ const OmnichannelStatus = memo(({
|
||||||
<UnreadBadge
|
<UnreadBadge
|
||||||
style={styles.queueIcon}
|
style={styles.queueIcon}
|
||||||
unread={queueSize}
|
unread={queueSize}
|
||||||
theme={theme}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
|
@ -658,5 +658,13 @@ export default {
|
||||||
You_will_be_logged_out_from_other_locations: 'You\'ll be logged out from other locations.',
|
You_will_be_logged_out_from_other_locations: 'You\'ll be logged out from other locations.',
|
||||||
Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully',
|
Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully',
|
||||||
Logout_failed: 'Logout failed!',
|
Logout_failed: 'Logout failed!',
|
||||||
Log_analytics_events: 'Log analytics events'
|
Log_analytics_events: 'Log analytics events',
|
||||||
|
Following: 'Following',
|
||||||
|
Threads_displaying_all: 'Displaying All',
|
||||||
|
Threads_displaying_following: 'Displaying Following',
|
||||||
|
Threads_displaying_unread: 'Displaying Unread',
|
||||||
|
No_threads: 'There are no threads',
|
||||||
|
No_threads_following: 'You are not following any threads',
|
||||||
|
No_threads_unread: 'There are no unread threads',
|
||||||
|
Messagebox_Send_to_channel: 'Send to channel'
|
||||||
};
|
};
|
||||||
|
|
|
@ -602,5 +602,13 @@ export default {
|
||||||
You_will_be_logged_out_from_other_locations: 'Você perderá a sessão de outros clientes',
|
You_will_be_logged_out_from_other_locations: 'Você perderá a sessão de outros clientes',
|
||||||
Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso',
|
Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso',
|
||||||
Logout_failed: 'Falha ao desconectar!',
|
Logout_failed: 'Falha ao desconectar!',
|
||||||
Log_analytics_events: 'Logar eventos no analytics'
|
Log_analytics_events: 'Logar eventos no analytics',
|
||||||
|
Following: 'Seguindo',
|
||||||
|
Threads_displaying_all: 'Mostrando Tudo',
|
||||||
|
Threads_displaying_following: 'Mostrando Seguindo',
|
||||||
|
Threads_displaying_unread: 'Mostrando Não Lidos',
|
||||||
|
No_threads: 'Não há tópicos',
|
||||||
|
No_threads_following: 'Você não está seguindo tópicos',
|
||||||
|
No_threads_unread: 'Não há tópicos não lidos',
|
||||||
|
Messagebox_Send_to_channel: 'Mostrar no canal'
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,4 +79,6 @@ export default class Message extends Model {
|
||||||
@json('blocks', sanitizer) blocks;
|
@json('blocks', sanitizer) blocks;
|
||||||
|
|
||||||
@field('e2e') e2e;
|
@field('e2e') e2e;
|
||||||
|
|
||||||
|
@field('tshow') tshow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,12 @@ export default class Subscription extends Model {
|
||||||
|
|
||||||
@field('group_mentions') groupMentions;
|
@field('group_mentions') groupMentions;
|
||||||
|
|
||||||
|
@json('tunread', sanitizer) tunread;
|
||||||
|
|
||||||
|
@json('tunread_user', sanitizer) tunreadUser;
|
||||||
|
|
||||||
|
@json('tunread_group', sanitizer) tunreadGroup;
|
||||||
|
|
||||||
@date('room_updated_at') roomUpdatedAt;
|
@date('room_updated_at') roomUpdatedAt;
|
||||||
|
|
||||||
@field('ro') ro;
|
@field('ro') ro;
|
||||||
|
|
|
@ -16,5 +16,7 @@ export default class User extends Model {
|
||||||
|
|
||||||
@field('login_email_password') loginEmailPassword;
|
@field('login_email_password') loginEmailPassword;
|
||||||
|
|
||||||
|
@field('show_message_in_main_thread') showMessageInMainThread;
|
||||||
|
|
||||||
@json('roles', sanitizer) roles;
|
@json('roles', sanitizer) roles;
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,6 +170,12 @@ export default schemaMigrations({
|
||||||
{
|
{
|
||||||
toVersion: 11,
|
toVersion: 11,
|
||||||
steps: [
|
steps: [
|
||||||
|
addColumns({
|
||||||
|
table: 'messages',
|
||||||
|
columns: [
|
||||||
|
{ name: 'tshow', type: 'boolean', isOptional: true }
|
||||||
|
]
|
||||||
|
}),
|
||||||
createTable({
|
createTable({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -182,6 +188,9 @@ export default schemaMigrations({
|
||||||
addColumns({
|
addColumns({
|
||||||
table: 'subscriptions',
|
table: 'subscriptions',
|
||||||
columns: [
|
columns: [
|
||||||
|
{ name: 'tunread', type: 'string', isOptional: true },
|
||||||
|
{ name: 'tunread_user', type: 'string', isOptional: true },
|
||||||
|
{ name: 'tunread_group', type: 'string', isOptional: true },
|
||||||
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -90,6 +90,7 @@ export default schemaMigrations({
|
||||||
addColumns({
|
addColumns({
|
||||||
table: 'users',
|
table: 'users',
|
||||||
columns: [
|
columns: [
|
||||||
|
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
||||||
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,9 @@ export default appSchema({
|
||||||
{ name: 'unread', type: 'number' },
|
{ name: 'unread', type: 'number' },
|
||||||
{ name: 'user_mentions', type: 'number' },
|
{ name: 'user_mentions', type: 'number' },
|
||||||
{ name: 'group_mentions', type: 'number' },
|
{ name: 'group_mentions', type: 'number' },
|
||||||
|
{ name: 'tunread', type: 'string', isOptional: true },
|
||||||
|
{ name: 'tunread_user', type: 'string', isOptional: true },
|
||||||
|
{ name: 'tunread_group', type: 'string', isOptional: true },
|
||||||
{ name: 'room_updated_at', type: 'number' },
|
{ name: 'room_updated_at', type: 'number' },
|
||||||
{ name: 'ro', type: 'boolean' },
|
{ name: 'ro', type: 'boolean' },
|
||||||
{ name: 'last_open', type: 'number', isOptional: true },
|
{ name: 'last_open', type: 'number', isOptional: true },
|
||||||
|
@ -108,7 +111,8 @@ export default appSchema({
|
||||||
{ name: 'translations', type: 'string', isOptional: true },
|
{ name: 'translations', type: 'string', isOptional: true },
|
||||||
{ name: 'tmsg', type: 'string', isOptional: true },
|
{ name: 'tmsg', type: 'string', isOptional: true },
|
||||||
{ name: 'blocks', type: 'string', isOptional: true },
|
{ name: 'blocks', type: 'string', isOptional: true },
|
||||||
{ name: 'e2e', type: 'string', isOptional: true }
|
{ name: 'e2e', type: 'string', isOptional: true },
|
||||||
|
{ name: 'tshow', type: 'boolean', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default appSchema({
|
||||||
{ name: 'statusText', type: 'string', isOptional: true },
|
{ name: 'statusText', type: 'string', isOptional: true },
|
||||||
{ name: 'roles', type: 'string', isOptional: true },
|
{ name: 'roles', type: 'string', isOptional: true },
|
||||||
{ name: 'login_email_password', type: 'boolean', isOptional: true },
|
{ name: 'login_email_password', type: 'boolean', isOptional: true },
|
||||||
|
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
||||||
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
{ name: 'avatar_etag', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -14,8 +14,12 @@ export const merge = (subscription, room) => {
|
||||||
}
|
}
|
||||||
if (room) {
|
if (room) {
|
||||||
if (room._updatedAt) {
|
if (room._updatedAt) {
|
||||||
subscription.roomUpdatedAt = room._updatedAt;
|
|
||||||
subscription.lastMessage = normalizeMessage(room.lastMessage);
|
subscription.lastMessage = normalizeMessage(room.lastMessage);
|
||||||
|
if (subscription.lastMessage) {
|
||||||
|
subscription.roomUpdatedAt = subscription.lastMessage.ts;
|
||||||
|
} else {
|
||||||
|
subscription.roomUpdatedAt = room._updatedAt;
|
||||||
|
}
|
||||||
subscription.description = room.description;
|
subscription.description = room.description;
|
||||||
subscription.topic = room.topic;
|
subscription.topic = room.topic;
|
||||||
subscription.announcement = room.announcement;
|
subscription.announcement = room.announcement;
|
||||||
|
|
|
@ -86,7 +86,7 @@ export async function resendMessage(message, tmid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function(rid, msg, tmid, user) {
|
export default async function(rid, msg, tmid, user, tshow) {
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const subsCollection = db.collections.get('subscriptions');
|
const subsCollection = db.collections.get('subscriptions');
|
||||||
|
@ -97,7 +97,7 @@ export default async function(rid, msg, tmid, user) {
|
||||||
const batch = [];
|
const batch = [];
|
||||||
|
|
||||||
let message = {
|
let message = {
|
||||||
_id: messageId, rid, msg, tmid
|
_id: messageId, rid, msg, tmid, tshow
|
||||||
};
|
};
|
||||||
message = await Encryption.encryptMessage(message);
|
message = await Encryption.encryptMessage(message);
|
||||||
|
|
||||||
|
@ -179,8 +179,9 @@ export default async function(rid, msg, tmid, user) {
|
||||||
};
|
};
|
||||||
if (tmid && tMessageRecord) {
|
if (tmid && tMessageRecord) {
|
||||||
m.tmid = tmid;
|
m.tmid = tmid;
|
||||||
m.tlm = messageDate;
|
// m.tlm = messageDate; // I don't think this is necessary... leaving it commented just in case...
|
||||||
m.tmsg = tMessageRecord.msg;
|
m.tmsg = tMessageRecord.msg;
|
||||||
|
m.tshow = tshow;
|
||||||
}
|
}
|
||||||
m.t = message.t;
|
m.t = message.t;
|
||||||
if (message.t === E2E_MESSAGE_TYPE) {
|
if (message.t === E2E_MESSAGE_TYPE) {
|
||||||
|
|
|
@ -273,6 +273,9 @@ export default function subscribeRooms() {
|
||||||
if (diff?.statusLivechat) {
|
if (diff?.statusLivechat) {
|
||||||
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
store.dispatch(setUser({ statusLivechat: diff.statusLivechat }));
|
||||||
}
|
}
|
||||||
|
if (['settings.preferences.showMessageInMainThread'] in diff) {
|
||||||
|
store.dispatch(setUser({ showMessageInMainThread: diff['settings.preferences.showMessageInMainThread'] }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (/subscriptions/.test(ev)) {
|
if (/subscriptions/.test(ev)) {
|
||||||
if (type === 'removed') {
|
if (type === 'removed') {
|
||||||
|
|
|
@ -491,7 +491,8 @@ const RocketChat = {
|
||||||
emails: result.me.emails,
|
emails: result.me.emails,
|
||||||
roles: result.me.roles,
|
roles: result.me.roles,
|
||||||
avatarETag: result.me.avatarETag,
|
avatarETag: result.me.avatarETag,
|
||||||
loginEmailPassword
|
loginEmailPassword,
|
||||||
|
showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true
|
||||||
};
|
};
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
@ -920,8 +921,12 @@ const RocketChat = {
|
||||||
getUidDirectMessage(room) {
|
getUidDirectMessage(room) {
|
||||||
const { id: userId } = reduxStore.getState().login.user;
|
const { id: userId } = reduxStore.getState().login.user;
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// legacy method
|
// legacy method
|
||||||
if (!room.uids && room.rid && room.t === 'd') {
|
if (!room?.uids && room.rid && room.t === 'd') {
|
||||||
return room.rid.replace(userId, '').trim();
|
return room.rid.replace(userId, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -929,8 +934,8 @@ const RocketChat = {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const me = room && room.uids && room.uids.find(uid => uid === userId);
|
const me = room.uids?.find(uid => uid === userId);
|
||||||
const other = room && room.uids && room.uids.filter(uid => uid !== userId);
|
const other = room.uids?.filter(uid => uid !== userId);
|
||||||
|
|
||||||
return other && other.length ? other[0] : me;
|
return other && other.length ? other[0] : me;
|
||||||
},
|
},
|
||||||
|
@ -1242,11 +1247,18 @@ const RocketChat = {
|
||||||
}
|
}
|
||||||
return this.post('chat.unfollowMessage', { mid });
|
return this.post('chat.unfollowMessage', { mid });
|
||||||
},
|
},
|
||||||
getThreadsList({ rid, count, offset }) {
|
getThreadsList({
|
||||||
// RC 1.0
|
rid, count, offset, text
|
||||||
return this.sdk.get('chat.getThreadsList', {
|
}) {
|
||||||
|
const params = {
|
||||||
rid, count, offset, sort: { ts: -1 }
|
rid, count, offset, sort: { ts: -1 }
|
||||||
});
|
};
|
||||||
|
if (text) {
|
||||||
|
params.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RC 1.0
|
||||||
|
return this.sdk.get('chat.getThreadsList', params);
|
||||||
},
|
},
|
||||||
getSyncThreadsList({ rid, updatedSince }) {
|
getSyncThreadsList({ rid, updatedSince }) {
|
||||||
// RC 1.0
|
// RC 1.0
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -37,9 +37,11 @@ const RoomItem = ({
|
||||||
alert,
|
alert,
|
||||||
hideUnreadStatus,
|
hideUnreadStatus,
|
||||||
unread,
|
unread,
|
||||||
tunread,
|
|
||||||
userMentions,
|
userMentions,
|
||||||
groupMentions,
|
groupMentions,
|
||||||
|
tunread,
|
||||||
|
tunreadUser,
|
||||||
|
tunreadGroup,
|
||||||
roomUpdatedAt,
|
roomUpdatedAt,
|
||||||
testID,
|
testID,
|
||||||
swipeEnabled,
|
swipeEnabled,
|
||||||
|
@ -113,10 +115,11 @@ const RoomItem = ({
|
||||||
/>
|
/>
|
||||||
<UnreadBadge
|
<UnreadBadge
|
||||||
unread={unread}
|
unread={unread}
|
||||||
tunread={tunread}
|
|
||||||
userMentions={userMentions}
|
userMentions={userMentions}
|
||||||
groupMentions={groupMentions}
|
groupMentions={groupMentions}
|
||||||
theme={theme}
|
tunread={tunread}
|
||||||
|
tunreadUser={tunreadUser}
|
||||||
|
tunreadGroup={tunreadGroup}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
@ -138,10 +141,11 @@ const RoomItem = ({
|
||||||
/>
|
/>
|
||||||
<UnreadBadge
|
<UnreadBadge
|
||||||
unread={unread}
|
unread={unread}
|
||||||
tunread={tunread}
|
|
||||||
userMentions={userMentions}
|
userMentions={userMentions}
|
||||||
groupMentions={groupMentions}
|
groupMentions={groupMentions}
|
||||||
theme={theme}
|
tunread={tunread}
|
||||||
|
tunreadUser={tunreadUser}
|
||||||
|
tunreadGroup={tunreadGroup}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -177,9 +181,11 @@ RoomItem.propTypes = {
|
||||||
alert: PropTypes.bool,
|
alert: PropTypes.bool,
|
||||||
hideUnreadStatus: PropTypes.bool,
|
hideUnreadStatus: PropTypes.bool,
|
||||||
unread: PropTypes.number,
|
unread: PropTypes.number,
|
||||||
tunread: PropTypes.array,
|
|
||||||
userMentions: PropTypes.number,
|
userMentions: PropTypes.number,
|
||||||
groupMentions: PropTypes.number,
|
groupMentions: PropTypes.number,
|
||||||
|
tunread: PropTypes.array,
|
||||||
|
tunreadUser: PropTypes.array,
|
||||||
|
tunreadGroup: PropTypes.array,
|
||||||
roomUpdatedAt: PropTypes.instanceOf(Date),
|
roomUpdatedAt: PropTypes.instanceOf(Date),
|
||||||
swipeEnabled: PropTypes.bool,
|
swipeEnabled: PropTypes.bool,
|
||||||
toggleFav: PropTypes.func,
|
toggleFav: PropTypes.func,
|
||||||
|
|
|
@ -178,6 +178,7 @@ class RoomItemContainer extends React.Component {
|
||||||
const avatar = getRoomAvatar(item);
|
const avatar = getRoomAvatar(item);
|
||||||
const isRead = getIsRead(item);
|
const isRead = getIsRead(item);
|
||||||
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
|
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
|
||||||
|
const alert = (item.alert || item.tunread?.length);
|
||||||
|
|
||||||
let accessibilityLabel = name;
|
let accessibilityLabel = name;
|
||||||
if (item.unread === 1) {
|
if (item.unread === 1) {
|
||||||
|
@ -203,7 +204,6 @@ class RoomItemContainer extends React.Component {
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
date={date}
|
date={date}
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
userMentions={item.userMentions}
|
|
||||||
width={width}
|
width={width}
|
||||||
favorite={item.f}
|
favorite={item.f}
|
||||||
toggleFav={toggleFav}
|
toggleFav={toggleFav}
|
||||||
|
@ -221,15 +221,18 @@ class RoomItemContainer extends React.Component {
|
||||||
prid={item.prid}
|
prid={item.prid}
|
||||||
status={status}
|
status={status}
|
||||||
hideUnreadStatus={item.hideUnreadStatus}
|
hideUnreadStatus={item.hideUnreadStatus}
|
||||||
alert={item.alert}
|
alert={alert}
|
||||||
roomUpdatedAt={item.roomUpdatedAt}
|
roomUpdatedAt={item.roomUpdatedAt}
|
||||||
lastMessage={item.lastMessage}
|
lastMessage={item.lastMessage}
|
||||||
showLastMessage={showLastMessage}
|
showLastMessage={showLastMessage}
|
||||||
username={username}
|
username={username}
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
unread={item.unread}
|
unread={item.unread}
|
||||||
tunread={item.tunread}
|
userMentions={item.userMentions}
|
||||||
groupMentions={item.groupMentions}
|
groupMentions={item.groupMentions}
|
||||||
|
tunread={item.tunread}
|
||||||
|
tunreadUser={item.tunreadUser}
|
||||||
|
tunreadGroup={item.tunreadGroup}
|
||||||
avatarETag={avatarETag || item.avatarETag}
|
avatarETag={avatarETag || item.avatarETag}
|
||||||
swipeEnabled={swipeEnabled}
|
swipeEnabled={swipeEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,9 +10,9 @@ export const getUnreadStyle = ({
|
||||||
let backgroundColor = themes[theme].unreadBackground;
|
let backgroundColor = themes[theme].unreadBackground;
|
||||||
const color = themes[theme].buttonText;
|
const color = themes[theme].buttonText;
|
||||||
if (userMentions > 0 || tunreadUser?.length) {
|
if (userMentions > 0 || tunreadUser?.length) {
|
||||||
backgroundColor = themes[theme].mentionMeColor;
|
backgroundColor = themes[theme].mentionMeBackground;
|
||||||
} else if (groupMentions > 0 || tunreadGroup?.length) {
|
} else if (groupMentions > 0 || tunreadGroup?.length) {
|
||||||
backgroundColor = themes[theme].mentionGroupColor;
|
backgroundColor = themes[theme].mentionGroupBackground;
|
||||||
} else if (tunread?.length > 0) {
|
} else if (tunread?.length > 0) {
|
||||||
backgroundColor = themes[theme].tunreadBackground;
|
backgroundColor = themes[theme].tunreadBackground;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ import { getUnreadStyle } from './getUnreadStyle';
|
||||||
const testsForTheme = (theme) => {
|
const testsForTheme = (theme) => {
|
||||||
const getUnreadStyleUtil = ({ ...props }) => getUnreadStyle({ theme, ...props });
|
const getUnreadStyleUtil = ({ ...props }) => getUnreadStyle({ theme, ...props });
|
||||||
|
|
||||||
|
test('render empty', () => {
|
||||||
|
expect(getUnreadStyleUtil({})).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
test('render unread', () => {
|
test('render unread', () => {
|
||||||
expect(getUnreadStyleUtil({
|
expect(getUnreadStyleUtil({
|
||||||
unread: 1
|
unread: 1
|
||||||
|
@ -28,7 +32,7 @@ const testsForTheme = (theme) => {
|
||||||
unread: 1,
|
unread: 1,
|
||||||
userMentions: 1
|
userMentions: 1
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
backgroundColor: themes[theme].mentionMeColor,
|
backgroundColor: themes[theme].mentionMeBackground,
|
||||||
color: themes[theme].buttonText
|
color: themes[theme].buttonText
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -38,7 +42,7 @@ const testsForTheme = (theme) => {
|
||||||
unread: 1,
|
unread: 1,
|
||||||
groupMentions: 1
|
groupMentions: 1
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
backgroundColor: themes[theme].mentionGroupColor,
|
backgroundColor: themes[theme].mentionGroupBackground,
|
||||||
color: themes[theme].buttonText
|
color: themes[theme].buttonText
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -50,7 +54,7 @@ const testsForTheme = (theme) => {
|
||||||
groupMentions: 1,
|
groupMentions: 1,
|
||||||
tunread: [1]
|
tunread: [1]
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
backgroundColor: themes[theme].mentionMeColor,
|
backgroundColor: themes[theme].mentionMeBackground,
|
||||||
color: themes[theme].buttonText
|
color: themes[theme].buttonText
|
||||||
});
|
});
|
||||||
expect(getUnreadStyleUtil({
|
expect(getUnreadStyleUtil({
|
||||||
|
@ -58,7 +62,7 @@ const testsForTheme = (theme) => {
|
||||||
groupMentions: 1,
|
groupMentions: 1,
|
||||||
tunread: [1]
|
tunread: [1]
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
backgroundColor: themes[theme].mentionGroupColor,
|
backgroundColor: themes[theme].mentionGroupBackground,
|
||||||
color: themes[theme].buttonText
|
color: themes[theme].buttonText
|
||||||
});
|
});
|
||||||
expect(getUnreadStyleUtil({
|
expect(getUnreadStyleUtil({
|
||||||
|
|
|
@ -22,10 +22,8 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
unreadText: {
|
unreadText: {
|
||||||
// overflow: 'hidden',
|
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
...sharedStyles.textSemibold,
|
...sharedStyles.textSemibold
|
||||||
fontWeight: '600'
|
|
||||||
},
|
},
|
||||||
textSmall: {
|
textSmall: {
|
||||||
fontSize: 10
|
fontSize: 10
|
||||||
|
|
|
@ -179,6 +179,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
statusText: user.statusText,
|
statusText: user.statusText,
|
||||||
roles: user.roles,
|
roles: user.roles,
|
||||||
loginEmailPassword: user.loginEmailPassword,
|
loginEmailPassword: user.loginEmailPassword,
|
||||||
|
showMessageInMainThread: user.showMessageInMainThread,
|
||||||
avatarETag: user.avatarETag
|
avatarETag: user.avatarETag
|
||||||
};
|
};
|
||||||
yield serversDB.action(async() => {
|
yield serversDB.action(async() => {
|
||||||
|
|
|
@ -190,7 +190,6 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='ThreadMessagesView'
|
name='ThreadMessagesView'
|
||||||
component={ThreadMessagesView}
|
component={ThreadMessagesView}
|
||||||
options={props => ThreadMessagesView.navigationOptions({ ...props, isMasterDetail: true })}
|
|
||||||
/>
|
/>
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='MarkdownTableView'
|
name='MarkdownTableView'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
|
|
||||||
|
@ -21,5 +22,26 @@ export const formatDate = date => moment(date).calendar(null, {
|
||||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||||
sameDay: 'LT',
|
sameDay: 'LT',
|
||||||
lastWeek: 'dddd',
|
lastWeek: 'dddd',
|
||||||
sameElse: 'MMM D'
|
sameElse: 'L'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const formatDateThreads = date => moment(date).calendar(null, {
|
||||||
|
sameDay: 'LT',
|
||||||
|
lastDay: `[${ I18n.t('Yesterday') }] LT`,
|
||||||
|
lastWeek: 'dddd LT',
|
||||||
|
sameElse: 'LL'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getBadgeColor = ({ subscription, messageId, theme }) => {
|
||||||
|
if (subscription?.tunreadUser?.includes(messageId)) {
|
||||||
|
return themes[theme].mentionMeBackground;
|
||||||
|
}
|
||||||
|
if (subscription?.tunreadGroup?.includes(messageId)) {
|
||||||
|
return themes[theme].mentionGroupBackground;
|
||||||
|
}
|
||||||
|
if (subscription?.tunread?.includes(messageId)) {
|
||||||
|
return themes[theme].tunreadBackground;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title;
|
||||||
|
|
|
@ -24,9 +24,6 @@ const styles = StyleSheet.create({
|
||||||
...sharedStyles.textSemibold,
|
...sharedStyles.textSemibold,
|
||||||
fontSize: TITLE_SIZE
|
fontSize: TITLE_SIZE
|
||||||
},
|
},
|
||||||
scroll: {
|
|
||||||
alignItems: 'center'
|
|
||||||
},
|
|
||||||
subtitle: {
|
subtitle: {
|
||||||
...sharedStyles.textRegular,
|
...sharedStyles.textRegular,
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
|
@ -36,11 +33,9 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
const SubTitle = React.memo(({
|
||||||
if (!subtitle && !usersTyping.length) {
|
usersTyping, subtitle, renderFunc, theme
|
||||||
return null;
|
}) => {
|
||||||
}
|
|
||||||
|
|
||||||
// typing
|
// typing
|
||||||
if (usersTyping.length) {
|
if (usersTyping.length) {
|
||||||
let usersText;
|
let usersText;
|
||||||
|
@ -57,6 +52,11 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderFunc
|
||||||
|
if (renderFunc) {
|
||||||
|
return renderFunc();
|
||||||
|
}
|
||||||
|
|
||||||
// subtitle
|
// subtitle
|
||||||
if (subtitle) {
|
if (subtitle) {
|
||||||
return (
|
return (
|
||||||
|
@ -69,12 +69,15 @@ const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
SubTitle.propTypes = {
|
SubTitle.propTypes = {
|
||||||
usersTyping: PropTypes.array,
|
usersTyping: PropTypes.array,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
subtitle: PropTypes.string
|
subtitle: PropTypes.string,
|
||||||
|
renderFunc: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderTitle = React.memo(({
|
const HeaderTitle = React.memo(({
|
||||||
|
@ -113,7 +116,7 @@ HeaderTitle.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = React.memo(({
|
const Header = React.memo(({
|
||||||
title, subtitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme
|
title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, roomUserId, theme
|
||||||
}) => {
|
}) => {
|
||||||
const portrait = height > width;
|
const portrait = height > width;
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
|
@ -126,6 +129,22 @@ const Header = React.memo(({
|
||||||
|
|
||||||
const onPress = () => goRoomActionsView();
|
const onPress = () => goRoomActionsView();
|
||||||
|
|
||||||
|
let renderFunc;
|
||||||
|
if (tmid) {
|
||||||
|
renderFunc = () => (
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Icon
|
||||||
|
type={prid ? 'discussion' : type}
|
||||||
|
tmid={tmid}
|
||||||
|
status={status}
|
||||||
|
roomUserId={roomUserId}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}>{parentTitle}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID='room-view-header-actions'
|
testID='room-view-header-actions'
|
||||||
|
@ -135,7 +154,7 @@ const Header = React.memo(({
|
||||||
disabled={tmid}
|
disabled={tmid}
|
||||||
>
|
>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />
|
{tmid ? null : <Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />}
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
title={title}
|
title={title}
|
||||||
tmid={tmid}
|
tmid={tmid}
|
||||||
|
@ -145,7 +164,7 @@ const Header = React.memo(({
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{tmid ? null : <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} />}
|
<SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} renderFunc={renderFunc} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -163,6 +182,7 @@ Header.propTypes = {
|
||||||
usersTyping: PropTypes.array,
|
usersTyping: PropTypes.array,
|
||||||
connecting: PropTypes.bool,
|
connecting: PropTypes.bool,
|
||||||
roomUserId: PropTypes.string,
|
roomUserId: PropTypes.string,
|
||||||
|
parentTitle: PropTypes.string,
|
||||||
goRoomActionsView: PropTypes.func
|
goRoomActionsView: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,9 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
const Icon = React.memo(({
|
const Icon = React.memo(({
|
||||||
roomUserId, type, status, theme
|
roomUserId, type, status, theme, tmid
|
||||||
}) => {
|
}) => {
|
||||||
if (type === 'd' && roomUserId) {
|
if ((type === 'd' || tmid) && roomUserId) {
|
||||||
return <Status size={10} style={styles.status} status={status} />;
|
return <Status size={10} style={styles.status} status={status} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +37,6 @@ const Icon = React.memo(({
|
||||||
let icon;
|
let icon;
|
||||||
if (type === 'discussion') {
|
if (type === 'discussion') {
|
||||||
icon = 'discussions';
|
icon = 'discussions';
|
||||||
} else if (type === 'thread') {
|
|
||||||
icon = 'threads';
|
|
||||||
} else if (type === 'c') {
|
} else if (type === 'c') {
|
||||||
icon = 'channel-public';
|
icon = 'channel-public';
|
||||||
} else if (type === 'l') {
|
} else if (type === 'l') {
|
||||||
|
@ -68,6 +66,7 @@ Icon.propTypes = {
|
||||||
roomUserId: PropTypes.string,
|
roomUserId: PropTypes.string,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
status: PropTypes.string,
|
status: PropTypes.string,
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
tmid: PropTypes.string
|
||||||
};
|
};
|
||||||
export default Icon;
|
export default Icon;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
import * as HeaderButton from '../../../containers/HeaderButton';
|
import * as HeaderButton from '../../../containers/HeaderButton';
|
||||||
import database from '../../../lib/database';
|
import database from '../../../lib/database';
|
||||||
import { getUserSelector } from '../../../selectors/login';
|
import { getUserSelector } from '../../../selectors/login';
|
||||||
import { logEvent, events } from '../../../utils/log';
|
import { logEvent, events } from '../../../utils/log';
|
||||||
|
|
||||||
|
class RightButtonsContainer extends Component {
|
||||||
class RightButtonsContainer extends React.PureComponent {
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
userId: PropTypes.string,
|
userId: PropTypes.string,
|
||||||
threadsEnabled: PropTypes.bool,
|
threadsEnabled: PropTypes.bool,
|
||||||
|
@ -23,30 +23,63 @@ class RightButtonsContainer extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isFollowingThread: true
|
isFollowingThread: true,
|
||||||
|
tunread: [],
|
||||||
|
tunreadUser: [],
|
||||||
|
tunreadGroup: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { tmid } = this.props;
|
const { tmid, rid } = this.props;
|
||||||
|
const db = database.active;
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
const db = database.active;
|
|
||||||
try {
|
try {
|
||||||
const threadRecord = await db.collections.get('messages').find(tmid);
|
const threadRecord = await db.collections.get('messages').find(tmid);
|
||||||
this.observeThead(threadRecord);
|
this.observeThread(threadRecord);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Can\'t find message to observe.');
|
console.log('Can\'t find message to observe.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rid) {
|
||||||
|
try {
|
||||||
|
const subCollection = db.collections.get('subscriptions');
|
||||||
|
const subRecord = await subCollection.find(rid);
|
||||||
|
this.observeSubscription(subRecord);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Can\'t find subscription to observe.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
const {
|
||||||
|
isFollowingThread, tunread, tunreadUser, tunreadGroup
|
||||||
|
} = this.state;
|
||||||
|
if (nextState.isFollowingThread !== isFollowingThread) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isEqual(nextState.tunread, tunread)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isEqual(nextState.tunreadUser, tunreadUser)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isEqual(nextState.tunreadGroup, tunreadGroup)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.threadSubscription && this.threadSubscription.unsubscribe) {
|
if (this.threadSubscription && this.threadSubscription.unsubscribe) {
|
||||||
this.threadSubscription.unsubscribe();
|
this.threadSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
if (this.subSubscription && this.subSubscription.unsubscribe) {
|
||||||
|
this.subSubscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observeThead = (threadRecord) => {
|
observeThread = (threadRecord) => {
|
||||||
const threadObservable = threadRecord.observe();
|
const threadObservable = threadRecord.observe();
|
||||||
this.threadSubscription = threadObservable
|
this.threadSubscription = threadObservable
|
||||||
.subscribe(thread => this.updateThread(thread));
|
.subscribe(thread => this.updateThread(thread));
|
||||||
|
@ -59,6 +92,22 @@ class RightButtonsContainer extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeSubscription = (subRecord) => {
|
||||||
|
const subObservable = subRecord.observe();
|
||||||
|
this.subSubscription = subObservable
|
||||||
|
.subscribe((sub) => {
|
||||||
|
this.updateSubscription(sub);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubscription = (sub) => {
|
||||||
|
this.setState({
|
||||||
|
tunread: sub?.tunread,
|
||||||
|
tunreadUser: sub?.tunreadUser,
|
||||||
|
tunreadGroup: sub?.tunreadGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
goThreadsView = () => {
|
goThreadsView = () => {
|
||||||
logEvent(events.ROOM_GO_THREADS);
|
logEvent(events.ROOM_GO_THREADS);
|
||||||
const {
|
const {
|
||||||
|
@ -93,7 +142,9 @@ class RightButtonsContainer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isFollowingThread } = this.state;
|
const {
|
||||||
|
isFollowingThread, tunread, tunreadUser, tunreadGroup
|
||||||
|
} = this.state;
|
||||||
const { t, tmid, threadsEnabled } = this.props;
|
const { t, tmid, threadsEnabled } = this.props;
|
||||||
if (t === 'l') {
|
if (t === 'l') {
|
||||||
return null;
|
return null;
|
||||||
|
@ -116,6 +167,13 @@ class RightButtonsContainer extends React.PureComponent {
|
||||||
iconName='threads'
|
iconName='threads'
|
||||||
onPress={this.goThreadsView}
|
onPress={this.goThreadsView}
|
||||||
testID='room-view-header-threads'
|
testID='room-view-header-threads'
|
||||||
|
badge={() => (
|
||||||
|
<HeaderButton.Badge
|
||||||
|
tunread={tunread}
|
||||||
|
tunreadUser={tunreadUser}
|
||||||
|
tunreadGroup={tunreadGroup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<HeaderButton.Item
|
<HeaderButton.Item
|
||||||
|
|
|
@ -27,7 +27,8 @@ class RoomHeaderView extends Component {
|
||||||
widthOffset: PropTypes.number,
|
widthOffset: PropTypes.number,
|
||||||
goRoomActionsView: PropTypes.func,
|
goRoomActionsView: PropTypes.func,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: PropTypes.number
|
height: PropTypes.number,
|
||||||
|
parentTitle: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
|
@ -75,7 +76,7 @@ class RoomHeaderView extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height
|
title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height, parentTitle
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let subtitle;
|
let subtitle;
|
||||||
|
@ -103,6 +104,7 @@ class RoomHeaderView extends Component {
|
||||||
roomUserId={roomUserId}
|
roomUserId={roomUserId}
|
||||||
goRoomActionsView={goRoomActionsView}
|
goRoomActionsView={goRoomActionsView}
|
||||||
connecting={connecting}
|
connecting={connecting}
|
||||||
|
parentTitle={parentTitle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -111,10 +113,12 @@ class RoomHeaderView extends Component {
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
let statusText;
|
let statusText;
|
||||||
let status = 'offline';
|
let status = 'offline';
|
||||||
const { roomUserId, type, visitor = {} } = ownProps;
|
const {
|
||||||
|
roomUserId, type, visitor = {}, tmid
|
||||||
|
} = ownProps;
|
||||||
|
|
||||||
if (state.meteor.connected) {
|
if (state.meteor.connected) {
|
||||||
if (type === 'd' && state.activeUsers[roomUserId]) {
|
if ((type === 'd' || (tmid && roomUserId)) && state.activeUsers[roomUserId]) {
|
||||||
({ status, statusText } = state.activeUsers[roomUserId]);
|
({ status, statusText } = state.activeUsers[roomUserId]);
|
||||||
} else if (type === 'l' && visitor?.status) {
|
} else if (type === 'l' && visitor?.status) {
|
||||||
({ status } = visitor);
|
({ status } = visitor);
|
||||||
|
|
|
@ -30,7 +30,8 @@ class List extends React.Component {
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
listRef: PropTypes.func,
|
listRef: PropTypes.func,
|
||||||
hideSystemMessages: PropTypes.array,
|
hideSystemMessages: PropTypes.array,
|
||||||
navigation: PropTypes.object
|
navigation: PropTypes.object,
|
||||||
|
showMessageInMainThread: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
// this.state.loading works for this.onEndReached and RoomView.init
|
// this.state.loading works for this.onEndReached and RoomView.init
|
||||||
|
@ -140,7 +141,7 @@ class List extends React.Component {
|
||||||
|
|
||||||
query = async() => {
|
query = async() => {
|
||||||
this.count += QUERY_SIZE;
|
this.count += QUERY_SIZE;
|
||||||
const { rid, tmid } = this.props;
|
const { rid, tmid, showMessageInMainThread } = this.props;
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
|
|
||||||
// handle servers with version < 3.0.0
|
// handle servers with version < 3.0.0
|
||||||
|
@ -167,14 +168,23 @@ class List extends React.Component {
|
||||||
)
|
)
|
||||||
.observe();
|
.observe();
|
||||||
} else if (rid) {
|
} else if (rid) {
|
||||||
|
const whereClause = [
|
||||||
|
Q.where('rid', rid),
|
||||||
|
Q.experimentalSortBy('ts', Q.desc),
|
||||||
|
Q.experimentalSkip(0),
|
||||||
|
Q.experimentalTake(this.count)
|
||||||
|
];
|
||||||
|
if (!showMessageInMainThread) {
|
||||||
|
whereClause.push(
|
||||||
|
Q.or(
|
||||||
|
Q.where('tmid', null),
|
||||||
|
Q.where('tshow', Q.eq(true))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
this.messagesObservable = db.collections
|
this.messagesObservable = db.collections
|
||||||
.get('messages')
|
.get('messages')
|
||||||
.query(
|
.query(...whereClause)
|
||||||
Q.where('rid', rid),
|
|
||||||
Q.experimentalSortBy('ts', Q.desc),
|
|
||||||
Q.experimentalSkip(0),
|
|
||||||
Q.experimentalTake(this.count)
|
|
||||||
)
|
|
||||||
.observe();
|
.observe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
const DateSeparator = React.memo(({ ts, unread, theme }) => {
|
const DateSeparator = React.memo(({ ts, unread, theme }) => {
|
||||||
const date = ts ? moment(ts).format('MMM DD, YYYY') : null;
|
const date = ts ? moment(ts).format('LL') : null;
|
||||||
const unreadLine = { backgroundColor: themes[theme].dangerColor };
|
const unreadLine = { backgroundColor: themes[theme].dangerColor };
|
||||||
const unreadText = { color: themes[theme].dangerColor };
|
const unreadText = { color: themes[theme].dangerColor };
|
||||||
if (ts && unread) {
|
if (ts && unread) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { themes } from '../../constants/colors';
|
||||||
import debounce from '../../utils/debounce';
|
import debounce from '../../utils/debounce';
|
||||||
import ReactionsModal from '../../containers/ReactionsModal';
|
import ReactionsModal from '../../containers/ReactionsModal';
|
||||||
import { LISTENER } from '../../containers/Toast';
|
import { LISTENER } from '../../containers/Toast';
|
||||||
import { isBlocked } from '../../utils/room';
|
import { getBadgeColor, isBlocked, makeThreadName } from '../../utils/room';
|
||||||
import { isReadOnly } from '../../utils/isReadOnly';
|
import { isReadOnly } from '../../utils/isReadOnly';
|
||||||
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
import { isIOS, isTablet } from '../../utils/deviceInfo';
|
||||||
import { showErrorAlert } from '../../utils/info';
|
import { showErrorAlert } from '../../utils/info';
|
||||||
|
@ -82,7 +82,8 @@ class RoomView extends React.Component {
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string.isRequired,
|
||||||
token: PropTypes.string.isRequired
|
token: PropTypes.string.isRequired,
|
||||||
|
showMessageInMainThread: PropTypes.bool
|
||||||
}),
|
}),
|
||||||
appState: PropTypes.string,
|
appState: PropTypes.string,
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
|
@ -108,17 +109,18 @@ class RoomView extends React.Component {
|
||||||
this.rid = props.route.params?.rid;
|
this.rid = props.route.params?.rid;
|
||||||
this.t = props.route.params?.t;
|
this.t = props.route.params?.t;
|
||||||
this.tmid = props.route.params?.tmid;
|
this.tmid = props.route.params?.tmid;
|
||||||
const room = props.route.params?.room;
|
|
||||||
const selectedMessage = props.route.params?.message;
|
const selectedMessage = props.route.params?.message;
|
||||||
const name = props.route.params?.name;
|
const name = props.route.params?.name;
|
||||||
const fname = props.route.params?.fname;
|
const fname = props.route.params?.fname;
|
||||||
const search = props.route.params?.search;
|
const search = props.route.params?.search;
|
||||||
const prid = props.route.params?.prid;
|
const prid = props.route.params?.prid;
|
||||||
|
const room = props.route.params?.room ?? {
|
||||||
|
rid: this.rid, t: this.t, name, fname, prid
|
||||||
|
};
|
||||||
|
const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room);
|
||||||
this.state = {
|
this.state = {
|
||||||
joined: true,
|
joined: true,
|
||||||
room: room || {
|
room,
|
||||||
rid: this.rid, t: this.t, name, fname, prid
|
|
||||||
},
|
|
||||||
roomUpdate: {},
|
roomUpdate: {},
|
||||||
member: {},
|
member: {},
|
||||||
lastOpen: null,
|
lastOpen: null,
|
||||||
|
@ -132,7 +134,7 @@ class RoomView extends React.Component {
|
||||||
reacting: false,
|
reacting: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
unreadsCount: null,
|
unreadsCount: null,
|
||||||
roomUserId: null
|
roomUserId
|
||||||
};
|
};
|
||||||
this.setHeader();
|
this.setHeader();
|
||||||
|
|
||||||
|
@ -292,24 +294,28 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state;
|
|
||||||
const {
|
const {
|
||||||
navigation, route, isMasterDetail, theme, baseUrl, user, insets
|
room, unreadsCount, roomUserId
|
||||||
|
} = this.state;
|
||||||
|
const {
|
||||||
|
navigation, isMasterDetail, theme, baseUrl, user, insets, route
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const rid = route.params?.rid;
|
const { rid, tmid } = this;
|
||||||
const prid = route.params?.prid;
|
const prid = room?.prid;
|
||||||
let title = route.params?.name;
|
let title = route.params?.name;
|
||||||
if ((room.id || room.rid) && !this.tmid) {
|
let parentTitle;
|
||||||
|
if ((room.id || room.rid) && !tmid) {
|
||||||
title = RocketChat.getRoomTitle(room);
|
title = RocketChat.getRoomTitle(room);
|
||||||
}
|
}
|
||||||
|
if (tmid) {
|
||||||
|
parentTitle = RocketChat.getRoomTitle(room);
|
||||||
|
}
|
||||||
const subtitle = room?.topic;
|
const subtitle = room?.topic;
|
||||||
const t = route.params?.t || room?.t;
|
const t = room?.t;
|
||||||
const tmid = route.params?.tmid;
|
|
||||||
const { id: userId, token } = user;
|
const { id: userId, token } = user;
|
||||||
const avatar = room?.name;
|
const avatar = room?.name;
|
||||||
const roomUserId = route.params?.roomUserId || stateRoomUserId;
|
|
||||||
const visitor = room?.visitor;
|
const visitor = room?.visitor;
|
||||||
if (!rid) {
|
if (!room?.rid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headerTitlePosition = getHeaderTitlePosition(insets);
|
const headerTitlePosition = getHeaderTitlePosition(insets);
|
||||||
|
@ -341,6 +347,7 @@ class RoomView extends React.Component {
|
||||||
prid={prid}
|
prid={prid}
|
||||||
tmid={tmid}
|
tmid={tmid}
|
||||||
title={title}
|
title={title}
|
||||||
|
parentTitle={parentTitle}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
type={t}
|
type={t}
|
||||||
roomUserId={roomUserId}
|
roomUserId={roomUserId}
|
||||||
|
@ -625,6 +632,7 @@ class RoomView extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onThreadPress = debounce(async(item) => {
|
onThreadPress = debounce(async(item) => {
|
||||||
|
const { roomUserId } = this.state;
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
if (item.tmid) {
|
if (item.tmid) {
|
||||||
if (!item.tmsg) {
|
if (!item.tmsg) {
|
||||||
|
@ -635,11 +643,11 @@ class RoomView extends React.Component {
|
||||||
name = I18n.t('Encrypted_message');
|
name = I18n.t('Encrypted_message');
|
||||||
}
|
}
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread'
|
rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId
|
||||||
});
|
});
|
||||||
} else if (item.tlm) {
|
} else if (item.tlm) {
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread'
|
rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 1000, true)
|
}, 1000, true)
|
||||||
|
@ -669,10 +677,10 @@ class RoomView extends React.Component {
|
||||||
this.setState(...args);
|
this.setState(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage = (message, tmid) => {
|
sendMessage = (message, tmid, tshow) => {
|
||||||
logEvent(events.ROOM_SEND_MESSAGE);
|
logEvent(events.ROOM_SEND_MESSAGE);
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user).then(() => {
|
RocketChat.sendMessage(this.rid, message, this.tmid || tmid, user, tshow).then(() => {
|
||||||
if (this.list && this.list.current) {
|
if (this.list && this.list.current) {
|
||||||
this.list.current.update();
|
this.list.current.update();
|
||||||
}
|
}
|
||||||
|
@ -761,15 +769,21 @@ class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFollowThread = async(isFollowingThread) => {
|
toggleFollowThread = async(isFollowingThread, tmid) => {
|
||||||
try {
|
try {
|
||||||
await RocketChat.toggleFollowMessage(this.tmid, !isFollowingThread);
|
await RocketChat.toggleFollowMessage(tmid ?? this.tmid, !isFollowingThread);
|
||||||
EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') });
|
EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBadgeColor = (messageId) => {
|
||||||
|
const { room } = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
return getBadgeColor({ subscription: room, theme, messageId });
|
||||||
|
}
|
||||||
|
|
||||||
navToRoomInfo = (navParam) => {
|
navToRoomInfo = (navParam) => {
|
||||||
const { navigation, user, isMasterDetail } = this.props;
|
const { navigation, user, isMasterDetail } = this.props;
|
||||||
logEvent(events[`ROOM_GO_${ navParam.t === 'd' ? 'USER' : 'ROOM' }_INFO`]);
|
logEvent(events[`ROOM_GO_${ navParam.t === 'd' ? 'USER' : 'ROOM' }_INFO`]);
|
||||||
|
@ -895,6 +909,8 @@ class RoomView extends React.Component {
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
getCustomEmoji={this.getCustomEmoji}
|
||||||
callJitsi={this.callJitsi}
|
callJitsi={this.callJitsi}
|
||||||
blockAction={this.blockAction}
|
blockAction={this.blockAction}
|
||||||
|
getBadgeColor={this.getBadgeColor}
|
||||||
|
toggleFollowThread={this.toggleFollowThread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1042,6 +1058,7 @@ class RoomView extends React.Component {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
|
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
|
||||||
|
showMessageInMainThread={user.showMessageInMainThread}
|
||||||
/>
|
/>
|
||||||
{this.renderFooter()}
|
{this.renderFooter()}
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
|
|
|
@ -75,7 +75,7 @@ const GROUPS_HEADER = 'Private_Groups';
|
||||||
const OMNICHANNEL_HEADER = 'Open_Livechats';
|
const OMNICHANNEL_HEADER = 'Open_Livechats';
|
||||||
const QUERY_SIZE = 20;
|
const QUERY_SIZE = 20;
|
||||||
|
|
||||||
const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus;
|
const filterIsUnread = s => (s.unread > 0 || s.tunread?.length > 0 || s.alert) && !s.hideUnreadStatus;
|
||||||
const filterIsFavorite = s => s.f;
|
const filterIsFavorite = s => s.f;
|
||||||
const filterIsOmnichannel = s => s.t === 'l';
|
const filterIsOmnichannel = s => s.t === 'l';
|
||||||
|
|
||||||
|
|
|
@ -63,8 +63,6 @@ export default StyleSheet.create({
|
||||||
marginHorizontal: 12
|
marginHorizontal: 12
|
||||||
},
|
},
|
||||||
queueIcon: {
|
queueIcon: {
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
marginHorizontal: 12
|
marginHorizontal: 12
|
||||||
},
|
},
|
||||||
groupTitleContainer: {
|
groupTitleContainer: {
|
||||||
|
|
|
@ -149,7 +149,7 @@ class SearchMessagesView extends React.Component {
|
||||||
item={item}
|
item={item}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
timeFormat='MMM Do YYYY, h:mm:ss a'
|
timeFormat='LLL'
|
||||||
isHeader
|
isHeader
|
||||||
showAttachment={() => {}}
|
showAttachment={() => {}}
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
getCustomEmoji={this.getCustomEmoji}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { themes } from '../../constants/colors';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
import { isAndroid, isTablet } from '../../utils/deviceInfo';
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
|
import { makeThreadName } from '../../utils/room';
|
||||||
|
|
||||||
const androidMarginLeft = isTablet ? 0 : 4;
|
const androidMarginLeft = isTablet ? 0 : 4;
|
||||||
|
|
||||||
|
@ -64,6 +65,13 @@ const Header = React.memo(({ room, thread, theme }) => {
|
||||||
|
|
||||||
const textColor = themes[theme].previewTintColor;
|
const textColor = themes[theme].previewTintColor;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
if (thread?.id) {
|
||||||
|
title = makeThreadName(thread);
|
||||||
|
} else {
|
||||||
|
title = RocketChat.getRoomTitle(room);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.inner}>
|
<View style={styles.inner}>
|
||||||
|
@ -78,7 +86,7 @@ const Header = React.memo(({ room, thread, theme }) => {
|
||||||
style={[styles.name, { color: textColor }]}
|
style={[styles.name, { color: textColor }]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{thread?.msg ?? RocketChat.getRoomTitle(room)}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { FlatList, InteractionManager } from 'react-native';
|
||||||
FlatList, View, Text, InteractionManager
|
|
||||||
} from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import moment from 'moment';
|
|
||||||
import orderBy from 'lodash/orderBy';
|
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
|
||||||
|
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { HeaderBackButton } from '@react-navigation/stack';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import Message from '../../containers/message';
|
import Item from './Item';
|
||||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
|
import { sanitizeLikeString } from '../../lib/database/utils';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
|
@ -25,25 +24,19 @@ import { withTheme } from '../../theme';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import * as HeaderButton from '../../containers/HeaderButton';
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
|
import * as List from '../../containers/List';
|
||||||
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />);
|
import Dropdown from './Dropdown';
|
||||||
Separator.propTypes = {
|
import DropdownItemHeader from './Dropdown/DropdownItemHeader';
|
||||||
theme: PropTypes.string
|
import { FILTER } from './filters';
|
||||||
};
|
import NoDataFound from './NoDataFound';
|
||||||
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
|
import { getBadgeColor, makeThreadName } from '../../utils/room';
|
||||||
|
import { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
|
import SearchHeader from './SearchHeader';
|
||||||
|
|
||||||
const API_FETCH_COUNT = 50;
|
const API_FETCH_COUNT = 50;
|
||||||
|
|
||||||
class ThreadMessagesView extends React.Component {
|
class ThreadMessagesView extends React.Component {
|
||||||
static navigationOptions = ({ navigation, isMasterDetail }) => {
|
|
||||||
const options = {
|
|
||||||
title: I18n.t('Threads')
|
|
||||||
};
|
|
||||||
if (isMasterDetail) {
|
|
||||||
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
navigation: PropTypes.object,
|
navigation: PropTypes.object,
|
||||||
|
@ -51,8 +44,8 @@ class ThreadMessagesView extends React.Component {
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
customEmojis: PropTypes.object,
|
isMasterDetail: PropTypes.bool,
|
||||||
isMasterDetail: PropTypes.bool
|
insets: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -63,9 +56,17 @@ class ThreadMessagesView extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
loading: false,
|
||||||
end: false,
|
end: false,
|
||||||
messages: []
|
messages: [],
|
||||||
|
displayingThreads: [],
|
||||||
|
subscription: {},
|
||||||
|
showFilterDropdown: false,
|
||||||
|
currentFilter: FILTER.ALL,
|
||||||
|
isSearching: false,
|
||||||
|
searchText: ''
|
||||||
};
|
};
|
||||||
this.subscribeData();
|
this.setHeader();
|
||||||
|
this.initSubscription();
|
||||||
|
this.subscribeMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -75,6 +76,15 @@ class ThreadMessagesView extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
insets
|
||||||
|
} = this.props;
|
||||||
|
if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) {
|
||||||
|
this.setHeader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
console.countReset(`${ this.constructor.name }.render calls`);
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
if (this.mountInteraction && this.mountInteraction.cancel) {
|
if (this.mountInteraction && this.mountInteraction.cancel) {
|
||||||
|
@ -91,48 +101,133 @@ class ThreadMessagesView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
getHeader = () => {
|
||||||
subscribeData = async() => {
|
const { isSearching } = this.state;
|
||||||
|
const {
|
||||||
|
navigation, isMasterDetail, insets, theme
|
||||||
|
} = this.props;
|
||||||
|
const headerTitlePosition = getHeaderTitlePosition(insets);
|
||||||
|
|
||||||
|
if (isSearching) {
|
||||||
|
return {
|
||||||
|
headerTitleAlign: 'left',
|
||||||
|
headerLeft: () => (
|
||||||
|
<HeaderButton.Container left>
|
||||||
|
<HeaderButton.Item
|
||||||
|
iconName='close'
|
||||||
|
onPress={this.onCancelSearchPress}
|
||||||
|
/>
|
||||||
|
</HeaderButton.Container>
|
||||||
|
),
|
||||||
|
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />,
|
||||||
|
headerTitleContainerStyle: {
|
||||||
|
left: headerTitlePosition.left,
|
||||||
|
right: headerTitlePosition.right
|
||||||
|
},
|
||||||
|
headerRight: () => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headerLeft: () => (
|
||||||
|
<HeaderBackButton
|
||||||
|
labelVisible={false}
|
||||||
|
onPress={() => navigation.pop()}
|
||||||
|
tintColor={themes[theme].headerTintColor}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
headerTitleAlign: 'center',
|
||||||
|
headerTitle: I18n.t('Threads'),
|
||||||
|
headerTitleContainerStyle: {
|
||||||
|
left: null,
|
||||||
|
right: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headerRight = () => (
|
||||||
|
<HeaderButton.Container>
|
||||||
|
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
|
||||||
|
</HeaderButton.Container>
|
||||||
|
);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeader = () => {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
const options = this.getHeader();
|
||||||
|
navigation.setOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
initSubscription = async() => {
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
|
|
||||||
|
// subscription query
|
||||||
const subscription = await db.collections
|
const subscription = await db.collections
|
||||||
.get('subscriptions')
|
.get('subscriptions')
|
||||||
.find(this.rid);
|
.find(this.rid);
|
||||||
const observable = subscription.observe();
|
const observable = subscription.observe();
|
||||||
this.subSubscription = observable
|
this.subSubscription = observable
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
this.subscription = data;
|
this.setState({ subscription: data });
|
||||||
});
|
|
||||||
this.messagesObservable = db.collections
|
|
||||||
.get('threads')
|
|
||||||
.query(
|
|
||||||
Q.where('rid', this.rid),
|
|
||||||
Q.where('t', Q.notEq('rm'))
|
|
||||||
)
|
|
||||||
.observeWithColumns(['updated_at']);
|
|
||||||
this.messagesSubscription = this.messagesObservable
|
|
||||||
.subscribe((data) => {
|
|
||||||
const messages = orderBy(data, ['ts'], ['desc']);
|
|
||||||
if (this.mounted) {
|
|
||||||
this.setState({ messages });
|
|
||||||
} else {
|
|
||||||
this.state.messages = messages;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.subscribeMessages(subscription);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeMessages = (subscription, searchText) => {
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
|
||||||
|
if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
|
||||||
|
this.messagesSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = [
|
||||||
|
Q.where('rid', this.rid),
|
||||||
|
Q.experimentalSortBy('tlm', Q.desc)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (searchText?.trim()) {
|
||||||
|
whereClause.push(Q.where('msg', Q.like(`%${ sanitizeLikeString(searchText.trim()) }%`)));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messagesObservable = db.collections
|
||||||
|
.get('threads')
|
||||||
|
.query(...whereClause)
|
||||||
|
.observeWithColumns(['updated_at']);
|
||||||
|
this.messagesSubscription = this.messagesObservable
|
||||||
|
.subscribe((messages) => {
|
||||||
|
const { currentFilter } = this.state;
|
||||||
|
const displayingThreads = this.getFilteredThreads(messages, subscription, currentFilter);
|
||||||
|
if (this.mounted) {
|
||||||
|
this.setState({ messages, displayingThreads });
|
||||||
|
} else {
|
||||||
|
this.state.messages = messages;
|
||||||
|
this.state.displayingThreads = displayingThreads;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
|
||||||
init = () => {
|
init = () => {
|
||||||
if (!this.subscription) {
|
const { subscription } = this.state;
|
||||||
|
if (!subscription) {
|
||||||
return this.load();
|
return this.load();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const lastThreadSync = new Date();
|
const lastThreadSync = new Date();
|
||||||
if (this.subscription.lastThreadSync) {
|
if (subscription.lastThreadSync) {
|
||||||
this.sync(this.subscription.lastThreadSync);
|
this.sync(subscription.lastThreadSync);
|
||||||
} else {
|
} else {
|
||||||
this.load(lastThreadSync);
|
this.load(lastThreadSync);
|
||||||
}
|
}
|
||||||
|
@ -142,9 +237,10 @@ class ThreadMessagesView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateThreads = async({ update, remove, lastThreadSync }) => {
|
updateThreads = async({ update, remove, lastThreadSync }) => {
|
||||||
|
const { subscription } = this.state;
|
||||||
// if there's no subscription, manage data on this.state.messages
|
// if there's no subscription, manage data on this.state.messages
|
||||||
// note: sync will never be called without subscription
|
// note: sync will never be called without subscription
|
||||||
if (!this.subscription) {
|
if (!subscription) {
|
||||||
this.setState(({ messages }) => ({ messages: [...messages, ...update] }));
|
this.setState(({ messages }) => ({ messages: [...messages, ...update] }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -152,7 +248,7 @@ class ThreadMessagesView extends React.Component {
|
||||||
try {
|
try {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const threadsCollection = db.collections.get('threads');
|
const threadsCollection = db.collections.get('threads');
|
||||||
const allThreadsRecords = await this.subscription.threads.fetch();
|
const allThreadsRecords = await subscription.threads.fetch();
|
||||||
let threadsToCreate = [];
|
let threadsToCreate = [];
|
||||||
let threadsToUpdate = [];
|
let threadsToUpdate = [];
|
||||||
let threadsToDelete = [];
|
let threadsToDelete = [];
|
||||||
|
@ -164,7 +260,7 @@ class ThreadMessagesView extends React.Component {
|
||||||
threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
|
threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
|
||||||
threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => {
|
threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => {
|
||||||
t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema);
|
t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema);
|
||||||
t.subscription.set(this.subscription);
|
t.subscription.set(subscription);
|
||||||
Object.assign(t, thread);
|
Object.assign(t, thread);
|
||||||
})));
|
})));
|
||||||
threadsToUpdate = threadsToUpdate.map((thread) => {
|
threadsToUpdate = threadsToUpdate.map((thread) => {
|
||||||
|
@ -185,7 +281,7 @@ class ThreadMessagesView extends React.Component {
|
||||||
...threadsToCreate,
|
...threadsToCreate,
|
||||||
...threadsToUpdate,
|
...threadsToUpdate,
|
||||||
...threadsToDelete,
|
...threadsToDelete,
|
||||||
this.subscription.prepareUpdate((s) => {
|
subscription.prepareUpdate((s) => {
|
||||||
s.lastThreadSync = lastThreadSync;
|
s.lastThreadSync = lastThreadSync;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -197,7 +293,9 @@ class ThreadMessagesView extends React.Component {
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
// eslint-disable-next-line react/sort-comp
|
||||||
load = debounce(async(lastThreadSync) => {
|
load = debounce(async(lastThreadSync) => {
|
||||||
const { loading, end, messages } = this.state;
|
const {
|
||||||
|
loading, end, messages, searchText
|
||||||
|
} = this.state;
|
||||||
if (end || loading || !this.mounted) {
|
if (end || loading || !this.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +304,7 @@ class ThreadMessagesView extends React.Component {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await RocketChat.getThreadsList({
|
const result = await RocketChat.getThreadsList({
|
||||||
rid: this.rid, count: API_FETCH_COUNT, offset: messages.length
|
rid: this.rid, count: API_FETCH_COUNT, offset: messages.length, text: searchText
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.updateThreads({ update: result.threads, lastThreadSync });
|
this.updateThreads({ update: result.threads, lastThreadSync });
|
||||||
|
@ -244,112 +342,168 @@ class ThreadMessagesView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage = lm => (
|
onSearchPress = () => {
|
||||||
lm ? moment(lm).calendar(null, {
|
this.setState({ isSearching: true }, () => this.setHeader());
|
||||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
|
||||||
sameDay: 'h:mm A',
|
|
||||||
lastWeek: 'dddd',
|
|
||||||
sameElse: 'MMM D'
|
|
||||||
}) : null
|
|
||||||
)
|
|
||||||
|
|
||||||
getCustomEmoji = (name) => {
|
|
||||||
const { customEmojis } = this.props;
|
|
||||||
const emoji = customEmojis[name];
|
|
||||||
if (emoji) {
|
|
||||||
return emoji;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showAttachment = (attachment) => {
|
onCancelSearchPress = () => {
|
||||||
const { navigation } = this.props;
|
this.setState({ isSearching: false, searchText: '' }, () => {
|
||||||
navigation.navigate('AttachmentView', { attachment });
|
const { subscription } = this.state;
|
||||||
|
this.setHeader();
|
||||||
|
this.subscribeMessages(subscription);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearchChangeText = debounce((searchText) => {
|
||||||
|
const { subscription } = this.state;
|
||||||
|
this.setState({ searchText }, () => this.subscribeMessages(subscription, searchText));
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
|
||||||
onThreadPress = debounce((item) => {
|
onThreadPress = debounce((item) => {
|
||||||
|
const { subscription } = this.state;
|
||||||
const { navigation, isMasterDetail } = this.props;
|
const { navigation, isMasterDetail } = this.props;
|
||||||
if (isMasterDetail) {
|
if (isMasterDetail) {
|
||||||
navigation.pop();
|
navigation.pop();
|
||||||
}
|
}
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread'
|
rid: item.subscription.id,
|
||||||
|
tmid: item.id,
|
||||||
|
name: makeThreadName(item),
|
||||||
|
t: 'thread',
|
||||||
|
roomUserId: RocketChat.getUidDirectMessage(subscription)
|
||||||
});
|
});
|
||||||
}, 1000, true)
|
}, 1000, true)
|
||||||
|
|
||||||
renderSeparator = () => {
|
getBadgeColor = (item) => {
|
||||||
|
const { subscription } = this.state;
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
return <Separator theme={theme} />;
|
return getBadgeColor({ subscription, theme, messageId: item?.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmpty = () => {
|
// helper to query threads
|
||||||
const { theme } = this.props;
|
getFilteredThreads = (messages, subscription, currentFilter) => {
|
||||||
return (
|
// const { currentFilter } = this.state;
|
||||||
<View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]} testID='thread-messages-view'>
|
const { user } = this.props;
|
||||||
<Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_thread_messages')}</Text>
|
if (currentFilter === FILTER.FOLLOWING) {
|
||||||
</View>
|
return messages?.filter(item => item?.replies?.find(u => u === user.id));
|
||||||
);
|
} else if (currentFilter === FILTER.UNREAD) {
|
||||||
}
|
return messages?.filter(item => subscription?.tunread?.includes(item?.id));
|
||||||
|
|
||||||
navToRoomInfo = (navParam) => {
|
|
||||||
const { navigation, user } = this.props;
|
|
||||||
if (navParam.rid === user.id) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
navigation.navigate('RoomInfoView', navParam);
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// method to update state with filtered threads
|
||||||
|
filterThreads = () => {
|
||||||
|
const { messages, subscription } = this.state;
|
||||||
|
const displayingThreads = this.getFilteredThreads(messages, subscription);
|
||||||
|
this.setState({ displayingThreads });
|
||||||
|
}
|
||||||
|
|
||||||
|
showFilterDropdown = () => this.setState({ showFilterDropdown: true })
|
||||||
|
|
||||||
|
closeFilterDropdown = () => this.setState({ showFilterDropdown: false })
|
||||||
|
|
||||||
|
onFilterSelected = (filter) => {
|
||||||
|
const { messages, subscription } = this.state;
|
||||||
|
const displayingThreads = this.getFilteredThreads(messages, subscription, filter);
|
||||||
|
this.setState({ currentFilter: filter, displayingThreads });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const {
|
const {
|
||||||
user, navigation, baseUrl, useRealName
|
user, navigation, baseUrl, useRealName
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const badgeColor = this.getBadgeColor(item);
|
||||||
return (
|
return (
|
||||||
<Message
|
<Item
|
||||||
key={item.id}
|
{...{
|
||||||
item={item}
|
item,
|
||||||
user={user}
|
user,
|
||||||
archived={false}
|
navigation,
|
||||||
broadcast={false}
|
baseUrl,
|
||||||
status={item.status}
|
useRealName,
|
||||||
navigation={navigation}
|
badgeColor
|
||||||
timeFormat='MMM D'
|
}}
|
||||||
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
|
onPress={this.onThreadPress}
|
||||||
onThreadPress={this.onThreadPress}
|
/>
|
||||||
baseUrl={baseUrl}
|
);
|
||||||
useRealName={useRealName}
|
}
|
||||||
getCustomEmoji={this.getCustomEmoji}
|
|
||||||
navToRoomInfo={this.navToRoomInfo}
|
renderHeader = () => {
|
||||||
showAttachment={this.showAttachment}
|
const { messages, currentFilter } = this.state;
|
||||||
|
if (!messages.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownItemHeader currentFilter={currentFilter} onPress={this.showFilterDropdown} />
|
||||||
|
<List.Separator />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent = () => {
|
||||||
|
const {
|
||||||
|
loading, messages, displayingThreads, currentFilter
|
||||||
|
} = this.state;
|
||||||
|
const { theme } = this.props;
|
||||||
|
if (!messages?.length || !displayingThreads?.length) {
|
||||||
|
let text;
|
||||||
|
if (currentFilter === FILTER.FOLLOWING) {
|
||||||
|
text = I18n.t('No_threads_following');
|
||||||
|
} else if (currentFilter === FILTER.UNREAD) {
|
||||||
|
text = I18n.t('No_threads_unread');
|
||||||
|
} else {
|
||||||
|
text = I18n.t('No_threads');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderHeader()}
|
||||||
|
<NoDataFound text={text} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={displayingThreads}
|
||||||
|
extraData={this.state}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
onEndReached={this.load}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
maxToRenderPerBatch={5}
|
||||||
|
windowSize={10}
|
||||||
|
initialNumToRender={7}
|
||||||
|
removeClippedSubviews={isIOS}
|
||||||
|
ItemSeparatorComponent={List.Separator}
|
||||||
|
ListHeaderComponent={this.renderHeader}
|
||||||
|
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
|
||||||
|
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const { loading, messages } = this.state;
|
const { showFilterDropdown, currentFilter } = this.state;
|
||||||
const { theme } = this.props;
|
|
||||||
|
|
||||||
if (!loading && messages.length === 0) {
|
|
||||||
return this.renderEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView testID='thread-messages-view'>
|
<SafeAreaView testID='thread-messages-view'>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<FlatList
|
{this.renderContent()}
|
||||||
data={messages}
|
{showFilterDropdown
|
||||||
extraData={this.state}
|
? (
|
||||||
renderItem={this.renderItem}
|
<Dropdown
|
||||||
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
|
currentFilter={currentFilter}
|
||||||
contentContainerStyle={styles.contentContainer}
|
onFilterSelected={this.onFilterSelected}
|
||||||
keyExtractor={item => item._id}
|
onClose={this.closeFilterDropdown}
|
||||||
onEndReached={this.load}
|
/>
|
||||||
onEndReachedThreshold={0.5}
|
)
|
||||||
maxToRenderPerBatch={5}
|
: null}
|
||||||
initialNumToRender={1}
|
|
||||||
ItemSeparatorComponent={this.renderSeparator}
|
|
||||||
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
|
|
||||||
/>
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -359,8 +513,7 @@ const mapStateToProps = state => ({
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
useRealName: state.settings.UI_Use_Real_Name,
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
customEmojis: state.customEmojis,
|
|
||||||
isMasterDetail: state.app.isMasterDetail
|
isMasterDetail: state.app.isMasterDetail
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(ThreadMessagesView));
|
export default connect(mapStateToProps)(withTheme(withSafeAreaInsets(ThreadMessagesView)));
|
||||||
|
|
|
@ -18,10 +18,13 @@ export default StyleSheet.create({
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
paddingBottom: 30
|
paddingBottom: 30
|
||||||
},
|
},
|
||||||
separator: {
|
dropdownContainer: {
|
||||||
height: StyleSheet.hairlineWidth,
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginLeft: 60,
|
position: 'absolute',
|
||||||
marginTop: 10
|
top: 0,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
...StyleSheet.absoluteFill
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,9 +54,9 @@ async function logout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockMessage(message) {
|
async function mockMessage(message) {
|
||||||
await element(by.id('messagebox-input')).tap();
|
await element(by.id('messagebox-input')).atIndex(0).tap();
|
||||||
await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`);
|
await element(by.id('messagebox-input')).atIndex(0).typeText(`${ data.random }${ message }`);
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).atIndex(0).tap();
|
||||||
await waitFor(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist().withTimeout(60000);
|
await waitFor(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist().withTimeout(60000);
|
||||||
await expect(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist();
|
await expect(element(by.label(`${ data.random }${ message }`)).atIndex(0)).toExist();
|
||||||
await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap();
|
await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap();
|
||||||
|
|
|
@ -294,24 +294,42 @@ describe('Room screen', () => {
|
||||||
await element(by.id('room-view-header-follow')).tap();
|
await element(by.id('room-view-header-follow')).tap();
|
||||||
await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000);
|
await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000);
|
||||||
await expect(element(by.id('room-view-header-unfollow'))).toExist();
|
await expect(element(by.id('room-view-header-unfollow'))).toExist();
|
||||||
await tapBack();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to thread from thread name', async() => {
|
it('should send message in thread only', async() => {
|
||||||
|
const messageText = 'threadonly';
|
||||||
|
await mockMessage(messageText);
|
||||||
|
await tapBack();
|
||||||
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
|
||||||
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
|
||||||
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
|
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
|
||||||
|
await waitFor(element(by.label(`${ data.random }${ messageText }`)).atIndex(0)).toNotExist().withTimeout(2000);
|
||||||
|
});
|
||||||
|
|
||||||
await mockMessage('dummymessagebetweenthethread');
|
it('should mark send to channel and show on main channel', async() => {
|
||||||
await element(by.label(thread)).atIndex(0).longPress();
|
const messageText = 'sendToChannel';
|
||||||
await expect(element(by.id('action-sheet'))).toExist();
|
await element(by.id(`message-thread-button-${ thread }`)).tap();
|
||||||
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
|
await element(by.id('messagebox-input')).atIndex(0).typeText(messageText);
|
||||||
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
await element(by.id('messagebox-send-to-channel')).tap();
|
||||||
await element(by.label('Reply in Thread')).tap();
|
|
||||||
await element(by.id('messagebox-input')).typeText('repliedagain');
|
|
||||||
await element(by.id('messagebox-send-message')).tap();
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
|
await tapBack();
|
||||||
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toExist();
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
|
||||||
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
|
||||||
|
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
|
||||||
|
await waitFor(element(by.label(messageText)).atIndex(0)).toExist().withTimeout(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to thread from thread name', async() => {
|
||||||
|
const messageText = 'navthreadname';
|
||||||
|
await mockMessage('dummymessagebetweenthethread');
|
||||||
|
await element(by.id(`message-thread-button-${ thread }`)).tap();
|
||||||
|
await element(by.id('messagebox-input')).atIndex(0).typeText(messageText);
|
||||||
|
await element(by.id('messagebox-send-to-channel')).tap();
|
||||||
|
await element(by.id('messagebox-send-message')).tap();
|
||||||
|
await tapBack();
|
||||||
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
|
||||||
|
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
|
||||||
|
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
|
||||||
|
|
||||||
await element(by.id(`message-thread-replied-on-${ thread }`)).tap();
|
await element(by.id(`message-thread-replied-on-${ thread }`)).tap();
|
||||||
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
|
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
|
||||||
|
@ -325,7 +343,7 @@ describe('Room screen', () => {
|
||||||
await element(by.id('room-view-header-threads')).tap();
|
await element(by.id('room-view-header-threads')).tap();
|
||||||
await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000);
|
await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000);
|
||||||
await expect(element(by.id('thread-messages-view'))).toExist();
|
await expect(element(by.id('thread-messages-view'))).toExist();
|
||||||
await element(by.id(`message-thread-button-${ thread }`)).atIndex(0).tap();
|
await element(by.id(`thread-messages-view-${ thread }`)).atIndex(0).tap();
|
||||||
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
|
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
|
||||||
await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000);
|
await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000);
|
||||||
await expect(element(by.id(`room-view-title-${ thread }`))).toExist();
|
await expect(element(by.id(`room-view-title-${ thread }`))).toExist();
|
||||||
|
|
|
@ -14,7 +14,7 @@ async function navigateToRoomInfo(type) {
|
||||||
room = privateRoomName;
|
room = privateRoomName;
|
||||||
}
|
}
|
||||||
await searchRoom(room);
|
await searchRoom(room);
|
||||||
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000);
|
await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toExist().withTimeout(60000);
|
||||||
await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap();
|
await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap();
|
||||||
await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
|
||||||
await element(by.id('room-view-header-actions')).tap();
|
await element(by.id('room-view-header-actions')).tap();
|
||||||
|
@ -167,6 +167,7 @@ describe('Room info screen', () => {
|
||||||
await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }new`);
|
await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }new`);
|
||||||
await element(by.type('UIScrollView')).atIndex(1).swipe('up');
|
await element(by.type('UIScrollView')).atIndex(1).swipe('up');
|
||||||
await element(by.id('room-info-edit-view-submit')).tap();
|
await element(by.id('room-info-edit-view-submit')).tap();
|
||||||
|
await waitForToast();
|
||||||
await tapBack();
|
await tapBack();
|
||||||
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
|
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
|
||||||
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`);
|
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`);
|
||||||
|
|
|
@ -1396,7 +1396,7 @@
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 4.11.0;
|
MARKETING_VERSION = 4.12.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
||||||
|
@ -1433,7 +1433,7 @@
|
||||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 4.11.0;
|
MARKETING_VERSION = 4.12.0;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>4.11.0</string>
|
<string>4.12.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>4.11.0</string>
|
<string>4.12.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
|
BIN
ios/custom.ttf
BIN
ios/custom.ttf
Binary file not shown.
|
@ -129,12 +129,8 @@
|
||||||
"@babel/core": "^7.8.4",
|
"@babel/core": "^7.8.4",
|
||||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||||
"@babel/runtime": "^7.8.4",
|
"@babel/runtime": "^7.8.4",
|
||||||
"@storybook/addon-actions": "5.3.19",
|
|
||||||
"@storybook/addon-links": "5.3.19",
|
|
||||||
"@storybook/addon-storyshots": "5.3.19",
|
"@storybook/addon-storyshots": "5.3.19",
|
||||||
"@storybook/addons": "5.3.19",
|
|
||||||
"@storybook/react-native": "5.3.19",
|
"@storybook/react-native": "5.3.19",
|
||||||
"@storybook/theming": "5.3.19",
|
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
|
|
|
@ -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';
|
import 'react-native-gesture-handler';
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
// jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => jest.fn(() => null));
|
jest.mock('../app/lib/database', () => jest.fn(() => null)); // comment this line to make storybook work
|
||||||
|
|
||||||
RNBootSplash.hide();
|
RNBootSplash.hide();
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ const ThemeStory = ({ theme }) => (
|
||||||
right={() => (
|
right={() => (
|
||||||
<HeaderButton.Container>
|
<HeaderButton.Container>
|
||||||
<HeaderButton.Item title='Threads' />
|
<HeaderButton.Item title='Threads' />
|
||||||
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={999} />} />
|
<HeaderButton.Item iconName='threads' badge={() => <HeaderButton.Badge tunread={[1]} />} />
|
||||||
</HeaderButton.Container>
|
</HeaderButton.Container>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -75,7 +75,11 @@ export default ({ theme }) => {
|
||||||
<RoomItem alert name='user mentions' unread={1} userMentions={1} />
|
<RoomItem alert name='user mentions' unread={1} userMentions={1} />
|
||||||
<RoomItem alert name='group mentions' unread={1} groupMentions={1} />
|
<RoomItem alert name='group mentions' unread={1} groupMentions={1} />
|
||||||
<RoomItem alert name='thread unread' tunread={[1]} />
|
<RoomItem alert name='thread unread' tunread={[1]} />
|
||||||
<RoomItem name='user mentions > group mentions' alert unread={1} userMentions={1} groupMentions={1} />
|
<RoomItem alert name='thread unread user' tunread={[1]} tunreadUser={[1]} />
|
||||||
|
<RoomItem alert name='thread unread group' tunread={[1]} tunreadGroup={[1]} />
|
||||||
|
<RoomItem name='user mentions priority 1' alert unread={1} userMentions={1} groupMentions={1} tunread={[1]} />
|
||||||
|
<RoomItem name='group mentions priority 2' alert unread={1} groupMentions={1} tunread={[1]} />
|
||||||
|
<RoomItem name='thread unread priority 3' alert unread={1} tunread={[1]} />
|
||||||
|
|
||||||
<Separator title='Last Message' />
|
<Separator title='Last Message' />
|
||||||
<RoomItem
|
<RoomItem
|
||||||
|
@ -119,7 +123,7 @@ export default ({ theme }) => {
|
||||||
<RoomItem
|
<RoomItem
|
||||||
showLastMessage
|
showLastMessage
|
||||||
alert
|
alert
|
||||||
unread={1000}
|
tunread={[1]}
|
||||||
lastMessage={lastMessage}
|
lastMessage={lastMessage}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -12,10 +12,12 @@ import UiKitModal from './UiKitModal';
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import './HeaderButtons';
|
import './HeaderButtons';
|
||||||
import './UnreadBadge';
|
import './UnreadBadge';
|
||||||
|
import '../../app/views/ThreadMessagesView/Item.stories.js';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
// import RoomViewHeader from './RoomViewHeader';
|
// import RoomViewHeader from './RoomViewHeader';
|
||||||
|
|
||||||
import MessageContext from '../../app/containers/message/Context';
|
import MessageContext from '../../app/containers/message/Context';
|
||||||
|
import { themes } from '../../app/constants/colors';
|
||||||
|
|
||||||
// MessageProvider
|
// MessageProvider
|
||||||
const baseUrl = 'https://open.rocket.chat';
|
const baseUrl = 'https://open.rocket.chat';
|
||||||
|
@ -53,7 +55,8 @@ const messageDecorator = story => (
|
||||||
replyBroadcast: () => {},
|
replyBroadcast: () => {},
|
||||||
onReactionPress: () => {},
|
onReactionPress: () => {},
|
||||||
onDiscussionPress: () => {},
|
onDiscussionPress: () => {},
|
||||||
onReactionLongPress: () => {}
|
onReactionLongPress: () => {},
|
||||||
|
getBadgeColor: () => themes.light.tunreadBackground
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{story()}
|
{story()}
|
||||||
|
|
63
yarn.lock
63
yarn.lock
|
@ -2248,42 +2248,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
type-detect "4.0.8"
|
type-detect "4.0.8"
|
||||||
|
|
||||||
"@storybook/addon-actions@5.3.19":
|
|
||||||
version "5.3.19"
|
|
||||||
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.3.19.tgz#50548fa6e84bc79ad95233ce23ade4878fc7cfac"
|
|
||||||
integrity sha512-gXF29FFUgYlUoFf1DcVCmH1chg2ElaHWMmCi5h7aZe+g6fXBQw0UtEdJnYLMOqZCIiWoZyuf1ETD0RbNHPhRIw==
|
|
||||||
dependencies:
|
|
||||||
"@storybook/addons" "5.3.19"
|
|
||||||
"@storybook/api" "5.3.19"
|
|
||||||
"@storybook/client-api" "5.3.19"
|
|
||||||
"@storybook/components" "5.3.19"
|
|
||||||
"@storybook/core-events" "5.3.19"
|
|
||||||
"@storybook/theming" "5.3.19"
|
|
||||||
core-js "^3.0.1"
|
|
||||||
fast-deep-equal "^2.0.1"
|
|
||||||
global "^4.3.2"
|
|
||||||
polished "^3.3.1"
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
react "^16.8.3"
|
|
||||||
react-inspector "^4.0.0"
|
|
||||||
uuid "^3.3.2"
|
|
||||||
|
|
||||||
"@storybook/addon-links@5.3.19":
|
|
||||||
version "5.3.19"
|
|
||||||
resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-5.3.19.tgz#3c23e886d44b56978ae254fed3bf8be54c877178"
|
|
||||||
integrity sha512-gn9u8lebREfRsyzxoDPG0O+kOf5aJ0BhzcCJGZZdqha0F6OWHhh8vJYZZvjJ/Qwze+Qt2zjrgWm+Q6+JLD8ugQ==
|
|
||||||
dependencies:
|
|
||||||
"@storybook/addons" "5.3.19"
|
|
||||||
"@storybook/client-logger" "5.3.19"
|
|
||||||
"@storybook/core-events" "5.3.19"
|
|
||||||
"@storybook/csf" "0.0.1"
|
|
||||||
"@storybook/router" "5.3.19"
|
|
||||||
core-js "^3.0.1"
|
|
||||||
global "^4.3.2"
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
qs "^6.6.0"
|
|
||||||
ts-dedent "^1.1.0"
|
|
||||||
|
|
||||||
"@storybook/addon-storyshots@5.3.19":
|
"@storybook/addon-storyshots@5.3.19":
|
||||||
version "5.3.19"
|
version "5.3.19"
|
||||||
resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-5.3.19.tgz#cb07ac3cc20d3a399ed4b6758008e10f910691d0"
|
resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-5.3.19.tgz#cb07ac3cc20d3a399ed4b6758008e10f910691d0"
|
||||||
|
@ -8464,14 +8428,6 @@ is-docker@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
|
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
|
||||||
integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
|
integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
|
||||||
|
|
||||||
is-dom@^1.0.9:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a"
|
|
||||||
integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==
|
|
||||||
dependencies:
|
|
||||||
is-object "^1.0.1"
|
|
||||||
is-window "^1.0.2"
|
|
||||||
|
|
||||||
is-dotfile@^1.0.0:
|
is-dotfile@^1.0.0:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
|
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
|
||||||
|
@ -8608,11 +8564,6 @@ is-obj@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
|
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
|
||||||
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
|
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
|
||||||
|
|
||||||
is-object@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
|
|
||||||
integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
|
|
||||||
|
|
||||||
is-plain-obj@^1.0.0:
|
is-plain-obj@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
||||||
|
@ -8711,11 +8662,6 @@ is-weakset@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
||||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
||||||
|
|
||||||
is-window@^1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
|
|
||||||
integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
|
|
||||||
|
|
||||||
is-windows@^1.0.2:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
|
@ -12701,15 +12647,6 @@ react-hotkeys@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.6.1"
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
react-inspector@^4.0.0:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8"
|
|
||||||
integrity sha512-xSiM6CE79JBqSj8Fzd9dWBHv57tLTH7OM57GP3VrE5crzVF3D5Khce9w1Xcw75OAbvrA0Mi2vBneR1OajKmXFg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.6.3"
|
|
||||||
is-dom "^1.0.9"
|
|
||||||
prop-types "^15.6.1"
|
|
||||||
|
|
||||||
react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
|
react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
Loading…
Reference in New Issue