From 278ed91f9a549262f475707151cc7fee3ceb9aa5 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:09:36 -0300 Subject: [PATCH] feat: forward message (#5110) * feat: share message * index, selectPersonOrChannel, types * share a message using the chat.postMessage and refactor the interfaces * minor tweak * removed rid in from select person or channel * change title * add pt-br translation * compareServerVersion GTE 6.2.0 * test for sharemessage * view to masterDetail * fix podfile * change from forward message to share message * change from share to forward * refactor the forward message view, tweak on some styles and add the cleanUpMessage * minor tweak * refactor to add MessagePreview and use the same message/index * fix e2e test * add the capability to filter the subscsription if the room is read only or not * minor tweak * fix disable the send button and add message has been shared * add try catch and toast or alert * fix interface --- android/app/src/main/assets/fonts/custom.ttf | Bin 55700 -> 56384 bytes app/containers/CustomIcon/mappedIcons.js | 4 + app/containers/CustomIcon/selection.json | 2 +- .../HeaderButton/HeaderButtonItem.tsx | 4 +- app/containers/MessageActions/index.tsx | 24 +++- app/containers/message/Message.tsx | 13 +++ app/containers/message/Preview.tsx | 35 ++++++ app/containers/message/index.tsx | 17 +-- app/containers/message/interfaces.ts | 1 + app/definitions/rest/v1/chat.ts | 6 + app/i18n/locales/en.json | 6 +- app/i18n/locales/pt-BR.json | 6 +- app/lib/methods/search.ts | 22 +++- app/lib/services/restApi.ts | 2 + app/stacks/InsideStack.tsx | 2 + app/stacks/MasterDetailStack/index.tsx | 2 + app/stacks/MasterDetailStack/types.ts | 8 +- app/stacks/types.ts | 3 + .../SelectPersonOrChannel.tsx | 76 +++++++++++++ app/views/ForwardMessageView/index.tsx | 104 ++++++++++++++++++ app/views/ForwardMessageView/interfaces.ts | 14 +++ app/views/ForwardMessageView/styles.ts | 22 ++++ e2e/tests/room/11-sharemessage.spec.ts | 93 ++++++++++++++++ ios/custom.ttf | Bin 55700 -> 56384 bytes 24 files changed, 448 insertions(+), 18 deletions(-) create mode 100644 app/containers/message/Preview.tsx create mode 100644 app/views/ForwardMessageView/SelectPersonOrChannel.tsx create mode 100644 app/views/ForwardMessageView/index.tsx create mode 100644 app/views/ForwardMessageView/interfaces.ts create mode 100644 app/views/ForwardMessageView/styles.ts create mode 100644 e2e/tests/room/11-sharemessage.spec.ts diff --git a/android/app/src/main/assets/fonts/custom.ttf b/android/app/src/main/assets/fonts/custom.ttf index 9d9d1f012c2f54f783439fbdfb1047107c96a3d7..a7cb347abd787a26b41a3d0a1fda7df2e2c068d1 100644 GIT binary patch delta 2089 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 PlmMcQJ<6T^ { +const Item = ({ title, iconName, onPress, testID, badge, color, disabled, ...props }: IHeaderButtonItem): React.ReactElement => { const { colors } = useTheme(); return ( - + <> {iconName ? ( diff --git a/app/containers/MessageActions/index.tsx b/app/containers/MessageActions/index.tsx index 38d77c14e..c58e4f038 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 { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; +import { compareServerVersion, getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; export interface IMessageActionsProps { @@ -30,6 +30,7 @@ export interface IMessageActionsProps { replyInit: (message: TAnyMessageModel, mention: boolean) => void; isMasterDetail: boolean; isReadOnly: boolean; + serverVersion?: string | null; Message_AllowDeleting?: boolean; Message_AllowDeleting_BlockDeleteInMinutes?: number; Message_AllowEditing?: boolean; @@ -74,7 +75,8 @@ const MessageActions = React.memo( forceDeleteMessagePermission, deleteOwnMessagePermission, pinMessagePermission, - createDirectMessagePermission + createDirectMessagePermission, + serverVersion }, ref ) => { @@ -188,6 +190,15 @@ const MessageActions = React.memo( } }; + const handleShareMessage = (message: TAnyMessageModel) => { + const params = { message }; + if (isMasterDetail) { + Navigation.navigate('ModalStackNavigator', { screen: 'ForwardMessageView', params }); + } else { + Navigation.navigate('NewMessageStackNavigator', { screen: 'ForwardMessageView', params }); + } + }; + const handleUnread = async (message: TAnyMessageModel) => { logEvent(events.ROOM_MSG_ACTION_UNREAD); const { id: messageId, ts } = message; @@ -389,6 +400,14 @@ const MessageActions = React.memo( onPress: () => handleCreateDiscussion(message) }); + if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.2.0') && !videoConfBlock) { + options.push({ + title: I18n.t('Forward'), + icon: 'arrow-forward', + onPress: () => handleShareMessage(message) + }); + } + // Permalink options.push({ title: I18n.t('Get_link'), @@ -508,6 +527,7 @@ const MessageActions = React.memo( ); const mapStateToProps = (state: IApplicationState) => ({ server: state.server.server, + serverVersion: state.server.version, Message_AllowDeleting: state.settings.Message_AllowDeleting as boolean, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes as number, Message_AllowEditing: state.settings.Message_AllowEditing as boolean, diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index d6c706cbf..089d70a2e 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -22,6 +22,19 @@ import { useTheme } from '../../theme'; import RightIcons from './Components/RightIcons'; const MessageInner = React.memo((props: IMessageInner) => { + if (props.isPreview) { + return ( + <> + + <> + + + + + + ); + } + if (props.type === 'discussion-created') { return ( <> diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx new file mode 100644 index 000000000..fe4f2bcf4 --- /dev/null +++ b/app/containers/message/Preview.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import Message from './index'; +import { useAppSelector } from '../../lib/hooks'; +import { getUserSelector } from '../../selectors/login'; +import { TAnyMessageModel, TGetCustomEmoji } from '../../definitions'; + +const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { + const { user, baseUrl, Message_TimeFormat, customEmojis, useRealName } = useAppSelector(state => ({ + user: getUserSelector(state), + baseUrl: state.server.server, + Message_TimeFormat: state.settings.Message_TimeFormat as string, + customEmojis: state.customEmojis, + useRealName: state.settings.UI_Use_Real_Name as boolean + })); + + const getCustomEmoji: TGetCustomEmoji = name => { + const emoji = customEmojis[name]; + return emoji ?? null; + }; + return ( + + ); +}; + +export default MessagePreview; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 920c68dcb..52d2f1781 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -28,7 +28,7 @@ interface IMessageContainerProps { baseUrl: string; Message_GroupingPeriod?: number; isReadReceiptEnabled?: boolean; - isThreadRoom: boolean; + isThreadRoom?: boolean; isSystemMessage?: boolean; useRealName?: boolean; autoTranslateRoom?: boolean; @@ -46,9 +46,9 @@ interface IMessageContainerProps { replyBroadcast?: (item: TAnyMessageModel) => void; reactionInit?: (item: TAnyMessageModel) => void; fetchThreadName?: (tmid: string, id: string) => Promise; - showAttachment: (file: IAttachment) => void; + showAttachment?: (file: IAttachment) => void; onReactionLongPress?: (item: TAnyMessageModel) => void; - navToRoomInfo: (navParam: IRoomInfoParam) => void; + navToRoomInfo?: (navParam: IRoomInfoParam) => void; handleEnterCall?: () => void; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void; @@ -56,8 +56,9 @@ interface IMessageContainerProps { toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise; jumpToMessage?: (link: string) => void; onPress?: () => void; - theme: TSupportedThemes; + theme?: TSupportedThemes; closeEmojiAndAction?: (action?: Function, params?: any) => void; + isPreview?: boolean; } interface IMessageContainerState { @@ -336,7 +337,7 @@ class MessageContainer extends React.Component {}, getCustomEmoji, isThreadRoom, handleEnterCall, @@ -345,7 +346,8 @@ class MessageContainer extends React.Component ); diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index c9a5d4536..0573d6c57 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -108,6 +108,7 @@ export interface IMessageInner type: MessageType; blocks: []; urls?: IUrl[]; + isPreview?: boolean; } export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar { diff --git a/app/definitions/rest/v1/chat.ts b/app/definitions/rest/v1/chat.ts index 4a15a5619..64a3b378f 100644 --- a/app/definitions/rest/v1/chat.ts +++ b/app/definitions/rest/v1/chat.ts @@ -82,4 +82,10 @@ export type ChatEndpoints = { 'chat.getMessageReadReceipts': { GET: (params: { messageId: string }) => { receipts: IReadReceipts[] }; }; + 'chat.postMessage': { + POST: (params: { roomId: string; text: string }) => { + message: IMessage; + success: boolean; + }; + }; }; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 7cc46dbad..c24b71d71 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -726,10 +726,14 @@ "Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.", "Learn_more": "Learn more", "and_N_more": "and {{count}} more", + "Forward_message": "Forward message", + "Person_or_channel": "Person or channel", + "Select": "Select", "Nickname": "Nickname", "Bio":"Bio", "decline": "Decline", "accept": "Accept", "Incoming_call_from": "Incoming call from", - "Call_started": "Call started" + "Call_started": "Call started", + "Message_has_been_shared":"Message has been shared" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index bf584460b..f2700f870 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -711,6 +711,9 @@ "Discard_changes_description": "Todas as alterações serão perdidas, se você sair sem salvar.", "Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente", "Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace.", + "Forward_message": "Encaminhar mensagem", + "Person_or_channel": "Pessoa ou canal", + "Select": "Selecionar", "Nickname": "Apelido", "Bio": "Biografia", "decline": "Recusar", @@ -718,5 +721,6 @@ "Incoming_call_from": "Chamada recebida de", "Call_started": "Chamada Iniciada", "Learn_more": "Saiba mais", - "and_N_more": "e mais {{count}}" + "and_N_more": "e mais {{count}}", + "Message_has_been_shared":"Menssagem foi compartilhada" } \ No newline at end of file diff --git a/app/lib/methods/search.ts b/app/lib/methods/search.ts index d4bc8c2f6..6734c2f93 100644 --- a/app/lib/methods/search.ts +++ b/app/lib/methods/search.ts @@ -4,14 +4,19 @@ import { sanitizeLikeString, slugifyLikeString } from '../database/utils'; import database from '../database/index'; import { store as reduxStore } from '../store/auxStore'; import { spotlight } from '../services/restApi'; -import { ISearch, ISearchLocal, IUserMessage, SubscriptionType } from '../../definitions'; -import { isGroupChat } from './helpers'; +import { ISearch, ISearchLocal, IUserMessage, SubscriptionType, TSubscriptionModel } from '../../definitions'; +import { isGroupChat, isReadOnly } from './helpers'; export type TSearch = ISearchLocal | IUserMessage | ISearch; let debounce: null | ((reason: string) => void) = null; -export const localSearchSubscription = async ({ text = '', filterUsers = true, filterRooms = true }): Promise => { +export const localSearchSubscription = async ({ + text = '', + filterUsers = true, + filterRooms = true, + filterMessagingAllowed = false +}): Promise => { const searchText = text.trim(); const db = database.active; const likeString = sanitizeLikeString(searchText); @@ -39,6 +44,17 @@ export const localSearchSubscription = async ({ text = '', filterUsers = true, f subscriptions = subscriptions.filter(item => item.t !== 'd' || isGroupChat(item)); } + if (filterMessagingAllowed) { + const username = reduxStore.getState().login.user.username as string; + const filteredSubscriptions = await Promise.all( + subscriptions.map(async item => { + const isItemReadOnly = await isReadOnly(item, username); + return isItemReadOnly ? null : item; + }) + ); + subscriptions = filteredSubscriptions.filter(item => item !== null) as TSubscriptionModel[]; + } + const search = subscriptions.slice(0, 7).map(item => ({ _id: item._id, rid: item.rid, diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index 5d13503c4..63253a60b 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -969,5 +969,7 @@ export const deleteOwnAccount = (password: string, confirmRelinquish = false): a // RC 0.67.0 sdk.post('users.deleteOwnAccount', { password, confirmRelinquish }); +export const postMessage = (roomId: string, text: string) => sdk.post('chat.postMessage', { roomId, text }); + export const notifyUser = (type: string, params: Record): Promise => sdk.methodCall('stream-notify-user', type, params); diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index b4c1d677e..0cea389e4 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -63,6 +63,7 @@ import JitsiMeetView from '../views/JitsiMeetView'; import StatusView from '../views/StatusView'; import ShareView from '../views/ShareView'; import CreateDiscussionView from '../views/CreateDiscussionView'; +import ForwardMessageView from '../views/ForwardMessageView'; import QueueListView from '../ee/omnichannel/views/QueueListView'; import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; @@ -257,6 +258,7 @@ const NewMessageStackNavigator = () => { {/* @ts-ignore */} + ); }; diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index 285e145e2..f6303f720 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -27,6 +27,7 @@ import AutoTranslateView from '../../views/AutoTranslateView'; import DirectoryView from '../../views/DirectoryView'; import NotificationPrefView from '../../views/NotificationPreferencesView'; import ForwardLivechatView from '../../views/ForwardLivechatView'; +import ForwardMessageView from '../../views/ForwardMessageView'; import CloseLivechatView from '../../views/CloseLivechatView'; import CannedResponsesListView from '../../views/CannedResponsesListView'; import CannedResponseDetail from '../../views/CannedResponseDetail'; @@ -141,6 +142,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => { /> + {/* @ts-ignore */} {/* @ts-ignore */} diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index a29a7a61e..d60d9bfa2 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -4,7 +4,7 @@ import { TServerModel, TThreadModel } from '../../definitions'; import { IAttachment } from '../../definitions/IAttachment'; import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; import { ILivechatTag } from '../../definitions/ILivechatTag'; -import { IMessage } from '../../definitions/IMessage'; +import { IMessage, TAnyMessageModel } from '../../definitions/IMessage'; import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription'; import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext'; @@ -118,6 +118,12 @@ export type ModalStackParamList = { rid: string; room: ISubscription; }; + ForwardMessageView: { + message: TAnyMessageModel; + }; + ForwardLivechatView: { + rid: string; + }; CloseLivechatView: { rid: string; departmentId?: string; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index 0f73fabed..8498841e2 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -245,6 +245,9 @@ export type NewMessageStackParamList = { message: IMessage; showCloseModal: boolean; }; + ForwardMessageView: { + message: TAnyMessageModel; + }; }; export type E2ESaveYourPasswordStackParamList = { diff --git a/app/views/ForwardMessageView/SelectPersonOrChannel.tsx b/app/views/ForwardMessageView/SelectPersonOrChannel.tsx new file mode 100644 index 000000000..eb1e97042 --- /dev/null +++ b/app/views/ForwardMessageView/SelectPersonOrChannel.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import { Text, View } from 'react-native'; +import { BlockContext } from '@rocket.chat/ui-kit'; + +import { getAvatarURL } from '../../lib/methods/helpers/getAvatarUrl'; +import I18n from '../../i18n'; +import { MultiSelect } from '../../containers/UIKit/MultiSelect'; +import styles from './styles'; +import { IForwardMessageViewSelectRoom } from './interfaces'; +import { ISearchLocal } from '../../definitions'; +import { localSearchSubscription } from '../../lib/methods'; +import { getRoomAvatar, getRoomTitle } from '../../lib/methods/helpers'; +import { useTheme } from '../../theme'; + +const SelectPersonOrChannel = ({ + server, + token, + userId, + onRoomSelect, + blockUnauthenticatedAccess, + serverVersion +}: IForwardMessageViewSelectRoom): React.ReactElement => { + const [rooms, setRooms] = useState([]); + const { colors } = useTheme(); + + const getRooms = async (keyword = '') => { + try { + const res = await localSearchSubscription({ text: keyword, filterMessagingAllowed: true }); + setRooms(res); + return res.map(item => ({ + value: item.rid, + text: { text: getRoomTitle(item) }, + imageUrl: getAvatar(item) + })); + } catch { + // do nothing + } + }; + + useEffect(() => { + getRooms(''); + }, []); + + const getAvatar = (item: ISearchLocal) => + getAvatarURL({ + text: getRoomAvatar(item), + type: item.t, + userId, + token, + server, + avatarETag: item.avatarETag, + rid: item.rid, + blockUnauthenticatedAccess, + serverVersion + }); + + return ( + + {I18n.t('Person_or_channel')} + ({ + value: room.rid, + text: { text: getRoomTitle(room) }, + imageUrl: getAvatar(room) + }))} + placeholder={{ text: `${I18n.t('Select')}` }} + context={BlockContext.FORM} + multiselect + /> + + ); +}; + +export default SelectPersonOrChannel; diff --git a/app/views/ForwardMessageView/index.tsx b/app/views/ForwardMessageView/index.tsx new file mode 100644 index 000000000..c76bac12e --- /dev/null +++ b/app/views/ForwardMessageView/index.tsx @@ -0,0 +1,104 @@ +import React, { useLayoutEffect, useState } from 'react'; +import { Alert, ScrollView, View } from 'react-native'; +import { StackNavigationOptions } from '@react-navigation/stack'; +import { RouteProp, StackActions, useNavigation, useRoute } from '@react-navigation/native'; + +import { getPermalinkMessage } from '../../lib/methods'; +import KeyboardView from '../../containers/KeyboardView'; +import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; +import I18n from '../../i18n'; +import * as HeaderButton from '../../containers/HeaderButton'; +import StatusBar from '../../containers/StatusBar'; +import { useTheme } from '../../theme'; +import { getUserSelector } from '../../selectors/login'; +import SafeAreaView from '../../containers/SafeAreaView'; +import styles from './styles'; +import SelectPersonOrChannel from './SelectPersonOrChannel'; +import { useAppSelector } from '../../lib/hooks'; +import { NewMessageStackParamList } from '../../stacks/types'; +import { postMessage } from '../../lib/services/restApi'; +import MessagePreview from '../../containers/message/Preview'; +import EventEmitter from '../../lib/methods/helpers/events'; +import { LISTENER } from '../../containers/Toast'; + +const ForwardMessageView = () => { + const [rooms, setRooms] = useState([]); + const [sending, setSending] = useState(false); + const navigation = useNavigation(); + const { colors } = useTheme(); + + const { + params: { message } + } = useRoute>(); + + const { blockUnauthenticatedAccess, server, serverVersion, user } = useAppSelector(state => ({ + user: getUserSelector(state), + server: state.server.server, + blockUnauthenticatedAccess: !!state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? true, + serverVersion: state.server.version as string + })); + + useLayoutEffect(() => { + const isSendButtonEnabled = rooms.length && !sending; + navigation.setOptions({ + title: I18n.t('Forward_message'), + headerRight: () => ( + + + + ), + headerLeft: () => + } as StackNavigationOptions); + }, [rooms.length, navigation, sending]); + + const handlePostMessage = async () => { + setSending(true); + const permalink = await getPermalinkMessage(message); + const msg = `[ ](${permalink})\n`; + try { + await Promise.all(rooms.map(roomId => postMessage(roomId, msg))); + EventEmitter.emit(LISTENER, { message: I18n.t('Message_has_been_shared') }); + navigation.dispatch(StackActions.pop()); + } catch (e: any) { + Alert.alert(I18n.t('Oops'), e.message); + } + setSending(false); + }; + + const selectRooms = ({ value }: { value: string[] }) => { + setRooms(value); + }; + + return ( + + + + + + + + + + + + ); +}; + +export default ForwardMessageView; diff --git a/app/views/ForwardMessageView/interfaces.ts b/app/views/ForwardMessageView/interfaces.ts new file mode 100644 index 000000000..ce589a529 --- /dev/null +++ b/app/views/ForwardMessageView/interfaces.ts @@ -0,0 +1,14 @@ +export interface IForwardMessageViewSelectRoom { + server: string; + token: string; + userId: string; + onRoomSelect: ({ value }: { value: string[] }) => void; + blockUnauthenticatedAccess: boolean; + serverVersion: string; +} + +export interface IForwardMessageViewSearchResult { + value: string; + text: { text: string }; + imageUrl: string; +} diff --git a/app/views/ForwardMessageView/styles.ts b/app/views/ForwardMessageView/styles.ts new file mode 100644 index 000000000..07a883bd0 --- /dev/null +++ b/app/views/ForwardMessageView/styles.ts @@ -0,0 +1,22 @@ +import { StyleSheet } from 'react-native'; + +import sharedStyles from '../Styles'; + +export default StyleSheet.create({ + container: { + flex: 1 + }, + inputContainer: { + marginTop: 16, + paddingHorizontal: 16, + marginBottom: 8 + }, + label: { + marginBottom: 4, + fontSize: 14, + ...sharedStyles.textMedium + }, + messageContainer: { + paddingVertical: 8 + } +}); diff --git a/e2e/tests/room/11-sharemessage.spec.ts b/e2e/tests/room/11-sharemessage.spec.ts new file mode 100644 index 000000000..64089d1dc --- /dev/null +++ b/e2e/tests/room/11-sharemessage.spec.ts @@ -0,0 +1,93 @@ +import { device, waitFor, element, by, expect } from 'detox'; + +import { + navigateToLogin, + login, + sleep, + platformTypes, + TTextMatcher, + tapBack, + navigateToRoom, + mockMessage +} from '../../helpers/app'; +import { createRandomRoom, createRandomUser, ITestUser } from '../../helpers/data_setup'; + +describe('Forward a message with another user', () => { + let user: ITestUser; + let otherUser: ITestUser; + let room: string; + let textMatcher: TTextMatcher; + let messageToUser: string; + let messageToRoom: string; + + beforeAll(async () => { + user = await createRandomUser(); + otherUser = await createRandomUser(); + ({ name: room } = await createRandomRoom(user)); + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); + ({ textMatcher } = platformTypes[device.getPlatform()]); + await navigateToLogin(); + await login(user.username, user.password); + }); + + describe('Usage', () => { + describe('Start a DM with other user', () => { + it('should create a DM', async () => { + await navigateToRoom(otherUser.username); + }); + it('should send a message and back to Rooms List View', async () => { + messageToUser = await mockMessage('Hello user'); + await tapBack(); + }); + }); + describe('Forward a message from room to the otherUser', () => { + it('should navigate to room and send a message', async () => { + await navigateToRoom(room); + messageToRoom = await mockMessage('Hello room'); + await sleep(300); + }); + it('should open the action sheet and tap Forward', async () => { + await waitFor(element(by[textMatcher](messageToRoom)).atIndex(0)) + .toBeVisible() + .withTimeout(2000); + await element(by[textMatcher](messageToRoom)).atIndex(0).longPress(); + await waitFor(element(by.id('action-sheet'))) + .toExist() + .withTimeout(2000); + await expect(element(by.id('action-sheet-handle'))).toBeVisible(); + await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); + await element(by[textMatcher]('Forward')).atIndex(0).tap(); + await sleep(300); + }); + it('should forward the message', async () => { + await waitFor(element(by.id('forward-message-view'))) + .toBeVisible() + .withTimeout(2000); + await element(by[textMatcher]('Select')).tap(); + await sleep(300); + await element(by.id('multi-select-search')).replaceText(`${otherUser.username}`); + await waitFor(element(by.id(`multi-select-item-${otherUser.username.toLowerCase()}`))) + .toExist() + .withTimeout(10000); + await element(by.id(`multi-select-item-${otherUser.username.toLowerCase()}`)).tap(); + await element(by.id('multi-select-search')).tapReturnKey(); + await sleep(300); + await waitFor(element(by.id('forward-message-view-send'))) + .toBeVisible() + .withTimeout(10000); + await element(by.id('forward-message-view-send')).tap(); + await sleep(300); + }); + it('should go to otherUser DM and verify if exist both messages', async () => { + await tapBack(); + await navigateToRoom(otherUser.username); + await waitFor(element(by[textMatcher](messageToUser))) + .toExist() + .withTimeout(2000); + await waitFor(element(by[textMatcher](messageToRoom))) + .toExist() + .withTimeout(2000); + }); + }); + }); +}); diff --git a/ios/custom.ttf b/ios/custom.ttf index 9d9d1f012c2f54f783439fbdfb1047107c96a3d7..a7cb347abd787a26b41a3d0a1fda7df2e2c068d1 100644 GIT binary patch delta 2089 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 PlmMcQJ<6T^