diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index 1b204e7ac..395f5f87d 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -330,7 +330,7 @@ const MessageActions = React.memo( let options: TActionSheetOptionsItem[] = []; // Reply - if (!isReadOnly) { + if (!isReadOnly && !tmid) { options = [ { title: I18n.t('Reply_in_Thread'), diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 7290c9a6f..067dbb502 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -55,7 +55,7 @@ import { } from '../../definitions'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; -import { hasPermission, debounce, isAndroid, isIOS, isTablet } from '../../lib/methods/helpers'; +import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersion } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; import { TSupportedThemes } from '../../theme'; import { ChatsStackParamList } from '../../stacks/types'; @@ -111,8 +111,8 @@ export interface IMessageBoxProps extends IBaseScreen void | null; + serverVersion: string; } interface IMessageBoxState { @@ -181,7 +181,7 @@ class MessageBox extends Component { commandPreview: [], showCommandPreview: false, command: {}, - tshow: false, + tshow: this.sendThreadToChannel, mentionLoading: false, permissionToUpload: true }; @@ -211,6 +211,23 @@ class MessageBox extends Component { }; } + get sendThreadToChannel() { + const { user, serverVersion, tmid } = this.props; + if (tmid && compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) { + return false; + } + if (tmid && user.alsoSendThreadToChannel === 'default') { + return false; + } + if (user.alsoSendThreadToChannel === 'always') { + return true; + } + if (user.alsoSendThreadToChannel === 'never') { + return false; + } + return true; + } + async componentDidMount() { const db = database.active; const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props; @@ -381,7 +398,12 @@ class MessageBox extends Component { } componentDidUpdate(prevProps: IMessageBoxProps) { - const { uploadFilePermission, goToCannedResponses } = this.props; + const { uploadFilePermission, goToCannedResponses, replyWithMention, threadsEnabled } = this.props; + if (prevProps.replyWithMention !== replyWithMention) { + if (threadsEnabled && replyWithMention) { + this.setState({ tshow: this.sendThreadToChannel }); + } + } if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) { this.setOptions(); } @@ -687,9 +709,13 @@ class MessageBox extends Component { }; clearInput = () => { + const { tshow } = this.state; + const { user, serverVersion } = this.props; this.setInput(''); this.setShowSend(false); - this.setState({ tshow: false }); + if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0') || (tshow && user.alsoSendThreadToChannel === 'default')) { + this.setState({ tshow: false }); + } }; canUploadFile = (file: any) => { @@ -974,7 +1000,7 @@ class MessageBox extends Component { // Normal message } else { // @ts-ignore - onSubmit(message, undefined, tshow); + onSubmit(message, undefined, tmid ? tshow : false); } }; @@ -1044,7 +1070,12 @@ class MessageBox extends Component { onPress={this.onPressSendToChannel} testID='messagebox-send-to-channel' > - + {I18n.t('Messagebox_Send_to_channel')} @@ -1213,7 +1244,8 @@ const mapStateToProps = (state: IApplicationState) => ({ FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled, - uploadFilePermission: state.permissions['mobile-upload-file'] + uploadFilePermission: state.permissions['mobile-upload-file'], + serverVersion: state.server.version }); const dispatchToProps = { diff --git a/app/definitions/ILoggedUser.ts b/app/definitions/ILoggedUser.ts index cf5d65788..9b7193e77 100644 --- a/app/definitions/ILoggedUser.ts +++ b/app/definitions/ILoggedUser.ts @@ -21,6 +21,7 @@ export interface ILoggedUser { showMessageInMainThread?: boolean; isFromWebView?: boolean; enableMessageParserEarlyAdoption: boolean; + alsoSendThreadToChannel: 'default' | 'always' | 'never'; } export interface ILoggedUserResultFromServer diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 187264989..c865bbc37 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -842,5 +842,7 @@ "error-init-video-conf": "Error starting video call", "totp-invalid": "Code or password invalid", "Close_Chat": "Close Chat", - "Select_tags": "Select tags" + "Select_tags": "Select tags", + "Also_send_thread_message_to_channel_behavior": "Also send thread message to channel", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Allow users to select the Also send to channel behavior" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 020de9326..e30e7ad6c 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -795,5 +795,7 @@ "Show_badge_for_mentions_Info": "Mostrar contador somente para menções diretas", "totp-invalid": "Código ou senha inválida", "Close_Chat": "Fechar Conversa", - "Select_tags": "Selecionar tag(s)" + "Select_tags": "Selecionar tag(s)", + "Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Permitir que os usuários selecionem o comportamento Também enviar para o canal" } \ No newline at end of file diff --git a/app/lib/methods/subscriptions/rooms.ts b/app/lib/methods/subscriptions/rooms.ts index bd5f2b425..37505ed63 100644 --- a/app/lib/methods/subscriptions/rooms.ts +++ b/app/lib/methods/subscriptions/rooms.ts @@ -295,6 +295,9 @@ export default function subscribeRooms() { if ((['settings.preferences.showMessageInMainThread'] as any) in diff) { store.dispatch(setUser({ showMessageInMainThread: diff['settings.preferences.showMessageInMainThread'] })); } + if ((['settings.preferences.alsoSendThreadToChannel'] as any) in diff) { + store.dispatch(setUser({ alsoSendThreadToChannel: diff['settings.preferences.alsoSendThreadToChannel'] })); + } } if (/subscriptions/.test(ev)) { if (type === 'removed') { diff --git a/app/lib/services/connect.ts b/app/lib/services/connect.ts index 4d4110b52..41f20cf2e 100644 --- a/app/lib/services/connect.ts +++ b/app/lib/services/connect.ts @@ -268,8 +268,10 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise< const result = sdk.current.currentLogin?.result; let enableMessageParserEarlyAdoption = true; + let showMessageInMainThread = false; if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) { enableMessageParserEarlyAdoption = result.me.settings?.preferences?.enableMessageParserEarlyAdoption ?? true; + showMessageInMainThread = result.me.settings?.preferences?.showMessageInMainThread ?? true; } if (result) { @@ -287,8 +289,9 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise< roles: result.me.roles, avatarETag: result.me.avatarETag, isFromWebView, - showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true, - enableMessageParserEarlyAdoption + showMessageInMainThread, + enableMessageParserEarlyAdoption, + alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel }; return user; } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 11d507eb6..4b7213f80 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -813,6 +813,10 @@ class RoomView extends React.Component { }; onReplyInit = (message: TAnyMessageModel, mention: boolean) => { + // If there's a thread already, we redirect to it + if (mention && !!message.tlm) { + return this.onThreadPress(message); + } this.setState({ selectedMessage: message, replying: true, @@ -833,6 +837,10 @@ class RoomView extends React.Component { }; onMessageLongPress = (message: TAnyMessageModel) => { + // if it's a thread message on main room, we disable the long press + if (message.tmid && !this.tmid) { + return; + } this.messagebox?.current?.closeEmojiAndAction(this.messageActions?.showMessageActions, message); }; diff --git a/app/views/UserPreferencesView/ListPicker.tsx b/app/views/UserPreferencesView/ListPicker.tsx new file mode 100644 index 000000000..1bd557d27 --- /dev/null +++ b/app/views/UserPreferencesView/ListPicker.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { StyleSheet, Text } from 'react-native'; + +import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; +import { CustomIcon } from '../../containers/CustomIcon'; +import * as List from '../../containers/List'; +import I18n from '../../i18n'; +import { useTheme } from '../../theme'; +import sharedStyles from '../Styles'; + +const styles = StyleSheet.create({ + title: { ...sharedStyles.textRegular, fontSize: 16 } +}); + +const OPTIONS = { + alsoSendThreadToChannel: [ + { + label: 'Default', + value: 'default' + }, + { + label: 'Always', + value: 'always' + }, + { + label: 'Never', + value: 'never' + } + ] +}; + +type TOptions = keyof typeof OPTIONS; + +interface IBaseParams { + preference: TOptions; + value: string; + onChangeValue: (param: { [key: string]: string }, onError: () => void) => void; +} + +const ListPicker = ({ + preference, + value, + title, + testID, + onChangeValue +}: { + title: string; + testID: string; +} & IBaseParams) => { + const { showActionSheet, hideActionSheet } = useActionSheet(); + const { colors } = useTheme(); + const [option, setOption] = useState( + value ? OPTIONS[preference].find(option => option.value === value) : OPTIONS[preference][0] + ); + + const getOptions = (): TActionSheetOptionsItem[] => + OPTIONS[preference].map(i => ({ + title: I18n.t(i.label, { defaultValue: i.label }), + onPress: () => { + hideActionSheet(); + onChangeValue({ [preference]: i.value.toString() }, () => setOption(option)); + setOption(i); + }, + right: option?.value === i.value ? () => : undefined + })); + + return ( + showActionSheet({ options: getOptions() })} + right={() => ( + + {option?.label ? I18n.t(option?.label, { defaultValue: option?.label }) : option?.label} + + )} + /> + ); +}; + +export default ListPicker; diff --git a/app/views/UserPreferencesView/index.tsx b/app/views/UserPreferencesView/index.tsx index fed7c5c7c..748e38d21 100644 --- a/app/views/UserPreferencesView/index.tsx +++ b/app/views/UserPreferencesView/index.tsx @@ -15,13 +15,14 @@ import { getUserSelector } from '../../selectors/login'; import { ProfileStackParamList } from '../../stacks/types'; import { Services } from '../../lib/services'; import { useAppSelector } from '../../lib/hooks'; +import ListPicker from './ListPicker'; interface IUserPreferencesViewProps { navigation: StackNavigationProp; } const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Element => { - const { enableMessageParserEarlyAdoption, id } = useAppSelector(state => getUserSelector(state)); + const { enableMessageParserEarlyAdoption, id, alsoSendThreadToChannel } = useAppSelector(state => getUserSelector(state)); const serverVersion = useAppSelector(state => state.server.version); const dispatch = useDispatch(); @@ -45,6 +46,16 @@ const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Ele } }; + const setAlsoSendThreadToChannel = async (param: { [key: string]: string }, onError: () => void) => { + try { + await Services.saveUserPreferences(param); + dispatch(setUser(param)); + } catch (e) { + log(e); + onError(); + } + }; + const renderMessageParserSwitch = (value: boolean) => ( ); @@ -74,6 +85,20 @@ const UserPreferencesView = ({ navigation }: IUserPreferencesViewProps): JSX.Ele ) : null} + {compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0') ? ( + + + + + + + ) : null} ); diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js index 4b038e42e..b8459f189 100644 --- a/e2e/helpers/data_setup.js +++ b/e2e/helpers/data_setup.js @@ -1,6 +1,7 @@ const axios = require('axios').default; const data = require('../data'); +const random = require('./random'); const TEAM_TYPE = { PUBLIC: 0, @@ -106,6 +107,8 @@ const changeChannelJoinCode = async (roomId, joinCode) => { try { await rocketchat.post('method.call/saveRoomSettings', { message: JSON.stringify({ + msg: 'method', + id: random(10), method: 'saveRoomSettings', params: [roomId, { joinCode }] })