[NEW] Threads (#2567)

* [IMPROVEMENT] Mentions layout without background

* Fix RoomItem

* Fix tests

* Smaller messagebox

* Messagebox colors tweak

* Beginning header buttons refactor

* Add HeaderButtons

* item with title

* Refactor

* Remove lib

* Refactor

* Update snapshot

* Send to channel on messagebox

* Add tshow

* Add showMessageInMainThread to login.user reducer

* Filter threads on main channel based on user setting

* Send tshow

* Add tunread

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

* Export UnreadBadge on index

* Add empty test

* Refactor

* Update tests

* Lint

* Thread unread user and group on RoomItem

* Thread badge working

* Started ThreadMessagesView.Item

* Fix separator

* Reactivity working

* Lint

* custom emojis aren't necessary

* Basic filter layout

* Filtering layout

* Refactor

* apply filter

* DropdownItemHeader

* default all

* few fixes

* No data found

* Fixes list performance issues

* Use locale on date formats

* Fixed minor styles

* Thread badge

* Refactor getBadgeColor

* Fix send to channel background color

* starting search threads

* Fix lint and tests

* Bump to 4.12.0 just for testing :)

* Search input layout

* query

* starting threads header

* fix unnecessary tlm on tmid messages

* Fix thread header

* lint

* Fix thread header on ShareView

* Add e2e tests

* Fix subscriptions sort

* Update stories and minor fixes

* Fix button sizes on Messagebox

* Remove comment

* Unnecessary conditional

* Add showMessageInMainThread to user collection

* Fix thread header

* Fix thread messages not working on tablet

* Reset Messagebox.tshow after sending a message

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

* Unnecessary theme prop

* Address comments

* Remove re-render

* Fix scroll indicator bug

* Fix style

* Minor i18n fix

* Fix dropdown height

* I18n ptbr

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,6 @@ class MessageContainer extends React.Component {
}),
rid: PropTypes.string,
timeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: PropTypes.any,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
@ -49,7 +48,9 @@ class MessageContainer extends React.Component {
navToRoomInfo: PropTypes.func,
callJitsi: PropTypes.func,
blockAction: PropTypes.func,
theme: PropTypes.string
theme: PropTypes.string,
getBadgeColor: PropTypes.func,
toggleFollowThread: PropTypes.func
}
static defaultProps = {
@ -265,10 +266,10 @@ class MessageContainer extends React.Component {
render() {
const { author } = this.state;
const {
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, getBadgeColor, toggleFollowThread
} = this.props;
const {
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage
id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies
} = item;
let message = msg;
@ -291,7 +292,10 @@ class MessageContainer extends React.Component {
onReactionPress: this.onReactionPress,
onEncryptedPress: this.onEncryptedPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress
onReactionLongPress: this.onReactionLongPress,
getBadgeColor,
toggleFollowThread,
replies
}}
>
<Message
@ -309,7 +313,6 @@ class MessageContainer extends React.Component {
avatar={avatar}
emoji={emoji}
timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import RNBootSplash from 'react-native-bootsplash';
import 'react-native-gesture-handler';
// 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();

View File

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

View File

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

View File

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

View File

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