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
This commit is contained in:
Reinaldo Neto 2023-08-04 11:09:36 -03:00 committed by GitHub
parent 77a81d577e
commit 278ed91f9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 448 additions and 18 deletions

View File

@ -1,5 +1,9 @@
export const mappedIcons = { export const mappedIcons = {
'arrow-forward': 59841,
'status-disabled': 59837, 'status-disabled': 59837,
'arrow-right': 59838,
'text-format': 59839,
'code-block': 59840,
'lamp-bulb': 59836, 'lamp-bulb': 59836,
'phone-in': 59835, 'phone-in': 59835,
'basketball': 59776, 'basketball': 59776,

File diff suppressed because one or more lines are too long

View File

@ -39,10 +39,10 @@ const styles = StyleSheet.create({
} }
}); });
const Item = ({ title, iconName, onPress, testID, badge, color, ...props }: IHeaderButtonItem): React.ReactElement => { const Item = ({ title, iconName, onPress, testID, badge, color, disabled, ...props }: IHeaderButtonItem): React.ReactElement => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<PlatformPressable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}> <PlatformPressable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} disabled={disabled} style={styles.container}>
<> <>
{iconName ? ( {iconName ? (
<CustomIcon name={iconName} size={24} color={color || colors.headerTintColor} {...props} /> <CustomIcon name={iconName} size={24} color={color || colors.headerTintColor} {...props} />

View File

@ -17,7 +17,7 @@ import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events'; import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods'; 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'; import { Services } from '../../lib/services';
export interface IMessageActionsProps { export interface IMessageActionsProps {
@ -30,6 +30,7 @@ export interface IMessageActionsProps {
replyInit: (message: TAnyMessageModel, mention: boolean) => void; replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean; isMasterDetail: boolean;
isReadOnly: boolean; isReadOnly: boolean;
serverVersion?: string | null;
Message_AllowDeleting?: boolean; Message_AllowDeleting?: boolean;
Message_AllowDeleting_BlockDeleteInMinutes?: number; Message_AllowDeleting_BlockDeleteInMinutes?: number;
Message_AllowEditing?: boolean; Message_AllowEditing?: boolean;
@ -74,7 +75,8 @@ const MessageActions = React.memo(
forceDeleteMessagePermission, forceDeleteMessagePermission,
deleteOwnMessagePermission, deleteOwnMessagePermission,
pinMessagePermission, pinMessagePermission,
createDirectMessagePermission createDirectMessagePermission,
serverVersion
}, },
ref 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) => { const handleUnread = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD); logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message; const { id: messageId, ts } = message;
@ -389,6 +400,14 @@ const MessageActions = React.memo(
onPress: () => handleCreateDiscussion(message) onPress: () => handleCreateDiscussion(message)
}); });
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '6.2.0') && !videoConfBlock) {
options.push({
title: I18n.t('Forward'),
icon: 'arrow-forward',
onPress: () => handleShareMessage(message)
});
}
// Permalink // Permalink
options.push({ options.push({
title: I18n.t('Get_link'), title: I18n.t('Get_link'),
@ -508,6 +527,7 @@ const MessageActions = React.memo(
); );
const mapStateToProps = (state: IApplicationState) => ({ const mapStateToProps = (state: IApplicationState) => ({
server: state.server.server, server: state.server.server,
serverVersion: state.server.version,
Message_AllowDeleting: state.settings.Message_AllowDeleting as boolean, Message_AllowDeleting: state.settings.Message_AllowDeleting as boolean,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes as number, Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes as number,
Message_AllowEditing: state.settings.Message_AllowEditing as boolean, Message_AllowEditing: state.settings.Message_AllowEditing as boolean,

View File

@ -22,6 +22,19 @@ import { useTheme } from '../../theme';
import RightIcons from './Components/RightIcons'; import RightIcons from './Components/RightIcons';
const MessageInner = React.memo((props: IMessageInner) => { const MessageInner = React.memo((props: IMessageInner) => {
if (props.isPreview) {
return (
<>
<User {...props} />
<>
<Content {...props} />
<Attachments {...props} />
</>
<Urls {...props} />
</>
);
}
if (props.type === 'discussion-created') { if (props.type === 'discussion-created') {
return ( return (
<> <>

View File

@ -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 (
<Message
item={message}
user={user}
rid={message.rid}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
timeFormat={Message_TimeFormat}
useRealName={useRealName}
isPreview
/>
);
};
export default MessagePreview;

View File

@ -28,7 +28,7 @@ interface IMessageContainerProps {
baseUrl: string; baseUrl: string;
Message_GroupingPeriod?: number; Message_GroupingPeriod?: number;
isReadReceiptEnabled?: boolean; isReadReceiptEnabled?: boolean;
isThreadRoom: boolean; isThreadRoom?: boolean;
isSystemMessage?: boolean; isSystemMessage?: boolean;
useRealName?: boolean; useRealName?: boolean;
autoTranslateRoom?: boolean; autoTranslateRoom?: boolean;
@ -46,9 +46,9 @@ interface IMessageContainerProps {
replyBroadcast?: (item: TAnyMessageModel) => void; replyBroadcast?: (item: TAnyMessageModel) => void;
reactionInit?: (item: TAnyMessageModel) => void; reactionInit?: (item: TAnyMessageModel) => void;
fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
showAttachment: (file: IAttachment) => void; showAttachment?: (file: IAttachment) => void;
onReactionLongPress?: (item: TAnyMessageModel) => void; onReactionLongPress?: (item: TAnyMessageModel) => void;
navToRoomInfo: (navParam: IRoomInfoParam) => void; navToRoomInfo?: (navParam: IRoomInfoParam) => void;
handleEnterCall?: () => void; handleEnterCall?: () => void;
blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void;
onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void; onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void;
@ -56,8 +56,9 @@ interface IMessageContainerProps {
toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: (link: string) => void; jumpToMessage?: (link: string) => void;
onPress?: () => void; onPress?: () => void;
theme: TSupportedThemes; theme?: TSupportedThemes;
closeEmojiAndAction?: (action?: Function, params?: any) => void; closeEmojiAndAction?: (action?: Function, params?: any) => void;
isPreview?: boolean;
} }
interface IMessageContainerState { interface IMessageContainerState {
@ -336,7 +337,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
isReadReceiptEnabled, isReadReceiptEnabled,
autoTranslateRoom, autoTranslateRoom,
autoTranslateLanguage, autoTranslateLanguage,
navToRoomInfo, navToRoomInfo = () => {},
getCustomEmoji, getCustomEmoji,
isThreadRoom, isThreadRoom,
handleEnterCall, handleEnterCall,
@ -345,7 +346,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
threadBadgeColor, threadBadgeColor,
toggleFollowThread, toggleFollowThread,
jumpToMessage, jumpToMessage,
highlighted highlighted,
isPreview
} = this.props; } = this.props;
const { const {
id, id,
@ -449,7 +451,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
isHeader={this.isHeader} isHeader={this.isHeader}
isThreadReply={this.isThreadReply} isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential} isThreadSequential={this.isThreadSequential}
isThreadRoom={isThreadRoom} isThreadRoom={!!isThreadRoom}
isInfo={this.isInfo} isInfo={this.isInfo}
isTemp={this.isTemp} isTemp={this.isTemp}
isEncrypted={this.isEncrypted} isEncrypted={this.isEncrypted}
@ -462,6 +464,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
highlighted={highlighted} highlighted={highlighted}
comment={comment} comment={comment}
isTranslated={isTranslated} isTranslated={isTranslated}
isPreview={isPreview}
/> />
</MessageContext.Provider> </MessageContext.Provider>
); );

View File

@ -108,6 +108,7 @@ export interface IMessageInner
type: MessageType; type: MessageType;
blocks: []; blocks: [];
urls?: IUrl[]; urls?: IUrl[];
isPreview?: boolean;
} }
export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar { export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar {

View File

@ -82,4 +82,10 @@ export type ChatEndpoints = {
'chat.getMessageReadReceipts': { 'chat.getMessageReadReceipts': {
GET: (params: { messageId: string }) => { receipts: IReadReceipts[] }; GET: (params: { messageId: string }) => { receipts: IReadReceipts[] };
}; };
'chat.postMessage': {
POST: (params: { roomId: string; text: string }) => {
message: IMessage;
success: boolean;
};
};
}; };

View File

@ -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.", "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", "Learn_more": "Learn more",
"and_N_more": "and {{count}} more", "and_N_more": "and {{count}} more",
"Forward_message": "Forward message",
"Person_or_channel": "Person or channel",
"Select": "Select",
"Nickname": "Nickname", "Nickname": "Nickname",
"Bio":"Bio", "Bio":"Bio",
"decline": "Decline", "decline": "Decline",
"accept": "Accept", "accept": "Accept",
"Incoming_call_from": "Incoming call from", "Incoming_call_from": "Incoming call from",
"Call_started": "Call started" "Call_started": "Call started",
"Message_has_been_shared":"Message has been shared"
} }

View File

@ -711,6 +711,9 @@
"Discard_changes_description": "Todas as alterações serão perdidas, se você sair sem salvar.", "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_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.", "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", "Nickname": "Apelido",
"Bio": "Biografia", "Bio": "Biografia",
"decline": "Recusar", "decline": "Recusar",
@ -718,5 +721,6 @@
"Incoming_call_from": "Chamada recebida de", "Incoming_call_from": "Chamada recebida de",
"Call_started": "Chamada Iniciada", "Call_started": "Chamada Iniciada",
"Learn_more": "Saiba mais", "Learn_more": "Saiba mais",
"and_N_more": "e mais {{count}}" "and_N_more": "e mais {{count}}",
"Message_has_been_shared":"Menssagem foi compartilhada"
} }

View File

@ -4,14 +4,19 @@ import { sanitizeLikeString, slugifyLikeString } from '../database/utils';
import database from '../database/index'; import database from '../database/index';
import { store as reduxStore } from '../store/auxStore'; import { store as reduxStore } from '../store/auxStore';
import { spotlight } from '../services/restApi'; import { spotlight } from '../services/restApi';
import { ISearch, ISearchLocal, IUserMessage, SubscriptionType } from '../../definitions'; import { ISearch, ISearchLocal, IUserMessage, SubscriptionType, TSubscriptionModel } from '../../definitions';
import { isGroupChat } from './helpers'; import { isGroupChat, isReadOnly } from './helpers';
export type TSearch = ISearchLocal | IUserMessage | ISearch; export type TSearch = ISearchLocal | IUserMessage | ISearch;
let debounce: null | ((reason: string) => void) = null; let debounce: null | ((reason: string) => void) = null;
export const localSearchSubscription = async ({ text = '', filterUsers = true, filterRooms = true }): Promise<ISearchLocal[]> => { export const localSearchSubscription = async ({
text = '',
filterUsers = true,
filterRooms = true,
filterMessagingAllowed = false
}): Promise<ISearchLocal[]> => {
const searchText = text.trim(); const searchText = text.trim();
const db = database.active; const db = database.active;
const likeString = sanitizeLikeString(searchText); 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)); 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 => ({ const search = subscriptions.slice(0, 7).map(item => ({
_id: item._id, _id: item._id,
rid: item.rid, rid: item.rid,

View File

@ -969,5 +969,7 @@ export const deleteOwnAccount = (password: string, confirmRelinquish = false): a
// RC 0.67.0 // RC 0.67.0
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish }); 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<string, any>): Promise<boolean> => export const notifyUser = (type: string, params: Record<string, any>): Promise<boolean> =>
sdk.methodCall('stream-notify-user', type, params); sdk.methodCall('stream-notify-user', type, params);

View File

@ -63,6 +63,7 @@ import JitsiMeetView from '../views/JitsiMeetView';
import StatusView from '../views/StatusView'; import StatusView from '../views/StatusView';
import ShareView from '../views/ShareView'; import ShareView from '../views/ShareView';
import CreateDiscussionView from '../views/CreateDiscussionView'; import CreateDiscussionView from '../views/CreateDiscussionView';
import ForwardMessageView from '../views/ForwardMessageView';
import QueueListView from '../ee/omnichannel/views/QueueListView'; import QueueListView from '../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../views/AddChannelTeamView'; import AddChannelTeamView from '../views/AddChannelTeamView';
import AddExistingChannelView from '../views/AddExistingChannelView'; import AddExistingChannelView from '../views/AddExistingChannelView';
@ -257,6 +258,7 @@ const NewMessageStackNavigator = () => {
<NewMessageStack.Screen name='CreateChannelView' component={CreateChannelView} /> <NewMessageStack.Screen name='CreateChannelView' component={CreateChannelView} />
{/* @ts-ignore */} {/* @ts-ignore */}
<NewMessageStack.Screen name='CreateDiscussionView' component={CreateDiscussionView} /> <NewMessageStack.Screen name='CreateDiscussionView' component={CreateDiscussionView} />
<NewMessageStack.Screen name='ForwardMessageView' component={ForwardMessageView} />
</NewMessageStack.Navigator> </NewMessageStack.Navigator>
); );
}; };

View File

@ -27,6 +27,7 @@ import AutoTranslateView from '../../views/AutoTranslateView';
import DirectoryView from '../../views/DirectoryView'; import DirectoryView from '../../views/DirectoryView';
import NotificationPrefView from '../../views/NotificationPreferencesView'; import NotificationPrefView from '../../views/NotificationPreferencesView';
import ForwardLivechatView from '../../views/ForwardLivechatView'; import ForwardLivechatView from '../../views/ForwardLivechatView';
import ForwardMessageView from '../../views/ForwardMessageView';
import CloseLivechatView from '../../views/CloseLivechatView'; import CloseLivechatView from '../../views/CloseLivechatView';
import CannedResponsesListView from '../../views/CannedResponsesListView'; import CannedResponsesListView from '../../views/CannedResponsesListView';
import CannedResponseDetail from '../../views/CannedResponseDetail'; import CannedResponseDetail from '../../views/CannedResponseDetail';
@ -141,6 +142,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
/> />
<ModalStack.Screen name='QueueListView' component={QueueListView} /> <ModalStack.Screen name='QueueListView' component={QueueListView} />
<ModalStack.Screen name='NotificationPrefView' component={NotificationPrefView} /> <ModalStack.Screen name='NotificationPrefView' component={NotificationPrefView} />
<ModalStack.Screen name='ForwardMessageView' component={ForwardMessageView} />
{/* @ts-ignore */} {/* @ts-ignore */}
<ModalStack.Screen name='ForwardLivechatView' component={ForwardLivechatView} /> <ModalStack.Screen name='ForwardLivechatView' component={ForwardLivechatView} />
{/* @ts-ignore */} {/* @ts-ignore */}

View File

@ -4,7 +4,7 @@ import { TServerModel, TThreadModel } from '../../definitions';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import { ILivechatTag } from '../../definitions/ILivechatTag'; import { ILivechatTag } from '../../definitions/ILivechatTag';
import { IMessage } from '../../definitions/IMessage'; import { IMessage, TAnyMessageModel } from '../../definitions/IMessage';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription'; import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription';
import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext'; import { TChangeAvatarViewContext } from '../../definitions/TChangeAvatarViewContext';
@ -118,6 +118,12 @@ export type ModalStackParamList = {
rid: string; rid: string;
room: ISubscription; room: ISubscription;
}; };
ForwardMessageView: {
message: TAnyMessageModel;
};
ForwardLivechatView: {
rid: string;
};
CloseLivechatView: { CloseLivechatView: {
rid: string; rid: string;
departmentId?: string; departmentId?: string;

View File

@ -245,6 +245,9 @@ export type NewMessageStackParamList = {
message: IMessage; message: IMessage;
showCloseModal: boolean; showCloseModal: boolean;
}; };
ForwardMessageView: {
message: TAnyMessageModel;
};
}; };
export type E2ESaveYourPasswordStackParamList = { export type E2ESaveYourPasswordStackParamList = {

View File

@ -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<ISearchLocal[]>([]);
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 (
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: colors.bodyText }]}>{I18n.t('Person_or_channel')}</Text>
<MultiSelect
onSearch={getRooms}
onChange={onRoomSelect}
options={rooms.map(room => ({
value: room.rid,
text: { text: getRoomTitle(room) },
imageUrl: getAvatar(room)
}))}
placeholder={{ text: `${I18n.t('Select')}` }}
context={BlockContext.FORM}
multiselect
/>
</View>
);
};
export default SelectPersonOrChannel;

View File

@ -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<string[]>([]);
const [sending, setSending] = useState(false);
const navigation = useNavigation();
const { colors } = useTheme();
const {
params: { message }
} = useRoute<RouteProp<NewMessageStackParamList, 'ForwardMessageView'>>();
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: () => (
<HeaderButton.Container>
<HeaderButton.Item
title={I18n.t('Send')}
color={isSendButtonEnabled ? colors.actionTintColor : colors.headerTintColor}
disabled={!isSendButtonEnabled}
onPress={handlePostMessage}
testID='forward-message-view-send'
/>
</HeaderButton.Container>
),
headerLeft: () => <HeaderButton.CloseModal />
} 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 (
<KeyboardView
style={{ backgroundColor: colors.auxiliaryBackground }}
contentContainerStyle={styles.container}
keyboardVerticalOffset={128}
>
<StatusBar />
<SafeAreaView testID='forward-message-view' style={styles.container}>
<ScrollView {...scrollPersistTaps}>
<SelectPersonOrChannel
server={server}
userId={user.id}
token={user.token}
onRoomSelect={selectRooms}
blockUnauthenticatedAccess={blockUnauthenticatedAccess}
serverVersion={serverVersion}
/>
<View pointerEvents='none' style={[styles.messageContainer, { backgroundColor: colors.backgroundColor }]}>
<MessagePreview message={message} />
</View>
</ScrollView>
</SafeAreaView>
</KeyboardView>
);
};
export default ForwardMessageView;

View File

@ -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;
}

View File

@ -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
}
});

View File

@ -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);
});
});
});
});

Binary file not shown.