forked from Ivasoft/mattermost-mobile
MM-41991 Gekidou Edit Post Screen (#6016)
* edit screen - in progress * edit screen - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post error component - in progress * edit post screen - post error component - in progress * edit post screen -emitEditing - in progress * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * able to edit post * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * updated errorLine * corrections * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * properly closes modal on tablets * starts with Save button set to false * refactored onTextSelectionChange * added useTheme to ErrorTextComponent * passing canEdit and hasFilesAttached * passing canEdit and hasFilesAttached * fix API call * change canEdit to canDelete * nearly there * displays alert * maxPostSize * autocomplete - fixing layout * autocomplete - fixing layout * autocomplete - work in progress * autocomplete - work in progress * clean up delete * fixing autocomplete * code fix * added server error message * update i18n * removed comment * code fix * fix bug on empty post message * post input top corrections * post draft limit * code corrections as per review * removed theme from useEffect * update edit_post - delete call * refactor PostInputRef to EditPostInputRef * autocomplete position fix and feedback addressed * Navigation title & subtitle fonts / navigation button builder * ux feedback * delay focus of edit input by 20 frames * properly dismiss the PostOptions screen this comes from the fix for the BottomSheet screen * using device info to check for physical keyboard * autocomplete with keyboard closed Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
@@ -667,6 +667,39 @@ export const markPostAsUnread = async (serverUrl: string, postId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const editPost = async (serverUrl: string, postId: string, postMessage: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const post = await queryPostById(database, postId);
|
||||
if (post) {
|
||||
const {update_at, edit_at, message: updatedMessage} = await client.patchPost({message: postMessage, id: postId});
|
||||
await database.write(async () => {
|
||||
await post.update((p) => {
|
||||
p.updateAt = update_at;
|
||||
p.editAt = edit_at;
|
||||
p.message = updatedMessage;
|
||||
});
|
||||
});
|
||||
}
|
||||
return {
|
||||
post,
|
||||
};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export async function fetchSavedPosts(serverUrl: string, teamId?: string, channelId?: string, page?: number, perPage?: number) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
||||
@@ -56,6 +56,7 @@ type Props = {
|
||||
postInputTop: number;
|
||||
rootId: string;
|
||||
channelId: string;
|
||||
fixedBottomPosition?: boolean;
|
||||
isSearch?: boolean;
|
||||
value: string;
|
||||
enableDateSuggestion?: boolean;
|
||||
@@ -63,6 +64,7 @@ type Props = {
|
||||
nestedScrollEnabled?: boolean;
|
||||
updateValue: (v: string) => void;
|
||||
hasFilesAttached: boolean;
|
||||
maxHeightOverride?: number;
|
||||
}
|
||||
|
||||
const Autocomplete = ({
|
||||
@@ -72,7 +74,9 @@ const Autocomplete = ({
|
||||
|
||||
//channelId,
|
||||
isSearch = false,
|
||||
fixedBottomPosition,
|
||||
value,
|
||||
maxHeightOverride,
|
||||
|
||||
//enableDateSuggestion = false,
|
||||
isAppsEnabled,
|
||||
@@ -97,6 +101,9 @@ const Autocomplete = ({
|
||||
const appsTakeOver = false; // showingAppCommand;
|
||||
|
||||
const maxListHeight = useMemo(() => {
|
||||
if (maxHeightOverride) {
|
||||
return maxHeightOverride;
|
||||
}
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
const offset = isTablet && isLandscape ? OFFSET_TABLET : 0;
|
||||
let postInputDiff = 0;
|
||||
@@ -106,7 +113,7 @@ const Autocomplete = ({
|
||||
postInputDiff = MAX_LIST_DIFF;
|
||||
}
|
||||
return MAX_LIST_HEIGHT - postInputDiff - offset;
|
||||
}, [postInputTop, isTablet, dimensions.width]);
|
||||
}, [maxHeightOverride, postInputTop, isTablet, dimensions.width]);
|
||||
|
||||
const wrapperStyles = useMemo(() => {
|
||||
const s = [];
|
||||
@@ -124,9 +131,11 @@ const Autocomplete = ({
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const s = [style.borders];
|
||||
if (!isSearch) {
|
||||
if (!isSearch && !fixedBottomPosition) {
|
||||
const offset = isTablet ? -OFFSET_TABLET : 0;
|
||||
s.push(style.base, {bottom: postInputTop + LIST_BOTTOM + offset});
|
||||
} else if (fixedBottomPosition) {
|
||||
s.push(style.base, {bottom: 0});
|
||||
}
|
||||
if (!hasElements) {
|
||||
s.push(style.hidden);
|
||||
|
||||
@@ -5,16 +5,17 @@ import React from 'react';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type ErrorProps = {
|
||||
error: ErrorText;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const ErrorTextComponent = ({error, testID, textStyle, theme}: ErrorProps) => {
|
||||
const ErrorTextComponent = ({error, testID, textStyle}: ErrorProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const message = typeof (error) === 'string' ? error : error.message;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const CHANNEL_DETAILS = 'ChannelDetails';
|
||||
export const CHANNEL_EDIT = 'ChannelEdit';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const EDIT_POST = 'EditPost';
|
||||
export const EDIT_PROFILE = 'EditProfile';
|
||||
export const EDIT_SERVER = 'EditServer';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
@@ -47,6 +48,7 @@ export default {
|
||||
CHANNEL_DETAILS,
|
||||
CUSTOM_STATUS_CLEAR_AFTER,
|
||||
CUSTOM_STATUS,
|
||||
EDIT_POST,
|
||||
EDIT_PROFILE,
|
||||
EDIT_SERVER,
|
||||
FORGOT_PASSWORD,
|
||||
|
||||
308
app/screens/edit_post/edit_post.tsx
Normal file
308
app/screens/edit_post/edit_post.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, Keyboard, KeyboardType, LayoutChangeEvent, Platform, SafeAreaView, useWindowDimensions, View} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import {deletePost, editPost} from '@actions/remote/post';
|
||||
import AutoComplete from '@components/autocomplete';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import PostError from '@screens/edit_post/post_error';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {switchKeyboardForCodeBlocks} from '@utils/markdown';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import EditPostInput, {EditPostInputRef} from './edit_post_input';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
body: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loader: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const RIGHT_BUTTON = buildNavigationButton('edit-post', 'edit_post.save.button');
|
||||
|
||||
type EditPostProps = {
|
||||
componentId: string;
|
||||
closeButtonId: string;
|
||||
post: PostModel;
|
||||
maxPostSize: number;
|
||||
hasFilesAttached: boolean;
|
||||
canDelete: boolean;
|
||||
}
|
||||
const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttached, canDelete}: EditPostProps) => {
|
||||
const [keyboardType, setKeyboardType] = useState<KeyboardType>('default');
|
||||
const [postMessage, setPostMessage] = useState(post.message);
|
||||
const [cursorPosition, setCursorPosition] = useState(post.message.length);
|
||||
const [errorLine, setErrorLine] = useState<string | undefined>();
|
||||
const [errorExtra, setErrorExtra] = useState<string | undefined>();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const layoutHeight = useSharedValue(0);
|
||||
const keyboardHeight = useSharedValue(0);
|
||||
|
||||
const postInputRef = useRef<EditPostInputRef>(null);
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const {width, height} = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [{
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}),
|
||||
...RIGHT_BUTTON,
|
||||
enabled: false,
|
||||
}],
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const showListener = Keyboard.addListener('keyboardWillShow', (e) => {
|
||||
const {height: end} = e.endCoordinates;
|
||||
|
||||
// on iPad if we use the hardware keyboard multiply its height by 2
|
||||
// otherwise use the software keyboard height
|
||||
const minKeyboardHeight = end < 100 ? end * 2 : end;
|
||||
keyboardHeight.value = minKeyboardHeight;
|
||||
});
|
||||
const hideListener = Keyboard.addListener('keyboardWillHide', () => {
|
||||
if (isTablet) {
|
||||
const offset = isLandscape ? 60 : 0;
|
||||
keyboardHeight.value = ((height - (layoutHeight.value + offset)) / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardHeight.value = 0;
|
||||
});
|
||||
|
||||
return () => {
|
||||
showListener.remove();
|
||||
hideListener.remove();
|
||||
};
|
||||
}, [isTablet, height]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
postInputRef.current?.focus();
|
||||
}, 320);
|
||||
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: { buttonId: string }) => {
|
||||
switch (buttonId) {
|
||||
case closeButtonId: {
|
||||
onClose();
|
||||
break;
|
||||
}
|
||||
case RIGHT_BUTTON.id:
|
||||
onSavePostMessage();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
}, [postMessage]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
// Workaround to avoid iOS emdash autocorrect in Code Blocks
|
||||
if (Platform.OS === 'ios') {
|
||||
onTextSelectionChange();
|
||||
}
|
||||
}, [postMessage]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
dismissModal({componentId});
|
||||
}, []);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
layoutHeight.value = e.nativeEvent.layout.height;
|
||||
}, [height]);
|
||||
|
||||
const onTextSelectionChange = useCallback((curPos: number = cursorPosition) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
setKeyboardType(switchKeyboardForCodeBlocks(postMessage, curPos));
|
||||
}
|
||||
setCursorPosition(curPos);
|
||||
}, [cursorPosition, postMessage]);
|
||||
|
||||
const toggleSaveButton = useCallback((enabled = true) => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [{
|
||||
...RIGHT_BUTTON,
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}),
|
||||
enabled,
|
||||
}],
|
||||
});
|
||||
}, [componentId, intl, theme]);
|
||||
|
||||
const onChangeText = useCallback((message: string) => {
|
||||
setPostMessage(message);
|
||||
const tooLong = message.trim().length > maxPostSize;
|
||||
|
||||
if (tooLong) {
|
||||
const line = intl.formatMessage({id: 'mobile.message_length.message_split_left', defaultMessage: 'Message exceeds the character limit'});
|
||||
const extra = `${message.trim().length} / ${maxPostSize}`;
|
||||
setErrorLine(line);
|
||||
setErrorExtra(extra);
|
||||
}
|
||||
toggleSaveButton(post.message !== message);
|
||||
}, [intl, maxPostSize, toggleSaveButton]);
|
||||
|
||||
const handleUIUpdates = useCallback((res) => {
|
||||
if (res?.error) {
|
||||
setIsUpdating(false);
|
||||
const errorMessage = intl.formatMessage({id: 'mobile.edit_post.error', defaultMessage: 'There was a problem editing this message. Please try again.'});
|
||||
setErrorLine(errorMessage);
|
||||
postInputRef?.current?.focus();
|
||||
} else {
|
||||
setIsUpdating(false);
|
||||
onClose();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDeletePost = useCallback(async () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'mobile.edit_post.delete_title', defaultMessage: 'Confirm Post Delete'}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.edit_post.delete_question',
|
||||
defaultMessage: 'Are you sure you want to delete this Post?',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
setIsUpdating(false);
|
||||
toggleSaveButton();
|
||||
setPostMessage(post.message);
|
||||
},
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const res = await deletePost(serverUrl, post);
|
||||
handleUIUpdates(res);
|
||||
},
|
||||
}],
|
||||
);
|
||||
}, [serverUrl, post.message]);
|
||||
|
||||
const onSavePostMessage = useCallback(async () => {
|
||||
setIsUpdating(true);
|
||||
setErrorLine(undefined);
|
||||
setErrorExtra(undefined);
|
||||
toggleSaveButton(false);
|
||||
if (!postMessage && canDelete && !hasFilesAttached) {
|
||||
handleDeletePost();
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await editPost(serverUrl, post.id, postMessage);
|
||||
handleUIUpdates(res);
|
||||
}, [toggleSaveButton, serverUrl, post.id, postMessage, onClose]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
return {bottom: 0};
|
||||
}
|
||||
|
||||
let bottom = 0;
|
||||
if (isTablet) {
|
||||
// 60 is the size of the navigation header
|
||||
const offset = isLandscape ? 60 : 0;
|
||||
|
||||
bottom = keyboardHeight.value - ((height - (layoutHeight.value + offset)) / 2);
|
||||
} else {
|
||||
bottom = keyboardHeight.value;
|
||||
}
|
||||
|
||||
return {
|
||||
bottom: withTiming(bottom, {duration: 250}),
|
||||
};
|
||||
});
|
||||
|
||||
if (isUpdating) {
|
||||
return (
|
||||
<View style={styles.loader}>
|
||||
<Loading color={theme.buttonBg}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeAreaView
|
||||
testID='edit_post.screen'
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<View style={styles.body}>
|
||||
{Boolean((errorLine || errorExtra)) &&
|
||||
<PostError
|
||||
errorExtra={errorExtra}
|
||||
errorLine={errorLine}
|
||||
/>
|
||||
}
|
||||
<EditPostInput
|
||||
hasError={Boolean(errorLine)}
|
||||
keyboardType={keyboardType}
|
||||
message={postMessage}
|
||||
onChangeText={onChangeText}
|
||||
onTextSelectionChange={onTextSelectionChange}
|
||||
ref={postInputRef}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<AutoComplete
|
||||
channelId={post.channelId}
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
nestedScrollEnabled={true}
|
||||
rootId={post.rootId}
|
||||
updateValue={onChangeText}
|
||||
value={postMessage}
|
||||
cursorPosition={cursorPosition}
|
||||
postInputTop={1}
|
||||
fixedBottomPosition={true}
|
||||
maxHeightOverride={isTablet ? 200 : undefined}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPost;
|
||||
93
app/screens/edit_post/edit_post_input/index.tsx
Normal file
93
app/screens/edit_post/edit_post_input/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {KeyboardType, Platform, TextInput, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
padding: 15,
|
||||
textAlignVertical: 'top',
|
||||
...typography('Body', 200),
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginTop: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
const HEIGHT_DIFF = Platform.select({android: 40, default: 30});
|
||||
|
||||
export type EditPostInputRef = {
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
type PostInputProps = {
|
||||
keyboardType: KeyboardType;
|
||||
message: string;
|
||||
hasError: boolean;
|
||||
onTextSelectionChange: (curPos: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
}
|
||||
|
||||
const EditPostInput = forwardRef<EditPostInputRef, PostInputProps>(({
|
||||
keyboardType, message, onChangeText, onTextSelectionChange, hasError,
|
||||
}: PostInputProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const {height} = useWindowDimensions();
|
||||
const textInputHeight = (height / 2) - HEIGHT_DIFF;
|
||||
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
const inputStyle = useMemo(() => {
|
||||
return [styles.input, {height: textInputHeight}];
|
||||
}, [textInputHeight, styles]);
|
||||
|
||||
const onSelectionChange = useCallback((event) => {
|
||||
const curPos = event.nativeEvent.selection.end;
|
||||
onTextSelectionChange(curPos);
|
||||
}, [onTextSelectionChange]);
|
||||
|
||||
const containerStyle = useMemo(() => [
|
||||
styles.inputContainer,
|
||||
hasError && {marginTop: 0},
|
||||
{height: textInputHeight},
|
||||
], [styles, textInputHeight]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current?.focus(),
|
||||
}), [inputRef.current]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
keyboardType={keyboardType}
|
||||
multiline={true}
|
||||
onChangeText={onChangeText}
|
||||
onSelectionChange={onSelectionChange}
|
||||
placeholder={intl.formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
style={inputStyle}
|
||||
testID='edit_post.message.input'
|
||||
underlineColorAndroid='transparent'
|
||||
value={message}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
EditPostInput.displayName = 'EditPostInput';
|
||||
|
||||
export default EditPostInput;
|
||||
32
app/screens/edit_post/index.tsx
Normal file
32
app/screens/edit_post/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
|
||||
import EditPost from './edit_post';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const enhance = withObservables([], ({database, post}: WithDatabaseArgs & { post: PostModel}) => {
|
||||
const maxPostSize = database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe(
|
||||
switchMap(({value}) => of$(parseInt(value.MaxPostSize || '0', 10) || MAX_MESSAGE_LENGTH_FALLBACK)),
|
||||
|
||||
);
|
||||
|
||||
const hasFilesAttached = post.files.observe().pipe(switchMap((files) => of$(files?.length > 0)));
|
||||
|
||||
return {
|
||||
maxPostSize,
|
||||
hasFilesAttached,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhance(EditPost));
|
||||
57
app/screens/edit_post/post_error/index.tsx
Normal file
57
app/screens/edit_post/post_error/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import ErrorText from '@components/error_text';
|
||||
|
||||
type PostErrorProps = {
|
||||
errorLine?: string;
|
||||
errorExtra?: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorContainerSplit: {
|
||||
paddingHorizontal: 15,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
errorContainer: {
|
||||
paddingHorizontal: 10,
|
||||
width: '100%',
|
||||
},
|
||||
errorWrap: {
|
||||
flexShrink: 1,
|
||||
paddingRight: 20,
|
||||
},
|
||||
errorWrapper: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const PostError = ({errorLine, errorExtra}: PostErrorProps) => {
|
||||
return (
|
||||
<View
|
||||
style={errorExtra ? styles.errorContainerSplit : styles.errorContainer}
|
||||
>
|
||||
{Boolean(errorLine) && (
|
||||
<ErrorText
|
||||
testID='edit_post.error.text'
|
||||
error={errorLine!}
|
||||
textStyle={styles.errorWrap}
|
||||
/>
|
||||
)}
|
||||
{Boolean(errorExtra) && (
|
||||
<ErrorText
|
||||
testID='edit_post.error.text.extra'
|
||||
error={errorExtra!}
|
||||
textStyle={!errorLine && styles.errorWrapper}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostError;
|
||||
@@ -48,7 +48,6 @@ const ProfileError = ({error}: DisplayErrorProps) => {
|
||||
name='alert-outline'
|
||||
/>
|
||||
<ErrorTextComponent
|
||||
theme={theme}
|
||||
testID='edit_profile.error.text'
|
||||
error={error}
|
||||
textStyle={style.text}
|
||||
|
||||
@@ -13,9 +13,6 @@ import {Screens} from '@constants';
|
||||
import {withServerDatabase} from '@database/components';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
|
||||
// TODO: Remove this and uncomment screens as they get added
|
||||
/* eslint-disable */
|
||||
|
||||
const withGestures = (Screen: React.ComponentType, styles: StyleProp<ViewStyle>) => {
|
||||
return function gestureHoc(props: any) {
|
||||
if (Platform.OS === 'android') {
|
||||
@@ -23,11 +20,11 @@ const withGestures = (Screen: React.ComponentType, styles: StyleProp<ViewStyle>)
|
||||
<GestureHandlerRootView style={[{flex: 1}, styles]}>
|
||||
<Screen {...props}/>
|
||||
</GestureHandlerRootView>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return <Screen {...props}/>;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const withIntl = (Screen: React.ComponentType) => {
|
||||
@@ -39,86 +36,103 @@ const withIntl = (Screen: React.ComponentType) => {
|
||||
>
|
||||
<Screen {...props}/>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const withSafeAreaInsets = (Screen: React.ComponentType) => {
|
||||
return function SafeAreaInsets(props: any){
|
||||
return function SafeAreaInsets(props: any) {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<Screen {...props} />
|
||||
<Screen {...props}/>
|
||||
</SafeAreaProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
let screen: any|undefined;
|
||||
let extraStyles: StyleProp<ViewStyle>;
|
||||
switch (screenName) {
|
||||
case Screens.ABOUT:
|
||||
screen = withServerDatabase(require('@screens/about').default);
|
||||
break;
|
||||
case Screens.BOTTOM_SHEET:
|
||||
screen = withServerDatabase(require('@screens/bottom_sheet').default);
|
||||
break;
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS:
|
||||
screen = withServerDatabase(require('@screens/custom_status').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS_CLEAR_AFTER:
|
||||
screen = withServerDatabase(require('@screens/custom_status_clear_after').default);
|
||||
break;
|
||||
case Screens.EMOJI_PICKER:
|
||||
screen = withServerDatabase(require('@screens/emoji_picker').default);
|
||||
break;
|
||||
case Screens.EDIT_PROFILE:
|
||||
screen = withServerDatabase((require('@screens/edit_profile').default));
|
||||
break;
|
||||
case Screens.EDIT_SERVER:
|
||||
screen = withIntl(require('@screens/edit_server').default);
|
||||
break;
|
||||
case Screens.FORGOT_PASSWORD:
|
||||
screen = withIntl(require('@screens/forgot_password').default);
|
||||
break;
|
||||
case Screens.GALLERY:
|
||||
screen = withServerDatabase((require('@screens/gallery').default));
|
||||
break;
|
||||
case Screens.IN_APP_NOTIFICATION: {
|
||||
const notificationScreen = require('@screens/in_app_notification').default;
|
||||
Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () => Platform.select({
|
||||
default: notificationScreen,
|
||||
ios: withSafeAreaInsets(notificationScreen),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
case Screens.LOGIN:
|
||||
screen = withIntl(require('@screens/login').default);
|
||||
break;
|
||||
case Screens.MFA:
|
||||
screen = withIntl(require('@screens/mfa').default);
|
||||
break;
|
||||
case Screens.BROWSE_CHANNELS:
|
||||
screen = withServerDatabase(require('@screens/browse_channels').default);
|
||||
break;
|
||||
case Screens.POST_OPTIONS:
|
||||
screen = withServerDatabase(require('@screens/post_options').default);
|
||||
break;
|
||||
case Screens.SSO:
|
||||
screen = withIntl(require('@screens/sso').default);
|
||||
break;
|
||||
case Screens.SAVED_POSTS:
|
||||
screen = withServerDatabase((require('@screens/home/saved_posts').default));
|
||||
break;
|
||||
case Screens.CREATE_DIRECT_MESSAGE:
|
||||
screen = withServerDatabase((require('@screens/create_direct_message').default));
|
||||
break;
|
||||
case Screens.THREAD:
|
||||
screen = withServerDatabase(require('@screens/thread').default);
|
||||
break;
|
||||
case Screens.ABOUT:
|
||||
screen = withServerDatabase(require('@screens/about').default);
|
||||
break;
|
||||
case Screens.BOTTOM_SHEET:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/bottom_sheet').default,
|
||||
);
|
||||
break;
|
||||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/custom_status').default,
|
||||
);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS_CLEAR_AFTER:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/custom_status_clear_after').default,
|
||||
);
|
||||
break;
|
||||
case Screens.EDIT_POST:
|
||||
screen = withServerDatabase(require('@screens/edit_post').default);
|
||||
break;
|
||||
case Screens.EDIT_PROFILE:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/edit_profile').default,
|
||||
);
|
||||
break;
|
||||
case Screens.EDIT_SERVER:
|
||||
screen = withIntl(require('@screens/edit_server').default);
|
||||
break;
|
||||
case Screens.EMOJI_PICKER:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/emoji_picker').default,
|
||||
);
|
||||
break;
|
||||
case Screens.FORGOT_PASSWORD:
|
||||
screen = withIntl(require('@screens/forgot_password').default);
|
||||
break;
|
||||
case Screens.GALLERY:
|
||||
screen = withServerDatabase(require('@screens/gallery').default);
|
||||
break;
|
||||
case Screens.IN_APP_NOTIFICATION: {
|
||||
const notificationScreen =
|
||||
require('@screens/in_app_notification').default;
|
||||
Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () =>
|
||||
Platform.select({
|
||||
default: notificationScreen,
|
||||
ios: withSafeAreaInsets(notificationScreen),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
case Screens.LOGIN:
|
||||
screen = withIntl(require('@screens/login').default);
|
||||
break;
|
||||
case Screens.MFA:
|
||||
screen = withIntl(require('@screens/mfa').default);
|
||||
break;
|
||||
case Screens.BROWSE_CHANNELS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/browse_channels').default,
|
||||
);
|
||||
break;
|
||||
case Screens.POST_OPTIONS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/post_options').default,
|
||||
);
|
||||
break;
|
||||
case Screens.SAVED_POSTS:
|
||||
screen = withServerDatabase((require('@screens/home/saved_posts').default));
|
||||
break;
|
||||
case Screens.SSO:
|
||||
screen = withIntl(require('@screens/sso').default);
|
||||
break;
|
||||
case Screens.THREAD:
|
||||
screen = withServerDatabase(require('@screens/thread').default);
|
||||
break;
|
||||
}
|
||||
|
||||
if (screen) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native';
|
||||
import {Navigation, Options, OptionsModalPresentationStyle} from 'react-native-navigation';
|
||||
import {ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton} from 'react-native-navigation';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -105,6 +105,18 @@ Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
orientation: Device.IS_TABLET ? undefined : ['portrait'],
|
||||
},
|
||||
topBar: {
|
||||
title: {
|
||||
fontFamily: 'Metropolis-SemiBold',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
subtitle: {
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Appearance.addChangeListener(() => {
|
||||
@@ -556,6 +568,16 @@ export async function dismissAllModals(options: Options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export const buildNavigationButton = (id: string, testID: string, icon?: ImageResource): OptionsTopBarButton => ({
|
||||
fontSize: 16,
|
||||
fontFamily: 'OpenSans-SemiBold',
|
||||
fontWeight: '600',
|
||||
id,
|
||||
icon,
|
||||
showAsAction: 'always',
|
||||
testID,
|
||||
});
|
||||
|
||||
export function setButtons(componentId: string, buttons: NavButtons = {leftButtons: [], rightButtons: []}) {
|
||||
const options = {
|
||||
topBar: {
|
||||
|
||||
@@ -122,8 +122,8 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca
|
||||
const canEdit = combineLatest([postEditTimeLimit, isLicensed, channel, currentUser, channelIsArchived, channelIsReadOnly, canEditUntil, canPostPermission]).pipe(switchMap(([lt, ls, c, u, isArchived, isReadOnly, until, canPost]) => {
|
||||
const isOwner = u.id === post.userId;
|
||||
const canEditPostPermission = canEditPost(isOwner, post, lt, ls, c, u);
|
||||
const timeReached = until === -1 || until > Date.now();
|
||||
return of$(canEditPostPermission && isSystemMessage(post) && !isArchived && !isReadOnly && !timeReached && canPost);
|
||||
const timeNotReached = (until === -1) || (until > Date.now());
|
||||
return of$(canEditPostPermission && !isArchived && !isReadOnly && timeNotReached && canPost);
|
||||
}));
|
||||
|
||||
const canMarkAsUnread = combineLatest([currentUser, channelIsArchived]).pipe(
|
||||
|
||||
@@ -2,22 +2,44 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {t} from '@i18n';
|
||||
import {dismissBottomSheet, goToScreen} from '@screens/navigation';
|
||||
import PostModel from '@typings/database/models/servers/post';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
import BaseOption from './base_option';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
post: PostModel;
|
||||
canDelete: boolean;
|
||||
}
|
||||
const EditOption = ({post}: Props) => {
|
||||
const EditOption = ({post, canDelete}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
// https://mattermost.atlassian.net/browse/MM-41991
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
goToScreen('EDIT_SCREEN_NOT_IMPLEMENTED_YET', '', {post});
|
||||
|
||||
const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'});
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-edit-post';
|
||||
const passProps = {post, closeButtonId, canDelete};
|
||||
const options = {
|
||||
modal: {swipeToDismiss: false},
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
testID: 'close.edit_post.button',
|
||||
icon: closeButton,
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(Screens.EDIT_POST, title, passProps, options);
|
||||
}, [post]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {ITEM_HEIGHT} from '@components/menu_item';
|
||||
import {Screens} from '@constants';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {isSystemMessage} from '@utils/post';
|
||||
|
||||
import CopyLinkOption from './options/copy_permalink_option';
|
||||
@@ -34,6 +36,7 @@ type PostOptionsProps = {
|
||||
location: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
thread: Partial<PostModel>;
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
const PostOptions = ({
|
||||
@@ -44,12 +47,31 @@ const PostOptions = ({
|
||||
canPin,
|
||||
canReply,
|
||||
combinedPost,
|
||||
componentId,
|
||||
isSaved,
|
||||
location,
|
||||
post,
|
||||
thread,
|
||||
}: PostOptionsProps) => {
|
||||
const managedConfig = useManagedConfig();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: { buttonId: string }) => {
|
||||
switch (buttonId) {
|
||||
case 'close-post-options': {
|
||||
dismissModal({componentId});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
|
||||
return () => {
|
||||
unsubscribe.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isSystemPost = isSystemMessage(post);
|
||||
|
||||
const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true';
|
||||
@@ -86,14 +108,19 @@ const PostOptions = ({
|
||||
postId={post.id}
|
||||
/>
|
||||
}
|
||||
{canCopyText && <CopyTextOption postMessage={post.message}/>}
|
||||
{Boolean(canCopyText && post.message) && <CopyTextOption postMessage={post.message}/>}
|
||||
{canPin &&
|
||||
<PinChannelOption
|
||||
isPostPinned={post.isPinned}
|
||||
postId={post.id}
|
||||
/>
|
||||
}
|
||||
{canEdit && <EditOption post={post}/>}
|
||||
{canEdit &&
|
||||
<EditOption
|
||||
post={post}
|
||||
canDelete={canDelete}
|
||||
/>
|
||||
}
|
||||
{canDelete &&
|
||||
<DeletePostOption
|
||||
combinedPost={combinedPost}
|
||||
|
||||
@@ -103,6 +103,8 @@
|
||||
"date_separator.today": "Today",
|
||||
"date_separator.yesterday": "Yesterday",
|
||||
"download.error": "Unable to download the file. Try again later",
|
||||
"edit_post.editPost": "Edit the post...",
|
||||
"edit_post.save": "Save",
|
||||
"edit_server.description": "Specify a display name for this server",
|
||||
"edit_server.display_help": "Server: {url}",
|
||||
"edit_server.save": "Save",
|
||||
@@ -228,6 +230,10 @@
|
||||
"mobile.downloader.disabled_title": "Download disabled",
|
||||
"mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n",
|
||||
"mobile.downloader.failed_title": "Download failed",
|
||||
"mobile.edit_post.delete_question": "Are you sure you want to delete this Post?",
|
||||
"mobile.edit_post.delete_title": "Confirm Post Delete",
|
||||
"mobile.edit_post.error": "There was a problem editing this message. Please try again.",
|
||||
"mobile.edit_post.title": "Editing Message",
|
||||
"mobile.emoji_picker.search.not_found_description": "Check the spelling or try another search.",
|
||||
"mobile.emoji_picker.search.not_found_title": "No results found for \"{searchTerm}\"",
|
||||
"mobile.error_handler.button": "Relaunch",
|
||||
@@ -271,6 +277,7 @@
|
||||
"mobile.markdown.link.copy_url": "Copy URL",
|
||||
"mobile.mention.copy_mention": "Copy Mention",
|
||||
"mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}",
|
||||
"mobile.message_length.message_split_left": "Message exceeds the character limit",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.notice_mobile_link": "mobile apps",
|
||||
"mobile.notice_platform_link": "server",
|
||||
@@ -441,11 +448,11 @@
|
||||
"status_dropdown.set_offline": "Offline",
|
||||
"status_dropdown.set_online": "Online",
|
||||
"status_dropdown.set_ooo": "Out Of Office",
|
||||
"suggestion.mention.channels": "",
|
||||
"suggestion.mention.morechannels": "",
|
||||
"suggestion.search.direct": "",
|
||||
"suggestion.search.private": "",
|
||||
"suggestion.search.public": "",
|
||||
"suggestion.mention.channels": "My Channels",
|
||||
"suggestion.mention.morechannels": "Other Channels",
|
||||
"suggestion.search.direct": "Direct Messages",
|
||||
"suggestion.search.private": "Private Channels",
|
||||
"suggestion.search.public": "Public Channels",
|
||||
"team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.",
|
||||
"team_list.no_other_teams.title": "No additional teams to join",
|
||||
"thread.header.thread": "Thread",
|
||||
|
||||
Reference in New Issue
Block a user