Skip to content

Commit

Permalink
Merge pull request #39 from simonyiszk/36-qna-screen
Browse files Browse the repository at this point in the history
36 qna screen
  • Loading branch information
berenteb authored Feb 28, 2024
2 parents 451b4d2 + 6358454 commit 7a54fe4
Show file tree
Hide file tree
Showing 20 changed files with 386 additions and 21 deletions.
3 changes: 3 additions & 0 deletions app/(tabs)/home/qna.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { QnaScreen } from '../../../components/qna/qna-screen';

export default QnaScreen;
3 changes: 3 additions & 0 deletions app/(tabs)/presentation/qna.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { QnaScreen } from '../../../components/qna/qna-screen';

export default QnaScreen;
2 changes: 1 addition & 1 deletion components/base/screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ interface ScreenProps extends ViewProps {

export function Screen({ className, analyticsScreenName, ...props }: ScreenProps) {
usePageView(analyticsScreenName);
return <View className={cn('bg-slate-100 dark:bg-slate-900 h-full', className)} {...props} />;
return <View className={cn('bg-slate-100 dark:bg-slate-900 flex-1', className)} {...props} />;
}
31 changes: 20 additions & 11 deletions components/base/scroll-content.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { ScrollView, ScrollViewProps } from 'react-native';
import { forwardRef } from 'react';
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native';

import { cn } from '../../utils/common.utils';

export function ScrollContent({ className, ...props }: ScrollViewProps) {
return (
<ScrollView
className={cn('px-5 pt-5', className)}
contentContainerStyle={{
paddingBottom: 130,
}}
{...props}
></ScrollView>
);
interface ScrollContentProps extends ScrollViewProps {
className?: string;
contentContainerStyle?: ViewStyle;
}

const ScrollContent = forwardRef<ScrollView, ScrollContentProps>(
({ className, contentContainerStyle, ...props }, ref) => {
return (
<ScrollView
ref={ref}
className={cn('px-5 pt-5', className)}
contentContainerStyle={{ paddingBottom: contentContainerStyle?.paddingBottom ?? 130, ...contentContainerStyle }}
{...props}
/>
);
}
);

export { ScrollContent };
2 changes: 1 addition & 1 deletion components/common/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Header({ children, className, corner, ...props }: HeaderProps) {
<Feather name='arrow-left' size={30} color={extendedColors.primary['500']} />
</Pressable>
)}
{corner}
<View className='flex-row items-center'>{corner}</View>
</View>
)}
{children}
Expand Down
13 changes: 10 additions & 3 deletions components/common/styled-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ const textStyles: Record<ButtonVariant, string> = {
};

interface StyledButtonProps extends Omit<PressableProps, 'children'> {
children: string;
children?: string;
leftIcon?: React.ComponentProps<typeof Feather>['name'];
rightIcon?: React.ComponentProps<typeof Feather>['name'];
variant?: keyof typeof buttonStyles;
}

const StyledButton = forwardRef<View, StyledButtonProps>(
({ children, variant = 'primary', leftIcon, rightIcon, className, ...props }, ref) => {
({ children, variant = 'primary', leftIcon, rightIcon, className, disabled, ...props }, ref) => {
return (
<Pressable
className={cn(
Expand All @@ -35,6 +35,9 @@ const StyledButton = forwardRef<View, StyledButtonProps>(
'px-4',
'py-2',
'items-center justify-center flex-row space-x-2',
{
'opacity-50': disabled,
},
className
)}
ref={ref}
Expand All @@ -43,7 +46,11 @@ const StyledButton = forwardRef<View, StyledButtonProps>(
{leftIcon && (
<Feather name={leftIcon} size={24} color={variant === 'primary' ? 'white' : extendedColors.primary['500']} />
)}
<StyledText className={cn(textStyles[variant], 'font-raleway-bold text-center text-lg')}>{children}</StyledText>
{children && (
<StyledText className={cn(textStyles[variant], 'font-raleway-bold text-center text-lg')}>
{children}
</StyledText>
)}
{rightIcon && (
<Feather name={rightIcon} size={24} color={variant === 'primary' ? 'white' : extendedColors.primary['500']} />
)}
Expand Down
57 changes: 57 additions & 0 deletions components/qna/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useRef, useState } from 'react';
import { TextInput, View } from 'react-native';

import { extendedColors } from '../../theme/extendedColors';
import { useKeyboardOffset } from '../../utils/keyboard.utils';
import { StyledButton } from '../common/styled-button';

interface InputProps {
placeholder: string;
onSubmit: (text: string) => void;
disabled?: boolean;
}

const MIN_CHARACTERS_TO_SEND = 5;

export function Input({ placeholder, onSubmit, disabled = false }: InputProps) {
const ref = useRef<TextInput>(null);
const [value, setValue] = useState('');
const keyboardOffset = useKeyboardOffset();
const onSend = () => {
if (value.length < MIN_CHARACTERS_TO_SEND) return;
ref.current?.clear();
onSubmit(value);
setValue('');
};
const isDisabled = value.length < MIN_CHARACTERS_TO_SEND || disabled;
return (
<View
style={{
bottom: Math.max(130, keyboardOffset + 16),
}}
className='absolute left-0 right-0 mx-5 flex-row space-x-3 rounded-2xl bg-white dark:bg-slate-800 px-3 py-2 shadow-md max-h-60'
>
<TextInput
ref={ref}
returnKeyType='send'
placeholderTextColor={extendedColors.slate['500'] + '80'}
autoCapitalize='sentences'
textAlignVertical='center'
blurOnSubmit
multiline
className='flex-1 text-slate-900 dark:text-white font-raleway-regular self-center'
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.nativeEvent.text)}
onSubmitEditing={onSend}
editable={!disabled}
/>
<StyledButton
disabled={isDisabled}
className='rounded-full p-1 h-8 w-8 self-end'
leftIcon='arrow-up'
onPress={onSend}
/>
</View>
);
}
38 changes: 38 additions & 0 deletions components/qna/qna-answer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect } from 'react';
import { Animated } from 'react-native';

import { QnaMessage } from '../../types/qna.type';
import { useAnimated } from '../../utils/animation.utils';
import { StyledText } from '../base/text';

interface QnaAnswerProps {
message: QnaMessage;
}

export function QnaAnswer({ message }: QnaAnswerProps) {
const { value, forward } = useAnimated();
useEffect(() => {
if (!message.isInitial) forward();
}, []);
return (
<Animated.View
style={{
transform: [
{
translateY: value.current.interpolate({
inputRange: [0, 1],
outputRange: [100, 0],
}),
},
],
opacity: value.current.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
}}
className='bg-white dark:bg-slate-800 rounded-t-2xl rounded-br-2xl p-3 mb-2 mr-5'
>
<StyledText className='text-slate-900 dark:text-white text-lg'>{message.text}</StyledText>
</Animated.View>
);
}
38 changes: 38 additions & 0 deletions components/qna/qna-question.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect } from 'react';
import { Animated } from 'react-native';

import { QnaMessage } from '../../types/qna.type';
import { useAnimated } from '../../utils/animation.utils';
import { StyledText } from '../base/text';

interface QnaQuestionProps {
message: QnaMessage;
}

export function QnaQuestion({ message }: QnaQuestionProps) {
const { value, forward } = useAnimated();
useEffect(() => {
if (!message.isInitial) forward();
}, []);
return (
<Animated.View
style={{
transform: [
{
translateY: value.current.interpolate({
inputRange: [0, 1],
outputRange: [100, 0],
}),
},
],
opacity: value.current.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
}}
className='bg-primary-500 dark:bg-primary-300 rounded-t-2xl rounded-bl-2xl p-3 mb-2 ml-5'
>
<StyledText className='text-white dark:text-slate-900 text-lg'>{message.text}</StyledText>
</Animated.View>
);
}
70 changes: 70 additions & 0 deletions components/qna/qna-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView } from 'react-native';

import { useMessaging } from '../../hooks/use-messaging';
import { usePresentation } from '../../hooks/use-presentation';
import { useSafeId } from '../../utils/common.utils';
import { Screen } from '../base/screen';
import { ScrollContent } from '../base/scroll-content';
import { Header } from '../common/header';
import { SkeletonTitle } from '../common/skeletons/skeleton-title';
import { Subtitle } from '../common/subtitle';
import { Title } from '../common/title';
import { Input } from './input';
import { QnaAnswer } from './qna-answer';
import { QnaQuestion } from './qna-question';

const MAX_QUESTION_COUNT = Infinity;

export function QnaScreen() {
const { t } = useTranslation();
const ref = useRef<ScrollView>(null);
const id = useSafeId();
const presentation = usePresentation(id);
const messaging = useMessaging();

useEffect(() => {
const timeout = setTimeout(() => {
ref.current?.scrollToEnd({ animated: true });
}, 1);
return () => clearTimeout(timeout);
}, [messaging.messages]);

const questionCount = useMemo(
() => messaging.messages.filter((message) => message.kind === 'question').length,
[messaging.messages]
);

const remainingQuestions = MAX_QUESTION_COUNT - questionCount;

return (
<Screen>
<Header>
<Title>{t('qna.title')}</Title>
{presentation.isLoading && <SkeletonTitle />}
{presentation.data && <Subtitle>{presentation.data.title}</Subtitle>}
</Header>
<ScrollContent
ref={ref}
contentContainerStyle={{
paddingBottom: 200,
}}
automaticallyAdjustKeyboardInsets
>
{messaging.messages.map((message, index) =>
message.kind === 'question' ? (
<QnaQuestion key={index} message={message} />
) : (
<QnaAnswer key={index} message={message} />
)
)}
</ScrollContent>
<Input
disabled={remainingQuestions === 0}
placeholder={`${t('qna.placeholder')} (${remainingQuestions} ${t('qna.remainingQuestions')})`}
onSubmit={messaging.sendMessageText}
/>
</Screen>
);
}
2 changes: 1 addition & 1 deletion components/schedule/elements/favorite-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function FavoriteButton({ presentation }: FavoriteButtonProps) {
};

return (
<Pressable onPress={onPress}>
<Pressable onPress={onPress} className='mr-3'>
<AntDesign
name={isFavorite ? 'star' : 'staro'}
color={isFavorite ? extendedColors.primary['500'] : extendedColors.slate['500']}
Expand Down
22 changes: 22 additions & 0 deletions components/schedule/elements/qna-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Feather } from '@expo/vector-icons';
import { useNavigation } from 'expo-router';
import { Pressable } from 'react-native';
import { NativeStackNavigationProp } from 'react-native-screens/native-stack';

import { extendedColors } from '../../../theme/extendedColors';

interface QnaButtonProps {
slug: string;
}

export function QnaButton({ slug }: QnaButtonProps) {
const router = useNavigation<NativeStackNavigationProp<{ qna: { id: string } }>>();
const onPress = () => {
router.navigate('qna', { id: slug });
};
return (
<Pressable onPress={onPress}>
<Feather name='message-circle' color={extendedColors.slate['500']} size={30} />
</Pressable>
);
}
10 changes: 9 additions & 1 deletion components/schedule/layouts/presentation-details-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SkeletonTitle } from '../../common/skeletons/skeleton-title';
import { Subtitle } from '../../common/subtitle';
import { Title } from '../../common/title';
import { FavoriteButton } from '../elements/favorite-button';
import { QnaButton } from '../elements/qna-button';

interface ScheduleDetailsPageProps {
slug: string;
Expand All @@ -20,7 +21,14 @@ export function PresentationDetailsPage({ slug }: ScheduleDetailsPageProps) {
const endTime = ConferenceService.getFormattedTimestamp(data?.endTime ?? '');
return (
<Screen analyticsScreenName={`presentation-details/` + slug}>
<Header corner={data ? <FavoriteButton presentation={data} /> : undefined}>
<Header
corner={
<>
{data && <FavoriteButton presentation={data} />}
<QnaButton slug={slug} />
</>
}
>
{isLoading && <SkeletonTitle />}
{data && (
<>
Expand Down
Loading

0 comments on commit 7a54fe4

Please sign in to comment.