-
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
Conversation
/update-branch |
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.
Great work! Some comments.
/update-branch |
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.
Thank you @Rajat-Dabade
Most flows around
- Creating Draft in DM, GM, Channel, Threads, Private Chanel, under deleted thread ✅
- Sending Threads, Editing threads work as expected. ✅
There couple of flow I wanted to bring to your notice
-
On the Draft Screen, if we hit on Android's back button (not the back arrow of MM app) the app closes instead of going back to channels list page
-
Once all drafts are sent out from the draft page, we have an empty page looking like below. would it be nice to add a 'No drafts at the moment' icon, like in web app. @asaadmahmood thoughts?
Thank you @yasserfaraazkhan for reviewing the code.
Let me check on this.
I remember having an empty screen for the draft view, let me add it. |
Can you please review again? Thanks. |
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.
Thank you @Rajat-Dabade
just 1 last change, in below flow
Scenarios 1:
Steps:
- From Draft page, Edit a draft. Verify you are taken to the Channel/DM/GM
- Edit the Draft in the Channel
- Click on the
<
back Icon on the channel
This is taking user to Draft page
. should it take user to Channels List page?
Scenarios 2:
With above steps, if we edit draft message (without sending it) and Click back button, the draft message is not updated.
Screen.Recording.2024-11-19.at.12.16.41.AM.mov
Scenario 3:
- The User group Name is shown like below
on web app it show as below
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.
Overall looks good to me, minor changes though.
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.
Verified on android and iOS device
- Drafts created in Channel/DM/GM
- send/edit/delete actions on drafts
- swipe left on draft to delete
- drafts on public channel and then convert
- Remove server that has drafts and add it back to see no drafts are present
- Theme applied on the page.
- Drafts on multiple teams
- Draft count updated when we leave channel, archive channel
Few observations,
- On android where we can see channels list page for fraction of a second, when we click on a particular Draft
az_recorder_20241211_150748.mp4
- Theme issues when Device is in Dark mode.
Steps:
- select Onxy (dark theme)
- create Draft in channel ,
- change to Denim theme
- go to channel and see the theme is applied
- go to Draft, long press draft, Click on Edit
Actual: we see Dark theme section
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.
Overall looks good, great job!
I have added a few comments
app/actions/local/draft.ts
Outdated
|
||
export async function parseMarkdownImages(markdown: string, imageMetadata: Dictionary<PostImage | undefined>) { | ||
let match; | ||
const imageRegex = /!\[.*?\]\((https:\/\/[^\s)]+)\)/g; |
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.
If the url is only with http this will not match. Is that what we want?
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.
This is absolutely a nice catch, I will update the regex. Thank you 🫡 .
app/actions/local/draft.ts
Outdated
while ((match = imageRegex.exec(markdown)) !== null) { | ||
const imageUrl = match[1]; | ||
// eslint-disable-next-line no-await-in-loop | ||
imageMetadata[imageUrl] = await getImageMetadata(imageUrl); |
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.
This looks prone to race
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.
So what I understand from the race condition you pointed out is the use of await
within the loop, which processes each imageUrl sequentially rather than concurrently which can lead to inefficiencies, especially if there are many URLs and the processing of one URL depends on the others completing.
Is this what you mean?
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.
Basically it can complete iterating while the valus on the object have not been set yet
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.
I have updated the code which ensures that the iteration waits for all asynchronous operation completes before proceeding. Promise.all
guarantees that the imageMetadata
object is updated after all getImageMetadata
calls are resolved.
const serverUrl = useServerUrl(); | ||
|
||
let uri = ''; | ||
if (author) { |
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.
Based on the prop, author should always be defined, something I'm missing?
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.
True, we don't need this check here. Will remove it.
import {typography} from '@utils/typography'; | ||
import {getUserTimezone} from '@utils/user'; | ||
|
||
import CompassIcon from '../compass_icon'; |
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.
import CompassIcon from '../compass_icon'; | |
import CompassIcon from '@components/compass_icon'; |
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.
The name of the folder does not make a lot of sense to me
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.
Changed the name to ProfileAavator, I think it makes more sense now.
|
||
return ( | ||
<View | ||
style={{flex: 1}} |
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.
Set this in the stylesheet
size={ICON_SIZE} | ||
color={changeOpacity(theme.centerChannelColor, 0.56)} | ||
/> | ||
<Text style={style.title}>{intl.formatMessage({id: 'draft.options.send.title', defaultMessage: 'Edit draft'})}</Text> |
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.
Use FormattedText
|
||
import React from 'react'; | ||
import {useIntl} from 'react-intl'; | ||
import {Image, Text, View} from 'react-native'; |
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.
Somewhere else you are using Image from expo-image for the same purpose, which makes sense, change it here
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.
Done.
onClose: () => void; | ||
} | ||
|
||
const logo = require('@assets/images/emojis/swipe.png'); |
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.
Perhaps change the name of the variable so that it makes sense instead of logo
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.
Changed it.
|
||
import type {AvailableScreens} from '@typings/screens/navigation'; | ||
|
||
const edges: Edge[] = ['left', 'right']; |
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.
Why not include bottom? Is it added elsewhere? I see top in the styles but not bottom. Or is it that we don't want it at all?
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.
This is what the design suggests https://www.figma.com/design/VbEx15Tpvi9mXhs9IEjJtq/MM-45558-Drafts-%26-Schedule-Messages?node-id=4013-70751&m=dev, also we have not used it in the Threads View.
// [^\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; |
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?
const promises = matches.map(async (match) => { | ||
const imageUrl = match[1]; | ||
if (isValidUrl(imageUrl)) { | ||
const metadata = await getImageMetadata(imageUrl); | ||
imageMetadata[imageUrl] = metadata; | ||
} | ||
}); |
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.
this does return anything so the promises will always be an array of undefined.
const promises = matches.map(async (match) => { | |
const imageUrl = match[1]; | |
if (isValidUrl(imageUrl)) { | |
const metadata = await getImageMetadata(imageUrl); | |
imageMetadata[imageUrl] = metadata; | |
} | |
}); | |
const promises = matches.reduce<Array<Promise<MetadataReturnedByGetImageImagedataType>>((result, match) => { | |
const imageUrl = match[1]; | |
if (isValidUrl(imageUrl)) { | |
result.push(getImageMetadata(imageUrl)); | |
} | |
return result; | |
}, []); |
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 for await
but this will make it sequential and not parallel
const theme = useTheme(); | ||
const styles = getStyleSheet(theme); | ||
const snapPoints = useMemo(() => { | ||
const bottomSheetAdjust = Platform.select({ios: 5, default: 20}); |
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.
what is this adjust and why is it needed ?
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.
SwipeableDraft
name convention does not match
@@ -182,3 +182,12 @@ export function areBothStringArraysEqual(a: string[], b: string[]) { | |||
|
|||
return areBothEqual; | |||
} | |||
|
|||
export function isValidUrl(url: string): boolean { |
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.
there is an isValidUrl
function in app/utils/url
which may cause some confusion to devs, either be more specific, use or modify the one we already have. Also if you decide to keep this and change the name, make sure to at least move it the right file, and do add test coverage for it
app/utils/draft/index.ts
Outdated
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.
add unit test for code coverage
app/utils/post/index.ts
Outdated
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.
add unit test for code coverage
Summary
Separate tab for Drafts in mobile.
Ticket Link
https://mattermost.atlassian.net/browse/MM-39356
Checklist
E2E iOS tests for PR
.Device Information
This PR was tested on: iOS version 17.5 (Both on iPhone and ipad)
Screenshots
Release Note
Note:
Will cover all the test case for this PR in separate PR.