diff --git a/app/components/global_threads/global_threads.tsx b/app/components/global_threads/global_threads.tsx index 6255054ef0..afb446fbf9 100644 --- a/app/components/global_threads/global_threads.tsx +++ b/app/components/global_threads/global_threads.tsx @@ -5,9 +5,14 @@ import React from 'react'; import {injectIntl, intlShape} from 'react-intl'; import {Alert, FlatList} from 'react-native'; +import {goToScreen} from '@actions/navigation'; +import {THREAD} from '@constants/screen'; +import EventEmitter from '@mm-redux/utils/event_emitter'; + import ThreadList from './thread_list'; import type {ActionResult} from '@mm-redux/types/actions'; +import type {Post} from '@mm-redux/types/posts'; import type {Team} from '@mm-redux/types/teams'; import type {Theme} from '@mm-redux/types/theme'; import type {ThreadsState, UserThread} from '@mm-redux/types/threads'; @@ -16,10 +21,12 @@ import type {$ID} from '@mm-redux/types/utilities'; type Props = { actions: { + getPostThread: (postId: string) => void; getThreads: (userId: $ID, teamId: $ID, before?: $ID, after?: $ID, perPage?: number, deleted?: boolean, unread?: boolean) => Promise; handleViewingGlobalThreadsAll: () => void; handleViewingGlobalThreadsUnreads: () => void; markAllThreadsInTeamRead: (userId: $ID, teamId: $ID) => void; + selectPost: (postId: string) => void; }; allThreadIds: $ID[]; intl: typeof intlShape; @@ -119,6 +126,23 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo ); }; + const goToThread = React.useCallback((post: Post) => { + actions.getPostThread(post.id); + actions.selectPost(post.id); + const passProps = { + channelId: post.channel_id, + rootId: post.id, + }; + goToScreen(THREAD, '', passProps); + }, []); + + React.useEffect(() => { + EventEmitter.on('goToThread', goToThread); + return () => { + EventEmitter.off('goToThread', goToThread); + }; + }, []); + return ( { }); test('Should goto threads when pressed on thread item', () => { - const goToScreen = jest.spyOn(navigationActions, 'goToScreen'); + EventEmitter.emit = jest.fn(); const wrapper = shallow( { const threadItem = wrapper.find({testID: `${testIDPrefix}.item`}); expect(threadItem.exists()).toBeTruthy(); threadItem.simulate('press'); - expect(goToScreen).toHaveBeenCalledWith(THREAD, expect.anything(), expect.anything()); + expect(EventEmitter.emit).toHaveBeenCalledWith('goToThread', expect.anything()); }); }); diff --git a/app/components/global_threads/thread_item/thread_item.tsx b/app/components/global_threads/thread_item/thread_item.tsx index 892b3a06fe..9a65e9c2e6 100644 --- a/app/components/global_threads/thread_item/thread_item.tsx +++ b/app/components/global_threads/thread_item/thread_item.tsx @@ -3,29 +3,28 @@ import React from 'react'; import {injectIntl, intlShape} from 'react-intl'; -import {View, Text, TouchableHighlight} from 'react-native'; +import {Keyboard, Text, TouchableHighlight, View} from 'react-native'; -import {goToScreen} from '@actions/navigation'; +import {showModalOverCurrentContext} from '@actions/navigation'; import FriendlyDate from '@components/friendly_date'; import RemoveMarkdown from '@components/remove_markdown'; -import {GLOBAL_THREADS, THREAD} from '@constants/screen'; +import {GLOBAL_THREADS} from '@constants/screen'; import {Posts, Preferences} from '@mm-redux/constants'; -import {Channel} from '@mm-redux/types/channels'; -import {Post} from '@mm-redux/types/posts'; -import {UserThread} from '@mm-redux/types/threads'; -import {UserProfile} from '@mm-redux/types/users'; +import EventEmitter from '@mm-redux/utils/event_emitter'; import {displayUsername} from '@mm-redux/utils/user_utils'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import ThreadFooter from '../thread_footer'; +import type {Channel} from '@mm-redux/types/channels'; +import type {Post} from '@mm-redux/types/posts'; import type {Theme} from '@mm-redux/types/theme'; +import type {UserThread} from '@mm-redux/types/threads'; +import type {UserProfile} from '@mm-redux/types/users'; export type DispatchProps = { actions: { getPost: (postId: string) => void; - getPostThread: (postId: string) => void; - selectPost: (postId: string) => void; }; } @@ -66,13 +65,18 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre const threadStarterName = displayUsername(threadStarter, Preferences.DISPLAY_PREFER_FULL_NAME); const showThread = () => { - actions.getPostThread(postItem.id); - actions.selectPost(postItem.id); + EventEmitter.emit('goToThread', postItem); + }; + + const showThreadOptions = () => { + const screen = 'GlobalThreadOptions'; const passProps = { - channelId: postItem.channel_id, - rootId: postItem.id, + rootId: post.id, }; - goToScreen(THREAD, '', passProps); + Keyboard.dismiss(); + requestAnimationFrame(() => { + showModalOverCurrentContext(screen, passProps); + }); }; const testIDPrefix = `${testID}.${postItem?.id}`; @@ -134,6 +138,7 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre return ( diff --git a/app/mm-redux/actions/posts.ts b/app/mm-redux/actions/posts.ts index 49da80192e..515934aa32 100644 --- a/app/mm-redux/actions/posts.ts +++ b/app/mm-redux/actions/posts.ts @@ -5,7 +5,7 @@ import {updateThreadLastViewedAt} from '@actions/views/threads'; import {Client4} from '@client/rest'; import {WebsocketEvents} from '@constants'; -import {THREAD} from '@constants/screen'; +import {GLOBAL_THREADS, THREAD} from '@constants/screen'; import {analytics} from '@init/analytics'; import {PostTypes, ChannelTypes, FileTypes, IntegrationTypes} from '@mm-redux/action_types'; import {handleFollowChanged, updateThreadRead} from '@mm-redux/actions/threads'; @@ -444,8 +444,8 @@ export function setUnreadPost(userId: string, postId: string, location: string) return {}; } const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state); - const isUnreadFromThreadScreen = collapsedThreadsEnabled && location === THREAD; - if (isUnreadFromThreadScreen) { + const isUnreadFromThread = collapsedThreadsEnabled && (location === THREAD || location === GLOBAL_THREADS); + if (isUnreadFromThread) { const currentTeamId = getThreadTeamId(state, postId); const threadId = post.root_id || post.id; const actions: GenericAction[] = []; diff --git a/app/screens/index.js b/app/screens/index.js index 1c87c75016..280b02cad7 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -105,6 +105,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { case 'Gallery': screen = require('@screens/gallery').default; break; + case 'GlobalThreadOptions': + screen = require('@screens/thread_options').default; + break; case 'InteractiveDialog': screen = require('@screens/interactive_dialog').default; break; diff --git a/app/screens/post_options/bindings/bindings.tsx b/app/screens/post_options/bindings/bindings.tsx index 058855a23f..1b20981627 100644 --- a/app/screens/post_options/bindings/bindings.tsx +++ b/app/screens/post_options/bindings/bindings.tsx @@ -200,7 +200,7 @@ class Option extends React.PureComponent { return ( { - this.props.onPress(); - }, 500); - - render() { - const {destructive, icon, testID, text, theme} = this.props; - const style = getStyleSheet(theme); - - const Touchable = Platform.select({ - ios: TouchableHighlight, - android: TouchableNativeFeedback, - }); - - const touchableProps = Platform.select({ - ios: { - underlayColor: 'rgba(0, 0, 0, 0.1)', - }, - android: { - background: TouchableNativeFeedback.Ripple( //eslint-disable-line new-cap - 'rgba(0, 0, 0, 0.1)', - false, - ), - }, - }); - - const imageStyle = [style.icon, destructive ? style.destructive : null]; - let image; - let iconStyle = [style.iconContainer]; - if (typeof icon === 'object') { - if (icon.uri) { - imageStyle.push({width: 24, height: 24}); - image = isValidUrl(icon.uri) && ( - - ); - } else { - iconStyle = [style.noIconContainer]; - } - } else { - image = ( - - ); - } - - return ( - - - - - {image} - - - - {text} - - - - - - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - height: 51, - width: '100%', - }, - destructive: { - color: '#D0021B', - }, - row: { - flex: 1, - flexDirection: 'row', - }, - iconContainer: { - alignItems: 'center', - height: 50, - justifyContent: 'center', - width: 60, - }, - noIconContainer: { - height: 50, - width: 18, - }, - icon: { - color: changeOpacity(theme.centerChannelColor, 0.64), - }, - textContainer: { - justifyContent: 'center', - flex: 1, - height: 50, - marginRight: 5, - }, - text: { - color: theme.centerChannelColor, - fontSize: 16, - lineHeight: 19, - opacity: 0.9, - letterSpacing: -0.45, - }, - footer: { - marginHorizontal: 17, - borderBottomWidth: 0.5, - borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2), - }, - }; -}); diff --git a/app/screens/post_options/post_option.tsx b/app/screens/post_options/post_option.tsx new file mode 100644 index 0000000000..8326f57a4c --- /dev/null +++ b/app/screens/post_options/post_option.tsx @@ -0,0 +1,153 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import { + Text, + Platform, + TouchableHighlight, + TouchableNativeFeedback, + View, +} from 'react-native'; +import FastImage from 'react-native-fast-image'; + +import CompassIcon from '@components/compass_icon'; +import {Theme} from '@mm-redux/types/theme'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {isValidUrl} from '@utils/url'; + +type Props = { + testID?: string; + destructive?: boolean; + icon: string | { + uri: string; + }; + onPress: () => void; + text: string; + theme: Theme; +}; + +function PostOption({destructive, icon, onPress, testID, text, theme}: Props) { + const style = getStyleSheet(theme); + + const handleOnPress = React.useCallback(preventDoubleTap(onPress, 500), []); + + let Touchable: React.ElementType; + if (Platform.OS === 'android') { + Touchable = TouchableNativeFeedback; + } else { + Touchable = TouchableHighlight; + } + + const touchableProps = Platform.select({ + ios: { + underlayColor: 'rgba(0, 0, 0, 0.1)', + }, + android: { + background: TouchableNativeFeedback.Ripple( //eslint-disable-line new-cap + 'rgba(0, 0, 0, 0.1)', + false, + ), + }, + }); + + const imageStyle = [style.icon, destructive ? style.destructive : null]; + let image; + let iconStyle = [style.iconContainer]; + if (typeof icon === 'object') { + if (icon.uri) { + imageStyle.push({width: 24, height: 24}); + image = isValidUrl(icon.uri) && ( + + ); + } else { + iconStyle = [style.noIconContainer]; + } + } else { + image = ( + + ); + } + + return ( + + + + + {image} + + + + {text} + + + + + + + ); +} + +export default PostOption; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + height: 51, + width: '100%', + }, + destructive: { + color: '#D0021B', + }, + row: { + flex: 1, + flexDirection: 'row', + }, + iconContainer: { + alignItems: 'center', + height: 50, + justifyContent: 'center', + width: 60, + }, + noIconContainer: { + height: 50, + width: 18, + }, + icon: { + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + textContainer: { + justifyContent: 'center', + flex: 1, + height: 50, + marginRight: 5, + }, + text: { + color: theme.centerChannelColor, + fontSize: 16, + lineHeight: 19, + opacity: 0.9, + letterSpacing: -0.45, + }, + footer: { + marginHorizontal: 17, + borderBottomWidth: 0.5, + borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2), + }, + }; +}); diff --git a/app/screens/thread_options/__snapshots__/thread_options.test.tsx.snap b/app/screens/thread_options/__snapshots__/thread_options.test.tsx.snap new file mode 100644 index 0000000000..ebb36003ff --- /dev/null +++ b/app/screens/thread_options/__snapshots__/thread_options.test.tsx.snap @@ -0,0 +1,278 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ThreadOptions should match snapshot, showing all possible options 1`] = ` + + + + + + + + + + + +`; diff --git a/app/screens/thread_options/index.ts b/app/screens/thread_options/index.ts new file mode 100644 index 0000000000..3d8090e43a --- /dev/null +++ b/app/screens/thread_options/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators, Dispatch} from 'redux'; + +import {showPermalink} from '@actions/views/permalink'; +import { + flagPost, + setUnreadPost, + unflagPost, +} from '@mm-redux/actions/posts'; +import {setThreadFollow, updateThreadRead} from '@mm-redux/actions/threads'; +import {getCurrentUserId} from '@mm-redux/selectors/entities/common'; +import {getPost} from '@mm-redux/selectors/entities/posts'; +import {getMyPreferences, getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getCurrentTeam, getCurrentTeamUrl} from '@mm-redux/selectors/entities/teams'; +import {getThread} from '@mm-redux/selectors/entities/threads'; +import {isPostFlagged} from '@mm-redux/utils/post_utils'; +import {getDimensions} from '@selectors/device'; + +import ThreadOptions, {OwnProps} from './thread_options'; + +import type {GlobalState} from '@mm-redux/types/store'; + +export function mapStateToProps(state: GlobalState, ownProps: OwnProps) { + const myPreferences = getMyPreferences(state); + return { + ...getDimensions(state), + currentTeamName: getCurrentTeam(state)?.name, + currentTeamUrl: getCurrentTeamUrl(state), + currentUserId: getCurrentUserId(state), + isFlagged: isPostFlagged(ownProps.rootId, myPreferences), + post: getPost(state, ownProps.rootId), + theme: getTheme(state), + thread: getThread(state, ownProps.rootId), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators({ + flagPost, + setThreadFollow, + setUnreadPost, + showPermalink, + unflagPost, + updateThreadRead, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ThreadOptions); diff --git a/app/screens/thread_options/thread_options.test.tsx b/app/screens/thread_options/thread_options.test.tsx new file mode 100644 index 0000000000..5d6ff879dc --- /dev/null +++ b/app/screens/thread_options/thread_options.test.tsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {shallow} from 'enzyme'; +import React from 'react'; + +import Preferences from '@mm-redux/constants/preferences'; +import {intl} from '@test/intl-test-helper'; + +import {ThreadOptions} from './thread_options'; + +import type {Post} from '@mm-redux/types/posts'; +import type {UserThread} from '@mm-redux/types/threads'; + +describe('ThreadOptions', () => { + const actions = { + flagPost: jest.fn(), + setThreadFollow: jest.fn(), + setUnreadPost: jest.fn(), + showPermalink: jest.fn(), + unflagPost: jest.fn(), + updateThreadRead: jest.fn(), + }; + + const post = { + id: 'post_id', + message: 'message', + is_pinned: false, + channel_id: 'channel_id', + } as Post; + + const thread = { + id: 'post_id', + unread_replies: 4, + } as UserThread; + + const baseProps = { + actions, + currentTeamName: 'current team name', + currentTeamUrl: 'http://localhost:8065/team-name', + currentUserId: 'user1', + deviceHeight: 600, + isFlagged: true, + intl, + post, + rootId: 'post_id', + theme: Preferences.THEMES.denim, + thread, + }; + + function getWrapper(props = {}) { + return shallow( + , + ); + } + + test('should match snapshot, showing all possible options', () => { + const wrapper = getWrapper(); + expect(wrapper.getElement()).toMatchSnapshot(); + expect(wrapper.findWhere((node) => node.key() === 'flagged')).toMatchObject({}); + expect(wrapper.findWhere((node) => node.key() === 'mark_as_read')).toMatchObject({}); + }); + + test('should show unflag option', () => { + const wrapper = getWrapper({isFlagged: false}); + expect(wrapper.findWhere((node) => node.key() === 'unflag')).toMatchObject({}); + }); + + test('should show unflag option', () => { + const wrapper = getWrapper({ + thread: { + ...thread, + unread_replies: 0, + }, + }); + expect(wrapper.findWhere((node) => node.key() === 'mark_as_unread')).toMatchObject({}); + }); +}); diff --git a/app/screens/thread_options/thread_options.tsx b/app/screens/thread_options/thread_options.tsx new file mode 100644 index 0000000000..38abe45413 --- /dev/null +++ b/app/screens/thread_options/thread_options.tsx @@ -0,0 +1,285 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import Clipboard from '@react-native-community/clipboard'; + +import React from 'react'; +import {intlShape, injectIntl} from 'react-intl'; +import {View} from 'react-native'; + +import {dismissModal} from '@actions/navigation'; +import FormattedText from '@components/formatted_text'; +import SlideUpPanel from '@components/slide_up_panel'; +import {BOTTOM_MARGIN} from '@components/slide_up_panel/slide_up_panel'; +import {GLOBAL_THREADS} from '@constants/screen'; +import EventEmitter from '@mm-redux/utils/event_emitter'; +import ThreadOption from '@screens/post_options/post_option'; +import {OPTION_HEIGHT, getInitialPosition} from '@screens/post_options/post_options_utils'; +import {t} from '@utils/i18n'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import type {Post} from '@mm-redux/types/posts'; +import type {Theme} from '@mm-redux/types/theme'; +import type {UserThread} from '@mm-redux/types/threads'; +import type {UserProfile} from '@mm-redux/types/users'; +import type {$ID} from '@mm-redux/types/utilities'; + +export type StateProps = { + currentTeamName: string; + currentTeamUrl: string; + currentUserId: $ID; + deviceHeight: number; + isFlagged: boolean; + post: Post; + theme: Theme; + thread: UserThread; +}; + +export type DispatchProps = { + actions: { + flagPost: (postId: $ID) => void; + setThreadFollow: (currentUserId: $ID, threadId: $ID, newState: boolean) => void; + setUnreadPost: (currentUserId: $ID, postId: $ID, location: string) => void; + showPermalink: (currentUserId: $ID, teamName: string, postId: $ID) => void; + unflagPost: (postId: $ID) => void; + updateThreadRead: (currentUserId: $ID, threadId: $ID, timestamp: number) => void; + }; +}; + +export type OwnProps = { + rootId: $ID; +}; + +type Props = StateProps & DispatchProps & OwnProps & { + intl: typeof intlShape; +}; + +function ThreadOptions({actions, currentTeamName, currentTeamUrl, currentUserId, deviceHeight, intl, isFlagged, post, theme, thread}: Props) { + const style = getStyleSheet(theme); + + const slideUpPanelRef = React.useRef(); + + const close = async (cb?: () => void) => { + await dismissModal(); + + if (typeof cb === 'function') { + requestAnimationFrame(cb); + } + }; + + const closeWithAnimation = (cb?: () => void) => { + if (slideUpPanelRef.current) { + slideUpPanelRef.current.closeWithAnimation(cb); + } else { + close(cb); + } + }; + + const getOption = (key: string, icon: string, message: Record, onPress: () => void, destructive = false) => { + const testID = `global_threads.options.${key}.action`; + return ( + + ); + }; + + // + // Option: Reply + // + const handleReply = () => { + closeWithAnimation(() => { + EventEmitter.emit('goToThread', post); + }); + }; + + const getReplyOption = () => { + const key = 'reply'; + const icon = 'reply-outline'; + const message = {id: t('mobile.post_info.reply'), defaultMessage: 'Reply'}; + const onPress = handleReply; + return getOption(key, icon, message, onPress); + }; + + // + // Option: Unfollow thread + // + const handleUnfollowThread = () => { + closeWithAnimation(() => { + actions.setThreadFollow(currentUserId, thread.id, false); + }); + }; + + const getUnfollowThread = () => { + const key = 'unfollow'; + const icon = 'message-minus-outline'; + const message = {id: t('global_threads.options.unfollow'), defaultMessage: 'Unfollow Thread'}; + const onPress = handleUnfollowThread; + return getOption(key, icon, message, onPress); + }; + + // + // Option: Open in Channel + // + const handleOpenInChannel = () => { + closeWithAnimation(() => { + actions.showPermalink(intl, currentTeamName, post.id); + }); + }; + + const getOpenInChannel = () => { + const key = 'open_in_channel'; + const icon = 'globe'; + const message = {id: t('global_threads.options.open_in_channel'), defaultMessage: 'Open in Channel'}; + const onPress = handleOpenInChannel; + return getOption(key, icon, message, onPress); + }; + + // + // Option: Mark as Read + // + const handleMarkAsRead = () => { + closeWithAnimation(() => { + actions.updateThreadRead( + currentUserId, + post.id, + Date.now(), + ); + }); + }; + + const handleMarkAsUnread = () => { + closeWithAnimation(() => { + actions.setUnreadPost(currentUserId, post.id, GLOBAL_THREADS); + }); + }; + + const getMarkAsUnreadOption = () => { + const icon = 'mark-as-unread'; + let key; + let message; + let onPress; + if (thread.unread_replies) { + key = 'mark_as_read'; + message = {id: t('global_threads.options.mark_as_read'), defaultMessage: 'Mark as Read'}; + onPress = handleMarkAsRead; + } else { + key = 'mark_as_unread'; + message = {id: t('mobile.post_info.mark_unread'), defaultMessage: 'Mark as Unread'}; + onPress = handleMarkAsUnread; + } + return getOption(key, icon, message, onPress); + }; + + // + // Option: Flag/Unflag + // + const handleFlagPost = () => { + closeWithAnimation(() => { + actions.flagPost(post.id); + }); + }; + + const handleUnflagPost = () => { + closeWithAnimation(() => { + actions.unflagPost(post.id); + }); + }; + + const getFlagOption = () => { + let key; + let message; + let onPress; + const icon = 'bookmark-outline'; + + if (isFlagged) { + key = 'unflag'; + message = {id: t('mobile.post_info.unflag'), defaultMessage: 'Unsave'}; + onPress = handleUnflagPost; + } else { + key = 'flagged'; + message = {id: t('mobile.post_info.flag'), defaultMessage: 'Save'}; + onPress = handleFlagPost; + } + + return getOption(key, icon, message, onPress); + }; + + // + // Option: Copy Link + // + const handleCopyPermalink = () => { + closeWithAnimation(() => { + const permalink = `${currentTeamUrl}/pl/${post.id}`; + Clipboard.setString(permalink); + }); + }; + + const getCopyPermalink = () => { + const key = 'permalink'; + const icon = 'link-variant'; + const message = {id: t('get_post_link_modal.title'), defaultMessage: 'Copy Link'}; + const onPress = handleCopyPermalink; + return getOption(key, icon, message, onPress); + }; + + const options = [ + getReplyOption(), + getUnfollowThread(), + getOpenInChannel(), + getMarkAsUnreadOption(), + getFlagOption(), + getCopyPermalink(), + ].filter((option) => option !== null); + + const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 2) * OPTION_HEIGHT); + const initialPosition = getInitialPosition(deviceHeight, marginFromTop); + + return ( + + 0 ? marginFromTop : 0} + onRequestClose={close} + initialPosition={initialPosition} + key={marginFromTop} + ref={slideUpPanelRef} + theme={theme} + > + + {options} + + + ); +} + +export {ThreadOptions}; +export default injectIntl(ThreadOptions); + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + flex: 1, + }, + title: { + color: changeOpacity(theme.centerChannelColor, 0.65), + fontSize: 12, + paddingLeft: 16, + paddingTop: 16, + paddingBottom: 8, + }, + }; +}); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index ecf435f811..31b5faf320 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -199,6 +199,10 @@ "global_threads.markAllRead.markRead": "Mark read", "global_threads.markAllRead.message": "This will clear any unread status for all of your threads shown here", "global_threads.markAllRead.title": "Are you sure you want to mark all threads as read?", + "global_threads.options.mark_as_read": "Mark as Read", + "global_threads.options.open_in_channel": "Open in Channel", + "global_threads.options.title": "THREAD ACTIONS", + "global_threads.options.unfollow": "Unfollow Thread", "global_threads.unreads": "Unreads", "integrations.add": "Add", "intro_messages.anyMember": " Any member can join and read this channel.",