react-native#NativeSyntheticEvent TypeScript Examples

The following examples show how to use react-native#NativeSyntheticEvent. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: TextInput.tsx    From companion-kit with MIT License 7 votes vote down vote up
_onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
        const { model, onBlur } = this.props;

        if (model) {
            model.focused = false;
        }

        if (onBlur) {
            onBlur(e);
        }
    }
Example #2
Source File: TextField.tsx    From natds-rn with ISC License 7 votes vote down vote up
statusActiveHandler = (
  event: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void,
  nativeEvent: NativeSyntheticEvent<TextInputFocusEventData>,
  status: boolean,
  setActive: Dispatch<SetStateAction<boolean>>
) => {
  setActive(status)
  if (event) event(nativeEvent)
}
Example #3
Source File: TextField.tsx    From react-native-jigsaw with MIT License 7 votes vote down vote up
_handleChangeText = (
    value: NativeSyntheticEvent<TextInputChangeEventData> | string
  ) => {
    if (this.props.disabled) {
      return;
    }

    if (typeof value === "string") {
      this.setState({ value });
      this.props.onChangeText && this.props.onChangeText(value);
    } else {
      this.setState({ value: value.nativeEvent.text });
      this.props.onChangeText &&
        this.props.onChangeText(value.nativeEvent.text);
    }
  };
Example #4
Source File: index.tsx    From react-native-actions-sheet with MIT License 7 votes vote down vote up
_onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    this.offsetY = event.nativeEvent.contentOffset.y;

    let correction = this.state.deviceHeight * 0.15;
    let distanceFromTop = this.actionSheetHeight + correction - this.offsetY;

    if (this.actionSheetHeight < this.offsetY) {
      if (!this.isReachedTop) {
        this.isReachedTop = true;
        this.props.onPositionChanged && this.props.onPositionChanged(true);
      }
    } else {
      if (this.isReachedTop) {
        this.isReachedTop = false;
        this.props.onPositionChanged && this.props.onPositionChanged(false);
      }
    }

    if (this.actionSheetHeight >= this.state.deviceHeight - 1) {
      if (distanceFromTop < this.state.paddingTop) {
        if (!this.props.drawUnderStatusBar) return;

        this.indicatorTranslateY.setValue(
          -this.state.paddingTop + (this.state.paddingTop - distanceFromTop)
        );
      } else {
        this.indicatorTranslateY.setValue(-this.state.paddingTop);
      }
    }
  };
Example #5
Source File: index.d.ts    From react-native-actions-sheet with MIT License 6 votes vote down vote up
_onScrollBegin: (_event: NativeSyntheticEvent<NativeScrollEvent>) => Promise<void>;
Example #6
Source File: index.d.ts    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
Example #7
Source File: index.tsx    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScrollBegin = async (
    _event: NativeSyntheticEvent<NativeScrollEvent>
  ) => {};
Example #8
Source File: index.tsx    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScrollBeginDrag = async (
    event: NativeSyntheticEvent<NativeScrollEvent>
  ) => {
    let verticalOffset = event.nativeEvent.contentOffset.y;
    this.prevScroll = verticalOffset;
  };
Example #9
Source File: index.tsx    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScrollEnd = async (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    let { springOffset, extraScroll } = this.props;
    let verticalOffset = event.nativeEvent.contentOffset.y;
    let correction = this.state.deviceHeight * 0.15;
    if (this.isRecoiling) return;

    if (this.prevScroll < verticalOffset || this.initialScrolling) {
      if (
        verticalOffset - this.prevScroll > (springOffset ?? 100) * 0.75 ||
        this.initialScrolling
      ) {
        this.isRecoiling = true;
        this._applyHeightLimiter();
        this.currentOffsetFromBottom =
          this.currentOffsetFromBottom <
          (this.props.initialOffsetFromBottom ?? 1)
            ? this.props.initialOffsetFromBottom ?? 1
            : 1;
        let scrollOffset =
          this.actionSheetHeight * this.currentOffsetFromBottom +
          correction +
          (extraScroll ?? 100);

        if (this.initialScrolling) {
          this.initialScrolling = false;
          scrollOffset = this.prevScroll;
        }

        this._scrollTo(scrollOffset);
        await waitAsync(300);
        this.isRecoiling = false;
        this.props.onPositionChanged && this.props.onPositionChanged(true);
      } else {
        this._returnToPrevScrollPosition(this.actionSheetHeight);
      }
    } else {
      if (this.prevScroll - verticalOffset > (springOffset ?? 100)) {
        this._hideModal(null);
      } else {
        if (this.isRecoiling) {
          return;
        }
        this.isRecoiling = true;
        this._returnToPrevScrollPosition(this.actionSheetHeight);
        await waitAsync(300);
        this.isRecoiling = false;
      }
    }
  };
Example #10
Source File: index.d.ts    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => Promise<void>;
Example #11
Source File: index.d.ts    From react-native-actions-sheet with MIT License 5 votes vote down vote up
_onScrollBeginDrag: (event: NativeSyntheticEvent<NativeScrollEvent>) => Promise<void>;
Example #12
Source File: AddToWalletButton.tsx    From stripe-react-native with MIT License 5 votes vote down vote up
/**
 *  Add to wallet button
 *
 * @example
 * ```ts
 *  <AddToWalletButton
 *    testEnv={true}
 *    style={styles.myButtonStyle}
 *    iOSButtonStyle="onLightBackground"
 *    cardDetails={{
 *      primaryAccountIdentifier: "V-123",
 *      name: "David Wallace",
 *      lastFour: "4242",
 *    }}
 *    ephemeralKey={myEphemeralKey} // This object is retrieved from your server. See https://stripe.com/docs/issuing/cards/digital-wallets?platform=react-native#update-your-backend
 *    onComplete={(error) => {
 *      Alert.alert(
 *        error ? error.code : 'Success',
 *        error
 *          ? error.message
 *          : 'Card was successfully added to the wallet.'
 *      );
 *    }}
 *  />
 * ```
 * @param __namedParameters Props
 * @returns JSX.Element
 * @category ReactComponents
 */
export function AddToWalletButton({ onComplete, ...props }: Props) {
  return (
    <AddToWalletButtonNative
      {...props}
      onCompleteAction={(
        value: NativeSyntheticEvent<{
          error: StripeError<CardActionError> | null;
        }>
      ) => onComplete(value.nativeEvent)}
    />
  );
}
Example #13
Source File: Swiper.native.tsx    From react-native-paper-dates with MIT License 4 votes vote down vote up
function SwiperInner({
  scrollMode,
  renderItem,
  renderHeader,
  renderFooter,
  selectedYear,
  initialIndex,
  width,
  height,
}: SwiperProps & { width: number; height: number }) {
  const idx = React.useRef<number>(initialIndex)
  const isHorizontal = scrollMode === 'horizontal'
  const [visibleIndexes, setVisibleIndexes] = React.useState<number[]>(
    getVisibleArray(initialIndex, { isHorizontal, height })
  )

  const parentRef = React.useRef<ScrollView | null>(null)

  const scrollTo = React.useCallback(
    (index: number, animated: boolean) => {
      idx.current = index
      setVisibleIndexes(getVisibleArray(index, { isHorizontal, height }))

      if (!parentRef.current) {
        return
      }
      const offset = isHorizontal
        ? getHorizontalMonthOffset(index, width)
        : getVerticalMonthsOffset(index) - montHeaderHeight

      if (isHorizontal) {
        parentRef.current.scrollTo({
          y: 0,
          x: offset,
          animated,
        })
      } else {
        parentRef.current.scrollTo({
          y: offset,
          x: 0,
          animated,
        })
      }
    },
    [parentRef, isHorizontal, width, height]
  )

  const onPrev = React.useCallback(() => {
    scrollTo(idx.current - 1, true)
  }, [scrollTo, idx])

  const onNext = React.useCallback(() => {
    scrollTo(idx.current + 1, true)
  }, [scrollTo, idx])

  const scrollToInitial = React.useCallback(() => {
    scrollTo(idx.current, false)
  }, [scrollTo])

  const onMomentumScrollEnd = React.useCallback(
    (e: NativeSyntheticEvent<NativeScrollEvent>) => {
      const contentOffset = e.nativeEvent.contentOffset
      const viewSize = e.nativeEvent.layoutMeasurement
      const newIndex = isHorizontal
        ? Math.floor(contentOffset.x / viewSize.width)
        : getIndexFromVerticalOffset(contentOffset.y - beginOffset)

      if (newIndex === 0) {
        return
      }

      if (idx.current !== newIndex) {
        idx.current = newIndex
        setVisibleIndexes(getVisibleArray(newIndex, { isHorizontal, height }))
      }
    },
    [idx, height, isHorizontal]
  )

  const renderProps = {
    index: 0,
    onPrev,
    onNext,
  }

  useYearChange(
    (newIndex) => {
      if (newIndex) {
        scrollTo(newIndex, false)
      }
    },
    {
      selectedYear,
      currentIndexRef: idx,
    }
  )

  return (
    <>
      <ScrollView
        ref={parentRef}
        horizontal={isHorizontal}
        pagingEnabled={isHorizontal}
        style={styles.viewPager}
        onMomentumScrollEnd={onMomentumScrollEnd}
        onScrollEndDrag={onMomentumScrollEnd}
        onLayout={scrollToInitial}
        showsHorizontalScrollIndicator={false}
        showsVerticalScrollIndicator={false}
        decelerationRate="fast"
        scrollEventThrottle={10}
      >
        <View
          style={[
            styles.inner,
            {
              height: isHorizontal
                ? height
                : estimatedMonthHeight * totalMonths,
              width: isHorizontal ? width * totalMonths : width,
            },
          ]}
        >
          {visibleIndexes
            ? new Array(visibleIndexes.length).fill(undefined).map((_, vi) => (
                <View
                  key={vi}
                  style={{
                    top: isHorizontal
                      ? 0
                      : getVerticalMonthsOffset(visibleIndexes[vi]),
                    left: isHorizontal
                      ? getHorizontalMonthOffset(visibleIndexes[vi], width)
                      : 0,
                    right: isHorizontal ? undefined : 0,
                    bottom: isHorizontal ? 0 : undefined,
                    position: 'absolute',
                    width: isHorizontal ? width : undefined,
                    height: isHorizontal
                      ? undefined
                      : getMonthHeight(scrollMode, visibleIndexes[vi]),
                  }}
                >
                  {renderItem({
                    index: visibleIndexes[vi],
                    onPrev: onPrev,
                    onNext: onNext,
                  })}
                </View>
              ))
            : null}
        </View>
      </ScrollView>
      {renderHeader && renderHeader(renderProps)}
      {renderFooter && renderFooter(renderProps)}
    </>
  )
}
Example #14
Source File: CardForm.tsx    From stripe-react-native with MIT License 4 votes vote down vote up
CardForm = forwardRef<CardFormView.Methods, Props>(
  (
    {
      onFormComplete,
      cardStyle,
      // isUserInteractionEnabled = true,
      // postalCodeEnabled = true,
      // onFocus,
      // onBlur,
      placeholders,
      ...props
    },
    ref
  ) => {
    const inputRef = useRef<any>(null);

    const onFormCompleteHandler = useCallback(
      (event: NativeSyntheticEvent<CardFormView.Details>) => {
        const card = event.nativeEvent;

        const data: CardFormView.Details = {
          last4: card.last4,
          expiryMonth: card.expiryMonth,
          expiryYear: card.expiryYear,
          complete: card.complete,
          brand: card.brand,
          country: card.country,
          postalCode: card.postalCode,
        };

        if (card.hasOwnProperty('number') || card.hasOwnProperty('cvc')) {
          data.number = card.number || '';
          data.cvc = card.cvc || '';
          if (__DEV__ && onFormComplete && card.complete) {
            console.warn(
              `[stripe-react-native] ⚠️ WARNING: You've enabled \`dangerouslyGetFullCardDetails\`, meaning full card details are being returned. Only do this if you're certain that you fulfill the necessary PCI compliance requirements. Make sure that you're not mistakenly logging or storing full card details! See the docs for details: https://stripe.com/docs/security/guide#validating-pci-compliance`
            );
          }
        }
        onFormComplete?.(data);
      },
      [onFormComplete]
    );

    const focus = () => {
      UIManager.dispatchViewManagerCommand(
        findNodeHandle(inputRef.current),
        'focus' as any,
        []
      );
    };

    const blur = () => {
      UIManager.dispatchViewManagerCommand(
        findNodeHandle(inputRef.current),
        'blur' as any,
        []
      );
    };

    useImperativeHandle(ref, () => ({
      focus,
      blur,
    }));

    const onFocusHandler = useCallback((event) => {
      const { focusedField } = event.nativeEvent;
      if (focusedField) {
        focusInput(inputRef.current);
        // onFocus?.(focusedField);
      } else {
        // onBlur?.();
      }
    }, []);

    useLayoutEffect(() => {
      const inputRefValue = inputRef.current;
      if (inputRefValue !== null) {
        registerInput(inputRefValue);
        return () => {
          unregisterInput(inputRefValue);
          if (currentlyFocusedInput() === inputRefValue) {
            inputRefValue.blur();
          }
        };
      }
      return () => {};
    }, [inputRef]);

    return (
      <CardFormNative
        ref={inputRef}
        onFormComplete={onFormCompleteHandler}
        cardStyle={{
          backgroundColor: cardStyle?.backgroundColor,
          borderColor: cardStyle?.borderColor,
          borderWidth: cardStyle?.borderWidth,
          borderRadius: cardStyle?.borderRadius,
          cursorColor: cardStyle?.cursorColor,
          fontSize: cardStyle?.fontSize,
          placeholderColor: cardStyle?.placeholderColor,
          textColor: cardStyle?.textColor,
          textErrorColor: cardStyle?.textErrorColor,
          fontFamily: cardStyle?.fontFamily,
          // disabledBackgroundColor: cardStyle?.disabledBackgroundColor,
          // type: cardStyle?.type,
        }}
        // isUserInteractionEnabledValue={isUserInteractionEnabled}
        placeholders={{
          number: placeholders?.number,
          expiration: placeholders?.expiration,
          cvc: placeholders?.cvc,
          postalCode: placeholders?.postalCode,
        }}
        onFocusChange={onFocusHandler}
        // postalCodeEnabled={postalCodeEnabled}
        {...props}
      />
    );
  }
)
Example #15
Source File: CardField.tsx    From stripe-react-native with MIT License 4 votes vote down vote up
CardField = forwardRef<CardFieldInput.Methods, Props>(
  (
    {
      onCardChange,
      onFocus,
      onBlur,
      cardStyle,
      placeholders,
      postalCodeEnabled,
      ...props
    },
    ref
  ) => {
    const inputRef = useRef<any>(null);

    const onCardChangeHandler = useCallback(
      (event: NativeSyntheticEvent<CardFieldInput.Details>) => {
        const card = event.nativeEvent;

        const data: CardFieldInput.Details = {
          last4: card.last4,
          expiryMonth: card.expiryMonth,
          expiryYear: card.expiryYear,
          complete: card.complete,
          brand: card.brand,
          validExpiryDate: card.validExpiryDate,
          validNumber: card.validNumber,
          validCVC: card.validCVC,
        };

        if (card.hasOwnProperty('postalCode')) {
          data.postalCode = card.postalCode || '';
        }
        if (card.hasOwnProperty('number') || card.hasOwnProperty('cvc')) {
          data.number = card.number || '';
          data.cvc = card.cvc || '';
          if (__DEV__ && onCardChange && card.complete) {
            console.warn(
              `[stripe-react-native] ⚠️ WARNING: You've enabled \`dangerouslyGetFullCardDetails\`, meaning full card details are being returned. Only do this if you're certain that you fulfill the necessary PCI compliance requirements. Make sure that you're not mistakenly logging or storing full card details! See the docs for details: https://stripe.com/docs/security/guide#validating-pci-compliance`
            );
          }
        }
        onCardChange?.(data);
      },
      [onCardChange]
    );

    const onFocusHandler = useCallback(
      (event) => {
        const { focusedField } = event.nativeEvent;
        if (focusedField) {
          focusInput(inputRef.current);
          onFocus?.(focusedField);
        } else {
          onBlur?.();
        }
      },
      [onFocus, onBlur]
    );

    const focus = () => {
      UIManager.dispatchViewManagerCommand(
        findNodeHandle(inputRef.current),
        'focus' as any,
        []
      );
    };

    const blur = () => {
      UIManager.dispatchViewManagerCommand(
        findNodeHandle(inputRef.current),
        'blur' as any,
        []
      );
    };

    const clear = () => {
      UIManager.dispatchViewManagerCommand(
        findNodeHandle(inputRef.current),
        'clear' as any,
        []
      );
    };

    useImperativeHandle(ref, () => ({
      focus,
      blur,
      clear,
    }));

    useLayoutEffect(() => {
      const inputRefValue = inputRef.current;
      if (inputRefValue !== null) {
        registerInput(inputRefValue);
        return () => {
          unregisterInput(inputRefValue);
          if (currentlyFocusedInput() === inputRefValue) {
            inputRefValue.blur();
          }
        };
      }
      return () => {};
    }, [inputRef]);

    return (
      <CardFieldNative
        ref={inputRef}
        onCardChange={onCardChangeHandler}
        onFocusChange={onFocusHandler}
        postalCodeEnabled={postalCodeEnabled ?? true}
        cardStyle={{
          backgroundColor: cardStyle?.backgroundColor,
          borderColor: cardStyle?.borderColor,
          borderWidth: cardStyle?.borderWidth,
          borderRadius: cardStyle?.borderRadius,
          cursorColor: cardStyle?.cursorColor,
          fontSize: cardStyle?.fontSize,
          placeholderColor: cardStyle?.placeholderColor,
          textColor: cardStyle?.textColor,
          textErrorColor: cardStyle?.textErrorColor,
          fontFamily: cardStyle?.fontFamily,
        }}
        placeholders={{
          number: placeholders?.number,
          expiration: placeholders?.expiration,
          cvc: placeholders?.cvc,
          postalCode: placeholders?.postalCode,
        }}
        {...props}
      />
    );
  }
)
Example #16
Source File: TextInput.tsx    From sellflow with MIT License 4 votes vote down vote up
function TextInput(props: Props, ref: Ref<NativeTextInput>) {
  let {
    autoCorrect = false,
    containerStyle,
    disabled = false,
    editable = true,
    errorMessage,
    errorMessageStyle,
    label,
    labelStyle,
    mode = 'outlined',
    multiline,
    onBlur,
    onChangeText,
    onFocus,
    showErrorIcon = true,
    style,
    keyboardType = 'default',
    ...otherProps
  } = props;

  let { colors, roundness, isRTL } = useTheme();

  let [isFocused, setIsFocused] = useState(false);

  let handleFocus = useCallback(
    (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
      if (disabled || !editable) {
        return;
      }

      onFocus && onFocus(e);
      setIsFocused(true);
    },
    [onFocus, disabled, editable],
  );

  let handleBlur = useCallback(
    (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
      if (disabled || !editable) {
        return;
      }

      onBlur && onBlur(e);
      setIsFocused(false);
    },
    [onBlur, disabled, editable],
  );

  let handleChangeText = useCallback(
    (text: string) => {
      if (disabled || !editable) {
        return;
      }
      if (
        (keyboardType === 'number-pad' || keyboardType === 'numeric') &&
        !text.match(/^-?[0-9]*$/)
      ) {
        return;
      }

      onChangeText && onChangeText(text);
    },
    [onChangeText, disabled, editable, keyboardType],
  );

  let isError = !!errorMessage;
  let hasLabel = !!label;

  let getColor = (target: 'border' | 'label') => {
    if (target === 'label') {
      return colors.placeholder;
    }
    if (disabled) {
      return colors.disabled;
    }
    if (isError) {
      return colors.error;
    }
    if (isFocused) {
      return colors.accent;
    }
    if (target === 'border') {
      return colors.border;
    }
  };

  let multilineStyle = {
    minHeight: mode === 'outlined' || hasLabel ? 60 : 0,
    height: 'auto',
  } as StyleProp<ViewStyle>;

  let wrapperStyle: StyleProp<ViewStyle> =
    mode === 'outlined'
      ? [
          styles.outlinedContainer,
          {
            borderRadius: roundness,
            backgroundColor: disabled ? colors.disabled : colors.surface,
            justifyContent: hasLabel ? 'space-between' : 'center',
          },
        ]
      : [
          styles.flatContainer,
          {
            justifyContent: hasLabel ? 'space-between' : 'flex-end',
          },
          hasLabel && { height: 60 },
        ];

  return (
    <>
      <View
        style={[
          { borderColor: getColor('border') },
          multiline && multilineStyle,
          containerStyle,
          ...wrapperStyle,
        ]}
      >
        {hasLabel && (
          <Text style={[styles.label, { color: getColor('label') }]}>
            {label}
          </Text>
        )}
        <NativeTextInput
          ref={ref}
          autoCorrect={autoCorrect}
          autoCapitalize="sentences"
          editable={!disabled && editable}
          underlineColorAndroid="transparent"
          placeholderTextColor={colors.placeholder}
          onFocus={handleFocus}
          onBlur={handleBlur}
          onChangeText={handleChangeText}
          style={[
            { color: disabled ? colors.placeholder : colors.text },
            style,
          ]}
          textAlign={isRTL ? 'right' : 'left'}
          {...otherProps}
        />
        {isError && showErrorIcon && (
          <View style={styles.errorIconContainer}>
            <IconButton icon="alert-circle-outline" color={colors.error} />
          </View>
        )}
      </View>
      {isError && (
        <Text
          numberOfLines={1}
          style={[
            styles.label,
            styles.errorMessage,
            mode === 'flat' && { paddingHorizontal: 0 },
            errorMessageStyle,
          ]}
        >
          {errorMessage}
        </Text>
      )}
    </>
  );
}
Example #17
Source File: index.tsx    From react-native-wheel-scrollview-picker with MIT License 4 votes vote down vote up
export default function ScrollPicker({
  itemHeight = 30,
  style,
  scrollViewComponent,
  ...props
}: ScrollPickerProps): JSX.Element {
  const [initialized, setInitialized] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(
    props.selectedIndex && props.selectedIndex >= 0 ? props.selectedIndex : 0
  );
  const sView = useRef<ScrollView>(null);
  const [isScrollTo, setIsScrollTo] = useState(false);
  const [dragStarted, setDragStarted] = useState(false);
  const [momentumStarted, setMomentumStarted] = useState(false);
  const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);

  const wrapperHeight =
    props.wrapperHeight ||
    (isViewStyle(style) && isNumeric(style.height)
      ? Number(style.height)
      : 0) ||
    itemHeight * 5;

  useEffect(
    function initialize() {
      if (initialized) return;
      setInitialized(true);

      setTimeout(() => {
        const y = itemHeight * selectedIndex;
        sView?.current?.scrollTo({ y: y });
      }, 0);

      return () => {
        timer && clearTimeout(timer);
      };
    },
    [initialized, itemHeight, selectedIndex, sView, timer]
  );

  const renderPlaceHolder = () => {
    const h = (wrapperHeight - itemHeight) / 2;
    const header = <View style={{ height: h, flex: 1 }} />;
    const footer = <View style={{ height: h, flex: 1 }} />;
    return { header, footer };
  };

  const renderItem = (
    data: ScrollPickerProps["dataSource"][0],
    index: number
  ) => {
    const isSelected = index === selectedIndex;
    const item = props.renderItem ? (
      props.renderItem(data, index, isSelected)
    ) : (
      <Text
        style={
          isSelected
            ? [styles.itemText, styles.itemTextSelected]
            : styles.itemText
        }
      >
        {data}
      </Text>
    );

    return (
      <View style={[styles.itemWrapper, { height: itemHeight }]} key={index}>
        {item}
      </View>
    );
  };
  const scrollFix = useCallback(
    (e: NativeSyntheticEvent<NativeScrollEvent>) => {
      let y = 0;
      const h = itemHeight;
      if (e.nativeEvent.contentOffset) {
        y = e.nativeEvent.contentOffset.y;
      }
      const _selectedIndex = Math.round(y / h);

      const _y = _selectedIndex * h;
      if (_y !== y) {
        // using scrollTo in ios, onMomentumScrollEnd will be invoked
        if (Platform.OS === "ios") {
          setIsScrollTo(true);
        }
        sView?.current?.scrollTo({ y: _y });
      }
      if (selectedIndex === _selectedIndex) {
        return;
      }
      // onValueChange
      if (props.onValueChange) {
        const selectedValue = props.dataSource[_selectedIndex];
        setSelectedIndex(_selectedIndex);
        props.onValueChange(selectedValue, _selectedIndex);
      }
    },
    [itemHeight, props, selectedIndex]
  );

  const onScrollBeginDrag = () => {
    setDragStarted(true);

    if (Platform.OS === "ios") {
      setIsScrollTo(false);
    }
    timer && clearTimeout(timer);
  };

  const onScrollEndDrag = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    setDragStarted(false);

    // if not used, event will be garbaged
    const _e: NativeSyntheticEvent<NativeScrollEvent> = { ...e };
    timer && clearTimeout(timer);
    setTimer(
      setTimeout(() => {
        if (!momentumStarted) {
          scrollFix(_e);
        }
      }, 50)
    );
  };
  const onMomentumScrollBegin = () => {
    setMomentumStarted(true);
    timer && clearTimeout(timer);
  };

  const onMomentumScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    setMomentumStarted(false);

    if (!isScrollTo && !dragStarted) {
      scrollFix(e);
    }
  };

  const { header, footer } = renderPlaceHolder();
  const highlightWidth = (isViewStyle(style) ? style.width : 0) || deviceWidth;
  const highlightColor = props.highlightColor || "#333";
  const highlightBorderWidth =
    props.highlightBorderWidth || StyleSheet.hairlineWidth;

  const wrapperStyle: ViewStyle = {
    height: wrapperHeight,
    flex: 1,
    backgroundColor: props.wrapperColor || "#fafafa",
    overflow: "hidden",
  };

  const highlightStyle: ViewStyle = {
    position: "absolute",
    top: (wrapperHeight - itemHeight) / 2,
    height: itemHeight,
    width: highlightWidth,
    borderTopColor: highlightColor,
    borderBottomColor: highlightColor,
    borderTopWidth: highlightBorderWidth,
    borderBottomWidth: highlightBorderWidth,
  };

  const CustomScrollViewComponent = scrollViewComponent || ScrollView;

  return (
    <View style={wrapperStyle}>
      <View style={highlightStyle} />
      <CustomScrollViewComponent
        ref={sView}
        bounces={false}
        showsVerticalScrollIndicator={false}
        nestedScrollEnabled
        onMomentumScrollBegin={(_e: any) => onMomentumScrollBegin()}
        onMomentumScrollEnd={(e: NativeSyntheticEvent<NativeScrollEvent>) =>
          onMomentumScrollEnd(e)
        }
        onScrollBeginDrag={(_e: any) => onScrollBeginDrag()}
        onScrollEndDrag={(e: NativeSyntheticEvent<NativeScrollEvent>) =>
          onScrollEndDrag(e)
        }
      >
        {header}
        {props.dataSource.map(renderItem)}
        {footer}
      </CustomScrollViewComponent>
    </View>
  );
}
Example #18
Source File: index.tsx    From react-native-better-image with MIT License 4 votes vote down vote up
BetterImage = ({
  viewStyle,
  thumbnailFadeDuration = 250,
  imageFadeDuration = 250,
  thumbnailSource,
  source,
  onLoadEnd,
  resizeMethod,
  resizeMode,
  thumbnailBlurRadius = 1,
  style,
  fallbackSource = { uri: '' },
  onError,
  children,
  ...otherProps
}: BetterImageProps) => {
  const imageOpacity = useRef(new Value(0)).current;
  const thumbnailOpacity = useRef(new Value(0)).current;
  const thumbnailAnimationProgress = useRef<
    Animated.CompositeAnimation | undefined
  >();
  const [hasError, setHasError] = useState(false);
  const [hasLoaded, setHasLoaded] = useState(false);

  const onImageLoad = () => {
    setHasLoaded(true);

    timing(imageOpacity, {
      toValue: 1,
      duration: imageFadeDuration,
      useNativeDriver: true,
    }).start(() => {
      thumbnailAnimationProgress.current?.stop();
      timing(thumbnailOpacity, {
        toValue: 0,
        duration: thumbnailFadeDuration,
        useNativeDriver: true,
      }).start();
    });

    onLoadEnd && onLoadEnd();
  };

  const onThumbnailLoad = () => {
    if (!hasLoaded) {
      const progress = timing(thumbnailOpacity, {
        toValue: 1,
        duration: thumbnailFadeDuration,
        useNativeDriver: true,
      });
      thumbnailAnimationProgress.current = progress;
      thumbnailAnimationProgress.current.start();
    }
  };

  const onImageLoadError = (
    event: NativeSyntheticEvent<ImageErrorEventData>
  ) => {
    setHasError(true);
    onError && onError(event);
  };

  useDeepCompareEffectNoCheck(
    useCallback(() => {
      imageOpacity.setValue(0);
      thumbnailOpacity.setValue(0);
      setHasError(false);
      setHasLoaded(false);
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []),
    [source, thumbnailSource]
  );

  const ImageComponent = children ? AnimatedImageBackground : AnimatedImage;

  return (
    <View style={[styles.imageContainerStyle, viewStyle]}>
      {thumbnailSource ? (
        <ImageComponent
          children={children}
          onLoadEnd={onThumbnailLoad}
          style={[
            styles.thumbnailImageStyle,
            { opacity: thumbnailOpacity },
            style,
          ]}
          source={thumbnailSource}
          blurRadius={thumbnailBlurRadius}
          resizeMethod={resizeMethod}
          resizeMode={resizeMode}
        />
      ) : null}
      <ImageComponent
        children={children}
        resizeMethod={resizeMethod}
        resizeMode={resizeMode}
        onLoadEnd={onImageLoad}
        onError={hasError ? () => null : onImageLoadError}
        source={hasError ? fallbackSource : source}
        style={[styles.imageStyle, { opacity: imageOpacity }, style]}
        {...otherProps}
      />
    </View>
  );
}
Example #19
Source File: MentionInput.tsx    From lowkey-react-native-mentions-input with MIT License 4 votes vote down vote up
MentionsInput = React.forwardRef(
  (
    {
      suggestedUsersComponent,
      textInputStyle,
      onFocusStateChange = () => {},
      onTextChange = () => {},
      onMarkdownChange = () => {},
      placeholder = 'Write a message...',
      placeholderTextColor,
      multiline,
      textInputTextStyle,
      leftComponent = <></>,
      rightComponent = <></>,
      innerComponent = <></>,
      users,
      ...props
    }: Props,
    ref
  ) => {
    const [isOpen, SetIsOpen] = useState(false);
    const [suggestedUsers, SetSuggesedUsers] = useState<SuggestedUsers[]>([]);
    const [matches, SetMatches] = useState<any[]>([]);
    const [mentions, SetMentions] = useState<any[]>([]);
    const [currentCursorPosition, SetCurrentCursorPosition] = useState(0);

    useEffect(() => {
      if (props.value === '' && (mentions.length > 0 || matches.length > 0)) {
        SetMatches([]);
        SetMentions([]);
        SetCurrentCursorPosition(1);
      }
    }, [matches, mentions, props.value]);

    const transformTag = useCallback((value: string) => {
      return value
        .replace(/\s+/g, '')
        .toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '');
    }, []);

    const handleSuggestionsOpen = useCallback(
      (values: RegExpMatchArray[], currentCursorPosition: number) => {
        let shouldPresentSuggestions = false;
        let newSuggestedUsers: Array<SuggestedUsers> = [];

        users.map(
          (user, index) =>
            (newSuggestedUsers[index] = {
              ...user,
              startPosition: 0,
            })
        );

        values.map((match) => {
          if (match === null) {
            return;
          }
          const matchStartPosition = match.index;
          if (typeof matchStartPosition === 'undefined') {
            return;
          }
          const matchEndPosition = matchStartPosition + match[0].length;

          if (
            currentCursorPosition > matchStartPosition &&
            currentCursorPosition <= matchEndPosition
          ) {
            shouldPresentSuggestions = true;
            newSuggestedUsers = newSuggestedUsers
              .filter((user) =>
                user.name
                  .toLowerCase()
                  .includes(match[0].substring(1).toLowerCase())
              )
              .map((user) => {
                user.startPosition = matchStartPosition;
                return user;
              });
          }
        });
        const isSameSuggestedUser =
          suggestedUsers.length === newSuggestedUsers.length &&
          suggestedUsers.every(
            (value, index) =>
              value.id === newSuggestedUsers[index].id &&
              value.startPosition == newSuggestedUsers[index].startPosition
          );

        SetIsOpen(shouldPresentSuggestions);
        if (!isSameSuggestedUser) {
          SetSuggesedUsers(newSuggestedUsers);
        }
      },

      [users, suggestedUsers]
    );

    const formatMarkdown = useCallback(
      (markdown: string) => {
        let parseHeadIndex = 0;
        let markdownArray = [];

        if (mentions.length === 0) {
          markdownArray.push({
            type: 'text',
            data: markdown,
          });
        }

        mentions.map((mention, index) => {
          let match = matches.find((m) => {
            return (
              m.index === mention.user.startPosition &&
              m[0] === `@${mention.user.name}`
            );
          });
          if (typeof match === 'undefined') {
            return;
          }
          markdownArray.push({
            type: 'text',
            data: markdown.substring(
              parseHeadIndex,
              mention.user.startPosition
            ),
          });
          markdownArray.push({
            type: 'mention',
            data: `<@${mention.user.name}::${mention.user.id}>`,
          });
          parseHeadIndex =
            mention.user.startPosition + mention.user.name.length + 1;

          if (index === mentions.length - 1) {
            markdownArray.push({
              type: 'text',
              data: markdown.substring(parseHeadIndex, markdown.length),
            });
          }
        });

        markdown = '';

        markdownArray.map((m) => {
          if (m.type === 'text') {
            markdown = markdown + encodeURIComponent(m.data);
          } else if (m.type === 'mention') {
            markdown = markdown + m.data;
          }
        });
        onMarkdownChange(markdown);
      },
      [onMarkdownChange, mentions, matches]
    );

    const handleDelete = useCallback(
      ({ nativeEvent }: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
        if (nativeEvent.key === 'Backspace') {
          mentions.map((mention, index) => {
            const matchStartPosition = mention.user.startPosition;
            const matchEndPosition =
              matchStartPosition + mention.user.name.length + 1;
            if (
              currentCursorPosition > matchStartPosition &&
              currentCursorPosition <= matchEndPosition
            ) {
              const newMentions = mentions;
              newMentions.splice(index, 1);
              SetMentions(newMentions);
            }
          });
        }
      },
      [mentions, currentCursorPosition]
    );

    const onSelectionChange = useCallback(
      ({
        nativeEvent,
      }: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
        if (nativeEvent.selection.start === nativeEvent.selection.end) {
          SetCurrentCursorPosition(nativeEvent.selection.start);
        }
      },
      []
    );

    const handleMentions = useCallback(
      (newText: string, currentCursorPosition: number) => {
        const pattern = PATTERNS.USERNAME_MENTION;

        let newMatches = [...matchAll(newText, pattern)];
        let newMentions = newText.length > 0 ? mentions : [];

        newMentions.map((mention) => {
          const matchStartPosition = mention.user.startPosition;

          if (decodeURI(newText).length - decodeURI(props.value).length > 0) {
            if (
              matchStartPosition + (newText.length - props.value.length) >
                currentCursorPosition &&
              currentCursorPosition !== props.value.length
            ) {
              mention.user.startPosition =
                mention.user.startPosition +
                (newText.length - props.value.length);
            }
          } else {
            if (matchStartPosition >= currentCursorPosition) {
              mention.user.startPosition =
                mention.user.startPosition +
                (newText.length - props.value.length);
            }
          }
          return mention;
        });

        onTextChange(newText);
        formatMarkdown(newText);

        const isSameMatch =
          matches.length === newMatches.length &&
          matches.every((value, index) => value === newMatches[index]);

        SetMentions(newMentions);

        if (!isSameMatch) {
          SetMatches(newMatches);
        }
      },
      [mentions, onTextChange, formatMarkdown, props.value, matches]
    );

    const onChangeText = useCallback(
      (newText: string) => {
        handleMentions(newText, currentCursorPosition);
      },
      [handleMentions, currentCursorPosition]
    );

    const handleAddMentions = useCallback(
      (user: {
        id: number;
        name: string;
        avatar: string;
        startPosition: number;
      }) => {
        const startPosition = user.startPosition;
        const mention = mentions.find(
          (m) => m.user.startPosition === startPosition
        );
        if (mention) {
          return;
        }

        const match = matches.find((m) => m.index === startPosition);
        let newMentions = mentions;
        const userName = transformTag(user.name);
        const newText =
          props.value.substring(0, match.index) +
          `@${userName} ` +
          props.value.substring(
            match.index + match[0].length,
            props.value.length
          );

        newMentions.push({
          user: {
            ...user,
            name: userName,
            startPosition: startPosition,
            test: 1000,
          },
        });
        newMentions.sort((a, b) =>
          a.user.startPosition > b.user.startPosition
            ? 1
            : b.user.startPosition > a.user.startPosition
            ? -1
            : 0
        );

        SetMentions(newMentions);
        SetIsOpen(false);
        const newCursor = match.index + user.name.length + 1;
        SetCurrentCursorPosition(newCursor);
        setTimeout(() => {
          handleMentions(newText, newCursor);
        }, 100);
      },
      [mentions, matches, transformTag, props.value, handleMentions]
    );

    const onFocus = useCallback(() => {
      onFocusStateChange(true);
    }, [onFocusStateChange]);

    const onBlur = useCallback(() => {
      onFocusStateChange(false);
    }, [onFocusStateChange]);

    useEffect(() => {
      formatMarkdown(props.value);
    }, [props.value, formatMarkdown]);

    useEffect(() => {
      let timeout = setTimeout(() => {
        handleSuggestionsOpen(matches, currentCursorPosition);
      }, 100);

      return () => clearTimeout(timeout);
    }, [handleSuggestionsOpen, matches, currentCursorPosition]);

    return (
      <View>
        <View>
          {isOpen && suggestedUsersComponent(suggestedUsers, handleAddMentions)}
          <View style={styles.inputContainerRow}>
            <View>{leftComponent}</View>
            <View style={[textInputStyle, styles.row]}>
              <TextInput
                {...props}
                onFocus={onFocus}
                onBlur={onBlur}
                placeholder={placeholder}
                placeholderTextColor={placeholderTextColor}
                multiline={multiline}
                value={decodeURI(props.value.replace(/%/g, encodeURI('%')))}
                onChangeText={onChangeText}
                onKeyPress={handleDelete}
                style={[
                  textInputTextStyle,
                  styles.flex,
                  { paddingBottom: multiline ? 5 : 0 },
                ]}
                onSelectionChange={onSelectionChange}
                //@ts-ignore
                ref={ref}
              />
              <View style={styles.innerContainer}>{innerComponent}</View>
            </View>
            {rightComponent}
          </View>
        </View>
      </View>
    );
  }
)
Example #20
Source File: index.tsx    From krmanga with MIT License 4 votes vote down vote up
function MangaView({
                       navigation, dispatch, isLogin, chapterList, bookInfo,
                       book_id, headerHeight, markRoast, chapter_num, episodeList, hasMore, loading,
                       currentChapterNum, pages
                   }: IProps) {

    const [endReached, setEndReached] = useState<boolean>(false);
    let [time, setTime] = useState<NodeJS.Timeout | null>(null);

    let flatListRef: FlatList<IEpisode> | null = null;
    const topPanelValue = useRef(new Animated.Value(0)).current;
    const bottomPanelValue = useRef(new Animated.Value(0)).current;
    const drawerX = useRef(new Animated.Value(-viewportWidth)).current;
    let panelEnable: boolean = true;


    useEffect(() => {
        loadData(true);
        return () => {
            StatusBar.setHidden(false);
            if (isLogin) {
                dispatch({
                    type: "mangaView/addHistory",
                    payload: {
                        book_id
                    }
                });
                dispatch({
                    type: "history/setScreenReload"
                });
                dispatch({
                    type: "downloadManage/setScreenReload"
                });
            }
            dispatch({
                type: "mangaView/setState",
                payload: {
                    ...initialState
                }
            });
        };
    }, []);

    const loadData = (refreshing: boolean, callback?: () => void) => {
        dispatch({
            type: "mangaView/fetchEpisodeList",
            payload: {
                refreshing,
                markRoast,
                chapter_num,
                book_id
            },
            callback
        });
    };

    const onEndReached = () => {
        if (!hasMore || loading) {
            return;
        }

        setEndReached(true);

        dispatch({
            type: "mangaView/fetchEpisodeList",
            payload: {
                book_id,
                chapter_num: currentChapterNum + 1
            },
            callback: () => {
                setEndReached(false);
            }
        });
    };

    const renderItem = ({ item }: ListRenderItemInfo<IEpisode>) => {
        return (
            <Item panelHandle={panelHandle} data={item} />
        );
    };

    const renderFooter = () => {
        if (endReached) {
            return <More />;
        }
        if (!hasMore) {
            return <End />;
        }

        return null;
    };

    const getItemLayout = (data: any, index: number) => {
        if (data[index] === undefined) {
            return { length: 0, offset: 0, index };
        }

        let offset = 0;
        const length = viewportWidth * data[index].multiple;

        for (let i = 0; i < index; i++) {
            offset += viewportWidth * data[i].multiple;
        }

        return { length: length, offset, index };
    };

    const scrollToIndex = (index: number) => {
        dispatch({
            type: "brief/setState",
            payload: {
                markChapterNum: episodeList[index].chapter_num,
                markRoast: episodeList[index].roast
            }
        });
        flatListRef?.scrollToIndex({ viewPosition: 0, index: index });
    };

    const lastChapter = () => {
        if (!loading) {
            dispatch({
                type: "mangaView/fetchEpisodeList",
                payload: {
                    refreshing: true,
                    book_id,
                    chapter_num: currentChapterNum - 1
                }
            });
        }
    };

    const nextChapter = () => {
        if (!loading) {
            dispatch({
                type: "mangaView/fetchEpisodeList",
                payload: {
                    refreshing: true,
                    book_id,
                    chapter_num: currentChapterNum + 1
                }
            });
        }
    };

    const showDrawer = () => {
        Animated.timing(drawerX, {
            toValue: 0,
            duration: 200,
            useNativeDriver: true
        }).start();
    };

    const hideDrawer = () => {
        Animated.timing(drawerX, {
            toValue: -viewportWidth,
            duration: 200,
            useNativeDriver: true
        }).start();
    };

    const hidePanel = () => {
        if (panelEnable) {
            Animated.parallel([
                Animated.timing(
                    topPanelValue,
                    {
                        toValue: -headerHeight - getStatusBarHeight(),
                        duration: 200,
                        easing: Easing.linear,
                        useNativeDriver: true
                    }
                ),
                Animated.timing(
                    bottomPanelValue,
                    {
                        toValue: hp(25),
                        duration: 200,
                        easing: Easing.linear,
                        useNativeDriver: true
                    }
                )
            ]).start(() => {
                StatusBar.setHidden(true);
                panelEnable = !panelEnable;
            });
        }
    };

    const showPanel = () => {
        if (!panelEnable) {
            Animated.parallel([
                Animated.timing(
                    topPanelValue,
                    {
                        toValue: 0,
                        duration: 200,
                        easing: Easing.linear,
                        useNativeDriver: true
                    }
                ),
                Animated.timing(
                    bottomPanelValue,
                    {
                        toValue: 0,
                        duration: 200,
                        easing: Easing.linear,
                        useNativeDriver: true
                    }
                )
            ]).start(() => {
                StatusBar.setHidden(false);
                panelEnable = !panelEnable;
            });
        }
    };

    const panelHandle = useCallback(() => {
        if (panelEnable) {
            hidePanel();
        } else {
            showPanel();
        }
    }, [panelEnable]);

    const debounce = (cb: any, wait = 250) => {
        if (time !== null) {
            clearTimeout(time);
        }

        let tempTime = setTimeout(() => {
            time = null;
            cb && cb();
        }, wait);

        setTime(tempTime);
    };

    const onScrollEndDrag = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
        let offset_total = 0;
        let current_episode_total = episodeList[0].episode_total;
        let current_chapter_id = episodeList[0].chapter_id;
        let current_chapter_num = episodeList[0].chapter_num;
        let current_number = episodeList[0].number;
        let current_roast = episodeList[0].roast;
        let current_title = episodeList[0].title;
        for (let i = 0; i < episodeList.length; i++) {
            if (nativeEvent.contentOffset.y >= offset_total) {
                current_episode_total = episodeList[i].episode_total;
                current_chapter_id = episodeList[i].chapter_id;
                current_chapter_num = episodeList[i].chapter_num;
                current_number = episodeList[i].number;
                current_roast = episodeList[i].roast;
                current_title = episodeList[i].title;
            } else {
                break;
            }
            offset_total = offset_total + episodeList[i].multiple * viewportWidth;
        }

        debounce(() => {
            dispatch({
                type: "mangaView/setState",
                payload: {
                    currentEpisodeTotal: current_episode_total,
                    currentChapterId: current_chapter_id,
                    currentChapterNum: current_chapter_num,
                    currentNumber: current_number,
                    showCurrentNumber: current_number,
                    currentRoast: current_roast,
                    currentTitle: current_title
                }
            });
            dispatch({
                type: "brief/setState",
                payload: {
                    markChapterNum: current_chapter_num,
                    markRoast: current_roast
                }
            });
        });

        hidePanel();
    };

    const goMangaChapter = useCallback((item: IChapter) => {
        dispatch({
            type: "mangaView/fetchEpisodeList",
            payload: {
                refreshing: true,
                book_id,
                chapter_num: item.chapter_num,
                callback: () => hideDrawer
            }
        });
    }, []);

    return (
        episodeList.length > 0 ?
            <View>
                <StatusBar barStyle="light-content" />
                <TopCtrPanel
                    book_id={book_id}
                    topPanelValue={topPanelValue}
                    navigation={navigation}
                />
                <BottomCtrPanel
                    bottomPanelValue={bottomPanelValue}
                    scrollToIndex={scrollToIndex}
                    showDrawer={showDrawer}
                    lastChapter={lastChapter}
                    nextChapter={nextChapter}
                />
                <FlatList
                    ref={ref => (flatListRef = ref)}
                    data={episodeList}
                    keyExtractor={(item, key) => `item-${item.id}-key-${key}`}
                    renderItem={renderItem}
                    ListFooterComponent={renderFooter}
                    getItemLayout={getItemLayout}
                    onScrollEndDrag={onScrollEndDrag}
                    initialScrollIndex={pages.episode_offset - 1}
                    onEndReached={onEndReached}
                    onEndReachedThreshold={0.1}
                    extraData={endReached}
                />
                <DarkDrawer
                    chapterList={chapterList}
                    bookInfo={bookInfo}
                    headerHeight={headerHeight}
                    drawerX={drawerX}
                    hideDrawer={hideDrawer}
                    goMangaView={goMangaChapter}
                />
                <BottomStatusBar />
            </View> : null
    );
}
Example #21
Source File: index.tsx    From krmanga with MIT License 4 votes vote down vote up
function Category({ dispatch, navigation, category_id, activeStatus, activeModel, bookList, loading, hasMore, refreshing }: IProps) {

    let scrollViewStartOffsetY: number = 0;
    const [endReached, setEndReached] = useState<boolean>(false);

    useFocusEffect(
        React.useCallback(() => {
            dispatch({
                type: "category/setState",
                payload: {
                    activeCategory: category_id
                }
            });
            loadData(true);
        }, [activeStatus])
    );

    const loadData = (refreshing: boolean, callback?: () => void) => {
        dispatch({
            type: `${activeModel}/fetchBookList`,
            payload: {
                refreshing,
                category_id
            },
            callback
        });
    };

    const goBrief = useCallback((data: IBook) => {
        navigation.navigate("Brief", {
            id: data.id
        });
    }, []);

    const renderItem = ({ item }: ListRenderItemInfo<IBook>) => {
        return (
            <BookCover
                key={item.id}
                data={item}
                goBrief={goBrief}
            />
        );
    };

    const onRefresh = () => {
        dispatch({
            type: `${activeModel}/fetchBookList`,
            payload: {
                refreshing: true,
                onRefresh: true,
                category_id
            }
        });
    };

    const renderFooter = () => {
        if (endReached) {
            return <More />;
        }
        if (!hasMore) {
            return <End />;
        }

        return null;
    };

    const onScrollBeginDrag = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
        scrollViewStartOffsetY = nativeEvent.contentOffset.y;
    };

    const onScrollEndDrag = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
        if (scrollViewStartOffsetY > nativeEvent.contentOffset.y) {
            dispatch({
                type: "category/setState",
                payload: {
                    hideHeader: false
                }
            });
        } else {
            dispatch({
                type: "category/setState",
                payload: {
                    hideHeader: true
                }
            });
        }
    };

    const onEndReached = () => {
        if (!hasMore || loading) {
            return;
        }
        setEndReached(true);

        loadData(false, () => {
            setEndReached(false);
        });
    };

    return (
        (loading && refreshing) ? <BookPlaceholder /> :
            <FlatList
                keyExtractor={(item, key) => `item-${item.id}-key-${key}`}
                data={bookList}
                extraData={endReached}
                renderItem={renderItem}
                refreshing={refreshing}
                style={styles.container}
                onRefresh={onRefresh}
                ListFooterComponent={renderFooter}
                scrollEventThrottle={1}
                onScrollBeginDrag={onScrollBeginDrag}
                onScrollEndDrag={onScrollEndDrag}
                numColumns={3}
                onEndReached={onEndReached}
                onEndReachedThreshold={0.1}
            />
    );
}
Example #22
Source File: index.tsx    From krmanga with MIT License 4 votes vote down vote up
function Brief({
                   navigation, dispatch, isLogin, headerHeight, bookInfo, book_id, markRoast, markChapterNum,
                   loading, collection_id, refreshing, chapterList
               }: IProps) {

    const [showTop, setShowTop] = useState<boolean>(true);
    const scrollY: Animated.Value = useRef(new Animated.Value(0)).current;
    const drawerX: Animated.Value = useRef(new Animated.Value(viewportWidth)).current;
    const fixedHeight = headerHeight + imageHeight;
    let compHeight: number;
    if (Platform.OS === "android") {
        compHeight = 30 - 11;
    } else {
        compHeight = isIphoneX() ? 30 - 22 : 30 - 11 + getStatusBarHeight();
    }
    const stickyHeader = fixedHeight + compHeight;

    useEffect(() => {
        loadData(true);
        return () => {
            dispatch({
                type: "brief/setState",
                payload: {
                    ...initialState
                }
            });
        };
    }, []);

    const getOpacity = () => {
        return scrollY.interpolate({
            inputRange: [
                headerHeight,
                fixedHeight
            ],
            outputRange: [1, 0],
            extrapolate: "clamp"
        });
    };

    const getBlurOpacity = () => {
        return scrollY.interpolate({
            inputRange: [
                stickyHeader - 1,
                stickyHeader
            ],
            outputRange: [0, 1],
            extrapolate: "clamp"
        });
    };

    const getLeftViewX = () => {
        return scrollY.interpolate({
            inputRange: [
                headerHeight,
                fixedHeight
            ],
            outputRange: [0, wp(22)],
            extrapolate: "clamp"
        });
    };

    const getRightViewX = () => {
        return scrollY.interpolate({
            inputRange: [
                headerHeight,
                fixedHeight
            ],
            outputRange: [0, wp(10)],
            extrapolate: "clamp"
        });
    };

    const getRightViewScale = () => {
        return scrollY.interpolate({
            inputRange: [
                headerHeight,
                fixedHeight
            ],
            outputRange: [1, 0.65],
            extrapolate: "clamp"
        });
    };

    const getRightFontSize = () => {
        return scrollY.interpolate({
            inputRange: [
                headerHeight,
                fixedHeight
            ],
            outputRange: [1, 1.5],
            extrapolate: "clamp"
        });
    };

    const getBgImageSize = () => {
        return scrollY.interpolate({
            inputRange: [-100, 0],
            outputRange: [1.2, 1],
            extrapolate: "clamp"
        });
    };

    const onClickCollection = useCallback(() => {
        if (!isLogin) {
            navigation.navigate("Login");
        } else {
            if (collection_id > 0) {
                dispatch({
                    type: "brief/delUserCollection",
                    payload: {
                        id: collection_id.toString()
                    }
                });
            } else {
                dispatch({
                    type: "brief/addUserCollection",
                    payload: {
                        book_id
                    }
                });
            }
            dispatch({
                type: "collection/screenReload"
            });
        }
    }, [isLogin, collection_id]);

    const onClickRead = useCallback(() => {
        if (markRoast > 0) {
            navigation.navigate("MangaView", {
                book_id,
                markRoast,
                chapter_num: markChapterNum
            });
        } else {
            navigation.navigate("MangaView", {
                book_id,
                chapter_num: 1
            });
        }
    }, [markRoast]);

    const goMangaView = useCallback((item: IChapter) => {
        navigation.navigate("MangaView", {
            book_id,
            chapter_num: item.chapter_num
        });
    }, []);

    const showDrawer = () => {
        Animated.timing(drawerX, {
            toValue: 0,
            duration: 200,
            useNativeDriver: true
        }).start();
    };

    const hideDrawer = () => {
        Animated.timing(drawerX, {
            toValue: viewportWidth,
            duration: 200,
            useNativeDriver: true
        }).start();
    };

    const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
        if (event.nativeEvent.contentOffset.y >= fixedHeight) {
            setShowTop(false);
        } else {
            setShowTop(true);
        }
    };

    const loadData = (refreshing: boolean, callback?: () => void) => {
        dispatch({
            type: "brief/fetchBrief",
            payload: {
                refreshing,
                book_id
            },
            callback
        });
    };

    return (
        (loading && refreshing) ? <BriefPlaceholder /> :
            <View style={styles.container}>
                <LightDrawer
                    chapterList={chapterList}
                    bookInfo={bookInfo}
                    headerHeight={headerHeight}
                    drawerX={drawerX}
                    goMangaView={goMangaView}
                    hideDrawer={hideDrawer}
                />
                <ImageBlurBackground
                    bookInfo={bookInfo}
                    imageSize={getBgImageSize()}
                />
                <TopBarWrapper
                    book_id={book_id}
                    headerHeight={headerHeight}
                    showTop={showTop}
                    opacity={getOpacity()}
                />
                <Animated.ScrollView
                    onScroll={Animated.event(
                        [
                            {
                                nativeEvent: { contentOffset: { y: scrollY } }
                            }
                        ],
                        {
                            useNativeDriver: true,
                            listener: onScroll
                        }
                    )}
                    overScrollMode="always"
                    scrollEventThrottle={1}
                >
                    <Header
                        stickyHeader={stickyHeader}
                        compHeight={compHeight}
                        scrollY={scrollY}
                        opacity={getOpacity()}
                        blurOpacity={getBlurOpacity()}
                        leftViewX={getLeftViewX()}
                        rightViewX={getRightViewX()}
                        rightViewScale={getRightViewScale()}
                        rightFontSize={getRightFontSize()}
                        showDrawer={showDrawer}
                        onClickRead={onClickRead}
                        onClickCollection={onClickCollection}
                    />
                    <List
                        chapterList={chapterList}
                        goMangaView={goMangaView}
                    />
                    <Footer />
                </Animated.ScrollView>
            </View>
    );
}
Example #23
Source File: MessageDetail.tsx    From lexicon with MIT License 4 votes vote down vote up
export default function MessageDetail() {
  const styles = useStyles();
  const { colors } = useTheme();

  const storage = useStorage();
  const user = storage.getItem('user');

  const { authorizedExtensions } = useSiteSettings();
  const extensions = authorizedExtensions?.split('|');
  const normalizedExtensions = formatExtensions(extensions);

  const ios = Platform.OS === 'ios';
  const screen = Dimensions.get('screen');

  const { navigate } = useNavigation<StackNavProp<'MessageDetail'>>();

  const {
    params: {
      id,
      postPointer,
      emptied,
      hyperlinkUrl = '',
      hyperlinkTitle = '',
    },
  } = useRoute<StackRouteProp<'MessageDetail'>>();

  const [hasOlderMessages, setHasOlderMessages] = useState(true);
  const [hasNewerMessages, setHasNewerMessages] = useState(true);
  const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
  const [loadingNewerMessages, setLoadingNewerMessages] = useState(false);
  const [refetching, setRefetching] = useState(false);
  const [isInitialRequest, setIsInitialRequest] = useState(true);
  const [textInputFocused, setInputFocused] = useState(false);

  const [title, setTitle] = useState('');
  const [message, setMessage] = useState('');

  const [startIndex, setStartIndex] = useState(0);
  const [endIndex, setEndIndex] = useState(0);
  const [initialHeight, setInitialHeight] = useState<number>();

  const [data, setData] = useState<Message>();
  const [members, setMembers] = useState<Array<User>>([]);
  const [userWhoComment, setUserWhoComment] = useState<Array<User>>([]);
  const [stream, setStream] = useState<Array<number>>([]);
  const virtualListRef = useRef<VirtualizedList<MessageContent>>(null);

  const [showUserList, setShowUserList] = useState(false);
  const [mentionLoading, setMentionLoading] = useState(false);
  const [mentionKeyword, setMentionKeyword] = useState('');
  const [cursorPosition, setCursorPosition] = useState<CursorPosition>({
    start: 0,
    end: 0,
  });

  let contentHeight = initialHeight ? initialHeight : 0;

  const messageRef = useRef<TextInputType>(null);

  const { mentionMembers } = useMention(
    mentionKeyword,
    showUserList,
    setMentionLoading,
  );

  const {
    data: baseData,
    loading: messageDetailLoading,
    refetch,
    fetchMore,
  } = useTopicDetail({
    variables: {
      topicId: id,
      postPointer,
    },
    onCompleted: ({ topicDetail: result }) => {
      if (result) {
        setIsInitialRequest(true);
        setTitle(result.title || '');

        const tempParticipants: Array<User> = [];
        result.details?.allowedUsers?.forEach((allowedUser) =>
          tempParticipants.push({
            id: allowedUser.id,
            username: allowedUser.username,
            avatar: getImage(allowedUser.avatarTemplate),
          }),
        );
        setMembers(tempParticipants);
        let userWhoComment: Array<User> = [];
        result.details?.participants.forEach((user) => {
          userWhoComment.push({
            id: user.id,
            username: user.username,
            avatar: getImage(user.avatar),
          });
        });
        setUserWhoComment(userWhoComment);
      }
    },
    onError: (error) => {
      loadingOlderMessages && setLoadingOlderMessages(false);
      errorHandlerAlert(error);
    },
    fetchPolicy: 'cache-and-network',
  });

  useEffect(() => {
    if (emptied) {
      setMessage('');
    }
  }, [emptied]);

  useEffect(() => {
    if (!baseData) {
      return;
    }

    const {
      topicDetail: { postStream, details },
    } = baseData;

    const {
      data: tempData,
      hasNewerMessage: newMessage,
      hasOlderMessage: oldMessage,
      baseStream,
      firstPostIndex,
      lastPostIndex,
    } = messageDetailHandler({ postStream, details });

    setData(tempData);
    setStream(baseStream);
    setHasNewerMessages(newMessage);
    setHasOlderMessages(oldMessage);
    setStartIndex(firstPostIndex);
    setEndIndex(lastPostIndex);
  }, [baseData]);

  useEffect(() => {
    if (!refetching) {
      return;
    }
    virtualListRef.current?.scrollToEnd({ animated: false });
    setRefetching(false);
  }, [refetching]);

  useEffect(() => {
    if (!hyperlinkUrl) {
      return;
    }
    const { newUrl, newTitle } = getHyperlink(hyperlinkUrl, hyperlinkTitle);
    const result = insertHyperlink(message, newTitle, newUrl);
    setMessage(result);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [hyperlinkTitle, hyperlinkUrl]);

  const { reply, loading: replyLoading } = useReplyPost({
    onCompleted: () => {
      setMessage('');
      refetch({ postPointer: stream.length || 1 }).then(() => {
        if (textInputFocused && data?.contents.length) {
          if (ios) {
            virtualListRef.current?.scrollToIndex({
              index: data.contents.length,
              animated: true,
            });
          } else {
            setTimeout(() => {
              virtualListRef.current?.scrollToIndex({
                index: data.contents.length,
                animated: true,
              });
            }, 500);
          }
        }
        setRefetching(true);
      });
    },
  });

  useMessageTiming(id, startIndex, data?.contents);

  const loadStartMore = async () => {
    if (
      loadingOlderMessages ||
      !hasOlderMessages ||
      !stream ||
      messageDetailLoading
    ) {
      return;
    }
    setLoadingOlderMessages(true);
    let nextEndIndex = startIndex;
    let newDataCount = Math.min(10, stream.length - nextEndIndex);
    let nextStartIndex = Math.max(0, nextEndIndex - newDataCount);

    let nextPosts = stream.slice(nextStartIndex, nextEndIndex);
    if (!nextPosts.length) {
      return;
    }
    await fetchMore({
      variables: {
        topicId: id,
        posts: nextPosts,
      },
    }).then(() => {
      setStartIndex(nextStartIndex);
      setLoadingOlderMessages(false);
    });
  };

  const loadEndMore = async () => {
    if (
      loadingNewerMessages ||
      !hasNewerMessages ||
      !stream ||
      messageDetailLoading
    ) {
      return;
    }
    setLoadingNewerMessages(true);
    let nextStartIndex = endIndex + 1;
    let newDataCount = Math.min(10, stream.length - nextStartIndex);
    let nextEndIndex = nextStartIndex + newDataCount;

    let nextPosts = stream.slice(nextStartIndex, nextEndIndex);
    if (!nextPosts.length) {
      return;
    }
    await fetchMore({
      variables: {
        topicId: id,
        posts: nextPosts,
      },
    });
    setEndIndex(nextEndIndex - 1);
    setLoadingNewerMessages(false);
  };

  const onPressSend = (message: string) => {
    setShowUserList(false);
    if (message.trim() !== '') {
      reply({
        variables: {
          replyInput: {
            topicId: id,
            raw: message,
          },
        },
      });
    }
  };

  const onPressImage = async () => {
    try {
      let result = await imagePickerHandler(normalizedExtensions);
      if (!user || !result || !result.uri) {
        return;
      }
      let imageUri = result.uri;
      Keyboard.dismiss();
      navigate('ImagePreview', {
        topicId: id,
        imageUri,
        postPointer: stream.length,
        message,
      });
    } catch (error) {
      errorHandlerAlert(error);
    }
    return;
  };

  const compareTime = (currIndex: number) => {
    if (currIndex === 0) {
      return true;
    }
    const currContentTime = data
      ? data.contents[currIndex].time
      : new Date().toDateString();
    const prevContentTime = data
      ? data.contents[currIndex - 1].time
      : new Date().toDateString();

    const time = new Date(currContentTime);
    const prevTime = new Date(prevContentTime);

    return (time.getTime() - prevTime.getTime()) / (1000 * 60) > 15;
  };

  const isPrev = (currIndex: number) => {
    if (data) {
      if (currIndex === 0) {
        return;
      }
      const currUserId = data.contents[currIndex].userId;
      const prevUserId = data.contents[currIndex - 1].userId;

      return currUserId === prevUserId;
    }
    return false;
  };

  const settings = (operation: Operation, currIndex: number) => {
    if (currIndex === -1) {
      return operation === Operation.USER;
    }
    const isPrevUser = isPrev(currIndex);
    if (!isPrevUser) {
      return operation === Operation.USER;
    }
    if (isPrevUser) {
      return operation === Operation.USER
        ? compareTime(currIndex)
        : !compareTime(currIndex);
    }
    return false;
  };

  const onPressLink = () => {
    navigate('HyperLink', {
      id,
      postPointer,
      prevScreen: 'MessageDetail',
    });
  };

  const onPressAvatar = (username: string) => {
    navigate('UserInformation', { username });
  };

  const renderItem = ({ item, index }: MessageDetailRenderItem) => {
    let user;

    if (item.userId === 0) {
      user = members.find((member) => member.id === -1);
    } else {
      user = userWhoComment.find((member) => member.id === item.userId);
    }

    const newTimestamp = compareTime(index);
    const isPrevUser = isPrev(index);
    const currSettings = settings(Operation.USER, index);
    const senderUsername = user?.username || '';

    return (
      <MessageItem
        content={item}
        sender={user}
        newTimestamp={newTimestamp}
        isPrev={isPrevUser}
        settings={currSettings}
        onPressAvatar={() => onPressAvatar(senderUsername)}
      />
    );
  };

  const keyExtractor = ({ id }: MessageContent) => `message-${id}`;
  const getItem = (data: Array<MessageContent>, index: number) => data[index];
  const getItemCount = (data: Array<MessageContent>) => data?.length;

  const renderFooter = (
    <KeyboardAccessoryView
      androidAdjustResize
      inSafeAreaView
      hideBorder
      alwaysVisible
      style={styles.keyboardAcc}
    >
      <MentionList
        showUserList={showUserList}
        members={mentionMembers}
        mentionLoading={mentionLoading}
        rawText={message}
        textRef={messageRef}
        setRawText={setMessage}
        setShowUserList={setShowUserList}
      />
      <View style={styles.footerContainer}>
        <Icon
          name="Photo"
          style={styles.footerIcon}
          onPress={onPressImage}
          color={colors.textLighter}
        />
        <Icon
          name="Link"
          style={styles.footerIcon}
          onPress={onPressLink}
          color={colors.textLighter}
        />
        <ReplyInputField
          inputRef={messageRef}
          loading={replyLoading}
          onPressSend={onPressSend}
          style={styles.inputContainer}
          message={message}
          setMessage={setMessage}
          onSelectedChange={(cursor) => {
            setCursorPosition(cursor);
          }}
          onChangeValue={(message: string) => {
            mentionHelper(
              message,
              cursorPosition,
              setShowUserList,
              setMentionLoading,
              setMentionKeyword,
            );
            setMessage(message);
          }}
          onFocus={() => {
            setInputFocused(true);
            if (contentHeight) {
              setTimeout(() => {
                virtualListRef.current?.scrollToOffset({
                  offset: contentHeight + (37 * screen.height) / 100,
                });
              }, 50);
            }
          }}
          onBlur={() => {
            setInputFocused(false);
            if (contentHeight) {
              setTimeout(() => {
                virtualListRef.current?.scrollToOffset({
                  offset: contentHeight - (37 * screen.height) / 100,
                });
              }, 50);
            }
          }}
        />
      </View>
    </KeyboardAccessoryView>
  );

  const onMessageScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (!initialHeight) {
      setInitialHeight(
        Math.round(
          event.nativeEvent.contentSize.height -
            event.nativeEvent.layoutMeasurement.height,
        ),
      );
    }
    contentHeight = event.nativeEvent.contentOffset.y;
  };

  const onMessageScrollHandler = ({ index }: OnScrollInfo) => {
    if (index) {
      setTimeout(
        () =>
          virtualListRef.current?.scrollToIndex({
            animated: true,
            index,
          }),
        ios ? 50 : 150,
      );
    }
  };

  const onContentSizeChange = () => {
    if (isInitialRequest) {
      let pointerToIndex = postPointer - 1 - startIndex;
      let index = Math.min(19, pointerToIndex);
      try {
        virtualListRef.current?.scrollToIndex({
          animated: true,
          index,
        });
      } catch {
        virtualListRef.current?.scrollToEnd();
      }
      setTimeout(() => setIsInitialRequest(false), 1000);
    }
  };

  if (messageDetailLoading && title === '') {
    return <LoadingOrError loading />;
  }

  return (
    <SafeAreaView style={styles.container}>
      {ios ? (
        <CustomHeader title={t('Message')} />
      ) : (
        <Divider style={styles.divider} />
      )}
      <AvatarRow
        title={title}
        posters={members}
        style={styles.participants}
        extended
      />
      <VirtualizedList
        ref={virtualListRef}
        refreshControl={
          <RefreshControl
            refreshing={refetching || loadingOlderMessages}
            onRefresh={loadStartMore}
            tintColor={colors.primary}
          />
        }
        data={data?.contents}
        getItem={getItem}
        getItemCount={getItemCount}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
        contentInset={{
          bottom: textInputFocused ? (35 * screen.height) / 100 : 0,
          top: contentHeight ? ((5 * screen.height) / 100) * -1 : 0,
        }}
        onEndReachedThreshold={0.1}
        onEndReached={loadEndMore}
        onContentSizeChange={onContentSizeChange}
        contentContainerStyle={styles.messages}
        ListFooterComponent={
          <FooterLoadingIndicator isHidden={!hasNewerMessages} />
        }
        onScroll={onMessageScroll}
        onScrollToIndexFailed={onMessageScrollHandler}
      />
      {renderFooter}
    </SafeAreaView>
  );
}
Example #24
Source File: index.tsx    From hive-keychain-mobile with MIT License 4 votes vote down vote up
UrlModal = ({
  isVisible,
  toggle,
  onNewSearch,
  url,
  setUrl,
  history,
  clearHistory,
}: Props) => {
  const urlInput: MutableRefObject<TextInput> = useRef();
  const insets = useSafeAreaInsets();
  const styles = getStyles(insets);
  if (isVisible && urlInput) {
    setTimeout(() => {
      const {current} = urlInput;
      if (current && !current.isFocused()) {
        current.focus();
      }
    }, SLIDE_TIME);
  }

  const onSubmitUrlFromInput = (
    obj: NativeSyntheticEvent<TextInputSubmitEditingEventData>,
  ) => {
    const url = obj.nativeEvent.text;
    onSubmitUrl(url);
  };

  const onSubmitUrl = (url: string) => {
    toggle(false);
    // Add duckduck go search for url with no domain
    if (url.includes(' ') || !url.includes('.')) {
      onNewSearch(`https://duckduckgo.com/?q=${url.replace(/ /g, '+')}`);
    } else {
      const hasProtocol = url.match(/^[a-z]*:\/\//);
      const sanitizedURL = hasProtocol ? url : `https://${url}`;
      onNewSearch(sanitizedURL);
    }
  };

  const dismissModal = () => {
    toggle(false);
  };

  return (
    <Modal
      isVisible={isVisible}
      style={styles.urlModal}
      onBackdropPress={dismissModal}
      onBackButtonPress={dismissModal}
      animationIn="slideInDown"
      animationOut="slideOutUp"
      backdropOpacity={0.8}
      animationInTiming={SLIDE_TIME}
      animationOutTiming={SLIDE_TIME}
      useNativeDriver>
      <View style={styles.urlModalContent}>
        <TextInput
          keyboardType="web-search"
          ref={urlInput}
          autoCapitalize="none"
          autoCorrect={false}
          clearButtonMode="never"
          onChangeText={setUrl}
          onSubmitEditing={onSubmitUrlFromInput}
          placeholder={translate('browser.search')}
          returnKeyType="go"
          style={styles.urlInput}
          value={url}
          selectTextOnFocus
        />
        {url.length ? (
          <TouchableOpacity
            style={styles.option}
            onPress={() => Share.share({message: url})}>
            <ShareIcon width={16} height={16} />
          </TouchableOpacity>
        ) : null}
        {url.length ? (
          <TouchableOpacity
            style={styles.option}
            onPress={() => Clipboard.setString(url)}>
            <Copy width={16} height={16} />
          </TouchableOpacity>
        ) : null}
        {url.length ? (
          <TouchableOpacity style={styles.option} onPress={() => setUrl('')}>
            <Text style={styles.eraseText}>X</Text>
          </TouchableOpacity>
        ) : null}
      </View>

      <ScrollView>
        <UrlAutocomplete onSubmit={onSubmitUrl} input={url} history={history} />
        {history.length ? (
          <TouchableOpacity onPress={clearHistory}>
            <Text style={styles.clearHistory}>
              {translate('browser.history.clear')}
            </Text>
          </TouchableOpacity>
        ) : null}
      </ScrollView>
    </Modal>
  );
}
Example #25
Source File: Input.tsx    From react-native-base-ui with MIT License 4 votes vote down vote up
Input = forwardRef<RNTextInput, InputProps>(
  (
    {
      // overrides
      overrides,
      // configs
      positive = false,
      error = false,
      disabled = false,
      clearable = DEFAULT_CLEARABLE,
      size = INPUT_SIZE.default,
      // text input props
      value: _providedValue = '',
      secureTextEntry: _providedSecureTextEntry,
      // enhancers
      startEnhancer = null,
      endEnhancer = null,
      // callbacks
      onChangeText: _providedOnChangeText,
      onBlur: _providedOnBlur,
      onFocus: _providedOnFocus,
      ...restInputProps
    },
    ref
  ) => {
    //#region state/refs
    const [value, setValue] = useState(_providedValue);
    const [focused, setFocused] = useState(false);
    const [masked, setMasked] = useState(_providedSecureTextEntry);
    const inputRef = useRef<RNTextInput>(null);
    //#endregion

    //#region variables
    const adjoined = useMemo(
      () =>
        startEnhancer && endEnhancer
          ? INPUT_ADJOINED.both
          : startEnhancer
          ? INPUT_ADJOINED.start
          : endEnhancer
          ? INPUT_ADJOINED.end
          : INPUT_ADJOINED.none,
      [startEnhancer, endEnhancer]
    );
    const hasIconTrailing = useMemo(
      () => clearable || _providedSecureTextEntry,
      [clearable, _providedSecureTextEntry]
    );
    //#endregion

    //#region styles
    const styles = useThemedStyle(
      stylesCreator,
      size,
      positive,
      error,
      disabled,
      focused,
      adjoined,
      hasIconTrailing
    );
    //#endregion

    //#region override components
    const [Container, containerProps] = useOverrideComponent(
      View,
      styles.container,
      overrides?.container
    );

    const [
      TextInput,
      { onBlur: _overrideOnBlur, onFocus: _overrideOnFocus, ...textInputProps },
    ] = useOverrideComponent(
      RNTextInput,
      styles.baseInput,
      overrides?.baseInput
    );

    const [StartEnhancer, startEnhancerProps] = useOverrideComponent(
      Enhancer,
      styles.startEnhancer,
      overrides?.startEnhancer
    );

    const [EndEnhancer, endEnhancerProps] = useOverrideComponent(
      Enhancer,
      styles.endEnhancer,
      overrides?.endEnhancer
    );

    const [ClearIconContainer, clearIconContainerProps] = useOverrideComponent(
      Button,
      styles.clearIconContainer,
      overrides?.clearIconContainer
    );

    const [ClearIcon, clearIconProps] = useOverrideComponent(
      Icon,
      styles.clearIcon,
      overrides?.clearIcon
    );

    const [
      MaskToggleContainer,
      maskToggleContainerProps,
    ] = useOverrideComponent(
      Button,
      styles.maskToggleContainer,
      overrides?.maskToggleContainer
    );

    const [MaskToggleHideIcon, maskToggleHideIconProps] = useOverrideComponent(
      Icon,
      styles.maskToggleHideIcon,
      overrides?.maskToggleHideIcon
    );

    const [MaskToggleShowIcon, maskToggleShowIconProps] = useOverrideComponent(
      Icon,
      styles.maskToggleShowIcon,
      overrides?.maskToggleShowIcon
    );
    //#endregion

    //#region variables
    /**
     * since react native does not support `placeholder` or `caretColor` in styles,
     * here we extract them from the generated style and provide them
     * as a props.
     */
    const placeholderTextColor = useMemo(
      // @ts-ignore
      () => textInputProps.style.placeholder,
      [textInputProps.style]
    );
    const selectionColor = useMemo(
      // @ts-ignore
      () => textInputProps.style.caretColor,
      [textInputProps.style]
    );
    //#endregion

    //#region callbacks
    const handleOnChange = useCallback(
      ({
        nativeEvent: { text },
      }: NativeSyntheticEvent<TextInputChangeEventData>) => {
        setValue(text);
      },
      []
    );
    const handleOnBlur = useCallback(
      (event: NativeSyntheticEvent<TextInputFocusEventData>) => {
        setFocused(false);

        if (_providedOnBlur) {
          _providedOnBlur(event);
        }

        if (_overrideOnBlur) {
          _overrideOnBlur(event);
        }
      },
      [_providedOnBlur, _overrideOnBlur]
    );
    const handleOnFocus = useCallback(
      (event: NativeSyntheticEvent<TextInputFocusEventData>) => {
        setFocused(true);

        if (_providedOnFocus) {
          _providedOnFocus(event);
        }

        if (_overrideOnFocus) {
          _overrideOnFocus(event);
        }
      },
      [_providedOnFocus, _overrideOnFocus]
    );
    const handleOnClear = useCallback(() => {
      setValue('');
    }, []);
    const handleOnMaskTogglePress = useCallback(() => {
      setMasked(state => !state);
    }, []);
    //#endregion

    //#region forward ref
    useImperativeHandle(ref, () => inputRef.current!);
    //#endregion

    //#region effects
    useEffect(() => {
      if (_providedOnChangeText) {
        _providedOnChangeText(value);
      }
    }, [value, _providedOnChangeText]);
    //#endregion

    return (
      <Container {...containerProps}>
        <StartEnhancer {...startEnhancerProps} children={startEnhancer} />
        <TextInput
          // @ts-ignore
          ref={inputRef}
          placeholderTextColor={placeholderTextColor}
          selectionColor={selectionColor}
          onChange={handleOnChange}
          onChangeText={text => console.log('onChangeText', text)}
          onFocus={handleOnFocus}
          onBlur={handleOnBlur}
          value={value}
          secureTextEntry={masked}
          {...restInputProps}
          {...textInputProps}
        />
        {clearable && value && value.length ? (
          <ClearIconContainer
            kind={BUTTON_KIND.minimal}
            size={BUTTON_SIZE.mini}
            onPress={handleOnClear}
            {...clearIconContainerProps}
          >
            {/** @ts-ignore */}
            <ClearIcon name="delete-alt" {...clearIconProps} />
          </ClearIconContainer>
        ) : null}
        {_providedSecureTextEntry && (
          <MaskToggleContainer
            kind={BUTTON_KIND.minimal}
            size={BUTTON_SIZE.mini}
            onPress={handleOnMaskTogglePress}
            {...maskToggleContainerProps}
          >
            {masked ? (
              <MaskToggleShowIcon name="show" {...maskToggleShowIconProps} />
            ) : (
              <MaskToggleHideIcon name="hide" {...maskToggleHideIconProps} />
            )}
          </MaskToggleContainer>
        )}
        <EndEnhancer {...endEnhancerProps} children={endEnhancer} />
      </Container>
    );
  }
)
Example #26
Source File: FieldSearchBarFull.tsx    From react-native-jigsaw with MIT License 4 votes vote down vote up
FieldSearchBarFull: React.FC<Props> = ({
  showIcon,
  Icon,
  icon = "search",
  placeholder = "",
  style,
  theme: { colors, typography },
  onChange: changeOverride,
  onSubmit: submitOverride,
  value,
  defaultValue,
}) => {
  const [focused, setIsFocused] = React.useState(false);

  const onBlur = () => {
    setIsFocused(false);
  };

  const [internalValue, setInternalValue] = React.useState<string | undefined>(
    value || defaultValue
  );

  React.useEffect(() => {
    if (value != null) {
      setInternalValue(value);
    }
  }, [value]);

  React.useEffect(() => {
    if (defaultValue != null) {
      setInternalValue(defaultValue);
    }
  }, [defaultValue]);

  const onChange = React.useCallback(
    (text: string) => {
      changeOverride && changeOverride(text);
    },
    [changeOverride]
  );

  const onFocus = () => {
    setIsFocused(true);
  };

  const onSubmit = (
    e: NativeSyntheticEvent<TextInputSubmitEditingEventData>
  ) => {
    submitOverride && submitOverride(e);
  };

  const { lineHeight, ...typeStyles } = typography.body2; // eslint-disable-line @typescript-eslint/no-unused-vars

  const handleChangeText = (newValue: string) => {
    setInternalValue(newValue);
    if (onChange) {
      onChange(newValue);
    }
  };

  return (
    <View style={[styles.container, style]}>
      {showIcon && (
        <Icon
          name={icon}
          size={Config.fieldSearchBarFullIconSize}
          color={focused ? colors.primary : colors.light}
        />
      )}
      <View style={{ marginLeft: showIcon ? 12 : 0, flex: 1 }}>
        <TextInput
          clearButtonMode="while-editing"
          placeholder={placeholder}
          value={internalValue}
          onBlur={onBlur}
          onFocus={onFocus}
          onChangeText={handleChangeText}
          onSubmitEditing={onSubmit}
          placeholderTextColor={colors.light}
          style={[
            {
              color: colors.medium,
            },
            typeStyles,
          ]}
        />
      </View>
    </View>
  );
}
Example #27
Source File: mention-input.tsx    From react-native-controlled-mentions with MIT License 4 votes vote down vote up
MentionInput: FC<MentionInputProps> = (
  {
    value,
    onChange,

    partTypes = [],

    inputRef: propInputRef,

    containerStyle,

    onSelectionChange,

    ...textInputProps
  },
) => {
  const textInput = useRef<TextInput | null>(null);

  const [selection, setSelection] = useState({start: 0, end: 0});

  const {
    plainText,
    parts,
  } = useMemo(() => parseValue(value, partTypes), [value, partTypes]);

  const handleSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
    setSelection(event.nativeEvent.selection);

    onSelectionChange && onSelectionChange(event);
  };

  /**
   * Callback that trigger on TextInput text change
   *
   * @param changedText
   */
  const onChangeInput = (changedText: string) => {
    onChange(generateValueFromPartsAndChangedText(parts, plainText, changedText));
  };

  /**
   * We memoize the keyword to know should we show mention suggestions or not
   */
  const keywordByTrigger = useMemo(() => {
    return getMentionPartSuggestionKeywords(
      parts,
      plainText,
      selection,
      partTypes,
    );
  }, [parts, plainText, selection, partTypes]);

  /**
   * Callback on mention suggestion press. We should:
   * - Get updated value
   * - Trigger onChange callback with new value
   */
  const onSuggestionPress = (mentionType: MentionPartType) => (suggestion: Suggestion) => {
    const newValue = generateValueWithAddedSuggestion(
      parts,
      mentionType,
      plainText,
      selection,
      suggestion,
    );

    if (!newValue) {
      return;
    }

    onChange(newValue);

    /**
     * Move cursor to the end of just added mention starting from trigger string and including:
     * - Length of trigger string
     * - Length of mention name
     * - Length of space after mention (1)
     *
     * Not working now due to the RN bug
     */
    // const newCursorPosition = currentPart.position.start + triggerPartIndex + trigger.length +
    // suggestion.name.length + 1;

    // textInput.current?.setNativeProps({selection: {start: newCursorPosition, end: newCursorPosition}});
  };

  const handleTextInputRef = (ref: TextInput) => {
    textInput.current = ref as TextInput;

    if (propInputRef) {
      if (typeof propInputRef === 'function') {
        propInputRef(ref);
      } else {
        (propInputRef as MutableRefObject<TextInput>).current = ref as TextInput;
      }
    }
  };

  const renderMentionSuggestions = (mentionType: MentionPartType) => (
    <React.Fragment key={mentionType.trigger}>
      {mentionType.renderSuggestions && mentionType.renderSuggestions({
        keyword: keywordByTrigger[mentionType.trigger],
        onSuggestionPress: onSuggestionPress(mentionType),
      })}
    </React.Fragment>
  );

  return (
    <View style={containerStyle}>
      {(partTypes
        .filter(one => (
          isMentionPartType(one)
          && one.renderSuggestions != null
          && !one.isBottomMentionSuggestionsRender
        )) as MentionPartType[])
        .map(renderMentionSuggestions)
      }

      <TextInput
        multiline

        {...textInputProps}

        ref={handleTextInputRef}

        onChangeText={onChangeInput}
        onSelectionChange={handleSelectionChange}
      >
        <Text>
          {parts.map(({text, partType, data}, index) => partType ? (
            <Text
              key={`${index}-${data?.trigger ?? 'pattern'}`}
              style={partType.textStyle ?? defaultMentionTextStyle}
            >
              {text}
            </Text>
          ) : (
            <Text key={index}>{text}</Text>
          ))}
        </Text>
      </TextInput>

      {(partTypes
        .filter(one => (
          isMentionPartType(one)
          && one.renderSuggestions != null
          && one.isBottomMentionSuggestionsRender
        )) as MentionPartType[])
        .map(renderMentionSuggestions)
      }
    </View>
  );
}