Merge branch 'develop' into feat.new-audio-player
This commit is contained in:
commit
186ab338e4
Binary file not shown.
|
@ -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
|
@ -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} />
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||||
import { StatusBar as StatusBarRN } from 'react-native';
|
import { StatusBar as StatusBarRN } from 'react-native';
|
||||||
|
|
||||||
import { useTheme } from '../theme';
|
import { useTheme } from '../theme';
|
||||||
|
import { isAndroid } from '../lib/methods/helpers';
|
||||||
|
|
||||||
const supportedStyles = {
|
const supportedStyles = {
|
||||||
'light-content': 'light-content',
|
'light-content': 'light-content',
|
||||||
|
@ -23,7 +24,9 @@ const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
|
||||||
barStyle = 'dark-content';
|
barStyle = 'dark-content';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isAndroid) {
|
||||||
StatusBarRN.setBackgroundColor(backgroundColor ?? colors.headerBackground);
|
StatusBarRN.setBackgroundColor(backgroundColor ?? colors.headerBackground);
|
||||||
|
}
|
||||||
StatusBarRN.setBarStyle(barStyle, true);
|
StatusBarRN.setBarStyle(barStyle, true);
|
||||||
}, [theme, barStyle, backgroundColor]);
|
}, [theme, barStyle, backgroundColor]);
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ const Chip = ({ item, onSelect, style }: IChip) => {
|
||||||
onPress={() => onSelect(item)}
|
onPress={() => onSelect(item)}
|
||||||
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
|
style={[styles.chip, { backgroundColor: colors.auxiliaryBackground }, style]}
|
||||||
background={Touchable.Ripple(colors.bannerBackground)}
|
background={Touchable.Ripple(colors.bannerBackground)}
|
||||||
|
testID={`multi-select-chip-${item.value}`}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
||||||
|
|
|
@ -13,13 +13,13 @@ import { CustomIcon } from '../../CustomIcon';
|
||||||
|
|
||||||
interface IItem {
|
interface IItem {
|
||||||
item: IItemData;
|
item: IItemData;
|
||||||
selected?: string;
|
selected: boolean;
|
||||||
onSelect: Function;
|
onSelect: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IItems {
|
interface IItems {
|
||||||
items: IItemData[];
|
items: IItemData[];
|
||||||
selected: string[];
|
selected: IItemData[];
|
||||||
onSelect: Function;
|
onSelect: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ const Items = ({ items, selected, onSelect }: IItems) => (
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
ItemSeparatorComponent={List.Separator}
|
ItemSeparatorComponent={List.Separator}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={selected.find(s => s === item.value)} />}
|
renderItem={({ item }) => <Item item={item} onSelect={onSelect} selected={!!selected.find(s => s.value === item.value)} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -17,16 +17,16 @@ interface IMultiSelectContentProps {
|
||||||
options?: IItemData[];
|
options?: IItemData[];
|
||||||
multiselect: boolean;
|
multiselect: boolean;
|
||||||
select: React.Dispatch<any>;
|
select: React.Dispatch<any>;
|
||||||
onChange: Function;
|
onChange: ({ value }: { value: string[] }) => void;
|
||||||
setCurrentValue: React.Dispatch<React.SetStateAction<string>>;
|
setCurrentValue: React.Dispatch<React.SetStateAction<string>>;
|
||||||
onHide: Function;
|
onHide: Function;
|
||||||
selectedItems: string[];
|
selectedItems: IItemData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultiSelectContent = React.memo(
|
export const MultiSelectContent = React.memo(
|
||||||
({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => {
|
({ onSearch, options, multiselect, select, onChange, setCurrentValue, onHide, selectedItems }: IMultiSelectContentProps) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [selected, setSelected] = useState<string[]>(Array.isArray(selectedItems) ? selectedItems : []);
|
const [selected, setSelected] = useState<IItemData[]>(Array.isArray(selectedItems) ? selectedItems : []);
|
||||||
const [items, setItems] = useState<IItemData[] | undefined>(options);
|
const [items, setItems] = useState<IItemData[] | undefined>(options);
|
||||||
const { hideActionSheet } = useActionSheet();
|
const { hideActionSheet } = useActionSheet();
|
||||||
|
|
||||||
|
@ -37,14 +37,14 @@ export const MultiSelectContent = React.memo(
|
||||||
} = item;
|
} = item;
|
||||||
if (multiselect) {
|
if (multiselect) {
|
||||||
let newSelect = [];
|
let newSelect = [];
|
||||||
if (!selected.includes(value)) {
|
if (!selected.find(s => s.value === value)) {
|
||||||
newSelect = [...selected, value];
|
newSelect = [...selected, item];
|
||||||
} else {
|
} else {
|
||||||
newSelect = selected.filter((s: any) => s !== value);
|
newSelect = selected.filter((s: any) => s.value !== value);
|
||||||
}
|
}
|
||||||
setSelected(newSelect);
|
setSelected(newSelect);
|
||||||
select(newSelect);
|
select(newSelect);
|
||||||
onChange({ value: newSelect });
|
onChange({ value: newSelect.map(s => s.value) });
|
||||||
} else {
|
} else {
|
||||||
onChange({ value });
|
onChange({ value });
|
||||||
setCurrentValue(text);
|
setCurrentValue(text);
|
||||||
|
|
|
@ -17,13 +17,21 @@ export interface IItemData {
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMultiSelectWithMultiSelect extends IMultiSelect {
|
||||||
|
multiselect: true;
|
||||||
|
onChange: ({ value }: { value: string[] }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMultiSelectWithoutMultiSelect extends IMultiSelect {
|
||||||
|
multiselect?: false;
|
||||||
|
onChange: ({ value }: { value: any }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface IMultiSelect {
|
interface IMultiSelect {
|
||||||
options?: IItemData[];
|
options?: IItemData[];
|
||||||
onChange: Function;
|
|
||||||
placeholder?: IText;
|
placeholder?: IText;
|
||||||
context?: BlockContext;
|
context?: BlockContext;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
multiselect?: boolean;
|
|
||||||
onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>;
|
onSearch?: (keyword: string) => IItemData[] | Promise<IItemData[] | undefined>;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
inputStyle?: TextStyle;
|
inputStyle?: TextStyle;
|
||||||
|
@ -46,9 +54,9 @@ export const MultiSelect = React.memo(
|
||||||
disabled,
|
disabled,
|
||||||
inputStyle,
|
inputStyle,
|
||||||
innerInputStyle
|
innerInputStyle
|
||||||
}: IMultiSelect) => {
|
}: IMultiSelectWithMultiSelect | IMultiSelectWithoutMultiSelect) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [selected, select] = useState<string[]>(Array.isArray(values) ? values : []);
|
const [selected, select] = useState<IItemData[]>(Array.isArray(values) ? values : []);
|
||||||
const [currentValue, setCurrentValue] = useState('');
|
const [currentValue, setCurrentValue] = useState('');
|
||||||
|
|
||||||
const { showActionSheet, hideActionSheet } = useActionSheet();
|
const { showActionSheet, hideActionSheet } = useActionSheet();
|
||||||
|
@ -57,7 +65,7 @@ export const MultiSelect = React.memo(
|
||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
select(values);
|
select(values);
|
||||||
}
|
}
|
||||||
}, [values]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (values && values.length && !multiselect) {
|
if (values && values.length && !multiselect) {
|
||||||
|
@ -95,13 +103,13 @@ export const MultiSelect = React.memo(
|
||||||
} = item;
|
} = item;
|
||||||
if (multiselect) {
|
if (multiselect) {
|
||||||
let newSelect = [];
|
let newSelect = [];
|
||||||
if (!selected.includes(value)) {
|
if (!selected.find(s => s.value === value)) {
|
||||||
newSelect = [...selected, value];
|
newSelect = [...selected, item];
|
||||||
} else {
|
} else {
|
||||||
newSelect = selected.filter((s: any) => s !== value);
|
newSelect = selected.filter((s: any) => s.value !== value);
|
||||||
}
|
}
|
||||||
select(newSelect);
|
select(newSelect);
|
||||||
onChange({ value: newSelect });
|
onChange({ value: newSelect.map(s => s.value) });
|
||||||
} else {
|
} else {
|
||||||
onChange({ value });
|
onChange({ value });
|
||||||
setCurrentValue(text);
|
setCurrentValue(text);
|
||||||
|
@ -119,12 +127,10 @@ export const MultiSelect = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context === BlockContext.FORM) {
|
if (context === BlockContext.FORM) {
|
||||||
const items: any = options.filter((option: any) => selected.includes(option.value));
|
|
||||||
|
|
||||||
button = (
|
button = (
|
||||||
<Input onPress={onShow} loading={loading} disabled={disabled} inputStyle={inputStyle} innerInputStyle={innerInputStyle}>
|
<Input onPress={onShow} loading={loading} disabled={disabled} inputStyle={inputStyle} innerInputStyle={innerInputStyle}>
|
||||||
{items.length ? (
|
{selected.length ? (
|
||||||
<Chips items={items} onSelect={(item: any) => (disabled ? {} : onSelect(item))} />
|
<Chips items={selected} onSelect={(item: any) => (disabled ? {} : onSelect(item))} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder.text}</Text>
|
<Text style={[styles.pickerText, { color: colors.auxiliaryText }]}>{placeholder.text}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -220,36 +220,37 @@ export const SectionMultiSelect = () =>
|
||||||
},
|
},
|
||||||
accessory: {
|
accessory: {
|
||||||
type: 'multi_static_select',
|
type: 'multi_static_select',
|
||||||
|
appId: 'app-id',
|
||||||
|
blockId: 'block-id',
|
||||||
|
actionId: 'action-id',
|
||||||
|
initialValue: ['option_1', 'option_2'],
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
|
value: 'option_1',
|
||||||
text: {
|
text: {
|
||||||
type: 'plain_text',
|
type: 'plain_text',
|
||||||
text: 'button'
|
text: 'lorem ipsum 🚀',
|
||||||
},
|
emoji: true
|
||||||
value: 1
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: {
|
value: 'option_2',
|
||||||
type: 'plain_text',
|
text: {
|
||||||
text: 'opt 1'
|
type: 'plain_text',
|
||||||
},
|
text: 'lorem ipsum 🚀',
|
||||||
value: 2
|
emoji: true
|
||||||
},
|
}
|
||||||
{
|
}
|
||||||
text: {
|
],
|
||||||
type: 'plain_text',
|
placeholder: {
|
||||||
text: 'opt 2'
|
type: 'plain_text',
|
||||||
},
|
text: 'Select an item'
|
||||||
value: 3
|
},
|
||||||
},
|
label: {
|
||||||
{
|
type: 'plain_text',
|
||||||
text: {
|
text: 'Label',
|
||||||
type: 'plain_text',
|
emoji: true
|
||||||
text: 'opt 3'
|
|
||||||
},
|
|
||||||
value: 4
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -138,7 +138,8 @@ class MessageParser extends UiKitParserMessage<React.ReactElement> {
|
||||||
|
|
||||||
multiStaticSelect(element: IElement, context: BlockContext) {
|
multiStaticSelect(element: IElement, context: BlockContext) {
|
||||||
const [{ loading, value }, action] = useBlockContext(element, context);
|
const [{ loading, value }, action] = useBlockContext(element, context);
|
||||||
return <MultiSelect {...element} value={value} onChange={action} context={context} loading={loading} multiselect />;
|
const valueFiltered = element.options?.filter(option => value.includes(option.value));
|
||||||
|
return <MultiSelect {...element} value={valueFiltered} onChange={action} context={context} loading={loading} multiselect />;
|
||||||
}
|
}
|
||||||
|
|
||||||
staticSelect(element: IElement, context: BlockContext) {
|
staticSelect(element: IElement, context: BlockContext) {
|
||||||
|
|
|
@ -309,7 +309,10 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
||||||
};
|
};
|
||||||
|
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
const { cached } = this.state;
|
const { cached, loading } = this.state;
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.togglePlayPause();
|
this.togglePlayPause();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -156,7 +156,6 @@ const ImageContainer = React.memo(
|
||||||
|
|
||||||
if (imageCached.description) {
|
if (imageCached.description) {
|
||||||
return (
|
return (
|
||||||
<Button disabled={isReply} onPress={onPress}>
|
|
||||||
<View>
|
<View>
|
||||||
<Markdown
|
<Markdown
|
||||||
msg={imageCached.description}
|
msg={imageCached.description}
|
||||||
|
@ -165,9 +164,10 @@ const ImageContainer = React.memo(
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
|
<Button disabled={isReply} onPress={onPress}>
|
||||||
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
||||||
</View>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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;
|
|
@ -127,7 +127,7 @@ const Video = React.memo(
|
||||||
|
|
||||||
const handleAutoDownload = async () => {
|
const handleAutoDownload = async () => {
|
||||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
|
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
|
||||||
if (isAutoDownloadEnabled) {
|
if (isAutoDownloadEnabled && file.video_type && isTypeSupported(file.video_type)) {
|
||||||
await handleDownload();
|
await handleDownload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ const Video = React.memo(
|
||||||
showAttachment(videoCached);
|
showAttachment(videoCached);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!loading && !cached) {
|
if (!loading && !cached && file.video_type && isTypeSupported(file.video_type)) {
|
||||||
handleDownload();
|
handleDownload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -22,6 +22,8 @@ export interface ILoggedUser {
|
||||||
isFromWebView?: boolean;
|
isFromWebView?: boolean;
|
||||||
enableMessageParserEarlyAdoption: boolean;
|
enableMessageParserEarlyAdoption: boolean;
|
||||||
alsoSendThreadToChannel: 'default' | 'always' | 'never';
|
alsoSendThreadToChannel: 'default' | 'always' | 'never';
|
||||||
|
bio?: string;
|
||||||
|
nickname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILoggedUserResultFromServer
|
export interface ILoggedUserResultFromServer
|
||||||
|
|
|
@ -7,6 +7,8 @@ export interface IProfileParams {
|
||||||
email: string | null;
|
email: string | null;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
|
bio?: string;
|
||||||
|
nickname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAvatarButton {
|
export interface IAvatarButton {
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -733,8 +733,14 @@
|
||||||
"Wi_Fi": "Wi-Fi",
|
"Wi_Fi": "Wi-Fi",
|
||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"Audio": "Audio",
|
"Audio": "Audio",
|
||||||
|
"Forward_message": "Forward message",
|
||||||
|
"Person_or_channel": "Person or channel",
|
||||||
|
"Select": "Select",
|
||||||
|
"Nickname": "Nickname",
|
||||||
|
"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"
|
||||||
}
|
}
|
|
@ -723,5 +723,11 @@
|
||||||
"decline": "Recusar",
|
"decline": "Recusar",
|
||||||
"accept": "Aceitar",
|
"accept": "Aceitar",
|
||||||
"Incoming_call_from": "Chamada recebida de",
|
"Incoming_call_from": "Chamada recebida de",
|
||||||
"Call_started": "Chamada Iniciada"
|
"Call_started": "Chamada Iniciada",
|
||||||
|
"Forward_message": "Encaminhar mensagem",
|
||||||
|
"Person_or_channel": "Pessoa ou canal",
|
||||||
|
"Select": "Selecionar",
|
||||||
|
"Nickname": "Apelido",
|
||||||
|
"Bio": "Biografia",
|
||||||
|
"Message_has_been_shared":"Menssagem foi compartilhada"
|
||||||
}
|
}
|
|
@ -29,4 +29,8 @@ export default class User extends Model {
|
||||||
@field('is_from_webview') isFromWebView;
|
@field('is_from_webview') isFromWebView;
|
||||||
|
|
||||||
@field('enable_message_parser_early_adoption') enableMessageParserEarlyAdoption;
|
@field('enable_message_parser_early_adoption') enableMessageParserEarlyAdoption;
|
||||||
|
|
||||||
|
@field('nickname') nickname;
|
||||||
|
|
||||||
|
@field('bio') bio;
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,6 +103,18 @@ export default schemaMigrations({
|
||||||
columns: [{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }]
|
columns: [{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toVersion: 13,
|
||||||
|
steps: [
|
||||||
|
addColumns({
|
||||||
|
table: 'users',
|
||||||
|
columns: [
|
||||||
|
{ name: 'nickname', type: 'string', isOptional: true },
|
||||||
|
{ name: 'bio', type: 'string', isOptional: true }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
export default appSchema({
|
export default appSchema({
|
||||||
version: 12,
|
version: 13,
|
||||||
tables: [
|
tables: [
|
||||||
tableSchema({
|
tableSchema({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -17,7 +17,9 @@ export default appSchema({
|
||||||
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
||||||
{ name: 'avatar_etag', type: 'string', isOptional: true },
|
{ name: 'avatar_etag', type: 'string', isOptional: true },
|
||||||
{ name: 'is_from_webview', type: 'boolean', isOptional: true },
|
{ name: 'is_from_webview', type: 'boolean', isOptional: true },
|
||||||
{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }
|
{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true },
|
||||||
|
{ name: 'nickname', type: 'string', isOptional: true },
|
||||||
|
{ name: 'bio', type: 'string', isOptional: true }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
tableSchema({
|
tableSchema({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -309,6 +309,12 @@ export default function subscribeRooms() {
|
||||||
if (unset?.avatarETag) {
|
if (unset?.avatarETag) {
|
||||||
store.dispatch(setUser({ avatarETag: '' }));
|
store.dispatch(setUser({ avatarETag: '' }));
|
||||||
}
|
}
|
||||||
|
if (diff?.bio) {
|
||||||
|
store.dispatch(setUser({ bio: diff.bio }));
|
||||||
|
}
|
||||||
|
if (diff?.nickname) {
|
||||||
|
store.dispatch(setUser({ nickname: diff.nickname }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (/subscriptions/.test(ev)) {
|
if (/subscriptions/.test(ev)) {
|
||||||
if (type === 'removed') {
|
if (type === 'removed') {
|
||||||
|
|
|
@ -304,7 +304,9 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise<
|
||||||
isFromWebView,
|
isFromWebView,
|
||||||
showMessageInMainThread,
|
showMessageInMainThread,
|
||||||
enableMessageParserEarlyAdoption,
|
enableMessageParserEarlyAdoption,
|
||||||
alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel
|
alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel,
|
||||||
|
bio: result.me.bio,
|
||||||
|
nickname: result.me.nickname
|
||||||
};
|
};
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,9 @@ import {
|
||||||
SubscriptionType,
|
SubscriptionType,
|
||||||
IUser,
|
IUser,
|
||||||
IAvatarSuggestion,
|
IAvatarSuggestion,
|
||||||
IProfileParams
|
IProfileParams,
|
||||||
|
RoomType,
|
||||||
|
IServerRoom
|
||||||
} from '../../definitions';
|
} from '../../definitions';
|
||||||
import { ISpotlight } from '../../definitions/ISpotlight';
|
import { ISpotlight } from '../../definitions/ISpotlight';
|
||||||
import { TEAM_TYPE } from '../../definitions/ITeam';
|
import { TEAM_TYPE } from '../../definitions/ITeam';
|
||||||
|
@ -334,6 +336,9 @@ export const getRoomInfo = (roomId: string) =>
|
||||||
// RC 0.72.0
|
// RC 0.72.0
|
||||||
sdk.get('rooms.info', { roomId });
|
sdk.get('rooms.info', { roomId });
|
||||||
|
|
||||||
|
export const getRoomByTypeAndName = (roomType: RoomType, roomName: string): Promise<IServerRoom> =>
|
||||||
|
sdk.methodCallWrapper('getRoomByTypeAndName', roomType, roomName);
|
||||||
|
|
||||||
export const getVisitorInfo = (visitorId: string) =>
|
export const getVisitorInfo = (visitorId: string) =>
|
||||||
// RC 2.3.0
|
// RC 2.3.0
|
||||||
sdk.get('livechat/visitors.info', { visitorId });
|
sdk.get('livechat/visitors.info', { visitorId });
|
||||||
|
@ -964,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);
|
||||||
|
|
|
@ -171,7 +171,9 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
roles: user.roles,
|
roles: user.roles,
|
||||||
isFromWebView: user.isFromWebView,
|
isFromWebView: user.isFromWebView,
|
||||||
showMessageInMainThread: user.showMessageInMainThread,
|
showMessageInMainThread: user.showMessageInMainThread,
|
||||||
avatarETag: user.avatarETag
|
avatarETag: user.avatarETag,
|
||||||
|
bio: user.bio,
|
||||||
|
nickname: user.nickname
|
||||||
};
|
};
|
||||||
yield serversDB.action(async () => {
|
yield serversDB.action(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -94,7 +94,9 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
||||||
status: userRecord.status,
|
status: userRecord.status,
|
||||||
statusText: userRecord.statusText,
|
statusText: userRecord.statusText,
|
||||||
roles: userRecord.roles,
|
roles: userRecord.roles,
|
||||||
avatarETag: userRecord.avatarETag
|
avatarETag: userRecord.avatarETag,
|
||||||
|
bio: userRecord.bio,
|
||||||
|
nickname: userRecord.nickname
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// search credentials on shared credentials (Experimental/Official)
|
// search credentials on shared credentials (Experimental/Official)
|
||||||
|
|
|
@ -64,6 +64,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';
|
||||||
|
@ -259,6 +260,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -142,6 +143,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 */}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -246,6 +246,9 @@ export type NewMessageStackParamList = {
|
||||||
message: IMessage;
|
message: IMessage;
|
||||||
showCloseModal: boolean;
|
showCloseModal: boolean;
|
||||||
};
|
};
|
||||||
|
ForwardMessageView: {
|
||||||
|
message: TAnyMessageModel;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type E2ESaveYourPasswordStackParamList = {
|
export type E2ESaveYourPasswordStackParamList = {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export interface ICreateDiscussionViewSelectChannel {
|
||||||
token: string;
|
token: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
initial: object;
|
initial: object;
|
||||||
onChannelSelect: Function;
|
onChannelSelect: ({ value }: { value: any }) => void;
|
||||||
blockUnauthenticatedAccess: boolean;
|
blockUnauthenticatedAccess: boolean;
|
||||||
serverVersion: string;
|
serverVersion: string;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export interface ICreateDiscussionViewSelectUsers {
|
||||||
token: string;
|
token: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
selected: any[];
|
selected: any[];
|
||||||
onUserSelect: Function;
|
onUserSelect: ({ value }: { value: string[] }) => void;
|
||||||
blockUnauthenticatedAccess: boolean;
|
blockUnauthenticatedAccess: boolean;
|
||||||
serverVersion: string;
|
serverVersion: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { IApplicationState, IServerRoom, IUser, SubscriptionType } from '../../d
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import Options from './Options';
|
import Options from './Options';
|
||||||
import { Services } from '../../lib/services';
|
import { Services } from '../../lib/services';
|
||||||
|
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
|
||||||
|
|
||||||
interface IDirectoryViewProps {
|
interface IDirectoryViewProps {
|
||||||
navigation: CompositeNavigationProp<
|
navigation: CompositeNavigationProp<
|
||||||
|
@ -163,13 +164,20 @@ class DirectoryView extends React.Component<IDirectoryViewProps, IDirectoryViewS
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.goRoom({ rid: result.room._id, name: item.username, t: SubscriptionType.DIRECT });
|
this.goRoom({ rid: result.room._id, name: item.username, t: SubscriptionType.DIRECT });
|
||||||
}
|
}
|
||||||
} else if (['p', 'c'].includes(item.t) && !item.teamMain) {
|
return;
|
||||||
const result = await Services.getRoomInfo(item._id);
|
}
|
||||||
if (result.success) {
|
const subscription = await getSubscriptionByRoomId(item._id);
|
||||||
|
if (subscription) {
|
||||||
|
this.goRoom(subscription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (['p', 'c'].includes(item.t) && !item.teamMain) {
|
||||||
|
const result = await Services.getRoomByTypeAndName(item.t, item.name || item.fname);
|
||||||
|
if (result) {
|
||||||
this.goRoom({
|
this.goRoom({
|
||||||
rid: item._id,
|
rid: item._id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
joinCodeRequired: result.room.joinCodeRequired,
|
joinCodeRequired: result.joinCodeRequired,
|
||||||
t: item.t as SubscriptionType,
|
t: item.t as SubscriptionType,
|
||||||
search: true
|
search: true
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
});
|
|
@ -90,6 +90,11 @@ const LivechatEditView = ({ user, navigation, route, theme }: ILivechatEditViewP
|
||||||
const [tagParam, setTags] = useState(livechat?.tags || []);
|
const [tagParam, setTags] = useState(livechat?.tags || []);
|
||||||
const [tagParamSelected, setTagParamSelected] = useState(livechat?.tags || []);
|
const [tagParamSelected, setTagParamSelected] = useState(livechat?.tags || []);
|
||||||
|
|
||||||
|
const tagOptions = tagParam.map((tag: string) => ({ text: { text: tag }, value: tag }));
|
||||||
|
const tagValues = Array.isArray(tagParamSelected)
|
||||||
|
? tagOptions.filter((option: any) => tagParamSelected.includes(option.value))
|
||||||
|
: [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const arr = [...tagParam, ...availableUserTags];
|
const arr = [...tagParam, ...availableUserTags];
|
||||||
const uniqueArray = arr.filter((val, i) => arr.indexOf(val) === i);
|
const uniqueArray = arr.filter((val, i) => arr.indexOf(val) === i);
|
||||||
|
@ -254,12 +259,12 @@ const LivechatEditView = ({ user, navigation, route, theme }: ILivechatEditViewP
|
||||||
|
|
||||||
<Text style={[styles.label, { color: themes[theme!].titleText }]}>{I18n.t('Tags')}</Text>
|
<Text style={[styles.label, { color: themes[theme!].titleText }]}>{I18n.t('Tags')}</Text>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={tagParam.map((tag: string) => ({ text: { text: tag }, value: tag }))}
|
options={tagOptions}
|
||||||
onChange={({ value }: { value: string[] }) => {
|
onChange={({ value }: { value: string[] }) => {
|
||||||
setTagParamSelected([...value]);
|
setTagParamSelected([...value]);
|
||||||
}}
|
}}
|
||||||
placeholder={{ text: I18n.t('Tags') }}
|
placeholder={{ text: I18n.t('Tags') }}
|
||||||
value={tagParamSelected}
|
value={tagValues}
|
||||||
context={BlockContext.FORM}
|
context={BlockContext.FORM}
|
||||||
multiselect
|
multiselect
|
||||||
disabled={!editLivechatRoomCustomFieldsPermission}
|
disabled={!editLivechatRoomCustomFieldsPermission}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Touch from '../../containers/Touch';
|
||||||
import KeyboardView from '../../containers/KeyboardView';
|
import KeyboardView from '../../containers/KeyboardView';
|
||||||
import sharedStyles from '../Styles';
|
import sharedStyles from '../Styles';
|
||||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||||
import { showErrorAlert, showConfirmationAlert } from '../../lib/methods/helpers';
|
import { showErrorAlert, showConfirmationAlert, compareServerVersion } from '../../lib/methods/helpers';
|
||||||
import { LISTENER } from '../../containers/Toast';
|
import { LISTENER } from '../../containers/Toast';
|
||||||
import EventEmitter from '../../lib/methods/helpers/events';
|
import EventEmitter from '../../lib/methods/helpers/events';
|
||||||
import { FormTextInput } from '../../containers/TextInput';
|
import { FormTextInput } from '../../containers/TextInput';
|
||||||
|
@ -36,6 +36,10 @@ import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSh
|
||||||
import { DeleteAccountActionSheetContent } from './components/DeleteAccountActionSheetContent';
|
import { DeleteAccountActionSheetContent } from './components/DeleteAccountActionSheetContent';
|
||||||
import ActionSheetContentWithInputAndSubmit from '../../containers/ActionSheet/ActionSheetContentWithInputAndSubmit';
|
import ActionSheetContentWithInputAndSubmit from '../../containers/ActionSheet/ActionSheetContentWithInputAndSubmit';
|
||||||
|
|
||||||
|
// https://github.com/RocketChat/Rocket.Chat/blob/174c28d40b3d5a52023ee2dca2e81dd77ff33fa5/apps/meteor/app/lib/server/functions/saveUser.js#L24-L25
|
||||||
|
const MAX_BIO_LENGTH = 260;
|
||||||
|
const MAX_NICKNAME_LENGTH = 120;
|
||||||
|
|
||||||
interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileStackParamList, 'ProfileView'> {
|
interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileStackParamList, 'ProfileView'> {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
@ -48,6 +52,7 @@ interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileSta
|
||||||
theme: TSupportedThemes;
|
theme: TSupportedThemes;
|
||||||
Accounts_AllowDeleteOwnAccount: boolean;
|
Accounts_AllowDeleteOwnAccount: boolean;
|
||||||
isMasterDetail: boolean;
|
isMasterDetail: boolean;
|
||||||
|
serverVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProfileViewState {
|
interface IProfileViewState {
|
||||||
|
@ -55,6 +60,8 @@ interface IProfileViewState {
|
||||||
name: string;
|
name: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
|
bio?: string;
|
||||||
|
nickname?: string;
|
||||||
newPassword: string | null;
|
newPassword: string | null;
|
||||||
currentPassword: string | null;
|
currentPassword: string | null;
|
||||||
customFields: {
|
customFields: {
|
||||||
|
@ -69,9 +76,11 @@ interface IProfileViewState {
|
||||||
class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> {
|
class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> {
|
||||||
private name?: TextInput | null;
|
private name?: TextInput | null;
|
||||||
private username?: TextInput | null;
|
private username?: TextInput | null;
|
||||||
private email?: TextInput;
|
private email?: TextInput | null;
|
||||||
private avatarUrl?: TextInput;
|
private avatarUrl?: TextInput | null;
|
||||||
private newPassword?: TextInput;
|
private newPassword?: TextInput | null;
|
||||||
|
private nickname?: TextInput | null;
|
||||||
|
private bio?: TextInput | null;
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { navigation, isMasterDetail } = this.props;
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
@ -98,6 +107,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
name: '',
|
name: '',
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
bio: '',
|
||||||
|
nickname: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
customFields: {},
|
customFields: {},
|
||||||
|
@ -123,7 +134,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
|
|
||||||
init = (user?: IUser) => {
|
init = (user?: IUser) => {
|
||||||
const { user: userProps } = this.props;
|
const { user: userProps } = this.props;
|
||||||
const { name, username, emails, customFields } = user || userProps;
|
const { name, username, emails, customFields, bio, nickname } = user || userProps;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
name: name as string,
|
name: name as string,
|
||||||
|
@ -131,12 +142,14 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
email: emails ? emails[0].address : null,
|
email: emails ? emails[0].address : null,
|
||||||
newPassword: null,
|
newPassword: null,
|
||||||
currentPassword: null,
|
currentPassword: null,
|
||||||
customFields: customFields || {}
|
customFields: customFields || {},
|
||||||
|
bio,
|
||||||
|
nickname
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
formIsChanged = () => {
|
formIsChanged = () => {
|
||||||
const { name, username, email, newPassword, customFields } = this.state;
|
const { name, username, email, newPassword, customFields, bio, nickname } = this.state;
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
let customFieldsChanged = false;
|
let customFieldsChanged = false;
|
||||||
|
|
||||||
|
@ -152,6 +165,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
return !(
|
return !(
|
||||||
user.name === name &&
|
user.name === name &&
|
||||||
user.username === username &&
|
user.username === username &&
|
||||||
|
user.bio === bio &&
|
||||||
|
user.nickname === nickname &&
|
||||||
!newPassword &&
|
!newPassword &&
|
||||||
user.emails &&
|
user.emails &&
|
||||||
user.emails[0].address === email &&
|
user.emails[0].address === email &&
|
||||||
|
@ -168,7 +183,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
|
|
||||||
this.setState({ saving: true });
|
this.setState({ saving: true });
|
||||||
|
|
||||||
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode } = this.state;
|
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode, bio, nickname } = this.state;
|
||||||
const { user, dispatch } = this.props;
|
const { user, dispatch } = this.props;
|
||||||
const params = {} as IProfileParams;
|
const params = {} as IProfileParams;
|
||||||
|
|
||||||
|
@ -187,6 +202,14 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
params.email = email;
|
params.email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.bio !== bio) {
|
||||||
|
params.bio = bio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.nickname !== nickname) {
|
||||||
|
params.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
// newPassword
|
// newPassword
|
||||||
if (newPassword) {
|
if (newPassword) {
|
||||||
params.newPassword = newPassword;
|
params.newPassword = newPassword;
|
||||||
|
@ -400,7 +423,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, username, email, newPassword, customFields, saving } = this.state;
|
const { name, username, email, newPassword, customFields, saving, nickname, bio } = this.state;
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
theme,
|
theme,
|
||||||
|
@ -410,7 +433,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
Accounts_AllowUserAvatarChange,
|
Accounts_AllowUserAvatarChange,
|
||||||
Accounts_AllowUsernameChange,
|
Accounts_AllowUsernameChange,
|
||||||
Accounts_CustomFields,
|
Accounts_CustomFields,
|
||||||
Accounts_AllowDeleteOwnAccount
|
Accounts_AllowDeleteOwnAccount,
|
||||||
|
serverVersion
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -431,9 +455,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
editable={Accounts_AllowRealNameChange}
|
editable={Accounts_AllowRealNameChange}
|
||||||
inputStyle={[!Accounts_AllowRealNameChange && styles.disabled]}
|
inputStyle={[!Accounts_AllowRealNameChange && styles.disabled]}
|
||||||
inputRef={e => {
|
inputRef={e => (this.name = e)}
|
||||||
this.name = e;
|
|
||||||
}}
|
|
||||||
label={I18n.t('Name')}
|
label={I18n.t('Name')}
|
||||||
placeholder={I18n.t('Name')}
|
placeholder={I18n.t('Name')}
|
||||||
value={name}
|
value={name}
|
||||||
|
@ -446,9 +468,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
editable={Accounts_AllowUsernameChange}
|
editable={Accounts_AllowUsernameChange}
|
||||||
inputStyle={[!Accounts_AllowUsernameChange && styles.disabled]}
|
inputStyle={[!Accounts_AllowUsernameChange && styles.disabled]}
|
||||||
inputRef={e => {
|
inputRef={e => (this.username = e)}
|
||||||
this.username = e;
|
|
||||||
}}
|
|
||||||
label={I18n.t('Username')}
|
label={I18n.t('Username')}
|
||||||
placeholder={I18n.t('Username')}
|
placeholder={I18n.t('Username')}
|
||||||
value={username}
|
value={username}
|
||||||
|
@ -461,28 +481,48 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
editable={Accounts_AllowEmailChange}
|
editable={Accounts_AllowEmailChange}
|
||||||
inputStyle={[!Accounts_AllowEmailChange && styles.disabled]}
|
inputStyle={[!Accounts_AllowEmailChange && styles.disabled]}
|
||||||
inputRef={e => {
|
inputRef={e => (this.email = e)}
|
||||||
if (e) {
|
|
||||||
this.email = e;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
label={I18n.t('Email')}
|
label={I18n.t('Email')}
|
||||||
placeholder={I18n.t('Email')}
|
placeholder={I18n.t('Email')}
|
||||||
value={email || undefined}
|
value={email || undefined}
|
||||||
onChangeText={value => this.setState({ email: value })}
|
onChangeText={value => this.setState({ email: value })}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
this.newPassword?.focus();
|
this.nickname?.focus();
|
||||||
}}
|
}}
|
||||||
testID='profile-view-email'
|
testID='profile-view-email'
|
||||||
/>
|
/>
|
||||||
|
{compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.5.0') ? (
|
||||||
|
<FormTextInput
|
||||||
|
inputRef={e => (this.nickname = e)}
|
||||||
|
label={I18n.t('Nickname')}
|
||||||
|
value={nickname}
|
||||||
|
onChangeText={value => this.setState({ nickname: value })}
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
this.bio?.focus();
|
||||||
|
}}
|
||||||
|
testID='profile-view-nickname'
|
||||||
|
maxLength={MAX_NICKNAME_LENGTH}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.1.0') ? (
|
||||||
|
<FormTextInput
|
||||||
|
inputRef={e => (this.bio = e)}
|
||||||
|
label={I18n.t('Bio')}
|
||||||
|
inputStyle={styles.inputBio}
|
||||||
|
multiline
|
||||||
|
maxLength={MAX_BIO_LENGTH}
|
||||||
|
value={bio}
|
||||||
|
onChangeText={value => this.setState({ bio: value })}
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
this.newPassword?.focus();
|
||||||
|
}}
|
||||||
|
testID='profile-view-bio'
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
editable={Accounts_AllowPasswordChange}
|
editable={Accounts_AllowPasswordChange}
|
||||||
inputStyle={[!Accounts_AllowPasswordChange && styles.disabled]}
|
inputStyle={[!Accounts_AllowPasswordChange && styles.disabled]}
|
||||||
inputRef={e => {
|
inputRef={e => (this.newPassword = e)}
|
||||||
if (e) {
|
|
||||||
this.newPassword = e;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
label={I18n.t('New_Password')}
|
label={I18n.t('New_Password')}
|
||||||
placeholder={I18n.t('New_Password')}
|
placeholder={I18n.t('New_Password')}
|
||||||
value={newPassword || undefined}
|
value={newPassword || undefined}
|
||||||
|
@ -539,6 +579,7 @@ const mapStateToProps = (state: IApplicationState) => ({
|
||||||
Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean,
|
Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean,
|
||||||
Accounts_CustomFields: state.settings.Accounts_CustomFields as string,
|
Accounts_CustomFields: state.settings.Accounts_CustomFields as string,
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
|
serverVersion: state.server.version,
|
||||||
Accounts_AllowDeleteOwnAccount: state.settings.Accounts_AllowDeleteOwnAccount as boolean
|
Accounts_AllowDeleteOwnAccount: state.settings.Accounts_AllowDeleteOwnAccount as boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,5 +22,9 @@ export default StyleSheet.create({
|
||||||
marginRight: 15,
|
marginRight: 15,
|
||||||
marginBottom: 15,
|
marginBottom: 15,
|
||||||
borderRadius: 4
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
inputBio: {
|
||||||
|
height: 100,
|
||||||
|
textAlignVertical: 'top'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -78,6 +78,11 @@ interface IRoomInfoEditViewProps extends IBaseScreen<ChatsStackParamList | Modal
|
||||||
deleteTeamPermission: string[];
|
deleteTeamPermission: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MESSAGE_TYPE_VALUES = MessageTypeValues.map(m => ({
|
||||||
|
value: m.value,
|
||||||
|
text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) }
|
||||||
|
}));
|
||||||
|
|
||||||
class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfoEditViewState> {
|
class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfoEditViewState> {
|
||||||
randomValue = random(15);
|
randomValue = random(15);
|
||||||
private querySubscription: Subscription | undefined;
|
private querySubscription: Subscription | undefined;
|
||||||
|
@ -447,15 +452,16 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const values = Array.isArray(systemMessages)
|
||||||
|
? MESSAGE_TYPE_VALUES.filter((option: any) => systemMessages.includes(option.value))
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={MessageTypeValues.map(m => ({
|
options={MESSAGE_TYPE_VALUES}
|
||||||
value: m.value,
|
onChange={({ value }) => this.setState({ systemMessages: value })}
|
||||||
text: { text: I18n.t('Hide_type_messages', { type: I18n.t(m.text) }) }
|
|
||||||
}))}
|
|
||||||
onChange={({ value }: { value: boolean }) => this.setState({ systemMessages: value })}
|
|
||||||
placeholder={{ text: I18n.t('Hide_System_Messages') }}
|
placeholder={{ text: I18n.t('Hide_System_Messages') }}
|
||||||
value={systemMessages as string[]}
|
value={values}
|
||||||
context={BlockContext.FORM}
|
context={BlockContext.FORM}
|
||||||
multiselect
|
multiselect
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, Text } from 'react-native';
|
import { StyleSheet, Text } from 'react-native';
|
||||||
|
|
||||||
import * as List from '../../containers/List';
|
import * as List from '../../containers/List';
|
||||||
|
@ -21,7 +21,7 @@ type TKey = 'desktopNotifications' | 'pushNotifications' | 'emailNotificationMod
|
||||||
interface IBaseParams {
|
interface IBaseParams {
|
||||||
preference: TKey;
|
preference: TKey;
|
||||||
value: string;
|
value: string;
|
||||||
onChangeValue: (param: { [key: string]: string }, onError: () => void) => void;
|
onChangeValue: (param: { [key: string]: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListPicker = ({
|
const ListPicker = ({
|
||||||
|
@ -36,17 +36,14 @@ const ListPicker = ({
|
||||||
} & IBaseParams) => {
|
} & IBaseParams) => {
|
||||||
const { showActionSheet, hideActionSheet } = useActionSheet();
|
const { showActionSheet, hideActionSheet } = useActionSheet();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [option, setOption] = useState(
|
const option = value ? OPTIONS[preference].find(option => option.value === value) : OPTIONS[preference][0];
|
||||||
value ? OPTIONS[preference].find(option => option.value === value) : OPTIONS[preference][0]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getOptions = () =>
|
const getOptions = () =>
|
||||||
OPTIONS[preference].map(i => ({
|
OPTIONS[preference].map(i => ({
|
||||||
title: I18n.t(i.label, { defaultValue: i.label }),
|
title: I18n.t(i.label, { defaultValue: i.label }),
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
hideActionSheet();
|
hideActionSheet();
|
||||||
onChangeValue({ [preference]: i.value.toString() }, () => setOption(option));
|
onChangeValue({ [preference]: i.value.toString() });
|
||||||
setOption(i);
|
|
||||||
},
|
},
|
||||||
right: option?.value === i.value ? () => <CustomIcon name={'check'} size={20} color={colors.tintActive} /> : undefined
|
right: option?.value === i.value ? () => <CustomIcon name={'check'} size={20} color={colors.tintActive} /> : undefined
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -44,18 +44,17 @@ const UserNotificationPreferencesView = () => {
|
||||||
getPreferences();
|
getPreferences();
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const onValueChangePicker = async (param: { [key: string]: string }, onError: () => void) => {
|
const onValueChangePicker = async (param: { [key: string]: string }) => {
|
||||||
|
const previousPreferences = preferences;
|
||||||
try {
|
try {
|
||||||
|
setPreferences({ ...previousPreferences, ...param });
|
||||||
const result = await Services.setUserPreferences(userId, param);
|
const result = await Services.setUserPreferences(userId, param);
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
const {
|
setPreferences(previousPreferences);
|
||||||
user: { settings }
|
|
||||||
} = result;
|
|
||||||
setPreferences(settings.preferences);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setPreferences(previousPreferences);
|
||||||
log(error);
|
log(error);
|
||||||
onError();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,12 @@ async function waitForToast() {
|
||||||
await sleep(600);
|
await sleep(600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dismissKeyboardAndScrollUp() {
|
||||||
|
await element(by.id('profile-view-list')).swipe('down');
|
||||||
|
await sleep(300);
|
||||||
|
await element(by.id('profile-view-list')).swipe('up');
|
||||||
|
}
|
||||||
|
|
||||||
describe('Profile screen', () => {
|
describe('Profile screen', () => {
|
||||||
let textMatcher: TTextMatcher;
|
let textMatcher: TTextMatcher;
|
||||||
let user: ITestUser;
|
let user: ITestUser;
|
||||||
|
@ -74,23 +80,27 @@ describe('Profile screen', () => {
|
||||||
it('should change name and username', async () => {
|
it('should change name and username', async () => {
|
||||||
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
||||||
await element(by.id('profile-view-username')).replaceText(`${user.username}new`);
|
await element(by.id('profile-view-username')).replaceText(`${user.username}new`);
|
||||||
// dismiss keyboard
|
await dismissKeyboardAndScrollUp();
|
||||||
await element(by.id('profile-view-list')).swipe('down');
|
await element(by.id('profile-view-submit')).tap();
|
||||||
|
await waitForToast();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change nickname and bio', async () => {
|
||||||
|
await element(by.id('profile-view-nickname')).replaceText(`nickname-${user.username}`);
|
||||||
|
await element(by.id('profile-view-bio')).replaceText(`bio-${user.username}`);
|
||||||
|
await dismissKeyboardAndScrollUp();
|
||||||
await element(by.id('profile-view-submit')).tap();
|
await element(by.id('profile-view-submit')).tap();
|
||||||
await waitForToast();
|
await waitForToast();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change email and password', async () => {
|
it('should change email and password', async () => {
|
||||||
await element(by.id('profile-view-list')).swipe('up');
|
await element(by.id('profile-view-list')).swipe('down');
|
||||||
await waitFor(element(by.id('profile-view-email')))
|
await waitFor(element(by.id('profile-view-email')))
|
||||||
.toBeVisible()
|
.toBeVisible()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${random()}@rocket.chat`);
|
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${random()}@rocket.chat`);
|
||||||
// dismiss keyboard
|
await dismissKeyboardAndScrollUp();
|
||||||
await element(by.id('profile-view-list')).swipe('down');
|
|
||||||
await element(by.id('profile-view-new-password')).replaceText(`${user.password}new`);
|
await element(by.id('profile-view-new-password')).replaceText(`${user.password}new`);
|
||||||
// dismiss keyboard
|
|
||||||
await element(by.id('profile-view-list')).swipe('down');
|
|
||||||
await waitFor(element(by.id('profile-view-submit')))
|
await waitFor(element(by.id('profile-view-submit')))
|
||||||
.toExist()
|
.toExist()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
|
|
|
@ -31,6 +31,7 @@ describe('Discussion', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create discussion from NewMessageView', async () => {
|
it('should create discussion from NewMessageView', async () => {
|
||||||
|
const selectUser = 'rocket.cat';
|
||||||
await waitFor(element(by.id('rooms-list-view-create-channel')))
|
await waitFor(element(by.id('rooms-list-view-create-channel')))
|
||||||
.toExist()
|
.toExist()
|
||||||
.withTimeout(2000);
|
.withTimeout(2000);
|
||||||
|
@ -53,6 +54,30 @@ describe('Discussion', () => {
|
||||||
.withTimeout(10000);
|
.withTimeout(10000);
|
||||||
await element(by.id(`multi-select-item-${room}`)).tap();
|
await element(by.id(`multi-select-item-${room}`)).tap();
|
||||||
await element(by.id('multi-select-discussion-name')).replaceText(discussionFromNewMessage);
|
await element(by.id('multi-select-discussion-name')).replaceText(discussionFromNewMessage);
|
||||||
|
await element(by[textMatcher]('Select users...')).tap();
|
||||||
|
await element(by.id('multi-select-search')).replaceText(`${selectUser}`);
|
||||||
|
await waitFor(element(by.id(`multi-select-item-${selectUser}`)))
|
||||||
|
.toExist()
|
||||||
|
.withTimeout(10000);
|
||||||
|
await element(by.id(`multi-select-item-${selectUser}`)).tap();
|
||||||
|
await sleep(300);
|
||||||
|
// checking if the chip was placed properly
|
||||||
|
await waitFor(element(by.id(`multi-select-chip-${selectUser}`)))
|
||||||
|
.toExist()
|
||||||
|
.withTimeout(10000);
|
||||||
|
// should keep the same chip even when the user does a new research
|
||||||
|
await element(by.id('multi-select-search')).replaceText(`user`);
|
||||||
|
await waitFor(element(by.id(`multi-select-item-${selectUser}`)))
|
||||||
|
.not.toExist()
|
||||||
|
.withTimeout(10000);
|
||||||
|
await waitFor(element(by.id(`multi-select-chip-${selectUser}`)))
|
||||||
|
.toExist()
|
||||||
|
.withTimeout(10000);
|
||||||
|
await sleep(500);
|
||||||
|
await element(by.id('multi-select-search')).tapReturnKey();
|
||||||
|
await sleep(500);
|
||||||
|
// removing the rocket.cat from the users
|
||||||
|
await element(by.id(`multi-select-chip-${selectUser}`)).tap();
|
||||||
await waitFor(element(by.id('create-discussion-submit')))
|
await waitFor(element(by.id('create-discussion-submit')))
|
||||||
.toExist()
|
.toExist()
|
||||||
.withTimeout(10000);
|
.withTimeout(10000);
|
||||||
|
|
|
@ -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