diff --git a/lib/package.json b/lib/package.json index b1da94163..4dfca6375 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,67 +1,66 @@ { "name": "lib", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf node_modules/", "test": "jest" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "babel-jest": "^26.6.3", "clean-webpack-plugin": "^4.0.0", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "mini-css-extract-plugin": "^1.6.2", "optimize-css-assets-webpack-plugin": "^6.0.1", "react-refresh": "^0.14.0", "terser-webpack-plugin": "^4.2.3", "webpack": "^4.46.0" }, "dependencies": { "@rainbow-me/rainbowkit": "^0.5.0", "dateformat": "^3.0.3", "emoji-regex": "^10.2.1", "eth-ens-namehash": "^2.0.8", "ethers": "^5.7.2", "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", "react": "18.1.0", "react-icomoon": "^2.4.1", "react-redux": "^7.1.1", "reselect": "^4.0.0", "reselect-map": "^1.0.5", "simple-markdown": "^0.7.2", "string-hash": "^1.1.3", "tcomb": "^3.2.29", "siwe": "^1.1.6", "tinycolor2": "^1.4.1", "tokenize-text": "^1.1.3", - "url-parse-lax": "^3.0.0", "util-inspect": "^0.1.8", "utils-copy-error": "^1.0.1", "wagmi": "^0.6.0" }, "jest": { "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/lib/utils/url-utils.js b/lib/utils/url-utils.js index fa40e2007..6ddff755c 100644 --- a/lib/utils/url-utils.js +++ b/lib/utils/url-utils.js @@ -1,98 +1,92 @@ // @flow -import urlParseLax from 'url-parse-lax'; - import { pendingThreadIDRegex } from '../shared/thread-utils.js'; export type URLInfo = { +year?: number, +month?: number, // 1-indexed +verify?: string, +calendar?: boolean, +chat?: boolean, +apps?: boolean, +thread?: string, +settings?: 'account' | 'danger-zone', +threadCreation?: boolean, +selectedUserList?: $ReadOnlyArray, ... }; // We use groups to capture parts of the URL and any changes // to regexes must be reflected in infoFromURL. const yearRegex = new RegExp('(/|^)year/([0-9]+)(/|$)', 'i'); const monthRegex = new RegExp('(/|^)month/([0-9]+)(/|$)', 'i'); const threadRegex = new RegExp('(/|^)thread/([0-9]+)(/|$)', 'i'); const verifyRegex = new RegExp('(/|^)verify/([a-f0-9]+)(/|$)', 'i'); const calendarRegex = new RegExp('(/|^)calendar(/|$)', 'i'); const chatRegex = new RegExp('(/|^)chat(/|$)', 'i'); const appsRegex = new RegExp('(/|^)apps(/|$)', 'i'); const accountSettingsRegex = new RegExp('(/|^)settings/account(/|$)', 'i'); const dangerZoneRegex = new RegExp('(/|^)settings/danger-zone(/|$)', 'i'); const threadPendingRegex = new RegExp( `(/|^)thread/(${pendingThreadIDRegex})(/|$)`, 'i', ); const threadCreationRegex = new RegExp( '(/|^)thread/new(/([0-9]+([+][0-9]+)*))?(/|$)', 'i', ); function infoFromURL(url: string): URLInfo { const yearMatches = yearRegex.exec(url); const monthMatches = monthRegex.exec(url); const threadMatches = threadRegex.exec(url); const verifyMatches = verifyRegex.exec(url); const calendarTest = calendarRegex.test(url); const chatTest = chatRegex.test(url); const appsTest = appsRegex.test(url); const accountSettingsTest = accountSettingsRegex.test(url); const dangerZoneTest = dangerZoneRegex.test(url); const threadPendingMatches = threadPendingRegex.exec(url); const threadCreateMatches = threadCreationRegex.exec(url); const returnObj = {}; if (yearMatches) { returnObj.year = parseInt(yearMatches[2], 10); } if (monthMatches) { const month = parseInt(monthMatches[2], 10); if (month < 1 || month > 12) { throw new Error('invalid_month'); } returnObj.month = month; } if (threadMatches) { returnObj.thread = threadMatches[2]; } if (threadPendingMatches) { returnObj.thread = threadPendingMatches[2]; } if (threadCreateMatches) { returnObj.threadCreation = true; returnObj.selectedUserList = threadCreateMatches[3]?.split('+') ?? []; } if (verifyMatches) { returnObj.verify = verifyMatches[2]; } if (calendarTest) { returnObj.calendar = true; } else if (chatTest) { returnObj.chat = true; } else if (appsTest) { returnObj.apps = true; } else if (accountSettingsTest) { returnObj.settings = 'account'; } else if (dangerZoneTest) { returnObj.settings = 'danger-zone'; } return returnObj; } -function normalizeURL(url: string): string { - return urlParseLax(url).href; -} - const setURLPrefix = 'SET_URL_PREFIX'; -export { infoFromURL, normalizeURL, setURLPrefix }; +export { infoFromURL, setURLPrefix }; diff --git a/native/markdown/markdown-link.react.js b/native/markdown/markdown-link.react.js index 035042b19..af97489dc 100644 --- a/native/markdown/markdown-link.react.js +++ b/native/markdown/markdown-link.react.js @@ -1,91 +1,90 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, Linking, Alert } from 'react-native'; -import { normalizeURL } from 'lib/utils/url-utils.js'; - import { MarkdownContext, type MarkdownContextType, } from './markdown-context.js'; import { MarkdownSpoilerContext } from './markdown-spoiler-context.js'; import { MessagePressResponderContext } from '../chat/message-press-responder-context.js'; import { TextMessageMarkdownContext } from '../chat/text-message-markdown-context.js'; +import { normalizeURL } from '../utils/url-utils.js'; function useDisplayLinkPrompt( inputURL: string, markdownContext: MarkdownContextType, messageKey: ?string, ) { const { setLinkModalActive } = markdownContext; const onDismiss = React.useCallback(() => { messageKey && setLinkModalActive({ [messageKey]: false }); }, [setLinkModalActive, messageKey]); const url = normalizeURL(inputURL); const onConfirm = React.useCallback(() => { onDismiss(); Linking.openURL(url); }, [url, onDismiss]); let displayURL = url.substring(0, 64); if (url.length > displayURL.length) { displayURL += '…'; } return React.useCallback(() => { messageKey && setLinkModalActive({ [messageKey]: true }); Alert.alert( 'External link', `You sure you want to open this link?\n\n${displayURL}`, [ { text: 'Cancel', style: 'cancel', onPress: onDismiss }, { text: 'Open', onPress: onConfirm }, ], { cancelable: true, onDismiss }, ); }, [setLinkModalActive, messageKey, displayURL, onConfirm, onDismiss]); } type TextProps = React.ElementConfig; type Props = { +target: string, +children: React.Node, ...TextProps, }; function MarkdownLink(props: Props): React.Node { const { target, ...rest } = props; const markdownContext = React.useContext(MarkdownContext); invariant(markdownContext, 'MarkdownContext should be set'); const markdownSpoilerContext = React.useContext(MarkdownSpoilerContext); // Since MarkdownSpoilerContext may not be set, we need // to default isRevealed to true for when // we use the ternary operator in the onPress const isRevealed = markdownSpoilerContext?.isRevealed ?? true; const textMessageMarkdownContext = React.useContext( TextMessageMarkdownContext, ); const messageKey = textMessageMarkdownContext?.messageKey; const messagePressResponderContext = React.useContext( MessagePressResponderContext, ); const onPressMessage = messagePressResponderContext?.onPressMessage; const onPressLink = useDisplayLinkPrompt(target, markdownContext, messageKey); return ( ); } export default MarkdownLink; diff --git a/native/package.json b/native/package.json index 00842ed72..5512350df 100644 --- a/native/package.json +++ b/native/package.json @@ -1,127 +1,129 @@ { "name": "native", "version": "0.0.1", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "yarn clean-commoncpp && yarn clean-android && yarn clean-ios && rm -rf node_modules/ && rm -f ios/.xcode.env.local && (yarn clean-rust || true)", "clean-commoncpp": "rm -rf cpp/CommonCpp/build && rm -rf cpp/CommonCpp/CryptoTools/build && rm -rf cpp/CommonCpp/DatabaseManagers/build && rm -rf cpp/CommonCpp/NativeModules/build && rm -rf cpp/CommonCpp/Tools/build", "clean-rust": "cargo clean --manifest-path native_rust_library/Cargo.toml", "clean-android": "rm -rf android/build android/app/build android/app/.cxx", "clean-ios": "rm -rf ios/Pods/", "clean-all": "yarn clean && rm -rf ~/Library/Developer/Xcode/DerivedData/Comm-*; cd android && (./gradlew clean || true)", "start": "COMM_DEV=1 yarn expo start --dev-client", "dev": "yarn start", "test": "yarn jest", "run-ios": "COMM_DEV=1 yarn expo run:ios", "run-android": "COMM_DEV=1 yarn expo run:android", "logfirebase": "adb shell logcat | grep -E -i 'FIRMessagingModule|firebase'", "redux-devtools": "redux-devtools --port=8043 --open", "codegen-jsi": "flow && babel codegen/src/ -d codegen/dist/ && node codegen/dist", "react-native": "PATH=/usr/bin:\"$PATH\" react-native", "expo": "PATH=/usr/bin:\"$PATH\" expo" }, "devDependencies": { "@babel/cli": "^7.8.4", "@babel/core": "^7.13.14", "@babel/node": "^7.8.7", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/preset-flow": "^7.9.0", "@redux-devtools/cli": "^1.0.7", "babel-jest": "^26.6.3", "babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-strict-mode": "0.0.2", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "fs-extra": "^8.1.0", "googleapis": "^89.0.0", "internal-ip": "4.3.0", "jest": "^26.6.3", "jetifier": "^1.6.4", "jsonwebtoken": "^9.0.0", "metro-react-native-babel-preset": "^0.72.3", "react-devtools": "^4.27.0", "react-native-codegen": "^0.70.6", "react-test-renderer": "18.1.0", "remote-redux-devtools": "git+https://git@github.com/zalmoxisus/remote-redux-devtools.git", "remotedev": "git+https://git@github.com/zalmoxisus/remotedev.git" }, "dependencies": { "@commapp/android-lifecycle": "0.0.1", "@commapp/sqlcipher-amalgamation": "^4.4.3-a", "@ethersproject/shims": "^5.7.0", "@expo/react-native-action-sheet": "^3.14.0", "@expo/vector-icons": "^13.0.0", "@gorhom/bottom-sheet": "^4.4.5", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/art": "^1.2.0", "@react-native-community/netinfo": "^9.3.7", "@react-native-masked-view/masked-view": "^0.2.8", "@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/devtools": "^6.0.10", "@react-navigation/drawer": "^6.5.0", "@react-navigation/elements": "^1.3.6", "@react-navigation/material-top-tabs": "^6.3.0", "@react-navigation/native": "^6.0.13", "@react-navigation/stack": "^6.3.2", "base-64": "^0.1.0", "ethers": "^5.7.2", "expo": "47.0.8", "expo-dev-client": "~2.0.1", "expo-font": "~11.0.1", "expo-haptics": "~12.0.1", "expo-image-manipulator": "~11.0.0", "expo-image-picker": "~14.0.2", "expo-media-library": "~15.0.0", "expo-secure-store": "~12.0.0", "expo-splash-screen": "~0.17.4", "find-root": "^1.1.0", "invariant": "^2.2.4", "lib": "0.0.1", "lodash": "^4.17.21", "lottie-react-native": "^5.1.4", "md5": "^2.2.1", "olm": "git+https://gitlab.matrix.org/matrix-org/olm.git#v3.2.4", "react": "18.1.0", "react-native": "^0.70.6", "react-native-background-upload": "^6.6.0", "react-native-camera": "^3.31.0", "react-native-device-info": "^10.3.0", "react-native-exit-app": "^1.1.0", "react-native-fast-image": "^8.3.0", "react-native-ffmpeg": "^0.4.4", "react-native-figma-squircle": "^0.1.2", "react-native-floating-action": "^1.22.0", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.8.0", "react-native-in-app-message": "^1.0.2", "react-native-keyboard-input": "6.0.1", "react-native-keychain": "^8.0.0", "react-native-orientation-locker": "^1.5.0", "react-native-pager-view": "^6.0.1", "react-native-progress": "^4.1.2", "react-native-reanimated": "^2.12.0", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "^3.18.2", "react-native-svg": "^12.3.0", "react-native-tab-view": "^3.3.0", "react-native-video": "^5.2.1", "react-native-webview": "^11.23.0", "react-redux": "^7.1.1", "reactotron-react-native": "^5.0.3", "reactotron-redux": "^3.1.3", "redux": "^4.0.4", "redux-persist": "^6.0.0", "redux-thunk": "^2.2.0", "reselect": "^4.0.0", "rn-emoji-keyboard": "^1.2.0", "shallowequal": "^1.0.2", "simple-markdown": "^0.7.2", - "tinycolor2": "^1.4.1" + "tinycolor2": "^1.4.1", + "url": "^0.11.0", + "url-parse-lax": "^3.0.0" }, "jest": { "preset": "react-native" } } diff --git a/native/utils/url-utils.js b/native/utils/url-utils.js index 6fe2d3f13..5566b6b13 100644 --- a/native/utils/url-utils.js +++ b/native/utils/url-utils.js @@ -1,73 +1,79 @@ // @flow import invariant from 'invariant'; import { Platform } from 'react-native'; import DeviceInfo from 'react-native-device-info'; +import urlParseLax from 'url-parse-lax'; import { natDevHostname, checkForMissingNatDevHostname, } from './dev-hostname.js'; const localhostHostname = 'localhost'; const localhostHostnameFromAndroidEmulator = '10.0.2.2'; const productionNodeServerURL = 'https://squadcal.org'; const productionLandingURL = 'https://comm.app'; const devIsEmulator: boolean = __DEV__ && DeviceInfo.isEmulatorSync(); function getDevServerHostname(): string { if (!devIsEmulator) { checkForMissingNatDevHostname(); return natDevHostname; } else if (Platform.OS === 'android') { return localhostHostnameFromAndroidEmulator; } else if (Platform.OS === 'ios') { return localhostHostname; } invariant(false, `unsupported platform: ${Platform.OS}`); } function getDevNodeServerURLFromHostname(hostname: string): string { return `http://${hostname}:3000/comm`; } function getDevLandingURLFromHostname(hostname: string): string { return `http://${hostname}:3000/commlanding`; } function getDevNodeServerURL(): string { const hostname = getDevServerHostname(); return getDevNodeServerURLFromHostname(hostname); } function getDevLandingURL(): string { const hostname = getDevServerHostname(); return getDevLandingURLFromHostname(hostname); } const nodeServerOptions: string[] = [ productionNodeServerURL, getDevNodeServerURL(), ]; const defaultURLPrefix: string = __DEV__ ? getDevNodeServerURL() : productionNodeServerURL; const defaultLandingURLPrefix: string = __DEV__ ? getDevLandingURL() : productionLandingURL; const natNodeServer: string = getDevNodeServerURLFromHostname(natDevHostname); const setCustomServer = 'SET_CUSTOM_SERVER'; +function normalizeURL(url: string): string { + return urlParseLax(url).href; +} + export { defaultURLPrefix, defaultLandingURLPrefix, getDevServerHostname, nodeServerOptions, natNodeServer, setCustomServer, + normalizeURL, };