diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index f16237245..36bcd7f04 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -71,6 +71,7 @@ type MarkdownProps = { textStyles?: MarkdownTextStyles; theme: Theme; value?: string; + onLinkLongPress?: (url?: string) => void; } const getStyleSheet = makeStyleSheetFromTheme((theme) => { @@ -133,7 +134,7 @@ const Markdown = ({ enableInlineLatex, enableLatex, maxNodes, imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth, location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns, - textStyles = {}, theme, value = '', baseParagraphStyle, + textStyles = {}, theme, value = '', baseParagraphStyle, onLinkLongPress, }: MarkdownProps) => { const style = getStyleSheet(theme); const managedConfig = useManagedConfig(); @@ -402,7 +403,10 @@ const Markdown = ({ const renderLink = ({children, href}: {children: ReactElement; href: string}) => { return ( - + {children} ); diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx index 64dee57b8..4cc662260 100644 --- a/app/components/markdown/markdown_link/markdown_link.tsx +++ b/app/components/markdown/markdown_link/markdown_link.tsx @@ -23,6 +23,7 @@ type MarkdownLinkProps = { experimentalNormalizeMarkdownLinks: string; href: string; siteURL: string; + onLinkLongPress?: (url?: string) => void; } const style = StyleSheet.create({ @@ -44,7 +45,7 @@ const parseLinkLiteral = (literal: string) => { return parsed.href; }; -const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL}: MarkdownLinkProps) => { +const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL, onLinkLongPress}: MarkdownLinkProps) => { const intl = useIntl(); const {bottom} = useSafeAreaInsets(); const managedConfig = useManagedConfig(); @@ -109,6 +110,11 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU const handleLongPress = useCallback(() => { if (managedConfig?.copyAndPasteProtection !== 'true') { + if (onLinkLongPress) { + onLinkLongPress(href); + return; + } + const renderContent = () => { return ( ({ }, })); +const style = StyleSheet.create({ + bottomSheet: { + flex: 1, + }, +}); + +const headerTestId = 'channel_info.extra.header'; + +const onCopy = async (text: string, isLink?: boolean) => { + Clipboard.setString(text); + await dismissBottomSheet(); + if ((Platform.OS === OS_VERSION.ANDROID && Number(Platform.Version) < ANDROID_33) || Platform.OS === OS_VERSION.IOS) { + showSnackBar({ + barType: isLink ? SNACK_BAR_TYPE.LINK_COPIED : SNACK_BAR_TYPE.TEXT_COPIED, + }); + } +}; + const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomStatusEnabled}: Props) => { + const intl = useIntl(); + const {bottom} = useSafeAreaInsets(); const theme = useTheme(); + const managedConfig = useManagedConfig(); + const styles = getStyleSheet(theme); const blockStyles = getMarkdownBlockStyles(theme); const textStyles = getMarkdownTextStyles(theme); @@ -81,6 +114,70 @@ const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomS ), }), [createdAt, createdBy, theme]); + const handleLongPress = useCallback((url?: string) => { + if (managedConfig?.copyAndPasteProtection !== 'true') { + const renderContent = () => ( + + { + onCopy(header!); + }} + testID={`${headerTestId}.bottom_sheet.copy_header_text`} + text={intl.formatMessage({ + id: 'mobile.markdown.copy_header', + defaultMessage: 'Copy header text', + })} + /> + {Boolean(url) && ( + { + onCopy(url!, true); + }} + testID={`${headerTestId}.bottom_sheet.copy_url`} + text={intl.formatMessage({ + id: 'mobile.markdown.link.copy_url', + defaultMessage: 'Copy URL', + })} + /> + )} + { + dismissBottomSheet(); + }} + testID={`${headerTestId}.bottom_sheet.cancel`} + text={intl.formatMessage({ + id: 'mobile.post.cancel', + defaultMessage: 'Cancel', + })} + /> + + ); + + bottomSheet({ + closeButtonId: 'close-markdown-link', + renderContent, + snapPoints: [1, bottomSheetSnapPoint(url ? 3 : 2, ITEM_HEIGHT, bottom)], + title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}), + theme, + }); + } + }, [ + header, + bottom, + theme, + intl.formatMessage, + managedConfig?.copyAndPasteProtection, + ]); + + const touchableHandleLongPress = useCallback(() => handleLongPress(), [handleLongPress]); + return ( {isCustomStatusEnabled && Boolean(customStatus) && @@ -131,25 +228,32 @@ const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomS id='channel_info.header' defaultMessage='Header:' style={styles.extraHeading} - testID='channel_info.extra.header' - /> - + + + } {Boolean(createdAt && createdBy) && diff --git a/app/screens/home/channel_list/servers/index.tsx b/app/screens/home/channel_list/servers/index.tsx index a1135ce4f..307c9a5af 100644 --- a/app/screens/home/channel_list/servers/index.tsx +++ b/app/screens/home/channel_list/servers/index.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; -import {StyleSheet} from 'react-native'; +import {Dimensions, StyleSheet} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import ServerIcon from '@components/server_icon'; @@ -27,7 +27,8 @@ export type ServersRef = { openServers: () => void; } -export const SERVER_ITEM_HEIGHT = 72; +export const SERVER_ITEM_HEIGHT = 75; +export const PUSH_ALERT_TEXT_HEIGHT = 42; const subscriptions: Map = new Map(); const styles = StyleSheet.create({ @@ -116,12 +117,18 @@ const Servers = React.forwardRef((_, ref) => { ); }; + const maxScreenHeight = Math.ceil(0.6 * Dimensions.get('window').height); + const maxSnapPoint = Math.min( + maxScreenHeight, + bottomSheetSnapPoint(registeredServers.current.length, SERVER_ITEM_HEIGHT, bottom) + TITLE_HEIGHT + BUTTON_HEIGHT + + (registeredServers.current.filter((s: ServersModel) => s.lastActiveAt).length * PUSH_ALERT_TEXT_HEIGHT), + ); const snapPoints: BottomSheetProps['snapPoints'] = [ 1, - bottomSheetSnapPoint(Math.min(2.5, registeredServers.current.length), 72, bottom) + TITLE_HEIGHT + BUTTON_HEIGHT, + maxSnapPoint, ]; - if (registeredServers.current.length > 1) { + if (maxSnapPoint === maxScreenHeight) { snapPoints.push('80%'); } diff --git a/app/screens/home/channel_list/servers/servers_list/index.tsx b/app/screens/home/channel_list/servers/servers_list/index.tsx index 22cac4f22..ead1bf96d 100644 --- a/app/screens/home/channel_list/servers/servers_list/index.tsx +++ b/app/screens/home/channel_list/servers/servers_list/index.tsx @@ -4,11 +4,12 @@ import {BottomSheetFlatList} from '@gorhom/bottom-sheet'; import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {FlatList, type ListRenderItemInfo, StyleSheet, View} from 'react-native'; +import {FlatList, StyleSheet, View, type ListRenderItemInfo} from 'react-native'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {BUTTON_HEIGHT} from '@screens/bottom_sheet'; import BottomSheetContent from '@screens/bottom_sheet/content'; import {addNewServer} from '@utils/server'; @@ -29,6 +30,9 @@ const styles = StyleSheet.create({ contentContainer: { marginVertical: 4, }, + serverList: { + marginBottom: BUTTON_HEIGHT, + }, }); const keyExtractor = (item: ServersModel) => item.url; @@ -68,6 +72,7 @@ const ServerList = ({servers}: Props) => { theme, [/* dependency array should remain empty */]); + const [newTheme, setNewTheme] = useState(undefined); const close = () => popTopScreen(componentId); - const setThemePreference = useCallback((newTheme?: string) => { - const allowedTheme = allowedThemeKeys.find((tk) => tk === newTheme); + useEffect(() => { const differentTheme = theme.type?.toLowerCase() !== newTheme?.toLowerCase(); if (!differentTheme) { close(); return; } + setThemePreference(); + }, [newTheme]); + + const setThemePreference = useCallback(() => { + const allowedTheme = allowedThemeKeys.find((tk) => tk === newTheme); const themeJson = Preferences.THEMES[allowedTheme as ThemeKey] || initialTheme; @@ -47,15 +52,20 @@ const DisplayTheme = ({allowedThemeKeys, componentId, currentTeamId, currentUser value: JSON.stringify(themeJson), }; savePreference(serverUrl, [pref]); - }, [allowedThemeKeys, currentTeamId, theme.type, serverUrl]); + }, [allowedThemeKeys, currentTeamId, theme.type, serverUrl, newTheme]); - useAndroidHardwareBackHandler(componentId, setThemePreference); + const onAndroidBack = () => { + setThemePreference(); + close(); + }; + + useAndroidHardwareBackHandler(componentId, onAndroidBack); return ( {initialTheme.type === 'custom' && ( diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 7ca1f1df3..2431004e3 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -613,6 +613,7 @@ "mobile.managed.settings": "Go to settings", "mobile.markdown.code.copy_code": "Copy Code", "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}", + "mobile.markdown.copy_header": "Copy header text", "mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:", "mobile.markdown.link.copy_url": "Copy URL", "mobile.mention.copy_mention": "Copy Mention", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 68f0f873a..0157d7ea5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -385,7 +385,7 @@ PODS: - React-Core - Starscream (~> 4.0.4) - SwiftyJSON (~> 5.0) - - react-native-notifications (5.0.0): + - react-native-notifications (5.1.0): - React-Core - react-native-paste-input (0.6.4): - React-Core @@ -946,7 +946,7 @@ SPEC CHECKSUMS: react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5 react-native-network-client: 953ab4d0914fdde6dc40924b5faad8631a587d4e - react-native-notifications: d309f7080aad71206882dbb98d9ed788969f3b6d + react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4 react-native-paste-input: 09f14cbfb646ad9d2ef79cdc6f3d45f337c10ad1 react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2 react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 @@ -1001,4 +1001,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 25f07cb9e5eed8c84db8e8723000e8470c349058 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.2 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index db9a2498a..959587a96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "mattermost-mobile", - "version": "2.9.0", + "version": "2.10.0", "hasInstallScript": true, "license": "Apache 2.0", "dependencies": { @@ -83,7 +83,7 @@ "react-native-localize": "3.0.2", "react-native-math-view": "3.9.5", "react-native-navigation": "7.37.0", - "react-native-notifications": "5.0.0", + "react-native-notifications": "5.1.0", "react-native-permissions": "3.8.4", "react-native-reanimated": "3.4.2", "react-native-safe-area-context": "4.7.1", @@ -19544,9 +19544,9 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "node_modules/react-native-notifications": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-5.0.0.tgz", - "integrity": "sha512-QXtBBmbDtwq9X8WAPLn+OctIeEtnJOQ+RCT6iweaypvFTydt2baLPtawTAbCSXKuWpVDqDAdmZnlQjCcavNzoA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-5.1.0.tgz", + "integrity": "sha512-laqDSDlCvEASmJR6cXpqaryK855ejQd07vrfYERzhv68YDOoSkKy/URExRP4vAfAOVqHhix80tLbNUcfvZk2VQ==", "peerDependencies": { "react": "*", "react-native": "*" @@ -28574,8 +28574,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz", "integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==", - "requires": { - } + "requires": {} }, "@webassemblyjs/ast": { "version": "1.11.1", @@ -37641,9 +37640,9 @@ } }, "react-native-notifications": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-5.0.0.tgz", - "integrity": "sha512-QXtBBmbDtwq9X8WAPLn+OctIeEtnJOQ+RCT6iweaypvFTydt2baLPtawTAbCSXKuWpVDqDAdmZnlQjCcavNzoA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-5.1.0.tgz", + "integrity": "sha512-laqDSDlCvEASmJR6cXpqaryK855ejQd07vrfYERzhv68YDOoSkKy/URExRP4vAfAOVqHhix80tLbNUcfvZk2VQ==", "requires": {} }, "react-native-permissions": { diff --git a/package.json b/package.json index 16cabecc4..e889b980c 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "react-native-localize": "3.0.2", "react-native-math-view": "3.9.5", "react-native-navigation": "7.37.0", - "react-native-notifications": "5.0.0", + "react-native-notifications": "5.1.0", "react-native-permissions": "3.8.4", "react-native-reanimated": "3.4.2", "react-native-safe-area-context": "4.7.1", diff --git a/patches/react-native-notifications+5.0.0.patch b/patches/react-native-notifications+5.1.0.patch similarity index 97% rename from patches/react-native-notifications+5.0.0.patch rename to patches/react-native-notifications+5.1.0.patch index e652f3263..dcb96e769 100644 --- a/patches/react-native-notifications+5.0.0.patch +++ b/patches/react-native-notifications+5.1.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-notifications/lib/android/app/build.gradle b/node_modules/react-native-notifications/lib/android/app/build.gradle -index d049e84..9ad7004 100644 +index 30bb01c..bba788d 100644 --- a/node_modules/react-native-notifications/lib/android/app/build.gradle +++ b/node_modules/react-native-notifications/lib/android/app/build.gradle @@ -96,9 +96,9 @@ android { @@ -7,10 +7,10 @@ index d049e84..9ad7004 100644 testOptions { unitTests.all { t -> - reports { -- html.enabled true +- html.required.set true - } + // reports { -+ // html.enabled true ++ // html.required.set true + // } testLogging { events "PASSED", "SKIPPED", "FAILED", "standardOut", "standardError" @@ -90,10 +90,10 @@ index 5b7f15f..9381794 100644 } } diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java -index 1e7e871..62e5cb8 100644 +index 1e7e871..36b96b6 100644 --- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java +++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java -@@ -14,17 +14,9 @@ public class NotificationIntentAdapter { +@@ -14,17 +14,11 @@ public class NotificationIntentAdapter { @SuppressLint("UnspecifiedImmutableFlag") public static PendingIntent createPendingNotificationIntent(Context appContext, PushNotificationProps notification) { @@ -108,9 +108,11 @@ index 1e7e871..62e5cb8 100644 - taskStackBuilder.addNextIntentWithParentStack(mainActivityIntent); - return taskStackBuilder.getPendingIntent((int) System.currentTimeMillis(), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); - } -+ Intent intent = appContext.getPackageManager().getLaunchIntentForPackage(appContext.getPackageName()); -+ intent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, notification.asBundle()); -+ return PendingIntent.getActivity(appContext, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); ++ Intent mainActivityIntent = appContext.getPackageManager().getLaunchIntentForPackage(appContext.getPackageName()); ++ mainActivityIntent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, notification.asBundle()); ++ TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(appContext); ++ taskStackBuilder.addNextIntentWithParentStack(mainActivityIntent); ++ return taskStackBuilder.getPendingIntent((int) System.currentTimeMillis(), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); } public static boolean canHandleTrampolineActivity(Context appContext) {