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:
parent
77a81d577e
commit
278ed91f9a
Binary file not shown.
|
@ -1,5 +1,9 @@
|
|||
export const mappedIcons = {
|
||||
'arrow-forward': 59841,
|
||||
'status-disabled': 59837,
|
||||
'arrow-right': 59838,
|
||||
'text-format': 59839,
|
||||
'code-block': 59840,
|
||||
'lamp-bulb': 59836,
|
||||
'phone-in': 59835,
|
||||
'basketball': 59776,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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();
|
||||
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 ? (
|
||||
<CustomIcon name={iconName} size={24} color={color || colors.headerTintColor} {...props} />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -22,6 +22,19 @@ import { useTheme } from '../../theme';
|
|||
import RightIcons from './Components/RightIcons';
|
||||
|
||||
const MessageInner = React.memo((props: IMessageInner) => {
|
||||
if (props.isPreview) {
|
||||
return (
|
||||
<>
|
||||
<User {...props} />
|
||||
<>
|
||||
<Content {...props} />
|
||||
<Attachments {...props} />
|
||||
</>
|
||||
<Urls {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'discussion-created') {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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;
|
|
@ -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<string | undefined>;
|
||||
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<void>;
|
||||
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<IMessageContainerProps, IMessageC
|
|||
isReadReceiptEnabled,
|
||||
autoTranslateRoom,
|
||||
autoTranslateLanguage,
|
||||
navToRoomInfo,
|
||||
navToRoomInfo = () => {},
|
||||
getCustomEmoji,
|
||||
isThreadRoom,
|
||||
handleEnterCall,
|
||||
|
@ -345,7 +346,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
threadBadgeColor,
|
||||
toggleFollowThread,
|
||||
jumpToMessage,
|
||||
highlighted
|
||||
highlighted,
|
||||
isPreview
|
||||
} = this.props;
|
||||
const {
|
||||
id,
|
||||
|
@ -449,7 +451,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
isThreadRoom={isThreadRoom}
|
||||
isThreadRoom={!!isThreadRoom}
|
||||
isInfo={this.isInfo}
|
||||
isTemp={this.isTemp}
|
||||
isEncrypted={this.isEncrypted}
|
||||
|
@ -462,6 +464,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
|||
highlighted={highlighted}
|
||||
comment={comment}
|
||||
isTranslated={isTranslated}
|
||||
isPreview={isPreview}
|
||||
/>
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
|
|
|
@ -108,6 +108,7 @@ export interface IMessageInner
|
|||
type: MessageType;
|
||||
blocks: [];
|
||||
urls?: IUrl[];
|
||||
isPreview?: boolean;
|
||||
}
|
||||
|
||||
export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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<ISearchLocal[]> => {
|
||||
export const localSearchSubscription = async ({
|
||||
text = '',
|
||||
filterUsers = true,
|
||||
filterRooms = true,
|
||||
filterMessagingAllowed = false
|
||||
}): Promise<ISearchLocal[]> => {
|
||||
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,
|
||||
|
|
|
@ -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<string, any>): Promise<boolean> =>
|
||||
sdk.methodCall('stream-notify-user', type, params);
|
||||
|
|
|
@ -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 = () => {
|
|||
<NewMessageStack.Screen name='CreateChannelView' component={CreateChannelView} />
|
||||
{/* @ts-ignore */}
|
||||
<NewMessageStack.Screen name='CreateDiscussionView' component={CreateDiscussionView} />
|
||||
<NewMessageStack.Screen name='ForwardMessageView' component={ForwardMessageView} />
|
||||
</NewMessageStack.Navigator>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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) => {
|
|||
/>
|
||||
<ModalStack.Screen name='QueueListView' component={QueueListView} />
|
||||
<ModalStack.Screen name='NotificationPrefView' component={NotificationPrefView} />
|
||||
<ModalStack.Screen name='ForwardMessageView' component={ForwardMessageView} />
|
||||
{/* @ts-ignore */}
|
||||
<ModalStack.Screen name='ForwardLivechatView' component={ForwardLivechatView} />
|
||||
{/* @ts-ignore */}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -245,6 +245,9 @@ export type NewMessageStackParamList = {
|
|||
message: IMessage;
|
||||
showCloseModal: boolean;
|
||||
};
|
||||
ForwardMessageView: {
|
||||
message: TAnyMessageModel;
|
||||
};
|
||||
};
|
||||
|
||||
export type E2ESaveYourPasswordStackParamList = {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
BIN
ios/custom.ttf
BIN
ios/custom.ttf
Binary file not shown.
Loading…
Reference in New Issue