forked from Ivasoft/mattermost-mobile
[Gekidou] retry post (#6293)
* Add try again functionality to failed posts * Fix attach files on Android * feedback review * Prevent android crash when uploading files for the first time * Update the timestamp for updateAt when retrying to post * Add POST TIME TO FAIL * use function isPostFailed
This commit is contained in:
@@ -110,6 +110,23 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
|||||||
return "MattermostManaged";
|
return "MattermostManaged";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void getFilePath(String filePath, Promise promise) {
|
||||||
|
Activity currentActivity = getCurrentActivity();
|
||||||
|
WritableMap map = Arguments.createMap();
|
||||||
|
|
||||||
|
if (currentActivity != null) {
|
||||||
|
Uri uri = Uri.parse(filePath);
|
||||||
|
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||||
|
if (path != null) {
|
||||||
|
String text = "file://" + path;
|
||||||
|
map.putString("filePath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.resolve(map);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void isRunningInSplitView(final Promise promise) {
|
public void isRunningInSplitView(final Promise promise) {
|
||||||
WritableMap result = Arguments.createMap();
|
WritableMap result = Arguments.createMap();
|
||||||
|
|||||||
@@ -101,8 +101,14 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
|||||||
|
|
||||||
const initialPostModels: Model[] = [];
|
const initialPostModels: Model[] = [];
|
||||||
|
|
||||||
const filesModels = await operator.handleFiles({files, prepareRecordsOnly: true});
|
if (files.length) {
|
||||||
initialPostModels.push(...filesModels);
|
for (const f of files) {
|
||||||
|
// Set the pending post Id
|
||||||
|
f.post_id = pendingPostId;
|
||||||
|
}
|
||||||
|
const filesModels = await operator.handleFiles({files, prepareRecordsOnly: true});
|
||||||
|
initialPostModels.push(...filesModels);
|
||||||
|
}
|
||||||
|
|
||||||
const postModels = await operator.handlePosts({
|
const postModels = await operator.handlePosts({
|
||||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||||
@@ -182,6 +188,77 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
|||||||
return {data: true};
|
return {data: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
||||||
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
if (!operator) {
|
||||||
|
return {error: `${serverUrl} database not found`};
|
||||||
|
}
|
||||||
|
|
||||||
|
let client: Client;
|
||||||
|
try {
|
||||||
|
client = NetworkManager.getClient(serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const apiPost = await post.toApi();
|
||||||
|
const newPost = {
|
||||||
|
...apiPost,
|
||||||
|
props: {
|
||||||
|
...apiPost.props,
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
id: '',
|
||||||
|
create_at: timestamp,
|
||||||
|
update_at: timestamp,
|
||||||
|
delete_at: 0,
|
||||||
|
} as Post;
|
||||||
|
|
||||||
|
// Update the local post to reflect the pending state in the UI
|
||||||
|
// timestamps will remain the same as the initial attempt for createAt
|
||||||
|
// but updateAt will be use for the optimistic post UI
|
||||||
|
post.prepareUpdate((p) => {
|
||||||
|
p.props = newPost.props;
|
||||||
|
p.updateAt = timestamp;
|
||||||
|
});
|
||||||
|
await operator.batchRecords([post]);
|
||||||
|
|
||||||
|
const created = await client.createPost(newPost);
|
||||||
|
const models = await operator.handlePosts({
|
||||||
|
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||||
|
order: [created.id],
|
||||||
|
posts: [created],
|
||||||
|
prepareRecordsOnly: true,
|
||||||
|
});
|
||||||
|
const {member} = await updateLastPostAt(serverUrl, created.channel_id, created.create_at, true);
|
||||||
|
if (member) {
|
||||||
|
models.push(member);
|
||||||
|
}
|
||||||
|
await operator.batchRecords(models);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||||
|
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||||
|
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||||
|
) {
|
||||||
|
await removePost(serverUrl, post);
|
||||||
|
} else {
|
||||||
|
post.prepareUpdate((p) => {
|
||||||
|
p.props = {
|
||||||
|
...p.props,
|
||||||
|
failed: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await operator.batchRecords([post]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {error: undefined};
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
|
export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||||
if (!database) {
|
if (!database) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import {removePost} from '@actions/local/post';
|
import {removePost} from '@actions/local/post';
|
||||||
|
import {retryFailedPost} from '@actions/remote/post';
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
@@ -30,9 +31,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add Create post local action
|
|
||||||
const retryPost = (serverUrl: string, post: PostModel) => post;
|
|
||||||
|
|
||||||
const Failed = ({post, theme}: FailedProps) => {
|
const Failed = ({post, theme}: FailedProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -49,7 +47,7 @@ const Failed = ({post, theme}: FailedProps) => {
|
|||||||
icon='send-outline'
|
icon='send-outline'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
retryPost(serverUrl, post);
|
retryFailedPost(serverUrl, post);
|
||||||
}}
|
}}
|
||||||
testID='post.failed.retry'
|
testID='post.failed.retry'
|
||||||
text={intl.formatMessage({id: 'mobile.post.failed_retry', defaultMessage: 'Try Again'})}
|
text={intl.formatMessage({id: 'mobile.post.failed_retry', defaultMessage: 'Try Again'})}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {ReactNode, useMemo, useRef} from 'react';
|
import React, {ReactNode, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {Keyboard, Platform, StyleProp, View, ViewStyle, TouchableHighlight} from 'react-native';
|
import {Keyboard, Platform, StyleProp, View, ViewStyle, TouchableHighlight} from 'react-native';
|
||||||
|
|
||||||
@@ -10,12 +10,13 @@ import {showPermalink} from '@actions/remote/permalink';
|
|||||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||||
import SystemAvatar from '@components/system_avatar';
|
import SystemAvatar from '@components/system_avatar';
|
||||||
import SystemHeader from '@components/system_header';
|
import SystemHeader from '@components/system_header';
|
||||||
|
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||||
import * as Screens from '@constants/screens';
|
import * as Screens from '@constants/screens';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||||
import {fromAutoResponder, isFromWebhook, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
|
import {fromAutoResponder, isFromWebhook, isPostFailed, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ const Post = ({
|
|||||||
const styles = getStyleSheet(theme);
|
const styles = getStyleSheet(theme);
|
||||||
const isAutoResponder = fromAutoResponder(post);
|
const isAutoResponder = fromAutoResponder(post);
|
||||||
const isPendingOrFailed = isPostPendingOrFailed(post);
|
const isPendingOrFailed = isPostPendingOrFailed(post);
|
||||||
|
const isFailed = isPostFailed(post);
|
||||||
const isSystemPost = isSystemMessage(post);
|
const isSystemPost = isSystemMessage(post);
|
||||||
const isWebHook = isFromWebhook(post);
|
const isWebHook = isFromWebhook(post);
|
||||||
const hasSameRoot = useMemo(() => {
|
const hasSameRoot = useMemo(() => {
|
||||||
@@ -184,6 +186,20 @@ const Post = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [, rerender] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let t: NodeJS.Timeout|undefined;
|
||||||
|
if (post.pendingPostId === post.id && !isFailed) {
|
||||||
|
t = setTimeout(() => rerender(true), POST_TIME_TO_FAIL - (Date.now() - post.updateAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (t) {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [post.id]);
|
||||||
|
|
||||||
const highlightSaved = isSaved && !skipSavedHeader;
|
const highlightSaved = isSaved && !skipSavedHeader;
|
||||||
const hightlightPinned = post.isPinned && !skipPinnedHeader;
|
const hightlightPinned = post.isPinned && !skipPinnedHeader;
|
||||||
const itemTestID = `${testID}.${post.id}`;
|
const itemTestID = `${testID}.${post.id}`;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export const PostTypes: Record<string, string> = {
|
|||||||
SYSTEM_AUTO_RESPONDER: 'system_auto_responder',
|
SYSTEM_AUTO_RESPONDER: 'system_auto_responder',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const POST_TIME_TO_FAIL = 10000;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5,
|
POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5,
|
||||||
POST_TYPES: PostTypes,
|
POST_TYPES: PostTypes,
|
||||||
|
|||||||
@@ -155,4 +155,25 @@ export default class PostModel extends Model implements PostModelInterface {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toApi = async (): Promise<Post> => ({
|
||||||
|
id: this.id,
|
||||||
|
create_at: this.createAt,
|
||||||
|
update_at: this.updateAt,
|
||||||
|
edit_at: this.editAt,
|
||||||
|
delete_at: this.deleteAt,
|
||||||
|
is_pinned: this.isPinned,
|
||||||
|
user_id: this.userId,
|
||||||
|
channel_id: this.channelId,
|
||||||
|
root_id: this.rootId,
|
||||||
|
original_id: this.originalId,
|
||||||
|
message: this.message,
|
||||||
|
type: this.type,
|
||||||
|
props: this.props,
|
||||||
|
pending_post_id: this.pendingPostId,
|
||||||
|
file_ids: (await this.files.fetchIds()),
|
||||||
|
metadata: (this.metadata ? this.metadata : {}) as PostMetadata,
|
||||||
|
hashtags: '',
|
||||||
|
reply_count: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Permissions from 'react-native-permissions';
|
|||||||
import {dismissBottomSheet} from '@screens/navigation';
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import {extractFileInfo, lookupMimeType} from '@utils/file';
|
import {extractFileInfo, lookupMimeType} from '@utils/file';
|
||||||
|
|
||||||
const ShareExtension = NativeModules.MattermostShare;
|
const MattermostManaged = NativeModules.MattermostManaged;
|
||||||
|
|
||||||
type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo';
|
type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo';
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ export default class FilePickerUtil {
|
|||||||
files.push(file);
|
files.push(file);
|
||||||
} else {
|
} else {
|
||||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||||
const uri = (await ShareExtension.getFilePath(file.uri)).filePath;
|
const uri = (await MattermostManaged.getFilePath(file.uri)).filePath;
|
||||||
const type = file.type || lookupMimeType(uri);
|
const type = file.type || lookupMimeType(uri);
|
||||||
let fileName = file.fileName;
|
let fileName = file.fileName;
|
||||||
if (type.includes('video/')) {
|
if (type.includes('video/')) {
|
||||||
@@ -224,7 +224,7 @@ export default class FilePickerUtil {
|
|||||||
|
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||||
const newUri = await ShareExtension.getFilePath(doc.uri);
|
const newUri = await MattermostManaged.getFilePath(doc.uri);
|
||||||
uri = newUri?.filePath;
|
uri = newUri?.filePath;
|
||||||
if (uri === undefined) {
|
if (uri === undefined) {
|
||||||
return {doc: undefined};
|
return {doc: undefined};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Post} from '@constants';
|
import {Post} from '@constants';
|
||||||
|
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||||
import {DEFAULT_LOCALE} from '@i18n';
|
import {DEFAULT_LOCALE} from '@i18n';
|
||||||
import {displayUsername} from '@utils/user';
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
@@ -38,8 +39,12 @@ export function isPostEphemeral(post: PostModel): boolean {
|
|||||||
return post.type === Post.POST_TYPES.EPHEMERAL || post.type === Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL || post.deleteAt > 0;
|
return post.type === Post.POST_TYPES.EPHEMERAL || post.type === Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL || post.deleteAt > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPostFailed(post: PostModel): boolean {
|
||||||
|
return post.props?.failed || ((post.pendingPostId === post.id) && (Date.now() > post.updateAt + POST_TIME_TO_FAIL));
|
||||||
|
}
|
||||||
|
|
||||||
export function isPostPendingOrFailed(post: PostModel): boolean {
|
export function isPostPendingOrFailed(post: PostModel): boolean {
|
||||||
return post.pendingPostId === post.id || post.props?.failed;
|
return post.pendingPostId === post.id || isPostFailed(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSystemMessage(post: PostModel | Post): boolean {
|
export function isSystemMessage(post: PostModel | Post): boolean {
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -5,7 +5,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mattermost-mobile",
|
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache 2.0",
|
"license": "Apache 2.0",
|
||||||
@@ -3074,7 +3073,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@mattermost/react-native-network-client": {
|
"node_modules/@mattermost/react-native-network-client": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "git+ssh://git@github.com/mattermost/react-native-network-client.git#aaf88d2142e8b5c3b89b17aa19605232c490b763",
|
"resolved": "git+ssh://git@github.com/mattermost/react-native-network-client.git#c17f1c042ac8c29a16e9222d4c1b172e93987b64",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"validator": "13.7.0",
|
"validator": "13.7.0",
|
||||||
@@ -25689,7 +25688,7 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@mattermost/react-native-network-client": {
|
"@mattermost/react-native-network-client": {
|
||||||
"version": "git+ssh://git@github.com/mattermost/react-native-network-client.git#aaf88d2142e8b5c3b89b17aa19605232c490b763",
|
"version": "git+ssh://git@github.com/mattermost/react-native-network-client.git#c17f1c042ac8c29a16e9222d4c1b172e93987b64",
|
||||||
"from": "@mattermost/react-native-network-client@github:mattermost/react-native-network-client",
|
"from": "@mattermost/react-native-network-client@github:mattermost/react-native-network-client",
|
||||||
"requires": {
|
"requires": {
|
||||||
"validator": "13.7.0",
|
"validator": "13.7.0",
|
||||||
|
|||||||
2
types/database/models/servers/post.d.ts
vendored
2
types/database/models/servers/post.d.ts
vendored
@@ -92,4 +92,6 @@ export default class PostModel extends Model {
|
|||||||
|
|
||||||
/** hasReplies: Async function to determine if the post is part of a thread */
|
/** hasReplies: Async function to determine if the post is part of a thread */
|
||||||
hasReplies: () => Promise<boolean>;
|
hasReplies: () => Promise<boolean>;
|
||||||
|
|
||||||
|
toApi: () => Promise<Post>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user