-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mobile drafts #8280
base: main
Are you sure you want to change the base?
Mobile drafts #8280
Changes from all commits
63b313f
11e758e
d72a19f
572d65b
d5389b8
ae55bde
afbe59d
6c99a7c
9fcbf84
d688908
324aa16
65b6edd
272c468
1fa189a
ba3ee8d
554123f
e629247
f8bcead
6577c6e
d331d46
62d0bbb
5308824
611cff0
aac1f38
8c153d6
112048a
2589127
a70b165
6f56a41
e140826
a85a1a4
c72c14d
f5b884a
cffb22b
b9c48d5
424c773
6f054ae
15e6bc3
faabf4e
9b130b6
c595c3d
b167354
eee6c26
47ac4fe
a842910
b26aa59
3d16827
5d7bc78
e667dd7
f152788
aa1ad72
f2e2e47
cd93248
8965394
8c8e6f6
be07659
ab7ce3a
2b5a671
22f1528
a542590
ea131a8
1ca7ddb
8a1952e
2941ab1
07ec62c
80681bd
9ea0914
ba7fb70
8d427db
2954843
d4c988b
2fca19c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,24 @@ | ||||||||||||||||||||||||||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||||||||||||||||||||||||||||
// See LICENSE.txt for license information. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import {DeviceEventEmitter, Image} from 'react-native'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import {Navigation, Screens} from '@constants'; | ||||||||||||||||||||||||||||||
import DatabaseManager from '@database/manager'; | ||||||||||||||||||||||||||||||
import {getDraft} from '@queries/servers/drafts'; | ||||||||||||||||||||||||||||||
import {goToScreen} from '@screens/navigation'; | ||||||||||||||||||||||||||||||
import {isTablet, isValidUrl} from '@utils/helpers'; | ||||||||||||||||||||||||||||||
import {logError} from '@utils/log'; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
export const switchToGlobalDrafts = async () => { | ||||||||||||||||||||||||||||||
const isTablelDevice = isTablet(); | ||||||||||||||||||||||||||||||
if (isTablelDevice) { | ||||||||||||||||||||||||||||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_DRAFTS); | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
goToScreen(Screens.GLOBAL_DRAFTS, '', {}, {topBar: {visible: false}}); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
export async function updateDraftFile(serverUrl: string, channelId: string, rootId: string, file: FileInfo, prepareRecordsOnly = false) { | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); | ||||||||||||||||||||||||||||||
|
@@ -197,3 +211,93 @@ export async function updateDraftPriority(serverUrl: string, channelId: string, | |||||||||||||||||||||||||||||
return {error}; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
export async function updateDraftMarkdownImageMetadata({ | ||||||||||||||||||||||||||||||
serverUrl, | ||||||||||||||||||||||||||||||
channelId, | ||||||||||||||||||||||||||||||
rootId, | ||||||||||||||||||||||||||||||
imageMetadata, | ||||||||||||||||||||||||||||||
prepareRecordsOnly = false, | ||||||||||||||||||||||||||||||
}: { | ||||||||||||||||||||||||||||||
serverUrl: string; | ||||||||||||||||||||||||||||||
channelId: string; | ||||||||||||||||||||||||||||||
rootId: string; | ||||||||||||||||||||||||||||||
imageMetadata: Dictionary<PostImage | undefined>; | ||||||||||||||||||||||||||||||
prepareRecordsOnly?: boolean; | ||||||||||||||||||||||||||||||
}) { | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); | ||||||||||||||||||||||||||||||
const draft = await getDraft(database, channelId, rootId); | ||||||||||||||||||||||||||||||
if (draft) { | ||||||||||||||||||||||||||||||
draft.prepareUpdate((d) => { | ||||||||||||||||||||||||||||||
d.metadata = { | ||||||||||||||||||||||||||||||
...d.metadata, | ||||||||||||||||||||||||||||||
images: imageMetadata, | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
d.updateAt = Date.now(); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
if (!prepareRecordsOnly) { | ||||||||||||||||||||||||||||||
await operator.batchRecords([draft], 'updateDraftImageMetadata'); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return {draft}; | ||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||
logError('Failed updateDraftImages', error); | ||||||||||||||||||||||||||||||
return {error}; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
async function getImageMetadata(url: string) { | ||||||||||||||||||||||||||||||
let height = 0; | ||||||||||||||||||||||||||||||
let width = 0; | ||||||||||||||||||||||||||||||
let format; | ||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||
await new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||
Image.getSize( | ||||||||||||||||||||||||||||||
url, | ||||||||||||||||||||||||||||||
(imageWidth, imageHeight) => { | ||||||||||||||||||||||||||||||
width = imageWidth; | ||||||||||||||||||||||||||||||
height = imageHeight; | ||||||||||||||||||||||||||||||
resolve(null); | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
(error) => { | ||||||||||||||||||||||||||||||
logError('Failed to get image size', error); | ||||||||||||||||||||||||||||||
reject(error); | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||
width = 0; | ||||||||||||||||||||||||||||||
height = 0; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
const match = url.match(/\.(\w+)(?=\?|$)/); | ||||||||||||||||||||||||||||||
if (match) { | ||||||||||||||||||||||||||||||
format = match[1]; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||
height, | ||||||||||||||||||||||||||||||
width, | ||||||||||||||||||||||||||||||
format, | ||||||||||||||||||||||||||||||
frame_count: 1, | ||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
export async function parseMarkdownImages(markdown: string, imageMetadata: Dictionary<PostImage | undefined>) { | ||||||||||||||||||||||||||||||
// Regex break down | ||||||||||||||||||||||||||||||
// ([a-zA-Z][a-zA-Z\d+\-.]*):\/\/ - Matches any valid scheme (protocol), such as http, https, ftp, mailto, file, etc. | ||||||||||||||||||||||||||||||
// [^\s()<>]+ - Matches the main part of the URL, excluding spaces, parentheses, and angle brackets. | ||||||||||||||||||||||||||||||
// (?:\([^\s()<>]+\))* - Allows balanced parentheses inside the URL path or query parameters. | ||||||||||||||||||||||||||||||
// !\[.*?\]\((...)\) - Matches an image markdown syntax ![alt text](image url) | ||||||||||||||||||||||||||||||
const imageRegex = /!\[.*?\]\((([a-zA-Z][a-zA-Z\d+\-.]*):\/\/[^\s()<>]+(?:\([^\s()<>]+\))*)\)/g; | ||||||||||||||||||||||||||||||
const matches = Array.from(markdown.matchAll(imageRegex)); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const promises = matches.map(async (match) => { | ||||||||||||||||||||||||||||||
const imageUrl = match[1]; | ||||||||||||||||||||||||||||||
if (isValidUrl(imageUrl)) { | ||||||||||||||||||||||||||||||
const metadata = await getImageMetadata(imageUrl); | ||||||||||||||||||||||||||||||
imageMetadata[imageUrl] = metadata; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
Comment on lines
+294
to
+300
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this does return anything so the promises will always be an array of undefined.
Suggested change
getImageMetadata should include the url in the return object so that you can map it after or something like that another option would be to use |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
await Promise.all(promises); | ||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||
// See LICENSE.txt for license information. | ||
|
||
import React, {useCallback} from 'react'; | ||
import {useIntl} from 'react-intl'; | ||
import {Keyboard, TouchableHighlight, View} from 'react-native'; | ||
|
||
import {switchToThread} from '@actions/local/thread'; | ||
import {switchToChannelById} from '@actions/remote/channel'; | ||
import DraftPost from '@components/draft/draft_post'; | ||
import DraftPostHeader from '@components/draft_post_header'; | ||
import Header from '@components/post_draft/draft_input/header'; | ||
import {Screens} from '@constants'; | ||
import {useServerUrl} from '@context/server'; | ||
import {useTheme} from '@context/theme'; | ||
import {useIsTablet} from '@hooks/device'; | ||
import {DRAFT_OPTIONS_BUTTON} from '@screens/draft_options'; | ||
import {openAsBottomSheet} from '@screens/navigation'; | ||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; | ||
|
||
import type ChannelModel from '@typings/database/models/servers/channel'; | ||
import type DraftModel from '@typings/database/models/servers/draft'; | ||
import type UserModel from '@typings/database/models/servers/user'; | ||
|
||
type Props = { | ||
channel: ChannelModel; | ||
location: string; | ||
draftReceiverUser?: UserModel; | ||
draft: DraftModel; | ||
layoutWidth: number; | ||
isPostPriorityEnabled: boolean; | ||
} | ||
|
||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { | ||
return { | ||
container: { | ||
paddingHorizontal: 20, | ||
paddingVertical: 16, | ||
width: '100%', | ||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.16), | ||
borderTopWidth: 1, | ||
}, | ||
pressInContainer: { | ||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16), | ||
}, | ||
postPriority: { | ||
marginTop: 10, | ||
marginLeft: -12, | ||
}, | ||
}; | ||
}); | ||
|
||
const Draft: React.FC<Props> = ({ | ||
channel, | ||
location, | ||
draft, | ||
draftReceiverUser, | ||
layoutWidth, | ||
isPostPriorityEnabled, | ||
}) => { | ||
const intl = useIntl(); | ||
const theme = useTheme(); | ||
const style = getStyleSheet(theme); | ||
const isTablet = useIsTablet(); | ||
const serverUrl = useServerUrl(); | ||
const showPostPriority = Boolean(isPostPriorityEnabled && draft.metadata?.priority && draft.metadata?.priority?.priority); | ||
|
||
const onLongPress = useCallback(() => { | ||
Keyboard.dismiss(); | ||
const title = isTablet ? intl.formatMessage({id: 'draft.options.title', defaultMessage: 'Draft Options'}) : 'Draft Options'; | ||
openAsBottomSheet({ | ||
closeButtonId: DRAFT_OPTIONS_BUTTON, | ||
screen: Screens.DRAFT_OPTIONS, | ||
theme, | ||
title, | ||
props: {channel, rootId: draft.rootId, draft, draftReceiverUserName: draftReceiverUser?.username}, | ||
}); | ||
}, [channel, draft, draftReceiverUser?.username, intl, isTablet, theme]); | ||
|
||
const onPress = useCallback(() => { | ||
if (draft.rootId) { | ||
switchToThread(serverUrl, draft.rootId, false); | ||
return; | ||
} | ||
switchToChannelById(serverUrl, channel.id, channel.teamId, false); | ||
}, [channel.id, channel.teamId, draft.rootId, serverUrl]); | ||
|
||
return ( | ||
<TouchableHighlight | ||
onLongPress={onLongPress} | ||
onPress={onPress} | ||
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)} | ||
testID='draft_post' | ||
> | ||
<View | ||
style={style.container} | ||
> | ||
<DraftPostHeader | ||
channel={channel} | ||
draftReceiverUser={draftReceiverUser} | ||
rootId={draft.rootId} | ||
testID='draft_post.channel_info' | ||
updateAt={draft.updateAt} | ||
/> | ||
{showPostPriority && draft.metadata?.priority && | ||
<View style={style.postPriority}> | ||
<Header | ||
noMentionsError={false} | ||
postPriority={draft.metadata?.priority} | ||
/> | ||
</View> | ||
} | ||
<DraftPost | ||
draft={draft} | ||
location={location} | ||
layoutWidth={layoutWidth} | ||
/> | ||
</View> | ||
|
||
</TouchableHighlight> | ||
); | ||
}; | ||
|
||
export default Draft; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||
// See LICENSE.txt for license information. | ||
|
||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react'; | ||
|
||
import Files from '@components/files/files'; | ||
import {observeCanDownloadFiles, observeConfigBooleanValue} from '@queries/servers/system'; | ||
|
||
import type {WithDatabaseArgs} from '@typings/database/database'; | ||
|
||
const enhance = withObservables([], ({database}: WithDatabaseArgs) => { | ||
return { | ||
canDownloadFiles: observeCanDownloadFiles(database), | ||
publicLinkEnabled: observeConfigBooleanValue(database, 'EnablePublicLink'), | ||
}; | ||
}); | ||
|
||
export default withDatabase(enhance(Files)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm, should this only allow http/https/ftp/ftps ? what about the extensions, do we want to do anything with that to only attempt those that are supported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0/5 on restricting to only http/https/ftp/ftps, since users can provide any valid URL, and as long as it returns an image, it should work. Also, I don't think extensions matter here because URLs don't always include extensions but can still return valid images.