From d04d0f27b6403047ba2eec9a204f0c40be388546 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 7 Apr 2021 15:31:25 -0300 Subject: [PATCH] [NEW] Basic support to Teams (#3016) * Database migration * RoomItem icon * Team icons * Teams group * Small tweak on RoomTypeIcon * RoomView Header * Add team's channels to RoomView header * Starting TeamChannelsView * Icon size * o data found * Update TeamChannelsView, add teams subscriptions and send params to TeamChannelsView * Use teams.ListRooms endpoint, render rooms list, remove unused functions * Show team main on TeamChannelsView * Disable swipe * Pagination working * Fix blinking no data found * Search working * Refactor to use BackgroundContainer while loading * Go to room * Cleanup * Go to actions * Events * Lint * Add debounce to go room * Fix for tablet * i18n * Small fix * Minor refactor * Use local data when it exists * Show last message * Force teams migration * Add stories to BackgroundContainer * Remove unused component * Move RoomViewHeader into containers folder * Refactoring * Testing RoomHeader * i18n * Fix server endpoint version * Fix events Co-authored-by: Gerzon Z --- .../__snapshots__/Storyshots.test.js.snap | 4693 +++++++++++++++++ .../BackgroundContainer/index.js} | 16 +- .../BackgroundContainer/index.stories.js | 49 + .../RoomHeader/RoomHeader.js} | 74 +- .../RoomHeader/RoomHeader.stories.js | 94 + .../Header => containers/RoomHeader}/index.js | 43 +- app/containers/RoomTypeIcon.js | 7 +- app/i18n/locales/en.json | 5 +- app/i18n/locales/pt-BR.json | 5 +- app/lib/database/model/Subscription.js | 4 + app/lib/database/model/migrations.js | 12 + app/lib/database/schema/app.js | 6 +- .../helpers/mergeSubscriptionsRooms.js | 2 + app/lib/rocketchat.js | 13 + app/presentation/RoomItem/RoomItem.js | 8 +- app/presentation/RoomItem/TypeIcon.js | 7 +- app/presentation/RoomItem/index.js | 1 + app/sagas/rooms.js | 11 + app/stacks/InsideStack.js | 6 + app/stacks/MasterDetailStack/index.js | 6 + app/utils/goRoom.js | 2 +- app/utils/log/events.js | 9 +- app/views/NewMessageView.js | 2 +- app/views/RoomActionsView/index.js | 2 +- app/views/RoomInfoView/index.js | 2 +- .../RoomView/{Header => }/LeftButtons.js | 4 +- .../RoomView/{Header => }/RightButtons.js | 35 +- app/views/RoomView/index.js | 23 +- app/views/RoomsListView/index.js | 13 +- app/views/TeamChannelsView.js | 369 ++ app/views/ThreadMessagesView/index.js | 4 +- e2e/tests/assorted/02-broadcast.spec.js | 2 +- e2e/tests/assorted/05-joinpublicroom.spec.js | 4 +- .../assorted/08-joinprotectedroom.spec.js | 2 +- e2e/tests/room/02-room.spec.js | 2 +- e2e/tests/room/03-roomactions.spec.js | 10 +- e2e/tests/room/04-discussion.spec.js | 4 +- e2e/tests/room/05-threads.spec.js | 14 +- e2e/tests/room/08-roominfo.spec.js | 2 +- storybook/stories/RoomViewHeader.js | 73 - storybook/stories/index.js | 2 + 41 files changed, 5456 insertions(+), 186 deletions(-) rename app/{views/ThreadMessagesView/NoDataFound.js => containers/BackgroundContainer/index.js} (56%) create mode 100644 app/containers/BackgroundContainer/index.stories.js rename app/{views/RoomView/Header/Header.js => containers/RoomHeader/RoomHeader.js} (65%) create mode 100644 app/containers/RoomHeader/RoomHeader.stories.js rename app/{views/RoomView/Header => containers/RoomHeader}/index.js (77%) rename app/views/RoomView/{Header => }/LeftButtons.js (93%) rename app/views/RoomView/{Header => }/RightButtons.js (85%) create mode 100644 app/views/TeamChannelsView.js delete mode 100644 storybook/stories/RoomViewHeader.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index d5d1d912..286e506e 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -1055,6 +1055,533 @@ exports[`Storyshots Avatar list Avatar 1`] = ` `; +exports[`Storyshots BackgroundContainer basic 1`] = ` + + + + + +`; + +exports[`Storyshots BackgroundContainer black theme - loading 1`] = ` + + + + + + +`; + +exports[`Storyshots BackgroundContainer black theme - text 1`] = ` + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + +`; + +exports[`Storyshots BackgroundContainer dark theme - loading 1`] = ` + + + + + + +`; + +exports[`Storyshots BackgroundContainer dark theme - text 1`] = ` + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + +`; + +exports[`Storyshots BackgroundContainer loading 1`] = ` + + + + + + +`; + +exports[`Storyshots BackgroundContainer long text 1`] = ` + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + +`; + +exports[`Storyshots BackgroundContainer text 1`] = ` + + + + + + Text here + + +`; + exports[`Storyshots Header Buttons badge 1`] = ` `; +exports[`Storyshots RoomHeader icons 1`] = ` +Array [ + + + + + + +  + + + private channel + + + + + + , + + + + + + +  + + + public channel + + + + + + , + + + + + + +  + + + discussion + + + + + + , + + + + + + +  + + + omnichannel + + + + + + , + + + + + + +  + + + private team + + + + + + , + + + + + + +  + + + public team + + + + + + , + + + + + + +  + + + group dm + + + + + + , + + + + + + +  + + + online dm + + + + + + , + + + + + + +  + + + away dm + + + + + + , + + + + + + +  + + + busy dm + + + + + + , + + + + + + +  + + + loading dm + + + + + + , + + + + + + +  + + + offline dm + + + + + + , +] +`; + +exports[`Storyshots RoomHeader landscape 1`] = ` +Array [ + + + + + + +  + + + title + + + + + + , + + + + + + +  + + + title + + + + subtitle + + + + + , + + + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + , +] +`; + +exports[`Storyshots RoomHeader themes 1`] = ` +Array [ + + + + + + +  + + + title + + + + subtitle + + + + + , + + + + + + +  + + + title + + + + subtitle + + + + + , + + + + + + +  + + + title + + + + subtitle + + + + + , +] +`; + +exports[`Storyshots RoomHeader thread 1`] = ` +Array [ + + + + + + + title + + + + +  + + + parent title + + + + + + , + + + + + + + markdown preview #3 4 5 + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + , +] +`; + +exports[`Storyshots RoomHeader title and subtitle 1`] = ` +Array [ + + + + + + +  + + + title + + + + + + , + + + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + , + + + + + + +  + + + title + + + + subtitle + + + + + , + + + + + + +  + + + title + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + , + + + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + , +] +`; + +exports[`Storyshots RoomHeader typing 1`] = ` +Array [ + + + + + + +  + + + title + + + + + user 1 + + + is typing + ... + + + + + , + + + + + + +  + + + title + + + + + user 1 and user 2 + + + are typing + ... + + + + + , + + + + + + +  + + + title + + + + + user 1, user 2, user 3, user 4, user 5 + + + are typing + ... + + + + + , +] +`; + exports[`Storyshots RoomItem list roomitem 1`] = ` ( +const BackgroundContainer = ({ theme, text, loading }) => ( - {text} + {text ? {text} : null} + {loading ? : null} ); -EmptyRoom.propTypes = { +BackgroundContainer.propTypes = { text: PropTypes.string, - theme: PropTypes.string + theme: PropTypes.string, + loading: PropTypes.bool }; -export default withTheme(EmptyRoom); +export default withTheme(BackgroundContainer); diff --git a/app/containers/BackgroundContainer/index.stories.js b/app/containers/BackgroundContainer/index.stories.js new file mode 100644 index 00000000..0b6c9a1c --- /dev/null +++ b/app/containers/BackgroundContainer/index.stories.js @@ -0,0 +1,49 @@ +/* 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 BackgroundContainer from '.'; +import { ThemeContext } from '../../theme'; +import { longText } from '../../../storybook/utils'; + +const stories = storiesOf('BackgroundContainer', module); + +stories.add('basic', () => ( + +)); + +stories.add('loading', () => ( + +)); + +stories.add('text', () => ( + +)); + +stories.add('long text', () => ( + +)); + +const ThemeStory = ({ theme, ...props }) => ( + + + +); + +stories.add('dark theme - loading', () => ( + +)); + +stories.add('dark theme - text', () => ( + +)); + +stories.add('black theme - loading', () => ( + +)); + +stories.add('black theme - text', () => ( + +)); diff --git a/app/views/RoomView/Header/Header.js b/app/containers/RoomHeader/RoomHeader.js similarity index 65% rename from app/views/RoomView/Header/Header.js rename to app/containers/RoomHeader/RoomHeader.js index 4cdccca8..6715fc13 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/containers/RoomHeader/RoomHeader.js @@ -4,16 +4,21 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; -import I18n from '../../../i18n'; -import sharedStyles from '../../Styles'; -import { themes } from '../../../constants/colors'; -import Markdown from '../../../containers/markdown'; -import RoomTypeIcon from '../../../containers/RoomTypeIcon'; +import I18n from '../../i18n'; +import sharedStyles from '../../views/Styles'; +import { themes } from '../../constants/colors'; +import Markdown from '../markdown'; +import RoomTypeIcon from '../RoomTypeIcon'; +import { withTheme } from '../../theme'; const HIT_SLOP = { top: 5, right: 5, bottom: 5, left: 5 }; const TITLE_SIZE = 16; +const SUBTITLE_SIZE = 12; + +const getSubTitleSize = scale => SUBTITLE_SIZE * scale; + const styles = StyleSheet.create({ container: { flex: 1, @@ -24,12 +29,12 @@ const styles = StyleSheet.create({ flexDirection: 'row' }, title: { - ...sharedStyles.textSemibold, - fontSize: TITLE_SIZE + flexShrink: 1, + ...sharedStyles.textSemibold }, subtitle: { - ...sharedStyles.textRegular, - fontSize: 12 + flexShrink: 1, + ...sharedStyles.textRegular }, typingUsers: { ...sharedStyles.textSemibold @@ -37,8 +42,9 @@ const styles = StyleSheet.create({ }); const SubTitle = React.memo(({ - usersTyping, subtitle, renderFunc, theme + usersTyping, subtitle, renderFunc, theme, scale }) => { + const fontSize = getSubTitleSize(scale); // typing if (usersTyping.length) { let usersText; @@ -48,7 +54,7 @@ const SubTitle = React.memo(({ usersText = usersTyping.join(', '); } return ( - + {usersText} { usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }... @@ -66,7 +72,7 @@ const SubTitle = React.memo(({ @@ -80,18 +86,20 @@ SubTitle.propTypes = { usersTyping: PropTypes.array, theme: PropTypes.string, subtitle: PropTypes.string, - renderFunc: PropTypes.func + renderFunc: PropTypes.func, + scale: PropTypes.number }; const HeaderTitle = React.memo(({ - title, tmid, prid, scale, theme + title, tmid, prid, scale, theme, testID }) => { + const titleStyle = { fontSize: TITLE_SIZE * scale, color: themes[theme].headerTitleColor }; if (!tmid && !prid) { return ( {title} @@ -102,10 +110,10 @@ const HeaderTitle = React.memo(({ ); }); @@ -115,11 +123,12 @@ HeaderTitle.propTypes = { tmid: PropTypes.string, prid: PropTypes.string, scale: PropTypes.number, - theme: PropTypes.string + theme: PropTypes.string, + testID: PropTypes.string }; const Header = React.memo(({ - title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, connecting, goRoomActionsView, theme, isGroupChat + title, subtitle, parentTitle, type, status, usersTyping, width, height, prid, tmid, onPress, theme, isGroupChat, teamMain, testID }) => { const portrait = height > width; let scale = 1; @@ -130,13 +139,11 @@ const Header = React.memo(({ } } - const onPress = () => goRoomActionsView(); - let renderFunc; if (tmid) { renderFunc = () => ( - + {parentTitle} ); @@ -144,7 +151,7 @@ const Header = React.memo(({ return ( - {tmid ? null : } + {tmid ? null : } - + ); }); @@ -175,17 +188,18 @@ Header.propTypes = { height: PropTypes.number.isRequired, prid: PropTypes.string, tmid: PropTypes.string, + teamMain: PropTypes.bool, status: PropTypes.string, theme: PropTypes.string, usersTyping: PropTypes.array, - connecting: PropTypes.bool, isGroupChat: PropTypes.bool, parentTitle: PropTypes.string, - goRoomActionsView: PropTypes.func + onPress: PropTypes.func, + testID: PropTypes.string }; Header.defaultProps = { usersTyping: [] }; -export default Header; +export default withTheme(Header); diff --git a/app/containers/RoomHeader/RoomHeader.stories.js b/app/containers/RoomHeader/RoomHeader.stories.js new file mode 100644 index 00000000..bfea5e27 --- /dev/null +++ b/app/containers/RoomHeader/RoomHeader.stories.js @@ -0,0 +1,94 @@ +/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */ +import React from 'react'; +import { View, Dimensions } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import RoomHeaderComponent from './RoomHeader'; +import Header from '../Header'; +import { longText } from '../../../storybook/utils'; +import { ThemeContext } from '../../theme'; + +const stories = storiesOf('RoomHeader', module); + +// TODO: refactor after react-navigation v6 +const HeaderExample = ({ title }) => ( +
( + + {title()} + + )} + /> +); + +const { width, height } = Dimensions.get('window'); + +const RoomHeader = ({ ...props }) => ( + alert('header pressed!')} {...props} /> +); + +stories.add('title and subtitle', () => ( + <> + } /> + } /> + } /> + } /> + } /> + +)); + +stories.add('icons', () => ( + <> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +)); + +stories.add('typing', () => ( + <> + } /> + } /> + } /> + +)); + +stories.add('landscape', () => ( + <> + } /> + } /> + } /> + +)); + +stories.add('thread', () => ( + <> + } /> + } /> + +)); + +const ThemeStory = ({ theme }) => ( + + } /> + +); + +stories.add('themes', () => ( + <> + + + + +)); diff --git a/app/views/RoomView/Header/index.js b/app/containers/RoomHeader/index.js similarity index 77% rename from app/views/RoomView/Header/index.js rename to app/containers/RoomHeader/index.js index a66f21a4..4eeab701 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/containers/RoomHeader/index.js @@ -3,42 +3,37 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; -import Header from './Header'; -import LeftButtons from './LeftButtons'; -import RightButtons from './RightButtons'; -import { withTheme } from '../../../theme'; -import { withDimensions } from '../../../dimensions'; -import I18n from '../../../i18n'; +import RoomHeader from './RoomHeader'; +import { withDimensions } from '../../dimensions'; +import I18n from '../../i18n'; -class RoomHeaderView extends Component { +class RoomHeaderContainer extends Component { static propTypes = { title: PropTypes.string, subtitle: PropTypes.string, type: PropTypes.string, prid: PropTypes.string, tmid: PropTypes.string, + teamMain: PropTypes.bool, usersTyping: PropTypes.string, status: PropTypes.string, statusText: PropTypes.string, connecting: PropTypes.bool, connected: PropTypes.bool, - theme: PropTypes.string, roomUserId: PropTypes.string, widthOffset: PropTypes.number, - goRoomActionsView: PropTypes.func, + onPress: PropTypes.func, width: PropTypes.number, height: PropTypes.number, parentTitle: PropTypes.string, - isGroupChat: PropTypes.bool + isGroupChat: PropTypes.bool, + testID: PropTypes.string }; shouldComponentUpdate(nextProps) { const { - type, title, subtitle, status, statusText, connecting, connected, goRoomActionsView, usersTyping, theme, width, height + type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height } = this.props; - if (nextProps.theme !== theme) { - return true; - } if (nextProps.type !== type) { return true; } @@ -69,7 +64,7 @@ class RoomHeaderView extends Component { if (!dequal(nextProps.usersTyping, usersTyping)) { return true; } - if (nextProps.goRoomActionsView !== goRoomActionsView) { + if (nextProps.onPress !== onPress) { return true; } return false; @@ -80,6 +75,7 @@ class RoomHeaderView extends Component { title, subtitle: subtitleProp, type, + teamMain, prid, tmid, widthOffset, @@ -88,13 +84,13 @@ class RoomHeaderView extends Component { connecting, connected, usersTyping, - goRoomActionsView, + onPress, roomUserId, - theme, width, height, parentTitle, - isGroupChat + isGroupChat, + testID } = this.props; let subtitle; @@ -107,23 +103,24 @@ class RoomHeaderView extends Component { } return ( -
); } @@ -153,6 +150,4 @@ const mapStateToProps = (state, ownProps) => { }; }; -export default connect(mapStateToProps)(withDimensions(withTheme(RoomHeaderView))); - -export { RightButtons, LeftButtons }; +export default connect(mapStateToProps)(withDimensions(RoomHeaderContainer)); diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js index 44044f33..55294310 100644 --- a/app/containers/RoomTypeIcon.js +++ b/app/containers/RoomTypeIcon.js @@ -13,7 +13,7 @@ const styles = StyleSheet.create({ }); const RoomTypeIcon = React.memo(({ - type, size, isGroupChat, status, style, theme + type, size, isGroupChat, status, style, theme, teamMain }) => { if (!type) { return null; @@ -31,7 +31,9 @@ const RoomTypeIcon = React.memo(({ } let icon = 'channel-private'; - if (type === 'discussion') { + if (teamMain) { + icon = `teams${ type === 'p' ? '-private' : '' }`; + } else if (type === 'discussion') { icon = 'discussions'; } else if (type === 'c') { icon = 'channel-public'; @@ -58,6 +60,7 @@ RoomTypeIcon.propTypes = { theme: PropTypes.string, type: PropTypes.string, isGroupChat: PropTypes.bool, + teamMain: PropTypes.bool, status: PropTypes.string, size: PropTypes.number, style: PropTypes.object diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index ce4243c4..ec289d27 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -706,5 +706,8 @@ "Enter_workspace_URL": "Enter workspace URL", "Workspace_URL_Example": "Ex. your-company.rocket.chat", "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by {{username}}", - "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}" + "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", + "Teams": "Teams", + "No_team_channels_found": "No channels found", + "Team_not_found": "Team not found" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 3d52af4a..4002b2d6 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -656,5 +656,8 @@ "This_room_encryption_has_been_disabled_by__username_": "A criptografia para essa sala foi desabilitada por {{username}}", "Apply_Your_Certificate": "Aplicar certificado", "Do_you_have_a_certificate": "Você tem um certificado?", - "Your_certificate": "Seu certificado" + "Your_certificate": "Seu certificado", + "Teams": "Times", + "No_team_channels_found": "Nenhum canal encontrado", + "Team_not_found": "Time não encontrado" } \ No newline at end of file diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index 378c9759..5b1ebd14 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -125,4 +125,8 @@ export default class Subscription extends Model { @field('e2e_key_id') e2eKeyId; @field('avatar_etag') avatarETag; + + @field('team_id') teamId; + + @field('team_main') teamMain; } diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js index c8f3d449..cdc65ef0 100644 --- a/app/lib/database/model/migrations.js +++ b/app/lib/database/model/migrations.js @@ -212,6 +212,18 @@ export default schemaMigrations({ ] }) ] + }, + { + toVersion: 13, + steps: [ + addColumns({ + table: 'subscriptions', + columns: [ + { name: 'team_id', type: 'string', isIndexed: true }, + { name: 'team_main', type: 'boolean', isOptional: true } + ] + }) + ] } ] }); diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 055b8f64..883e6dfd 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 12, + version: 13, tables: [ tableSchema({ name: 'subscriptions', @@ -57,7 +57,9 @@ export default appSchema({ { name: 'e2e_key', type: 'string', isOptional: true }, { name: 'encrypted', type: 'boolean', isOptional: true }, { name: 'e2e_key_id', type: 'string', isOptional: true }, - { name: 'avatar_etag', type: 'string', isOptional: true } + { name: 'avatar_etag', type: 'string', isOptional: true }, + { name: 'team_id', type: 'string', isIndexed: true }, + { name: 'team_main', type: 'boolean', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js index 895f2a8a..82dfc4c6 100644 --- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -42,6 +42,8 @@ export const merge = (subscription, room) => { subscription.encrypted = room.encrypted; subscription.e2eKeyId = room.e2eKeyId; subscription.avatarETag = room.avatarETag; + subscription.teamId = room.teamId; + subscription.teamMain = room.teamMain; if (!subscription.roles || !subscription.roles.length) { subscription.roles = []; } diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 26298392..3cce8ed0 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -931,6 +931,19 @@ const RocketChat = { // RC 2.3.0 return this.sdk.get('livechat/visitors.info', { visitorId }); }, + getTeamListRoom({ + teamId, count, offset, type, filter + }) { + const params = { + teamId, count, offset, type + }; + + if (filter) { + params.filter = filter; + } + // RC 3.13.0 + return this.sdk.get('teams.listRooms', params); + }, closeLivechat(rid, comment) { // RC 0.29.0 return this.methodCallWrapper('livechat:closeRoom', rid, comment, { clientAction: true }); diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js index 27cf9f37..b3922787 100644 --- a/app/presentation/RoomItem/RoomItem.js +++ b/app/presentation/RoomItem/RoomItem.js @@ -44,7 +44,8 @@ const RoomItem = ({ onPress, toggleFav, toggleRead, - hideChannel + hideChannel, + teamMain }) => ( <Title name={name} @@ -159,6 +164,7 @@ RoomItem.propTypes = { isFocused: PropTypes.bool, isGroupChat: PropTypes.bool, isRead: PropTypes.bool, + teamMain: PropTypes.bool, date: PropTypes.string, accessibilityLabel: PropTypes.string, lastMessage: PropTypes.object, diff --git a/app/presentation/RoomItem/TypeIcon.js b/app/presentation/RoomItem/TypeIcon.js index 2eeded24..425ee6db 100644 --- a/app/presentation/RoomItem/TypeIcon.js +++ b/app/presentation/RoomItem/TypeIcon.js @@ -4,14 +4,15 @@ import PropTypes from 'prop-types'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; const TypeIcon = React.memo(({ - type, prid, status, isGroupChat -}) => <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />); + type, prid, status, isGroupChat, teamMain +}) => <RoomTypeIcon type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} teamMain={teamMain} />); TypeIcon.propTypes = { type: PropTypes.string, status: PropTypes.string, prid: PropTypes.string, - isGroupChat: PropTypes.bool + isGroupChat: PropTypes.bool, + teamMain: PropTypes.bool }; export default TypeIcon; diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index 1f5bb9e7..80bcf063 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -188,6 +188,7 @@ class RoomItemContainer extends React.Component { tunreadUser={item.tunreadUser} tunreadGroup={item.tunreadGroup} swipeEnabled={swipeEnabled} + teamMain={item.teamMain} /> ); } diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index f963a294..4b0047a1 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -12,6 +12,7 @@ import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRo import RocketChat from '../lib/rocketchat'; import buildMessage from '../lib/methods/helpers/buildMessage'; import protectedFunction from '../lib/methods/helpers/protectedFunction'; +import UserPreferences from '../lib/userPreferences'; const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) { const serversDB = database.servers; @@ -47,6 +48,16 @@ const handleRoomsRequest = function* handleRoomsRequest({ params }) { // Server not found } } + + // Force fetch all subscriptions to update columns related to Teams feature + // TODO: remove it a couple of releases + const teamsMigrationKey = `${ server }_TEAMS_MIGRATION`; + const teamsMigration = yield UserPreferences.getBoolAsync(teamsMigrationKey); + if (!teamsMigration) { + roomsUpdatedAt = null; + UserPreferences.setBoolAsync(teamsMigrationKey, true); + } + const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(roomsUpdatedAt); const { subscriptions } = yield mergeSubscriptionsRooms(subscriptionsResult, roomsResult); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index a0af9617..bda56f0d 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -29,6 +29,7 @@ import ForwardLivechatView from '../views/ForwardLivechatView'; import LivechatEditView from '../views/LivechatEditView'; import PickerView from '../views/PickerView'; import ThreadMessagesView from '../views/ThreadMessagesView'; +import TeamChannelsView from '../views/TeamChannelsView'; import MarkdownTableView from '../views/MarkdownTableView'; import ReadReceiptsView from '../views/ReadReceiptView'; import { themes } from '../constants/colors'; @@ -168,6 +169,11 @@ const ChatsStackNavigator = () => { component={ThreadMessagesView} options={ThreadMessagesView.navigationOptions} /> + <ChatsStack.Screen + name='TeamChannelsView' + component={TeamChannelsView} + options={TeamChannelsView.navigationOptions} + /> <ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.js index db49cf0e..7307ba6c 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.js @@ -30,6 +30,7 @@ import ForwardLivechatView from '../../views/ForwardLivechatView'; import LivechatEditView from '../../views/LivechatEditView'; import PickerView from '../../views/PickerView'; import ThreadMessagesView from '../../views/ThreadMessagesView'; +import TeamChannelsView from '../../views/TeamChannelsView'; import MarkdownTableView from '../../views/MarkdownTableView'; import ReadReceiptsView from '../../views/ReadReceiptView'; import ProfileView from '../../views/ProfileView'; @@ -193,6 +194,11 @@ const ModalStackNavigator = React.memo(({ navigation }) => { name='ThreadMessagesView' component={ThreadMessagesView} /> + <ModalStack.Screen + name='TeamChannelsView' + component={TeamChannelsView} + options={TeamChannelsView.navigationOptions} + /> <ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} diff --git a/app/utils/goRoom.js b/app/utils/goRoom.js index f1263095..94adfde4 100644 --- a/app/utils/goRoom.js +++ b/app/utils/goRoom.js @@ -2,7 +2,7 @@ import Navigation from '../lib/Navigation'; import RocketChat from '../lib/rocketchat'; const navigate = ({ item, isMasterDetail, ...props }) => { - let navigationMethod = Navigation.navigate; + let navigationMethod = props.navigationMethod ?? Navigation.navigate; if (isMasterDetail) { navigationMethod = Navigation.replace; diff --git a/app/utils/log/events.js b/app/utils/log/events.js index 2e1e8f9a..fc8d7042 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -212,6 +212,7 @@ export default { ROOM_JOIN: 'room_join', ROOM_GO_RA: 'room_go_ra', ROOM_TOGGLE_FOLLOW_THREADS: 'room_toggle_follow_threads', + ROOM_GO_TEAM_CHANNELS: 'room_go_team_channels', ROOM_GO_SEARCH: 'room_go_search', ROOM_GO_THREADS: 'room_go_threads', ROOM_GO_ROOM_INFO: 'room_go_room_info', @@ -310,5 +311,11 @@ export default { // E2E ENCRYPTION SECURITY VIEW E2E_SEC_CHANGE_PASSWORD: 'e2e_sec_change_password', - E2E_SEC_RESET_OWN_KEY: 'e2e_sec_reset_own_key' + E2E_SEC_RESET_OWN_KEY: 'e2e_sec_reset_own_key', + + // TEAM CHANNELS VIEW + TC_SEARCH: 'tc_search', + TC_CANCEL_SEARCH: 'tc_cancel_search', + TC_GO_ACTIONS: 'tc_go_actions', + TC_GO_ROOM: 'tc_go_room' }; diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index 7e2aa232..78178c4c 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -175,7 +175,7 @@ class NewMessageView extends React.Component { {maxUsers > 2 ? this.renderButton({ onPress: this.createGroupChat, title: I18n.t('Create_Direct_Messages'), - icon: 'team', + icon: 'message', testID: 'new-message-view-create-direct-message' }) : null} {this.renderButton({ diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 2686b60e..89bc5f22 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -461,7 +461,7 @@ class RoomActionsView extends React.Component { ? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text> : ( <View style={styles.roomTitleRow}> - <RoomTypeIcon type={room.prid ? 'discussion' : room.t} status={room.visitor?.status} /> + <RoomTypeIcon type={room.prid ? 'discussion' : room.t} teamMain={room.teamMain} status={room.visitor?.status} /> <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text> </View> ) diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 5966f312..a49aab5b 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -41,7 +41,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type == ) : ( <View style={styles.roomTitleRow}> - <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' status={room.visitor?.status} /> + <RoomTypeIcon type={room.prid ? 'discussion' : room.t} teamMain={room.teamMain} key='room-info-type' status={room.visitor?.status} /> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text> </View> ) diff --git a/app/views/RoomView/Header/LeftButtons.js b/app/views/RoomView/LeftButtons.js similarity index 93% rename from app/views/RoomView/Header/LeftButtons.js rename to app/views/RoomView/LeftButtons.js index 68ad9b08..089c284d 100644 --- a/app/views/RoomView/Header/LeftButtons.js +++ b/app/views/RoomView/LeftButtons.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { StyleSheet } from 'react-native'; import { HeaderBackButton } from '@react-navigation/stack'; -import { themes } from '../../../constants/colors'; -import Avatar from '../../../containers/Avatar'; +import { themes } from '../../constants/colors'; +import Avatar from '../../containers/Avatar'; const styles = StyleSheet.create({ avatar: { diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/RightButtons.js similarity index 85% rename from app/views/RoomView/Header/RightButtons.js rename to app/views/RoomView/RightButtons.js index 37939b1e..debc3edb 100644 --- a/app/views/RoomView/Header/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; -import * as HeaderButton from '../../../containers/HeaderButton'; -import database from '../../../lib/database'; -import { getUserSelector } from '../../../selectors/login'; -import { logEvent, events } from '../../../utils/log'; +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 Component { static propTypes = { @@ -15,6 +15,7 @@ class RightButtonsContainer extends Component { rid: PropTypes.string, t: PropTypes.string, tmid: PropTypes.string, + teamId: PropTypes.bool, navigation: PropTypes.object, isMasterDetail: PropTypes.bool, toggleFollowThread: PropTypes.func @@ -109,6 +110,21 @@ class RightButtonsContainer extends Component { }); } + goTeamChannels = () => { + logEvent(events.ROOM_GO_TEAM_CHANNELS); + const { + navigation, isMasterDetail, teamId + } = this.props; + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { + screen: 'TeamChannelsView', + params: { teamId } + }); + } else { + navigation.navigate('TeamChannelsView', { teamId }); + } + } + goThreadsView = () => { logEvent(events.ROOM_GO_THREADS); const { @@ -146,7 +162,9 @@ class RightButtonsContainer extends Component { const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; - const { t, tmid, threadsEnabled } = this.props; + const { + t, tmid, threadsEnabled, teamId + } = this.props; if (t === 'l') { return null; } @@ -163,6 +181,13 @@ class RightButtonsContainer extends Component { } return ( <HeaderButton.Container> + {teamId ? ( + <HeaderButton.Item + iconName='channel-public' + onPress={this.goTeamChannels} + testID='room-view-header-team-channels' + /> + ) : null} {threadsEnabled ? ( <HeaderButton.Item iconName='threads' diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 405e386e..5853717e 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -29,7 +29,9 @@ import styles from './styles'; import log, { logEvent, events } from '../../utils/log'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; -import RoomHeaderView, { RightButtons, LeftButtons } from './Header'; +import RoomHeader from '../../containers/RoomHeader'; +import LeftButtons from './LeftButtons'; +import RightButtons from './RightButtons'; import StatusBar from '../../containers/StatusBar'; import Separator from './Separator'; import { themes } from '../../constants/colors'; @@ -317,13 +319,23 @@ class RoomView extends React.Component { } const subtitle = room?.topic; const t = room?.t; + const teamMain = room?.teamMain; + const teamId = room?.teamId; const { id: userId, token } = user; const avatar = room?.name; const visitor = room?.visitor; if (!room?.rid) { return; } - const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: tmid ? 1 : 2 }); + + let numIconsRight = 2; + if (tmid) { + numIconsRight = 1; + } else if (teamId) { + numIconsRight = 3; + } + const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight }); + navigation.setOptions({ headerShown: true, headerTitleAlign: 'left', @@ -347,24 +359,27 @@ class RoomView extends React.Component { /> ), headerTitle: () => ( - <RoomHeaderView + <RoomHeader rid={rid} prid={prid} tmid={tmid} title={title} + teamMain={teamMain} parentTitle={parentTitle} subtitle={subtitle} type={t} roomUserId={roomUserId} visitor={visitor} isGroupChat={isGroupChat} - goRoomActionsView={this.goRoomActionsView} + onPress={this.goRoomActionsView} + testID={`room-view-title-${ title }`} /> ), headerRight: () => ( <RightButtons rid={rid} tmid={tmid} + teamId={teamId} t={t} navigation={navigation} toggleFollowThread={this.toggleFollowThread} diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index a0b3984e..4c12c4dd 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -68,6 +68,7 @@ const CHATS_HEADER = 'Chats'; const UNREAD_HEADER = 'Unread'; const FAVORITES_HEADER = 'Favorites'; const DISCUSSIONS_HEADER = 'Discussions'; +const TEAMS_HEADER = 'Teams'; const CHANNELS_HEADER = 'Channels'; const DM_HEADER = 'Direct_Messages'; const GROUPS_HEADER = 'Private_Groups'; @@ -77,6 +78,8 @@ const QUERY_SIZE = 20; 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'; +const filterIsTeam = s => s.teamMain; +const filterIsDiscussion = s => s.prid; const shouldUpdateProps = [ 'searchText', @@ -475,10 +478,12 @@ class RoomsListView extends React.Component { // type if (groupByType) { - const discussions = chats.filter(s => s.prid); - const channels = chats.filter(s => s.t === 'c' && !s.prid); - const privateGroup = chats.filter(s => s.t === 'p' && !s.prid); - const direct = chats.filter(s => s.t === 'd' && !s.prid); + const teams = chats.filter(s => filterIsTeam(s)); + const discussions = chats.filter(s => filterIsDiscussion(s)); + const channels = chats.filter(s => s.t === 'c' && !filterIsDiscussion(s) && !filterIsTeam(s)); + const privateGroup = chats.filter(s => s.t === 'p' && !filterIsDiscussion(s) && !filterIsTeam(s)); + const direct = chats.filter(s => s.t === 'd' && !filterIsDiscussion(s) && !filterIsTeam(s)); + tempChats = this.addRoomsGroup(teams, TEAMS_HEADER, tempChats); tempChats = this.addRoomsGroup(discussions, DISCUSSIONS_HEADER, tempChats); tempChats = this.addRoomsGroup(channels, CHANNELS_HEADER, tempChats); tempChats = this.addRoomsGroup(privateGroup, GROUPS_HEADER, tempChats); diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js new file mode 100644 index 00000000..ab8d261e --- /dev/null +++ b/app/views/TeamChannelsView.js @@ -0,0 +1,369 @@ +import React from 'react'; +import { Keyboard } from 'react-native'; +import PropTypes from 'prop-types'; +import { Q } from '@nozbe/watermelondb'; +import { withSafeAreaInsets } from 'react-native-safe-area-context'; +import { connect } from 'react-redux'; +import { FlatList } from 'react-native-gesture-handler'; +import { HeaderBackButton } from '@react-navigation/stack'; + +import StatusBar from '../containers/StatusBar'; +import RoomHeader from '../containers/RoomHeader'; +import { withTheme } from '../theme'; +import SearchHeader from './ThreadMessagesView/SearchHeader'; +import log, { events, logEvent } from '../utils/log'; +import database from '../lib/database'; +import { getUserSelector } from '../selectors/login'; +import { getHeaderTitlePosition } from '../containers/Header'; +import * as HeaderButton from '../containers/HeaderButton'; +import BackgroundContainer from '../containers/BackgroundContainer'; +import SafeAreaView from '../containers/SafeAreaView'; +import ActivityIndicator from '../containers/ActivityIndicator'; +import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem'; +import RocketChat from '../lib/rocketchat'; +import { withDimensions } from '../dimensions'; +import { isIOS } from '../utils/deviceInfo'; +import { themes } from '../constants/colors'; +import debounce from '../utils/debounce'; +import { showErrorAlert } from '../utils/info'; +import { goRoom } from '../utils/goRoom'; +import I18n from '../i18n'; + +const API_FETCH_COUNT = 50; + +const getItemLayout = (data, index) => ({ + length: data.length, + offset: ROW_HEIGHT * index, + index +}); +const keyExtractor = item => item._id; + +class TeamChannelsView extends React.Component { + static propTypes = { + route: PropTypes.object, + navigation: PropTypes.object, + isMasterDetail: PropTypes.bool, + insets: PropTypes.object, + theme: PropTypes.string, + useRealName: PropTypes.bool, + width: PropTypes.number, + StoreLastMessage: PropTypes.bool + } + + constructor(props) { + super(props); + this.teamId = props.route.params?.teamId; + this.state = { + loading: true, + loadingMore: false, + data: [], + total: -1, + isSearching: false, + searchText: '', + search: [] + }; + this.loadTeam(); + } + + componentDidMount() { + this.load(); + } + + loadTeam = async() => { + const db = database.active; + try { + const subCollection = db.get('subscriptions'); + this.teamChannels = await subCollection.query( + Q.where('team_id', Q.eq(this.teamId)) + ); + this.team = this.teamChannels?.find(channel => channel.teamMain); + this.setHeader(); + + if (!this.team) { + throw new Error(); + } + } catch { + const { navigation } = this.props; + navigation.pop(); + showErrorAlert(I18n.t('Team_not_found')); + } + } + + load = debounce(async() => { + const { + loadingMore, total, data, search, isSearching, searchText + } = this.state; + + const length = isSearching ? search.length : data.length; + if (loadingMore || length === total) { + return; + } + + this.setState({ loadingMore: true }); + try { + const result = await RocketChat.getTeamListRoom({ + teamId: this.teamId, + offset: length, + count: API_FETCH_COUNT, + type: 'all', + filter: searchText + }); + + if (result.success) { + const newState = { + loading: false, + loadingMore: false, + total: result.total + }; + const rooms = result.rooms.map((room) => { + const record = this.teamChannels?.find(c => c.rid === room._id); + return record ?? room; + }); + if (isSearching) { + newState.search = [...search, ...rooms]; + } else { + newState.data = [...data, ...rooms]; + } + + this.setState(newState); + } else { + this.setState({ loading: false, loadingMore: false }); + } + } catch (e) { + log(e); + this.setState({ loading: false, loadingMore: false }); + } + }, 300) + + getHeader = () => { + const { isSearching } = this.state; + const { + navigation, isMasterDetail, insets, theme + } = this.props; + + const { team } = this; + if (!team) { + return; + } + + const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 }); + + 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 = { + headerShown: true, + headerTitleAlign: 'left', + headerTitleContainerStyle: { + left: headerTitlePosition.left, + right: headerTitlePosition.right + }, + headerTitle: () => ( + <RoomHeader + title={RocketChat.getRoomTitle(team)} + subtitle={team.topic} + type={team.t} + onPress={this.goRoomActionsView} + teamMain + /> + ) + }; + + if (isMasterDetail) { + options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />; + } else { + options.headerLeft = () => ( + <HeaderBackButton + labelVisible={false} + onPress={() => navigation.pop()} + tintColor={themes[theme].headerTintColor} + /> + ); + } + + 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); + } + + onSearchPress = () => { + logEvent(events.TC_SEARCH); + this.setState({ isSearching: true }, () => this.setHeader()); + } + + onSearchChangeText = debounce((searchText) => { + this.setState({ + searchText, search: [], loading: !!searchText, loadingMore: false, total: -1 + }, () => { + if (searchText) { + this.load(); + } + }); + }, 300) + + onCancelSearchPress = () => { + logEvent(events.TC_CANCEL_SEARCH); + const { isSearching } = this.state; + if (!isSearching) { + return; + } + Keyboard.dismiss(); + this.setState({ isSearching: false, search: [] }, () => { + this.setHeader(); + }); + }; + + goRoomActionsView = (screen) => { + logEvent(events.TC_GO_ACTIONS); + const { team } = this; + const { + navigation, isMasterDetail + } = this.props; + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { + screen: screen ?? 'RoomActionsView', + params: { + rid: team.rid, t: team.t, room: team, showCloseModal: false + } + }); + } else { + navigation.navigate('RoomActionsView', { + rid: team.rid, t: team.t, room: team + }); + } + } + + getRoomTitle = item => RocketChat.getRoomTitle(item) + + getRoomAvatar = item => RocketChat.getRoomAvatar(item) + + onPressItem = debounce(async(item) => { + logEvent(events.TC_GO_ROOM); + const { navigation, isMasterDetail } = this.props; + try { + let params = {}; + if (item.rid) { + params = item; + } else { + const { room } = await RocketChat.getRoomInfo(item._id); + params = { + rid: item._id, name: RocketChat.getRoomTitle(room), joinCodeRequired: room.joinCodeRequired, t: room.t, teamId: room.teamId + }; + } + if (isMasterDetail) { + navigation.pop(); + } + goRoom({ item: params, isMasterDetail, navigationMethod: navigation.push }); + } catch (e) { + // do nothing + } + }, 1000, true); + + renderItem = ({ item }) => { + const { + StoreLastMessage, + useRealName, + theme, + width + } = this.props; + return ( + <RoomItem + item={item} + theme={theme} + type={item.t} + showLastMessage={StoreLastMessage} + onPress={this.onPressItem} + width={width} + useRealName={useRealName} + getRoomTitle={this.getRoomTitle} + getRoomAvatar={this.getRoomAvatar} + swipeEnabled={false} + /> + ); + }; + + renderFooter = () => { + const { loadingMore } = this.state; + const { theme } = this.props; + if (loadingMore) { + return <ActivityIndicator theme={theme} />; + } + return null; + } + + renderScroll = () => { + const { + loading, data, search, isSearching, searchText + } = this.state; + if (loading) { + return <BackgroundContainer loading />; + } + if (isSearching && !search.length) { + return <BackgroundContainer text={searchText ? I18n.t('No_team_channels_found') : ''} />; + } + if (!isSearching && !data.length) { + return <BackgroundContainer text={I18n.t('No_team_channels_found')} />; + } + + return ( + <FlatList + data={isSearching ? search : data} + extraData={isSearching ? search : data} + keyExtractor={keyExtractor} + renderItem={this.renderItem} + getItemLayout={getItemLayout} + removeClippedSubviews={isIOS} + keyboardShouldPersistTaps='always' + onEndReached={() => this.load()} + onEndReachedThreshold={0.5} + ListFooterComponent={this.renderFooter} + /> + ); + }; + + render() { + console.count(`${ this.constructor.name }.render calls`); + return ( + <SafeAreaView testID='team-channels-view'> + <StatusBar /> + {this.renderScroll()} + </SafeAreaView> + ); + } +} + +const mapStateToProps = state => ({ + baseUrl: state.server.server, + user: getUserSelector(state), + useRealName: state.settings.UI_Use_Real_Name, + isMasterDetail: state.app.isMasterDetail, + StoreLastMessage: state.settings.Store_Last_Message +}); + +export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(TeamChannelsView)))); diff --git a/app/views/ThreadMessagesView/index.js b/app/views/ThreadMessagesView/index.js index 417c1032..48cab9c9 100644 --- a/app/views/ThreadMessagesView/index.js +++ b/app/views/ThreadMessagesView/index.js @@ -28,7 +28,7 @@ import * as List from '../../containers/List'; import Dropdown from './Dropdown'; import DropdownItemHeader from './Dropdown/DropdownItemHeader'; import { FILTER } from './filters'; -import NoDataFound from './NoDataFound'; +import BackgroundContainer from '../../containers/BackgroundContainer'; import { isIOS } from '../../utils/deviceInfo'; import { getBadgeColor, makeThreadName } from '../../utils/room'; import { getHeaderTitlePosition } from '../../containers/Header'; @@ -463,7 +463,7 @@ class ThreadMessagesView extends React.Component { return ( <> {this.renderHeader()} - <NoDataFound text={text} /> + <BackgroundContainer text={text} /> </> ); } diff --git a/e2e/tests/assorted/02-broadcast.spec.js b/e2e/tests/assorted/02-broadcast.spec.js index 9e87c5e1..97e5783c 100644 --- a/e2e/tests/assorted/02-broadcast.spec.js +++ b/e2e/tests/assorted/02-broadcast.spec.js @@ -32,7 +32,7 @@ describe('Broadcast room', () => { await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible().withTimeout(60000); - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); await element(by.id('room-actions-info')).tap(); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); diff --git a/e2e/tests/assorted/05-joinpublicroom.spec.js b/e2e/tests/assorted/05-joinpublicroom.spec.js index 90707f9f..1cf9a7fd 100644 --- a/e2e/tests/assorted/05-joinpublicroom.spec.js +++ b/e2e/tests/assorted/05-joinpublicroom.spec.js @@ -15,7 +15,7 @@ async function navigateToRoom() { } async function navigateToRoomActions() { - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); } @@ -39,7 +39,7 @@ describe('Join public room', () => { // Render - Header describe('Header', async() => { it('should have actions button ', async() => { - await expect(element(by.id('room-view-header-actions'))).toBeVisible(); + await expect(element(by.id('room-header'))).toBeVisible(); }); }); diff --git a/e2e/tests/assorted/08-joinprotectedroom.spec.js b/e2e/tests/assorted/08-joinprotectedroom.spec.js index 32b23e9b..965af00a 100644 --- a/e2e/tests/assorted/08-joinprotectedroom.spec.js +++ b/e2e/tests/assorted/08-joinprotectedroom.spec.js @@ -16,7 +16,7 @@ async function navigateToRoom() { } async function navigateToRoomActions() { - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); } diff --git a/e2e/tests/room/02-room.spec.js b/e2e/tests/room/02-room.spec.js index 228c340b..e5fb4292 100644 --- a/e2e/tests/room/02-room.spec.js +++ b/e2e/tests/room/02-room.spec.js @@ -30,7 +30,7 @@ describe('Room screen', () => { // Render - Header describe('Header', async() => { it('should have actions button ', async() => { - await expect(element(by.id('room-view-header-actions'))).toExist(); + await expect(element(by.id('room-header'))).toExist(); }); it('should have threads button ', async() => { diff --git a/e2e/tests/room/03-roomactions.spec.js b/e2e/tests/room/03-roomactions.spec.js index 74b3114b..494a8e39 100644 --- a/e2e/tests/room/03-roomactions.spec.js +++ b/e2e/tests/room/03-roomactions.spec.js @@ -16,7 +16,7 @@ async function navigateToRoomActions(type) { await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); } @@ -218,7 +218,7 @@ describe('Room actions screen', () => { await starMessage('messageToStar') //Back into Room Actions - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); //Go to starred messages @@ -246,7 +246,7 @@ describe('Room actions screen', () => { await pinMessage('messageToPin') //Back into Room Actions - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom'); await waitFor(element(by.id('room-actions-pinned'))).toExist(); @@ -270,7 +270,7 @@ describe('Room actions screen', () => { await mockMessage('messageToFind'); //Back into Room Actions - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await element(by.id('room-actions-search')).tap(); @@ -499,7 +499,7 @@ describe('Room actions screen', () => { }); it('should navigate to direct message', async() => { - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await element(by.id('room-actions-members')).tap(); await waitFor(element(by.id('room-members-view'))).toExist().withTimeout(2000); diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js index 55ee40ab..d1686840 100644 --- a/e2e/tests/room/04-discussion.spec.js +++ b/e2e/tests/room/04-discussion.spec.js @@ -74,8 +74,8 @@ describe('Discussion', () => { describe('Check RoomActionsView render', async() => { it('should navigete to RoomActionsView', async() => { - await waitFor(element(by.id('room-view-header-actions'))).toBeVisible().withTimeout(5000); - await element(by.id('room-view-header-actions')).tap(); + await waitFor(element(by.id('room-header'))).toBeVisible().withTimeout(5000); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); }); diff --git a/e2e/tests/room/05-threads.spec.js b/e2e/tests/room/05-threads.spec.js index 349c5d6d..1b7c64b8 100644 --- a/e2e/tests/room/05-threads.spec.js +++ b/e2e/tests/room/05-threads.spec.js @@ -30,7 +30,7 @@ describe('Threads', () => { // Render - Header describe('Header', async() => { it('should have actions button ', async() => { - await expect(element(by.id('room-view-header-actions'))).toExist(); + await expect(element(by.id('room-header'))).toExist(); }); it('should have threads button ', async() => { @@ -105,8 +105,8 @@ describe('Threads', () => { 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 waitFor(element(by.id('room-header').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('room-header').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); }); @@ -118,8 +118,8 @@ describe('Threads', () => { 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 waitFor(element(by.id('room-header').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('room-header').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); }); @@ -133,8 +133,8 @@ describe('Threads', () => { 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 waitFor(element(by.id('room-header').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('room-header').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(); diff --git a/e2e/tests/room/08-roominfo.spec.js b/e2e/tests/room/08-roominfo.spec.js index 9fd9aebb..b172935c 100644 --- a/e2e/tests/room/08-roominfo.spec.js +++ b/e2e/tests/room/08-roominfo.spec.js @@ -17,7 +17,7 @@ async function navigateToRoomInfo(type) { await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); - await element(by.id('room-view-header-actions')).tap(); + await element(by.id('room-header')).tap(); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await element(by.id('room-actions-info')).tap(); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); diff --git a/storybook/stories/RoomViewHeader.js b/storybook/stories/RoomViewHeader.js deleted file mode 100644 index cce19f67..00000000 --- a/storybook/stories/RoomViewHeader.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import { ScrollView, View, StyleSheet } from 'react-native'; -import { HeaderBackButton } from '@react-navigation/stack'; - -import HeaderComponent from '../../app/views/RoomView/Header/Header'; -// import { CustomHeaderButtons, Item } from '../../app/containers/HeaderButton'; -import StoriesSeparator from './StoriesSeparator'; -import { isIOS } from '../../app/utils/deviceInfo'; -import { themes } from '../../app/constants/colors'; - -let _theme = 'light'; - -const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'row', - height: isIOS ? 44 : 56, - borderTopWidth: 1, - borderBottomWidth: 1, - marginVertical: 6 - } -}); - -const Header = props => ( - <View style={[styles.container, { backgroundColor: themes[_theme].headerBackground }]}> - <HeaderBackButton /> - <HeaderComponent - title='test' - type='d' - width={375} - height={480} - theme={_theme} - {...props} - /> - {/* not working because we use withTheme */} - {/* <CustomHeaderButtons> - <Item title='thread' iconName='thread' /> - </CustomHeaderButtons> - <CustomHeaderButtons> - <Item title='more' iconName='menu' /> - </CustomHeaderButtons> */} - </View> -); - -// eslint-disable-next-line react/prop-types -export default ({ theme }) => { - _theme = theme; - return ( - <ScrollView style={{ backgroundColor: themes[theme].auxiliaryBackground }}> - <StoriesSeparator title='Basic' theme={theme} /> - <Header /> - - <StoriesSeparator title='Types' theme={theme} /> - <Header type='d' /> - <Header type='c' /> - <Header type='p' /> - <Header type='discussion' /> - <Header type='thread' /> - - <StoriesSeparator title='Typing' theme={theme} /> - <Header usersTyping={['diego.mello']} /> - <Header usersTyping={['diego.mello', 'rocket.cat']} /> - <Header usersTyping={['diego.mello', 'rocket.cat', 'detoxrn']} /> - - <StoriesSeparator title='Title scroll' theme={theme} /> - <Header title='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' /> - <Header - title='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' - usersTyping={['diego.mello', 'rocket.cat', 'detoxrn']} - /> - </ScrollView> - ); -}; diff --git a/storybook/stories/index.js b/storybook/stories/index.js index 8edb159b..cd84085c 100644 --- a/storybook/stories/index.js +++ b/storybook/stories/index.js @@ -14,6 +14,8 @@ import Markdown from './Markdown'; import './HeaderButtons'; import './UnreadBadge'; import '../../app/views/ThreadMessagesView/Item.stories.js'; +import '../../app/containers/BackgroundContainer/index.stories.js'; +import '../../app/containers/RoomHeader/RoomHeader.stories.js'; import Avatar from './Avatar'; // import RoomViewHeader from './RoomViewHeader';