Merge pull request #3287 from RocketChat/new.add-discusions-roomactionsview
[NEW] Add Discussions to RoomActionsView
This commit is contained in:
commit
f0b5cd69e1
|
@ -35,7 +35,7 @@ const styles = StyleSheet.create({
|
|||
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
|
||||
<View style={styles.container}>
|
||||
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
|
||||
{text ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||
{text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
|
||||
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { withTheme } from '../theme';
|
||||
import I18n from '../i18n';
|
||||
import { useTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { themes } from '../constants/colors';
|
||||
import TextInput from '../presentation/TextInput';
|
||||
|
@ -19,14 +20,13 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
interface ISearchHeader {
|
||||
theme?: string;
|
||||
interface ISearchHeaderProps {
|
||||
onSearchChangeText?: (text: string) => void;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
// TODO: it might be useful to refactor this component for reusage
|
||||
const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
|
||||
const titleColorStyle = { color: themes[theme!].headerTitleColor };
|
||||
const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {
|
||||
const { theme } = useTheme();
|
||||
const isLight = theme === 'light';
|
||||
const { isLandscape } = useOrientation();
|
||||
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
|
||||
|
@ -36,14 +36,14 @@ const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
|
|||
<View style={styles.container}>
|
||||
<TextInput
|
||||
autoFocus
|
||||
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]}
|
||||
placeholder='Search'
|
||||
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
|
||||
placeholder={I18n.t('Search')}
|
||||
onChangeText={onSearchChangeText}
|
||||
theme={theme!}
|
||||
testID='thread-messages-view-search-header'
|
||||
theme={theme}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(SearchHeader);
|
||||
export default SearchHeader;
|
||||
|
|
|
@ -5,7 +5,7 @@ import Touchable from 'react-native-platform-touchable';
|
|||
import { CustomIcon } from '../lib/Icons';
|
||||
import { themes } from '../constants/colors';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { withTheme } from '../theme';
|
||||
import { useTheme } from '../theme';
|
||||
import { TThreadModel } from '../definitions/IThread';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -48,13 +48,12 @@ interface IThreadDetails {
|
|||
badgeColor?: string;
|
||||
toggleFollowThread: Function;
|
||||
style: ViewStyle;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, theme }: IThreadDetails) => {
|
||||
let tcount: number | string = item?.tcount ?? 0;
|
||||
|
||||
if (tcount >= 1000) {
|
||||
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
|
||||
const { theme } = useTheme();
|
||||
let { tcount } = item;
|
||||
if (tcount && tcount >= 1000) {
|
||||
tcount = '+999';
|
||||
}
|
||||
|
||||
|
@ -82,7 +81,6 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
|
|||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.badgeContainer}>
|
||||
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
|
||||
<Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}>
|
||||
|
@ -97,4 +95,4 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
|
|||
);
|
||||
};
|
||||
|
||||
export default withTheme(ThreadDetails);
|
||||
export default ThreadDetails;
|
||||
|
|
|
@ -25,7 +25,7 @@ import { isValidURL } from '../../utils/url';
|
|||
import NewMarkdown from './new';
|
||||
|
||||
interface IMarkdownProps {
|
||||
msg: string;
|
||||
msg?: string;
|
||||
md: MarkdownAST;
|
||||
mentions: UserMention[];
|
||||
getCustomEmoji: Function;
|
||||
|
|
|
@ -24,7 +24,6 @@ const Thread = React.memo(
|
|||
item={{
|
||||
tcount,
|
||||
replies,
|
||||
tlm,
|
||||
id
|
||||
}}
|
||||
user={user}
|
||||
|
|
|
@ -147,6 +147,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
|
|||
if ((item.tlm || item.tmid) && !isThreadRoom) {
|
||||
this.onThreadPress();
|
||||
}
|
||||
|
||||
const { onDiscussionPress } = this.props;
|
||||
|
||||
if (onDiscussionPress) {
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
},
|
||||
300,
|
||||
true
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { MarkdownAST } from '@rocket.chat/message-parser';
|
||||
|
||||
export type TMessageType = 'discussion-created' | 'jitsi_call_started';
|
||||
|
||||
export interface IMessageAttachments {
|
||||
attachments: any;
|
||||
timeFormat: string;
|
||||
|
@ -140,7 +142,7 @@ export interface IMessageInner
|
|||
IMessageThread,
|
||||
IMessageAttachments,
|
||||
IMessageBroadcast {
|
||||
type: string;
|
||||
type: TMessageType;
|
||||
blocks: [];
|
||||
}
|
||||
|
||||
|
|
|
@ -44,9 +44,9 @@ export interface IThread {
|
|||
msg?: string;
|
||||
t?: SubscriptionType;
|
||||
rid: string;
|
||||
_updatedAt: Date;
|
||||
ts: Date;
|
||||
u: IUserMessage;
|
||||
_updatedAt?: Date;
|
||||
ts?: Date;
|
||||
u?: IUserMessage;
|
||||
alias?: string;
|
||||
parseUrls?: boolean;
|
||||
groupable?: boolean;
|
||||
|
@ -61,11 +61,11 @@ export interface IThread {
|
|||
reactions?: IReaction[];
|
||||
role?: string;
|
||||
drid?: string;
|
||||
dcount?: number;
|
||||
dcount?: number | string;
|
||||
dlm?: number;
|
||||
tmid?: string;
|
||||
tcount?: number;
|
||||
tlm?: Date;
|
||||
tcount?: number | string;
|
||||
tlm?: string;
|
||||
replies?: string[];
|
||||
mentions?: IUserMention[];
|
||||
channels?: IUserChannel[];
|
||||
|
|
|
@ -775,6 +775,7 @@
|
|||
"creating_discussion": "creating discussion",
|
||||
"Canned_Responses": "Canned Responses",
|
||||
"No_match_found": "No match found.",
|
||||
"No_discussions": "No discussions",
|
||||
"Check_canned_responses": "Check on canned responses.",
|
||||
"Searching": "Searching",
|
||||
"Use": "Use",
|
||||
|
|
|
@ -811,6 +811,16 @@ const RocketChat = {
|
|||
encrypted
|
||||
});
|
||||
},
|
||||
getDiscussions({ roomId, offset, count, text }) {
|
||||
const params = {
|
||||
roomId,
|
||||
offset,
|
||||
count,
|
||||
...(text && { text })
|
||||
};
|
||||
// RC 2.4.0
|
||||
return this.sdk.get('chat.getDiscussions', params);
|
||||
},
|
||||
createTeam({ name, users, type, readOnly, broadcast, encrypted }) {
|
||||
const params = {
|
||||
name,
|
||||
|
|
|
@ -66,6 +66,7 @@ import QueueListView from '../ee/omnichannel/views/QueueListView';
|
|||
import AddChannelTeamView from '../views/AddChannelTeamView';
|
||||
import AddExistingChannelView from '../views/AddExistingChannelView';
|
||||
import SelectListView from '../views/SelectListView';
|
||||
import DiscussionsView from '../views/DiscussionsView';
|
||||
import {
|
||||
AdminPanelStackParamList,
|
||||
ChatsStackParamList,
|
||||
|
@ -92,7 +93,8 @@ const ChatsStackNavigator = () => {
|
|||
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
|
||||
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} />
|
||||
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} options={RoomMembersView.navigationOptions} />
|
||||
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||
<ChatsStack.Screen
|
||||
name='SearchMessagesView'
|
||||
component={SearchMessagesView}
|
||||
|
|
|
@ -58,6 +58,7 @@ import QueueListView from '../../ee/omnichannel/views/QueueListView';
|
|||
import AddChannelTeamView from '../../views/AddChannelTeamView';
|
||||
import AddExistingChannelView from '../../views/AddExistingChannelView';
|
||||
import SelectListView from '../../views/SelectListView';
|
||||
import DiscussionsView from '../../views/DiscussionsView';
|
||||
import { ModalContainer } from './ModalContainer';
|
||||
import {
|
||||
MasterDetailChatsStackParamList,
|
||||
|
@ -167,6 +168,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
|||
<ModalStack.Screen name='LivechatEditView' component={LivechatEditView} options={LivechatEditView.navigationOptions} />
|
||||
<ModalStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
|
||||
<ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} />
|
||||
<ModalStack.Screen name='DiscussionsView' component={DiscussionsView} />
|
||||
<ModalStack.Screen name='TeamChannelsView' component={TeamChannelsView} options={TeamChannelsView.navigationOptions} />
|
||||
<ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} />
|
||||
<ModalStack.Screen
|
||||
|
|
|
@ -56,6 +56,10 @@ export type ModalStackParamList = {
|
|||
rid: string;
|
||||
room: ISubscription;
|
||||
};
|
||||
DiscussionsView: {
|
||||
rid: string;
|
||||
t: SubscriptionType;
|
||||
};
|
||||
SearchMessagesView: {
|
||||
rid: string;
|
||||
t: SubscriptionType;
|
||||
|
|
|
@ -54,6 +54,10 @@ export type ChatsStackParamList = {
|
|||
rid: string;
|
||||
room: ISubscription;
|
||||
};
|
||||
DiscussionsView: {
|
||||
rid: string;
|
||||
t: SubscriptionType;
|
||||
};
|
||||
SearchMessagesView: {
|
||||
rid: string;
|
||||
t: SubscriptionType;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { TThreadModel } from '../../definitions/IThread';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../Styles';
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginTop: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
detailsContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
detailContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 8
|
||||
},
|
||||
detailText: {
|
||||
fontSize: 10,
|
||||
marginLeft: 2,
|
||||
...sharedStyles.textSemibold
|
||||
}
|
||||
});
|
||||
|
||||
interface IDiscussionDetails {
|
||||
item: TThreadModel;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => {
|
||||
const { theme } = useTheme();
|
||||
let { dcount } = item;
|
||||
|
||||
if (dcount && dcount >= 1000) {
|
||||
dcount = '+999';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
<View style={styles.detailsContainer}>
|
||||
<View style={styles.detailContainer}>
|
||||
<CustomIcon name={'discussions'} size={24} color={themes[theme!].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
|
||||
{dcount}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailContainer}>
|
||||
<CustomIcon name={'clock'} size={24} color={themes[theme!].auxiliaryText} />
|
||||
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
|
||||
{date}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionDetails;
|
|
@ -0,0 +1,96 @@
|
|||
/* 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 { Provider } from 'react-redux';
|
||||
|
||||
import * as List from '../../containers/List';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { ThemeContext } from '../../theme';
|
||||
import { store } from '../../../storybook/stories';
|
||||
import Item from './Item';
|
||||
|
||||
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 stories = storiesOf('Discussions.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={{
|
||||
dcount: 1000,
|
||||
replies: [...new Array(1000)]
|
||||
}}
|
||||
/>
|
||||
<List.Separator />
|
||||
<BaseItem
|
||||
item={{
|
||||
msg: '',
|
||||
attachments: [{ title: 'Attachment title' }]
|
||||
}}
|
||||
/>
|
||||
<List.Separator />
|
||||
<BaseItem useRealName />
|
||||
</>
|
||||
));
|
||||
|
||||
const ThemeStory = ({ theme }) => (
|
||||
<ThemeContext.Provider value={{ theme }}>
|
||||
<BaseItem badgeColor={themes[theme].mentionMeColor} />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
||||
stories.add('themes', () => (
|
||||
<>
|
||||
<ThemeStory theme='light' />
|
||||
<ThemeStory theme='dark' />
|
||||
<ThemeStory theme='black' />
|
||||
</>
|
||||
));
|
|
@ -0,0 +1,105 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import moment from 'moment';
|
||||
|
||||
import { useTheme } from '../../theme';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import sharedStyles from '../Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import Markdown from '../../containers/markdown';
|
||||
import { formatDateThreads, makeThreadName } from '../../utils/room';
|
||||
import DiscussionDetails from './DiscussionDetails';
|
||||
import { TThreadModel } from '../../definitions/IThread';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
padding: 16
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'column',
|
||||
flex: 1
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 2,
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
title: {
|
||||
flexShrink: 1,
|
||||
fontSize: 18,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
time: {
|
||||
fontSize: 14,
|
||||
marginLeft: 4,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
avatar: {
|
||||
marginRight: 8
|
||||
},
|
||||
messageContainer: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
markdown: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
interface IItem {
|
||||
item: TThreadModel;
|
||||
baseUrl: string;
|
||||
onPress: {
|
||||
(...args: any[]): void;
|
||||
stop(): void;
|
||||
};
|
||||
}
|
||||
|
||||
const Item = ({ item, baseUrl, onPress }: IItem): JSX.Element => {
|
||||
const { theme } = useTheme();
|
||||
const username = item?.u?.username;
|
||||
let messageTime = '';
|
||||
let messageDate = '';
|
||||
|
||||
if (item?.ts) {
|
||||
messageTime = moment(item.ts).format('LT');
|
||||
messageDate = formatDateThreads(item.ts);
|
||||
}
|
||||
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onPress(item)}
|
||||
testID={`discussions-view-${item.msg}`}
|
||||
style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||
<View style={styles.container}>
|
||||
<Avatar style={styles.avatar} text={item?.u?.username} size={36} borderRadius={4} theme={theme} />
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
{username}
|
||||
</Text>
|
||||
{messageTime ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{messageTime}</Text> : null}
|
||||
</View>
|
||||
<View style={styles.messageContainer}>
|
||||
{username ? (
|
||||
/* @ts-ignore */
|
||||
<Markdown
|
||||
msg={makeThreadName(item)}
|
||||
baseUrl={baseUrl}
|
||||
username={username}
|
||||
theme={theme}
|
||||
numberOfLines={2}
|
||||
style={[styles.markdown]}
|
||||
preview
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{messageDate ? <DiscussionDetails item={item} date={messageDate} /> : null}
|
||||
</View>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Item;
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,208 @@
|
|||
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
||||
import { RouteProp } from '@react-navigation/core';
|
||||
|
||||
import { IApplicationState } from '../../definitions';
|
||||
import { ChatsStackParamList } from '../../stacks/types';
|
||||
import ActivityIndicator from '../../containers/ActivityIndicator';
|
||||
import I18n from '../../i18n';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import log from '../../utils/log';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { themes } from '../../constants/colors';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import * as List from '../../containers/List';
|
||||
import BackgroundContainer from '../../containers/BackgroundContainer';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { getHeaderTitlePosition } from '../../containers/Header';
|
||||
import { useTheme } from '../../theme';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import SearchHeader from '../../containers/SearchHeader';
|
||||
import { TThreadModel } from '../../definitions/IThread';
|
||||
import Item from './Item';
|
||||
|
||||
const API_FETCH_COUNT = 50;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
marginBottom: 30
|
||||
}
|
||||
});
|
||||
|
||||
interface IDiscussionsViewProps {
|
||||
navigation: StackNavigationProp<ChatsStackParamList, 'DiscussionsView'>;
|
||||
route: RouteProp<ChatsStackParamList, 'DiscussionsView'>;
|
||||
item: TThreadModel;
|
||||
}
|
||||
|
||||
const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): JSX.Element => {
|
||||
const rid = route.params?.rid;
|
||||
const t = route.params?.t;
|
||||
|
||||
const baseUrl = useSelector((state: IApplicationState) => state.server?.server);
|
||||
const isMasterDetail = useSelector((state: IApplicationState) => state.app?.isMasterDetail);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [discussions, setDiscussions] = useState([]);
|
||||
const [search, setSearch] = useState([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searchTotal, setSearchTotal] = useState(0);
|
||||
|
||||
const { theme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const load = async (text = '') => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await RocketChat.getDiscussions({
|
||||
roomId: rid,
|
||||
offset: isSearching ? search.length : discussions.length,
|
||||
count: API_FETCH_COUNT,
|
||||
text
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
if (isSearching) {
|
||||
setSearch(result.messages);
|
||||
setSearchTotal(result.total);
|
||||
} else {
|
||||
setDiscussions(result.messages);
|
||||
setTotal(result.total);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSearchChangeText = debounce(async (text: string) => {
|
||||
setIsSearching(true);
|
||||
await load(text);
|
||||
}, 300);
|
||||
|
||||
const onCancelSearchPress = () => {
|
||||
setIsSearching(false);
|
||||
setSearch([]);
|
||||
setSearchTotal(0);
|
||||
};
|
||||
|
||||
const onSearchPress = () => {
|
||||
setIsSearching(true);
|
||||
};
|
||||
|
||||
const setHeader = () => {
|
||||
let options: Partial<StackNavigationOptions>;
|
||||
if (isSearching) {
|
||||
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
|
||||
options = {
|
||||
headerTitleAlign: 'left',
|
||||
headerLeft: () => (
|
||||
<HeaderButton.Container left>
|
||||
<HeaderButton.Item iconName='close' onPress={onCancelSearchPress} />
|
||||
</HeaderButton.Container>
|
||||
),
|
||||
headerTitle: () => (
|
||||
<SearchHeader onSearchChangeText={onSearchChangeText} testID='discussion-messages-view-search-header' />
|
||||
),
|
||||
headerTitleContainerStyle: {
|
||||
left: headerTitlePosition.left,
|
||||
right: headerTitlePosition.right
|
||||
},
|
||||
headerRight: () => null
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
options = {
|
||||
headerLeft: () => (
|
||||
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
|
||||
),
|
||||
headerTitleAlign: 'center',
|
||||
headerTitle: I18n.t('Discussions'),
|
||||
headerTitleContainerStyle: {
|
||||
left: null,
|
||||
right: null
|
||||
},
|
||||
headerRight: () => (
|
||||
<HeaderButton.Container>
|
||||
<HeaderButton.Item iconName='search' onPress={onSearchPress} />
|
||||
</HeaderButton.Container>
|
||||
)
|
||||
};
|
||||
|
||||
if (isMasterDetail) {
|
||||
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const options = setHeader();
|
||||
navigation.setOptions(options);
|
||||
}, [navigation, isSearching]);
|
||||
|
||||
const onDiscussionPress = debounce(
|
||||
(item: TThreadModel) => {
|
||||
if (item.drid && item.t) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.drid,
|
||||
prid: item.rid,
|
||||
name: item.msg,
|
||||
t
|
||||
});
|
||||
}
|
||||
},
|
||||
1000,
|
||||
true
|
||||
);
|
||||
|
||||
const renderItem = ({ item }: { item: TThreadModel }) => (
|
||||
<Item
|
||||
{...{
|
||||
item,
|
||||
baseUrl
|
||||
}}
|
||||
onPress={onDiscussionPress}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!discussions?.length) {
|
||||
return <BackgroundContainer loading={loading} text={I18n.t('No_discussions')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView testID='discussions-view'>
|
||||
<StatusBar />
|
||||
<FlatList
|
||||
data={isSearching ? search : discussions}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.msg}
|
||||
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
onEndReachedThreshold={0.5}
|
||||
removeClippedSubviews={isIOS}
|
||||
onEndReached={() => (isSearching ? searchTotal : total) > API_FETCH_COUNT ?? load()}
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
|
||||
scrollIndicatorInsets={{ right: 1 }}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionsView;
|
|
@ -941,7 +941,7 @@ class RoomActionsView extends React.Component {
|
|||
canReturnQueue,
|
||||
canViewCannedResponse
|
||||
} = this.state;
|
||||
const { rid, t } = room;
|
||||
const { rid, t, prid } = room;
|
||||
const isGroupChat = RocketChat.isGroupChat(room);
|
||||
|
||||
return (
|
||||
|
@ -1009,6 +1009,27 @@ class RoomActionsView extends React.Component {
|
|||
</>
|
||||
) : null}
|
||||
|
||||
{['c', 'p', 'd'].includes(t) && !prid ? (
|
||||
<>
|
||||
<List.Item
|
||||
title='Discussions'
|
||||
onPress={() =>
|
||||
this.onPressTouchable({
|
||||
route: 'DiscussionsView',
|
||||
params: {
|
||||
rid,
|
||||
t
|
||||
}
|
||||
})
|
||||
}
|
||||
testID='room-actions-discussions'
|
||||
left={() => <List.Icon name='discussions' />}
|
||||
showActionIndicator
|
||||
/>
|
||||
<List.Separator />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{['c', 'p', 'd'].includes(t) ? (
|
||||
<>
|
||||
<List.Item
|
||||
|
|
|
@ -459,7 +459,7 @@ class RoomView extends React.Component {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('RoomActionsView', {
|
||||
navigation.push('RoomActionsView', {
|
||||
rid: this.rid,
|
||||
t: this.t,
|
||||
room,
|
||||
|
|
|
@ -218,7 +218,9 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
|
|||
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
|
||||
</HeaderButton.Container>
|
||||
),
|
||||
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />,
|
||||
headerTitle: () => (
|
||||
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='team-channels-view-search-header' />
|
||||
),
|
||||
headerTitleContainerStyle: {
|
||||
left: headerTitlePosition.left,
|
||||
right: headerTitlePosition.right
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
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 * as List from '../../containers/List';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { ThemeContext } from '../../theme';
|
||||
import { store } from '../../../storybook/stories';
|
||||
import Item from './Item';
|
||||
|
||||
const author = {
|
||||
|
@ -49,28 +49,6 @@ const listDecorator = story => (
|
|||
</ScrollView>
|
||||
);
|
||||
|
||||
const reducers = combineReducers({
|
||||
login: () => ({
|
||||
user: {
|
||||
id: 'abc',
|
||||
username: 'rocket.cat',
|
||||
name: 'Rocket Cat'
|
||||
}
|
||||
}),
|
||||
server: () => ({
|
||||
server: 'https://open.rocket.chat',
|
||||
version: '3.7.0'
|
||||
}),
|
||||
share: () => ({
|
||||
server: 'https://open.rocket.chat',
|
||||
version: '3.7.0'
|
||||
}),
|
||||
settings: () => ({
|
||||
blockUnauthenticatedAccess: false
|
||||
})
|
||||
});
|
||||
const store = createStore(reducers);
|
||||
|
||||
const stories = storiesOf('Thread Messages.Item', module)
|
||||
.addDecorator(listDecorator)
|
||||
.addDecorator(story => <Provider store={store}>{story()}</Provider>);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import { withTheme } from '../../theme';
|
||||
import { useTheme } from '../../theme';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import sharedStyles from '../Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
@ -59,7 +59,6 @@ const styles = StyleSheet.create({
|
|||
interface IItem {
|
||||
item: TThreadModel;
|
||||
baseUrl: string;
|
||||
theme?: string;
|
||||
useRealName: boolean;
|
||||
user: any;
|
||||
badgeColor?: string;
|
||||
|
@ -67,7 +66,8 @@ interface IItem {
|
|||
toggleFollowThread: (isFollowing: boolean, id: string) => void;
|
||||
}
|
||||
|
||||
const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => {
|
||||
const Item = ({ item, baseUrl, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => {
|
||||
const { theme } = useTheme();
|
||||
const username = (useRealName && item?.u?.name) || item?.u?.username;
|
||||
let time;
|
||||
if (item?.ts) {
|
||||
|
@ -89,16 +89,18 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
|
|||
<Text style={[styles.time, { color: themes[theme!].auxiliaryText }]}>{time}</Text>
|
||||
</View>
|
||||
<View style={styles.messageContainer}>
|
||||
<Markdown
|
||||
// @ts-ignore
|
||||
msg={makeThreadName(item)}
|
||||
baseUrl={baseUrl}
|
||||
username={username!}
|
||||
theme={theme!}
|
||||
numberOfLines={2}
|
||||
style={[styles.markdown]}
|
||||
preview
|
||||
/>
|
||||
{makeThreadName(item) && username ? (
|
||||
/* @ts-ignore */
|
||||
<Markdown
|
||||
msg={makeThreadName(item)}
|
||||
baseUrl={baseUrl}
|
||||
username={username}
|
||||
theme={theme}
|
||||
numberOfLines={2}
|
||||
style={[styles.markdown]}
|
||||
preview
|
||||
/>
|
||||
) : null}
|
||||
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
|
||||
</View>
|
||||
<ThreadDetails item={item} user={user} toggleFollowThread={toggleFollowThread} style={styles.threadDetails} />
|
||||
|
@ -108,4 +110,4 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
|
|||
);
|
||||
};
|
||||
|
||||
export default withTheme(Item);
|
||||
export default Item;
|
||||
|
|
|
@ -143,7 +143,9 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
|
|||
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
|
||||
</HeaderButton.Container>
|
||||
),
|
||||
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />,
|
||||
headerTitle: () => (
|
||||
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='thread-messages-view-search-header' />
|
||||
),
|
||||
headerTitleContainerStyle: {
|
||||
left: headerTitlePosition.left,
|
||||
right: headerTitlePosition.right
|
||||
|
|
|
@ -107,7 +107,7 @@ describe('Discussion', () => {
|
|||
});
|
||||
|
||||
describe('Check RoomActionsView render', () => {
|
||||
it('should navigete to RoomActionsView', async () => {
|
||||
it('should navigate to RoomActionsView', async () => {
|
||||
await waitFor(element(by.id('room-header')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
|
@ -173,4 +173,51 @@ describe('Discussion', () => {
|
|||
await expect(element(by.id('room-info-view-edit-button'))).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Open Discussion from DiscussionsView', () => {
|
||||
const discussionName = `${data.random}message`;
|
||||
it('should go back to main room', async () => {
|
||||
await tapBack();
|
||||
await waitFor(element(by.id('room-actions-view')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
await tapBack();
|
||||
await waitFor(element(by.id(`room-view-title-${discussionName}`)))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await tapBack();
|
||||
await navigateToRoom();
|
||||
});
|
||||
|
||||
it('should navigate to DiscussionsView', async () => {
|
||||
await waitFor(element(by.id(`room-view-title-${channel}`)))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await waitFor(element(by.id('room-header')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
await element(by.id('room-header')).tap();
|
||||
await waitFor(element(by.id('room-actions-discussions')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
await element(by.id('room-actions-discussions')).tap();
|
||||
await waitFor(element(by.id('discussions-view')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
});
|
||||
|
||||
it('should navigate to discussion', async () => {
|
||||
const discussionName = `${data.random} Discussion NewMessageView`;
|
||||
await waitFor(element(by.label(discussionName)).atIndex(0))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await element(by.label(discussionName)).atIndex(0).tap();
|
||||
await waitFor(element(by.id(`room-view-title-${discussionName}`)))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await waitFor(element(by.id('messagebox')))
|
||||
.toBeVisible()
|
||||
.withTimeout(60000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import './Markdown';
|
|||
import './HeaderButtons';
|
||||
import './UnreadBadge';
|
||||
import '../../app/views/ThreadMessagesView/Item.stories.js';
|
||||
import '../../app/views/DiscussionsView/Item.stories.js';
|
||||
import './Avatar';
|
||||
import './NewMarkdown';
|
||||
import '../../app/containers/BackgroundContainer/index.stories.js';
|
||||
|
|
Loading…
Reference in New Issue