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";
|
||||
}
|
||||
|
||||
@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
|
||||
public void isRunningInSplitView(final Promise promise) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
|
||||
@@ -101,8 +101,14 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
|
||||
const initialPostModels: Model[] = [];
|
||||
|
||||
const filesModels = await operator.handleFiles({files, prepareRecordsOnly: true});
|
||||
initialPostModels.push(...filesModels);
|
||||
if (files.length) {
|
||||
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({
|
||||
actionType: ActionType.POSTS.RECEIVED_NEW,
|
||||
@@ -182,6 +188,77 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
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) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {removePost} from '@actions/local/post';
|
||||
import {retryFailedPost} from '@actions/remote/post';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
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 intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -49,7 +47,7 @@ const Failed = ({post, theme}: FailedProps) => {
|
||||
icon='send-outline'
|
||||
onPress={() => {
|
||||
dismissBottomSheet();
|
||||
retryPost(serverUrl, post);
|
||||
retryFailedPost(serverUrl, post);
|
||||
}}
|
||||
testID='post.failed.retry'
|
||||
text={intl.formatMessage({id: 'mobile.post.failed_retry', defaultMessage: 'Try Again'})}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {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 SystemAvatar from '@components/system_avatar';
|
||||
import SystemHeader from '@components/system_header';
|
||||
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||
import * as Screens from '@constants/screens';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
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 {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -114,6 +115,7 @@ const Post = ({
|
||||
const styles = getStyleSheet(theme);
|
||||
const isAutoResponder = fromAutoResponder(post);
|
||||
const isPendingOrFailed = isPostPendingOrFailed(post);
|
||||
const isFailed = isPostFailed(post);
|
||||
const isSystemPost = isSystemMessage(post);
|
||||
const isWebHook = isFromWebhook(post);
|
||||
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 hightlightPinned = post.isPinned && !skipPinnedHeader;
|
||||
const itemTestID = `${testID}.${post.id}`;
|
||||
|
||||
@@ -33,6 +33,8 @@ export const PostTypes: Record<string, string> = {
|
||||
SYSTEM_AUTO_RESPONDER: 'system_auto_responder',
|
||||
};
|
||||
|
||||
export const POST_TIME_TO_FAIL = 10000;
|
||||
|
||||
export default {
|
||||
POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5,
|
||||
POST_TYPES: PostTypes,
|
||||
|
||||
@@ -155,4 +155,25 @@ export default class PostModel extends Model implements PostModelInterface {
|
||||
|
||||
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 {extractFileInfo, lookupMimeType} from '@utils/file';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
const MattermostManaged = NativeModules.MattermostManaged;
|
||||
|
||||
type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo';
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class FilePickerUtil {
|
||||
files.push(file);
|
||||
} else {
|
||||
// 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);
|
||||
let fileName = file.fileName;
|
||||
if (type.includes('video/')) {
|
||||
@@ -224,7 +224,7 @@ export default class FilePickerUtil {
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// 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;
|
||||
if (uri === undefined) {
|
||||
return {doc: undefined};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Post} from '@constants';
|
||||
import {POST_TIME_TO_FAIL} from '@constants/post';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
return post.pendingPostId === post.id || post.props?.failed;
|
||||
return post.pendingPostId === post.id || isPostFailed(post);
|
||||
}
|
||||
|
||||
export function isSystemMessage(post: PostModel | Post): boolean {
|
||||
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -5,7 +5,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache 2.0",
|
||||
@@ -3074,7 +3073,7 @@
|
||||
},
|
||||
"node_modules/@mattermost/react-native-network-client": {
|
||||
"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",
|
||||
"dependencies": {
|
||||
"validator": "13.7.0",
|
||||
@@ -25689,7 +25688,7 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@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",
|
||||
"requires": {
|
||||
"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: () => Promise<boolean>;
|
||||
|
||||
toApi: () => Promise<Post>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user