ソースを参照

active links in description

Viktoriia 2 ヶ月 前
コミット
c272089f78
3 ファイル変更204 行追加5 行削除
  1. 2 1
      App.tsx
  2. 7 1
      app.config.ts
  3. 195 3
      src/components/Input/index.tsx

+ 2 - 1
App.tsx

@@ -67,7 +67,8 @@ const linking = {
   config: {
     screens: {
       publicProfileView: '/profile/:userId',
-      inAppEvent: '/event/:url'
+      inAppEvent: '/event/:url',
+      inAppMapTab: '/map/:lon/:lat'
     }
   }
 };

+ 7 - 1
app.config.ts

@@ -86,7 +86,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       NSLocationWhenInUseUsageDescription:
         'NomadMania app needs access to your location to show relevant data.',
       NSLocationAlwaysAndWhenInUseUsageDescription:
-        'NomadMania app needs access to your location to show relevant data.'
+        'NomadMania app needs access to your location to show relevant data.',
+      LSApplicationQueriesSchemes: ['comgooglemaps']
     },
     privacyManifests: {
       NSPrivacyAccessedAPITypes: [
@@ -117,6 +118,11 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
             scheme: 'https',
             host: 'nomadmania.com',
             pathPrefix: '/event/'
+          },
+          {
+            scheme: 'https',
+            host: 'nomadmania.com',
+            pathPrefix: '/map/'
           }
         ],
         category: ['BROWSABLE', 'DEFAULT']

+ 195 - 3
src/components/Input/index.tsx

@@ -6,10 +6,13 @@ import {
   InputModeOptions,
   NativeSyntheticEvent,
   TextInputFocusEventData,
-  TouchableOpacity
+  TouchableOpacity,
+  Linking,
+  Platform
 } from 'react-native';
 import { styling } from './style';
 import { Colors } from 'src/theme';
+import { GOOGLE_MAP_PLACES_APIKEY } from 'src/constants';
 
 type Props = {
   placeholder?: string;
@@ -31,6 +34,149 @@ type Props = {
   setValue?: (value: string) => void;
 };
 
+const parseTextWithLinks = (text?: string): React.ReactNode => {
+  if (!text) return null;
+
+  const urlRegex = /((https?:\/\/[^\s]+)|(?<!\w)(www\.[^\s]+))/g;
+  const eventPageRegex = /Event page\s+(https?:\/\/[^\s]+)\s+([^\n]+)/i;
+
+  const result: React.ReactNode[] = [];
+  let lastIndex = 0;
+
+  const eventMatch = text.match(eventPageRegex);
+  let handledEvent = false;
+
+  if (eventMatch) {
+    const [fullMatch, eventUrl, locationCandidate] = eventMatch;
+    const eventStart = eventMatch.index ?? 0;
+    const eventEnd = eventStart + fullMatch.length;
+
+    if (eventStart > 0) {
+      result.push(<Text key="text-before-event">{text.slice(0, eventStart)}</Text>);
+    }
+
+    result.push(
+      <Text
+        key="event-url"
+        style={{ color: Colors.ORANGE, textDecorationLine: 'underline' }}
+        onPress={() => Linking.openURL(eventUrl)}
+      >
+        {eventUrl}
+      </Text>
+    );
+
+    result.push(
+      <Text
+        key="event-location"
+        style={{ color: Colors.ORANGE, textDecorationLine: 'none' }}
+        onPress={() => openLocation(locationCandidate)}
+      >
+        {' ' + locationCandidate.trim()}
+      </Text>
+    );
+
+    lastIndex = eventEnd;
+    handledEvent = true;
+  }
+
+  Array.from(text.matchAll(urlRegex)).forEach((match, index) => {
+    const matchText = match[0];
+    const matchIndex = match.index ?? 0;
+
+    if (handledEvent && matchIndex < lastIndex) return;
+
+    if (lastIndex < matchIndex) {
+      result.push(<Text key={`text-${index}`}>{text.slice(lastIndex, matchIndex)}</Text>);
+    }
+
+    let url = matchText.startsWith('http') ? matchText : `https://${matchText}`;
+    const isGoogleMaps = /google\.com\/maps|maps\.google\.com/.test(url);
+
+    const handlePress = async () => {
+      if (isGoogleMaps) {
+        const latLngMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
+        const lat = latLngMatch?.[1];
+        const lng = latLngMatch?.[2];
+
+        let mapsUrl = url;
+
+        if (Platform.OS === 'ios') {
+          mapsUrl = lat && lng ? `comgooglemaps://?q=${lat},${lng}` : url;
+        } else {
+          mapsUrl = lat && lng ? `geo:${lat},${lng}?q=${lat},${lng}` : url;
+        }
+
+        const supported = await Linking.canOpenURL(mapsUrl);
+        if (supported) {
+          await Linking.openURL(mapsUrl);
+        } else {
+          await Linking.openURL(url);
+        }
+      } else {
+        await Linking.openURL(url);
+      }
+    };
+
+    result.push(
+      <Text
+        key={`link-${index}`}
+        style={{ color: Colors.ORANGE, textDecorationLine: 'underline' }}
+        onPress={handlePress}
+      >
+        {matchText}
+      </Text>
+    );
+
+    lastIndex = matchIndex + matchText.length;
+  });
+
+  if (lastIndex < text.length) {
+    result.push(<Text key="text-end">{text.slice(lastIndex)}</Text>);
+  }
+
+  return result;
+};
+
+const openLocation = async (address: string) => {
+  const endpoint =
+    `https://maps.googleapis.com/maps/api/place/findplacefromtext/json` +
+    `?input=${encodeURIComponent(address)}` +
+    `&inputtype=textquery` +
+    `&fields=geometry,formatted_address,name` +
+    `&key=${GOOGLE_MAP_PLACES_APIKEY}`;
+
+  try {
+    const response = await fetch(endpoint);
+    const data = await response.json();
+
+    if (data.status === 'OK' && data.candidates?.length > 0) {
+      const place = data.candidates[0];
+      const { lat, lng } = place.geometry.location;
+      const label = place.name || place.formatted_address || address;
+      const encodedLabel = encodeURIComponent(label);
+      const fallbackUrl = `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`;
+
+      let mapsUrl: string;
+
+      if (Platform.OS === 'ios') {
+        const canOpenGoogleMaps = await Linking.canOpenURL('comgooglemaps://');
+        mapsUrl = canOpenGoogleMaps
+          ? `comgooglemaps://?center=${lat},${lng}&q=${encodedLabel}@${lat},${lng}`
+          : `http://maps.apple.com/?ll=${lat},${lng}&q=${encodedLabel}`;
+      } else {
+        mapsUrl = `geo:${lat},${lng}?q=${encodedLabel}`;
+      }
+
+      const supported = await Linking.canOpenURL(mapsUrl);
+      await Linking.openURL(supported ? mapsUrl : fallbackUrl);
+    } else {
+      console.warn('Places API did not return valid results:', data.status);
+    }
+  } catch (error) {
+    console.error('Places API error:', error);
+  }
+};
+
 export const Input: FC<Props> = ({
   onChange,
   placeholder,
@@ -76,7 +222,53 @@ export const Input: FC<Props> = ({
             {icon}
           </View>
         ) : null}
-        <TextInput
+        {editable === false ? (
+          <Text
+            style={[
+              {
+                width: '100%',
+                flex: 1
+              },
+              !icon ? { padding: 10 } : null
+            ]}
+          >
+            {parseTextWithLinks(value)}
+          </Text>
+        ) : (
+          <TextInput
+            editable={editable}
+            value={value}
+            inputMode={inputMode ?? 'text'}
+            secureTextEntry={isPrivate ?? false}
+            placeholder={placeholder}
+            onChangeText={onChange}
+            multiline={multiline}
+            contextMenuHidden={inputMode === 'none'}
+            caretHidden={inputMode === 'none'}
+            onPress={() => {
+              setFocused(true);
+              if (isFocused) {
+                isFocused(true);
+              }
+            }}
+            onFocus={() => {
+              if (inputMode !== 'none') {
+                setFocused(true);
+                if (isFocused) {
+                  isFocused(true);
+                }
+              }
+            }}
+            onBlur={(e) => {
+              setFocused(false);
+              if (onBlur) {
+                onBlur(e);
+              }
+            }}
+            style={[{ height: '100%', width: '100%', flex: 1 }, !icon ? { padding: 10 } : null]}
+          />
+        )}
+        {/* <TextInput
           editable={editable}
           value={value}
           inputMode={inputMode ?? 'text'}
@@ -107,7 +299,7 @@ export const Input: FC<Props> = ({
             }
           }}
           style={[{ height: '100%', width: '100%', flex: 1 }, !icon ? { padding: 10 } : null]}
-        />
+        /> */}
         {clearIcon && value && value?.length > 0 ? (
           <TouchableOpacity
             style={styles.clearIcon}