diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index a913e4ed..9c1b442b 100644 --- a/app/containers/MessageActions/index.tsx +++ b/app/containers/MessageActions/index.tsx @@ -17,7 +17,7 @@ import Header, { HEADER_HEIGHT, IHeader } from './Header'; import events from '../../lib/methods/helpers/log/events'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { getPermalinkMessage } from '../../lib/methods'; -import { hasPermission } from '../../lib/methods/helpers'; +import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; export interface IMessageActionsProps { @@ -41,6 +41,7 @@ export interface IMessageActionsProps { deleteMessagePermission?: string[]; forceDeleteMessagePermission?: string[]; pinMessagePermission?: string[]; + createDirectMessagePermission?: string[]; } export interface IMessageActions { @@ -70,7 +71,8 @@ const MessageActions = React.memo( editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, - pinMessagePermission + pinMessagePermission, + createDirectMessagePermission }, ref ) => { @@ -235,6 +237,23 @@ const MessageActions = React.memo( replyInit(message, false); }; + const handleReplyInDM = async (message: TAnyMessageModel) => { + if (message?.u?.username) { + const result = await Services.createDirectMessage(message.u.username); + if (result.success) { + const { room } = result; + const params = { + rid: room.rid, + name: getRoomTitle(room), + t: room.t, + roomUserId: getUidDirectMessage(room), + replyInDM: message + }; + Navigation.replace('RoomView', params); + } + } + }; + const handleStar = async (message: TAnyMessageModel) => { logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR); try { @@ -345,6 +364,15 @@ const MessageActions = React.memo( }); } + // Reply in DM + if (room.t !== 'd' && room.t !== 'l' && createDirectMessagePermission) { + options.push({ + title: I18n.t('Reply_in_direct_message'), + icon: 'arrow-back', + onPress: () => handleReplyInDM(message) + }); + } + // Edit if (allowEdit(message)) { options.push({ @@ -480,7 +508,8 @@ const mapStateToProps = (state: IApplicationState) => ({ editMessagePermission: state.permissions['edit-message'], deleteMessagePermission: state.permissions['delete-message'], forceDeleteMessagePermission: state.permissions['force-delete-message'], - pinMessagePermission: state.permissions['pin-message'] + pinMessagePermission: state.permissions['pin-message'], + createDirectMessagePermission: state.permissions['create-d'] }); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 4f4686a6..e08986b8 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -862,6 +862,7 @@ "Select_Members": "Select Members", "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", + "Reply_in_direct_message": "Reply in Direct Message", "room_archived": "archived room", "room_unarchived": "unarchived room" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index b5a4ae81..ab98121b 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -817,6 +817,7 @@ "Select_Members": "Selecionar Membros", "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", + "Reply_in_direct_message": "Responder por mensagem direta", "room_archived": "{{username}} arquivou a sala", "room_unarchived": "{{username}} desarquivou a sala" } \ No newline at end of file diff --git a/app/stacks/types.ts b/app/stacks/types.ts index d83f7f2c..cb00e8f1 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -5,7 +5,7 @@ import { IItem } from '../views/TeamChannelsView'; import { IOptionsField } from '../views/NotificationPreferencesView/options'; import { IServer } from '../definitions/IServer'; import { IAttachment } from '../definitions/IAttachment'; -import { IMessage, TMessageModel } from '../definitions/IMessage'; +import { IMessage, TAnyMessageModel, TMessageModel } from '../definitions/IMessage'; import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription'; import { ICannedResponse } from '../definitions/ICannedResponse'; import { TDataSelect } from '../definitions/IDataSelect'; @@ -37,6 +37,7 @@ export type ChatsStackParamList = { roomUserId?: string | null; usedCannedResponse?: string; status?: string; + replyInDM?: TAnyMessageModel; } | undefined; // Navigates back to RoomView already on stack RoomActionsView: { diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 4d0dee2d..6a889943 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -225,6 +225,7 @@ class RoomView extends React.Component { private retryFindTimeout?: ReturnType; private messageErrorActions?: IMessageErrorActions | null; private messageActions?: IMessageActions | null; + private replyInDM?: TAnyMessageModel; // Type of InteractionManager.runAfterInteractions private didMountInteraction?: { then: (onfulfilled?: (() => any) | undefined, onrejected?: (() => any) | undefined) => Promise; @@ -259,6 +260,7 @@ class RoomView extends React.Component { this.jumpToMessageId = props.route.params?.jumpToMessageId; this.jumpToThreadId = props.route.params?.jumpToThreadId; const roomUserId = props.route.params?.roomUserId ?? getUidDirectMessage(room); + this.replyInDM = props.route.params?.replyInDM; this.state = { joined: true, room, @@ -332,6 +334,9 @@ class RoomView extends React.Component { if (isIOS && this.rid) { this.updateUnreadCount(); } + if (this.replyInDM) { + this.onReplyInit(this.replyInDM, false); + } }); if (isTablet) { EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands); diff --git a/e2e/data.ts b/e2e/data.ts index 63bb8c0c..1db7d862 100644 --- a/e2e/data.ts +++ b/e2e/data.ts @@ -51,9 +51,11 @@ const data = { detoxpublicprotected: { name: 'detox-public-protected', joinCode: '123' - }, - detoxpublicignore: { - name: `detox-public-ignore-${value}` + } + }, + userRegularChannels: { + detoxpublic: { + name: `detox-public-${value}` } }, groups: { diff --git a/e2e/data/data.cloud.ts b/e2e/data/data.cloud.ts index 56c5f502..8b55f108 100644 --- a/e2e/data/data.cloud.ts +++ b/e2e/data/data.cloud.ts @@ -54,6 +54,11 @@ const data = { joinCode: '123' } }, + userRegularChannels: { + detoxpublic: { + name: `detox-public-${value}` + } + }, groups: { private: { name: `detox-private-${value}` diff --git a/e2e/data/data.docker.ts b/e2e/data/data.docker.ts index 82360c20..164be787 100644 --- a/e2e/data/data.docker.ts +++ b/e2e/data/data.docker.ts @@ -53,6 +53,11 @@ const data = { joinCode: '123' } }, + userRegularChannels: { + detoxpublic: { + name: `detox-public-${value}` + } + }, groups: { private: { name: `detox-private-${value}` diff --git a/e2e/helpers/data_setup.ts b/e2e/helpers/data_setup.ts index b8cb9b4b..9a49682a 100644 --- a/e2e/helpers/data_setup.ts +++ b/e2e/helpers/data_setup.ts @@ -158,6 +158,21 @@ const setup = async () => { await login(data.users.regular.username, data.users.regular.password); + for (const channelKey in data.userRegularChannels) { + if (Object.prototype.hasOwnProperty.call(data.userRegularChannels, channelKey)) { + const channel = data.userRegularChannels[channelKey as TDataChannels]; + const { + data: { + channel: { _id } + } + } = await createChannelIfNotExists(channel.name); + + if ('joinCode' in channel) { + await changeChannelJoinCode(_id, channel.joinCode); + } + } + } + for (const groupKey in data.groups) { if (Object.prototype.hasOwnProperty.call(data.groups, groupKey)) { const group = data.groups[groupKey as TDataGroups]; diff --git a/e2e/tests/room/02-room.spec.ts b/e2e/tests/room/02-room.spec.ts index 3267029a..39746d52 100644 --- a/e2e/tests/room/02-room.spec.ts +++ b/e2e/tests/room/02-room.spec.ts @@ -13,6 +13,7 @@ import { platformTypes, TTextMatcher } from '../../helpers/app'; +import { sendMessage } from '../../helpers/data_setup'; async function navigateToRoom(roomName: string) { await searchRoom(`${roomName}`); @@ -496,6 +497,37 @@ describe('Room screen', () => { await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)) .toNotExist() .withTimeout(2000); + await tapBack(); + }); + + it('should reply in DM to another user', async () => { + const channelName = data.userRegularChannels.detoxpublic.name; + const stringToReply = 'Message to reply in DM'; + await waitFor(element(by.id('rooms-list-view'))) + .toBeVisible() + .withTimeout(2000); + await navigateToRoom(channelName); + await sendMessage(data.users.alternate, channelName, stringToReply); + await waitFor(element(by[textMatcher](stringToReply)).atIndex(0)) + .toBeVisible() + .withTimeout(3000); + await element(by[textMatcher](stringToReply)).atIndex(0).longPress(); + await waitFor(element(by.id('action-sheet'))) + .toExist() + .withTimeout(2000); + await expect(element(by.id('action-sheet-handle'))).toBeVisible(); + await waitFor(element(by[textMatcher]('Reply in Direct Message')).atIndex(0)) + .toExist() + .withTimeout(6000); + await element(by[textMatcher]('Reply in Direct Message')).atIndex(0).tap(); + await waitFor(element(by.id(`room-view-title-${data.users.alternate.username}`))) + .toExist() + .withTimeout(6000); + await element(by.id('messagebox-input')).replaceText(`${data.random} replied in dm`); + await waitFor(element(by.id('messagebox-send-message'))) + .toExist() + .withTimeout(2000); + await element(by.id('messagebox-send-message')).tap(); }); }); }); diff --git a/e2e/tests/room/10-ignoreuser.spec.ts b/e2e/tests/room/10-ignoreuser.spec.ts index 9f057253..264fab84 100644 --- a/e2e/tests/room/10-ignoreuser.spec.ts +++ b/e2e/tests/room/10-ignoreuser.spec.ts @@ -69,9 +69,8 @@ describe('Ignore/Block User', () => { }); describe('Ignore user from Message', () => { it('should ignore user from message', async () => { - const channelName = data.channels.detoxpublicignore.name; + const channelName = data.userRegularChannels.detoxpublic.name; await navigateToRoom(channelName); - await element(by.id('room-view-join-button')).tap(); await sleep(300); await sendMessage(data.users.alternate, channelName, 'message-01'); await sendMessage(data.users.alternate, channelName, 'message-02');