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:
Avinash Lingaloo
2022-03-13 00:22:24 +04:00
committed by GitHub
parent 35fe4081f7
commit 9e77c419b1
15 changed files with 722 additions and 96 deletions

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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,

View 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;

View 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;

View 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));

View 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;

View File

@@ -48,7 +48,6 @@ const ProfileError = ({error}: DisplayErrorProps) => {
name='alert-outline'
/>
<ErrorTextComponent
theme={theme}
testID='edit_profile.error.text'
error={error}
textStyle={style.text}

View File

@@ -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) {

View File

@@ -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: {

View File

@@ -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(

View File

@@ -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 (

View File

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

View File

@@ -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",