Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { I18nManager, StyleSheet, TextInput, TextInputProps } from 'react-native

import throttle from 'lodash/throttle';

import {
ChannelContextValue,
useChannelContext,
} from '../../contexts/channelContext/ChannelContext';
import {
MessageInputContextValue,
useMessageInputContext,
Expand Down Expand Up @@ -53,22 +49,22 @@ const isCommand = (text: string) => text[0] === '/' && text.split(' ').length <=

type AutoCompleteInputPropsWithContext<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = Pick<ChannelContextValue<StreamChatGenerics>, 'giphyEnabled'> &
Pick<
MessageInputContextValue<StreamChatGenerics>,
| 'additionalTextInputProps'
| 'autoCompleteSuggestionsLimit'
| 'giphyActive'
| 'maxMessageLength'
| 'mentionAllAppUsersEnabled'
| 'mentionAllAppUsersQuery'
| 'numberOfLines'
| 'onChange'
| 'setGiphyActive'
| 'setInputBoxRef'
| 'text'
| 'triggerSettings'
> &
> = Pick<
MessageInputContextValue<StreamChatGenerics>,
| 'additionalTextInputProps'
| 'autoCompleteSuggestionsLimit'
| 'giphyActive'
| 'giphyEnabled'
| 'maxMessageLength'
| 'mentionAllAppUsersEnabled'
| 'mentionAllAppUsersQuery'
| 'numberOfLines'
| 'onChange'
| 'setGiphyActive'
| 'setInputBoxRef'
| 'text'
| 'triggerSettings'
> &
Pick<
SuggestionsContextValue<StreamChatGenerics>,
'closeSuggestions' | 'openSuggestions' | 'updateSuggestions'
Expand Down Expand Up @@ -484,8 +480,8 @@ export const AutoCompleteInput = <
>(
props: AutoCompleteInputProps<StreamChatGenerics>,
) => {
const { giphyEnabled } = useChannelContext<StreamChatGenerics>();
const {
giphyEnabled,
additionalTextInputProps,
autoCompleteSuggestionsLimit,
giphyActive,
Expand Down
27 changes: 18 additions & 9 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,9 +781,11 @@ const ChannelWithContext = <
}

// If the event is typing.start or typing.stop, set the typing state
const isTypingEvent = event.type === 'typing.start' || event.type === 'typing.stop';
if (isTypingEvent) {
setTyping(channel);
if (event.type === 'typing.start' || event.type === 'typing.stop') {
if (event.user?.id !== client.userID) {
setTyping(channel);
}
return;
} else {
if (thread?.id) {
const updatedThreadMessages =
Expand Down Expand Up @@ -1758,6 +1760,11 @@ const ChannelWithContext = <
const sendMessageRef =
useRef<InputMessageInputContextValue<StreamChatGenerics>['sendMessage']>(sendMessage);
sendMessageRef.current = sendMessage;
const sendMessageStable = useCallback<
InputMessageInputContextValue<StreamChatGenerics>['sendMessage']
>((...args) => {
return sendMessageRef.current(...args);
}, []);

const inputMessageInputContext = useCreateInputMessageInputContext<StreamChatGenerics>({
additionalTextInputProps,
Expand Down Expand Up @@ -1811,7 +1818,7 @@ const ChannelWithContext = <
quotedMessage,
SendButton,
sendImageAsync,
sendMessage: (...args) => sendMessageRef.current(...args),
sendMessage: sendMessageStable,
SendMessageDisallowedIndicator,
setInputRef,
setQuotedMessageState,
Expand Down Expand Up @@ -1944,11 +1951,13 @@ const ChannelWithContext = <
VideoThumbnail,
});

const suggestionsContext = {
AutoCompleteSuggestionHeader,
AutoCompleteSuggestionItem,
AutoCompleteSuggestionList,
};
const suggestionsContext = useMemo(() => {
return {
AutoCompleteSuggestionHeader,
AutoCompleteSuggestionItem,
AutoCompleteSuggestionList,
};
}, [AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList]);

const threadContext = useCreateThreadContext({
allowThreadMessagesInChannel,
Expand Down
182 changes: 112 additions & 70 deletions package/src/components/Message/MessageSimple/MessageSimple.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { Dimensions, LayoutChangeEvent, StyleSheet, View } from 'react-native';

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
Expand All @@ -12,6 +12,8 @@ import Animated, {
withSpring,
} from 'react-native-reanimated';

const AnimatedWrapper = Animated.createAnimatedComponent(View);

import {
MessageContextValue,
useMessageContext,
Expand Down Expand Up @@ -205,77 +207,104 @@ const MessageSimpleWithContext = <

const translateX = useSharedValue(0);
const touchStart = useSharedValue<{ x: number; y: number } | null>(null);
const isSwiping = useSharedValue<boolean>(false);
const [isBeingSwiped, setIsBeingSwiped] = useState<boolean>(false);

const onSwipeToReply = () => {
const onSwipeToReply = useCallback(() => {
clearQuotedMessageState();
setQuotedMessageState(message);
};
}, [clearQuotedMessageState, message, setQuotedMessageState]);

const THRESHOLD = 25;

const triggerHaptic = NativeHandlers.triggerHaptic;

const swipeGesture = Gesture.Pan()
.hitSlop(messageSwipeToReplyHitSlop)
.onBegin((event) => {
touchStart.value = { x: event.x, y: event.y };
})
.onTouchesMove((event, state) => {
if (!touchStart.value || !event.changedTouches.length) {
state.fail();
return;
}

const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
const isHorizontalPanning = xDiff > yDiff;

if (isHorizontalPanning) {
state.activate();
} else {
state.fail();
}
})
.onStart(() => {
translateX.value = 0;
})
.onChange(({ translationX }) => {
if (translationX > 0) {
translateX.value = translationX;
}
})
.onEnd(() => {
if (translateX.value >= THRESHOLD) {
runOnJS(onSwipeToReply)();
if (triggerHaptic) {
runOnJS(triggerHaptic)('impactMedium');
}
}
translateX.value = withSpring(0, {
dampingRatio: 1,
duration: 500,
overshootClamping: true,
stiffness: 1,
});
});

const messageBubbleAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));

const swipeContentAnimatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]),
transform: [
{
translateX: interpolate(
translateX.value,
[0, THRESHOLD],
[-THRESHOLD, 0],
Extrapolation.CLAMP,
),
},
],
}));
const swipeGesture = useMemo(
() =>
Gesture.Pan()
.hitSlop(messageSwipeToReplyHitSlop)
.onBegin((event) => {
touchStart.value = { x: event.x, y: event.y };
})
.onTouchesMove((event, state) => {
if (!touchStart.value || !event.changedTouches.length) {
state.fail();
return;
}

const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x);
const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y);
const isHorizontalPanning = xDiff > yDiff;

if (isHorizontalPanning) {
state.activate();
isSwiping.value = true;
runOnJS(setIsBeingSwiped)(true);
} else {
state.fail();
}
})
.onStart(() => {
translateX.value = 0;
})
.onChange(({ translationX }) => {
if (translationX > 0) {
translateX.value = translationX;
}
})
.onEnd(() => {
if (translateX.value >= THRESHOLD) {
runOnJS(onSwipeToReply)();
if (triggerHaptic) {
runOnJS(triggerHaptic)('impactMedium');
}
}
translateX.value = withSpring(
0,
{
dampingRatio: 1,
duration: 500,
overshootClamping: true,
stiffness: 1,
},
() => {
isSwiping.value = false;
runOnJS(setIsBeingSwiped)(false);
},
);
}),
[isSwiping, messageSwipeToReplyHitSlop, onSwipeToReply, touchStart, translateX, triggerHaptic],
);

const messageBubbleAnimatedStyle = useAnimatedStyle(
() =>
isSwiping.value
? {
transform: [{ translateX: translateX.value }],
}
: {},
[],
);

const swipeContentAnimatedStyle = useAnimatedStyle(
() =>
isSwiping.value
? {
opacity: interpolate(translateX.value, [0, THRESHOLD], [0, 1]),
transform: [
{
translateX: interpolate(
translateX.value,
[0, THRESHOLD],
[-THRESHOLD, 0],
Extrapolation.CLAMP,
),
},
],
}
: {},
[],
);

const renderMessageBubble = useMemo(
() => (
Expand Down Expand Up @@ -309,18 +338,31 @@ const MessageSimpleWithContext = <
() => (
<GestureDetector gesture={swipeGesture}>
<View hitSlop={messageSwipeToReplyHitSlop} style={[styles.contentWrapper, contentWrapper]}>
<Animated.View
style={[styles.swipeContentContainer, swipeContentAnimatedStyle, swipeContentContainer]}
>
{MessageSwipeContent ? <MessageSwipeContent /> : null}
</Animated.View>
<Animated.View style={messageBubbleAnimatedStyle}>{renderMessageBubble}</Animated.View>
{isBeingSwiped ? (
<>
<AnimatedWrapper
style={[
styles.swipeContentContainer,
swipeContentAnimatedStyle,
swipeContentContainer,
]}
>
{MessageSwipeContent ? <MessageSwipeContent /> : null}
</AnimatedWrapper>
<AnimatedWrapper pointerEvents='box-none' style={messageBubbleAnimatedStyle}>
{renderMessageBubble}
</AnimatedWrapper>
</>
) : (
renderMessageBubble
)}
</View>
</GestureDetector>
),
[
MessageSwipeContent,
contentWrapper,
isBeingSwiped,
messageBubbleAnimatedStyle,
messageSwipeToReplyHitSlop,
renderMessageBubble,
Expand Down
Loading