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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ColorValue, Pressable, StyleSheet, View, ViewStyle } from 'react-native

import { MessageTextContainer } from './MessageTextContainer';

import { useA11yLabel } from '../../../a11y/hooks/useA11yLabel';
import { useChatContext } from '../../../contexts';
import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext';
import {
Expand Down Expand Up @@ -127,6 +128,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
hidePaddingBottom,
} = props;
const { client } = useChatContext();
const accessibilityHint = useA11yLabel('a11y/Double tap and hold to activate contextual menu');
const {
Attachment,
FileAttachmentGroup,
Expand Down Expand Up @@ -318,6 +320,8 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {

return (
<Pressable
accessibilityHint={accessibilityHint}
accessible={message.poll_id ? false : undefined}
disabled={preventPress}
onLongPress={(event) => {
if (onLongPress) {
Expand Down
31 changes: 29 additions & 2 deletions package/src/components/Poll/Poll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import { StyleSheet, Text, View } from 'react-native';
import { PollOption as PollOptionClass } from 'stream-chat';

import { PollOption, ShowAllOptionsButton } from './components';
import { PollUIStateProvider } from './contexts/PollUIStateContext';

import { usePollAccessibilityActions } from './hooks/usePollAccessibilityActions';
import { usePollAccessibilityLabel } from './hooks/usePollAccessibilityLabel';
import { usePollState } from './hooks/usePollState';

import { useA11yLabel } from '../../a11y/hooks/useA11yLabel';
import {
PollContextProvider,
PollContextValue,
useTheme,
useTranslationContext,
} from '../../contexts';
import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext';
import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';

import { primitives } from '../../theme';
Expand Down Expand Up @@ -61,6 +66,10 @@ export const PollContent = () => {
const styles = useStyles();
const { PollButtons: PollButtonsComponent, PollHeader: PollHeaderComponent } =
useComponentsContext();
const { enabled: a11yEnabled } = useAccessibilityContext();
const accessibilityHint = useA11yLabel('a11y/Double tap and hold to activate contextual menu');
const accessibilityLabel = usePollAccessibilityLabel();
const { accessibilityActions, onAccessibilityAction } = usePollAccessibilityActions();

const {
theme: {
Expand All @@ -70,8 +79,24 @@ export const PollContent = () => {
},
} = useTheme();

// NOTE: Android custom accessibilityActions are broken in RN < 0.83.2 —
// see facebook/react-native#47268, fixed by PR #52724. On affected versions
// the actions menu surfaces only a subset of the list and dispatch
// announces "Action not supported". iOS works correctly on all versions.
// Once the SDK's minimum RN reaches 0.83.2, wrap the descendants below in
// <View importantForAccessibility='no-hide-descendants'> so Android
// TalkBack groups them under the composite rather than exposing each
// interactive child as a separate focus stop.
return (
<View style={[styles.container, container]}>
<View
accessibilityActions={accessibilityActions}
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
accessibilityRole={a11yEnabled ? 'button' : undefined}
accessible={a11yEnabled || undefined}
onAccessibilityAction={onAccessibilityAction}
style={[styles.container, container]}
>
<PollHeaderComponent />
<View style={[styles.optionsWrapper, optionsWrapper]}>
{options?.slice(0, defaultPollOptionCount)?.map((option: PollOptionClass) => (
Expand All @@ -93,7 +118,9 @@ export const Poll = ({ message, poll }: PollProps) => {
poll,
}}
>
{PollContentOverride ? <PollContentOverride /> : <PollContent />}
<PollUIStateProvider>
{PollContentOverride ? <PollContentOverride /> : <PollContent />}
</PollUIStateProvider>
</PollContextProvider>
);
};
Expand Down
81 changes: 37 additions & 44 deletions package/src/components/Poll/components/PollButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Modal, StyleSheet, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

Expand All @@ -13,13 +13,22 @@ import { useChatContext, usePollContext, useTheme, useTranslationContext } from
import { primitives } from '../../../theme';
import { defaultPollOptionCount } from '../../../utils/constants';
import { SafeAreaViewWrapper } from '../../UIComponents/SafeAreaViewWrapper';
import {
useAddCommentOpen,
useAllCommentsOpen,
useAllOptionsOpen,
usePollUIStateContext,
useSuggestOptionOpen,
useViewResultsOpen,
} from '../contexts/PollUIStateContext';
import { useIsPollCreatedByCurrentUser } from '../hook/useIsPollCreatedByCurrentUser';
import { usePollState } from '../hooks/usePollState';

export const ViewResultsButton = (props: PollButtonProps) => {
const { t } = useTranslationContext();
const { message, poll } = usePollContext();
const [showResults, setShowResults] = useState(false);
const { closeViewResults, openViewResults } = usePollUIStateContext();
const showResults = useViewResultsOpen();
const { onPress } = props;

const onPressHandler = useCallback(() => {
Expand All @@ -28,15 +37,11 @@ export const ViewResultsButton = (props: PollButtonProps) => {
return;
}

setShowResults(true);
}, [message, onPress, poll]);
openViewResults();
}, [message, onPress, openViewResults, poll]);

const styles = useStyles();

const onRequestClose = useCallback(() => {
setShowResults(false);
}, []);

return (
<>
<GenericPollButton
Expand All @@ -46,10 +51,10 @@ export const ViewResultsButton = (props: PollButtonProps) => {
type='outline'
/>
{showResults ? (
<Modal animationType='slide' onRequestClose={onRequestClose} visible={showResults}>
<Modal animationType='slide' onRequestClose={closeViewResults} visible={showResults}>
<GestureHandlerRootView style={styles.modalRoot}>
<SafeAreaViewWrapper style={styles.safeArea}>
<PollModalHeader onPress={onRequestClose} title={t('Poll Results')} />
<PollModalHeader onPress={closeViewResults} title={t('Poll Results')} />
<PollResults message={message} poll={poll} />
</SafeAreaViewWrapper>
</GestureHandlerRootView>
Expand All @@ -61,7 +66,8 @@ export const ViewResultsButton = (props: PollButtonProps) => {

export const ShowAllOptionsButton = (props: PollButtonProps) => {
const { t } = useTranslationContext();
const [showAllOptions, setShowAllOptions] = useState(false);
const { closeAllOptions, openAllOptions } = usePollUIStateContext();
const showAllOptions = useAllOptionsOpen();
const { message, poll } = usePollContext();
const { options } = usePollState();
const { onPress } = props;
Expand All @@ -72,12 +78,8 @@ export const ShowAllOptionsButton = (props: PollButtonProps) => {
return;
}

setShowAllOptions(true);
}, [message, onPress, poll]);

const onRequestClose = useCallback(() => {
setShowAllOptions(false);
}, []);
openAllOptions();
}, [message, onPress, openAllOptions, poll]);

const styles = useStyles();

Expand All @@ -90,10 +92,10 @@ export const ShowAllOptionsButton = (props: PollButtonProps) => {
/>
) : null}
{showAllOptions ? (
<Modal animationType='slide' onRequestClose={onRequestClose} visible={showAllOptions}>
<Modal animationType='slide' onRequestClose={closeAllOptions} visible={showAllOptions}>
<GestureHandlerRootView style={styles.modalRoot}>
<SafeAreaViewWrapper style={styles.safeArea}>
<PollModalHeader onPress={onRequestClose} title={t('Poll Options')} />
<PollModalHeader onPress={closeAllOptions} title={t('Poll Options')} />
<PollAllOptions message={message} poll={poll} />
</SafeAreaViewWrapper>
</GestureHandlerRootView>
Expand All @@ -107,7 +109,8 @@ export const ShowAllCommentsButton = (props: PollButtonProps) => {
const { t } = useTranslationContext();
const { message, poll } = usePollContext();
const { answersCount } = usePollState();
const [showAnswers, setShowAnswers] = useState(false);
const { closeAllComments, openAllComments } = usePollUIStateContext();
const showAnswers = useAllCommentsOpen();
const { onPress } = props;

const onPressHandler = useCallback(() => {
Expand All @@ -116,15 +119,11 @@ export const ShowAllCommentsButton = (props: PollButtonProps) => {
return;
}

setShowAnswers(true);
}, [message, onPress, poll]);
openAllComments();
}, [message, onPress, openAllComments, poll]);

const styles = useStyles();

const onRequestClose = useCallback(() => {
setShowAnswers(false);
}, []);

return (
<>
{answersCount && answersCount > 0 ? (
Expand All @@ -134,10 +133,10 @@ export const ShowAllCommentsButton = (props: PollButtonProps) => {
/>
) : null}
{showAnswers ? (
<Modal animationType='slide' onRequestClose={onRequestClose} visible={showAnswers}>
<Modal animationType='slide' onRequestClose={closeAllComments} visible={showAnswers}>
<GestureHandlerRootView style={styles.modalRoot}>
<SafeAreaViewWrapper style={styles.safeArea}>
<PollModalHeader onPress={onRequestClose} title={t('Poll Comments')} />
<PollModalHeader onPress={closeAllComments} title={t('Poll Comments')} />
<PollAnswersList message={message} poll={poll} />
</SafeAreaViewWrapper>
</GestureHandlerRootView>
Expand All @@ -151,7 +150,8 @@ export const SuggestOptionButton = (props: PollButtonProps) => {
const { t } = useTranslationContext();
const { message, poll } = usePollContext();
const { addOption, allowUserSuggestedOptions, isClosed } = usePollState();
const [showAddOptionDialog, setShowAddOptionDialog] = useState(false);
const { closeSuggestOption, openSuggestOption } = usePollUIStateContext();
const showAddOptionDialog = useSuggestOptionOpen();
const { onPress } = props;

const onPressHandler = useCallback(() => {
Expand All @@ -160,12 +160,8 @@ export const SuggestOptionButton = (props: PollButtonProps) => {
return;
}

setShowAddOptionDialog(true);
}, [message, onPress, poll]);

const onRequestClose = useCallback(() => {
setShowAddOptionDialog(false);
}, []);
openSuggestOption();
}, [message, onPress, openSuggestOption, poll]);

return (
<>
Expand All @@ -174,7 +170,7 @@ export const SuggestOptionButton = (props: PollButtonProps) => {
) : null}
{showAddOptionDialog ? (
<PollInputDialog
closeDialog={onRequestClose}
closeDialog={closeSuggestOption}
onSubmit={addOption}
placeholder={t('Enter a new option')}
title={t('Suggest an option')}
Expand All @@ -189,7 +185,8 @@ export const AddCommentButton = (props: PollButtonProps) => {
const { t } = useTranslationContext();
const { message, poll } = usePollContext();
const { addComment, allowAnswers, isClosed, ownAnswer } = usePollState();
const [showAddCommentDialog, setShowAddCommentDialog] = useState(false);
const { closeAddComment, openAddComment } = usePollUIStateContext();
const showAddCommentDialog = useAddCommentOpen();
const { onPress } = props;

const onPressHandler = useCallback(() => {
Expand All @@ -198,12 +195,8 @@ export const AddCommentButton = (props: PollButtonProps) => {
return;
}

setShowAddCommentDialog(true);
}, [message, onPress, poll]);

const onRequestClose = useCallback(() => {
setShowAddCommentDialog(false);
}, []);
openAddComment();
}, [message, onPress, openAddComment, poll]);

return (
<>
Expand All @@ -212,7 +205,7 @@ export const AddCommentButton = (props: PollButtonProps) => {
) : null}
{showAddCommentDialog ? (
<PollInputDialog
closeDialog={onRequestClose}
closeDialog={closeAddComment}
initialValue={ownAnswer?.answer_text ?? ''}
onSubmit={addComment}
placeholder={t('Your comment')}
Expand Down
17 changes: 4 additions & 13 deletions package/src/components/Poll/components/PollOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { useComponentsContext } from '../../../contexts/componentsContext/Compon

import { Check } from '../../../icons';
import { primitives } from '../../../theme';
import { useNotificationApi } from '../../Notifications';
import { ProgressBar } from '../../ProgressControl/ProgressBar';
import { UserAvatarStack } from '../../ui/Avatar/AvatarStack';
import { useIsPollCreatedByCurrentUser } from '../hook/useIsPollCreatedByCurrentUser';
import { usePollState } from '../hooks/usePollState';
import { usePollVoteToggle } from '../hooks/usePollVoteToggle';

const pollVoteAccessibilityStates = {
checked: { checked: true, selected: true },
Expand Down Expand Up @@ -161,7 +161,6 @@ export const PollOption = ({ option, showProgressBar = true, forceIncoming }: Po
export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
const { message, poll } = usePollContext();
const { isClosed, ownVotesByOptionId } = usePollState();
const { runWithNotificationTarget } = useNotificationApi();
const ownCapabilities = useOwnCapabilitiesContext();
const {
theme: { semantics },
Expand All @@ -179,24 +178,16 @@ export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
},
} = useTheme();

const toggleVote = useCallback(async () => {
await runWithNotificationTarget(async () => {
if (ownVotesByOptionId[option.id]) {
await poll.removeVote(ownVotesByOptionId[option.id]?.id, message.id);
} else {
await poll.castVote(option.id, message.id);
}
});
}, [message.id, option.id, ownVotesByOptionId, poll, runWithNotificationTarget]);
const toggleVote = usePollVoteToggle();

const onPressHandler = useCallback(() => {
if (onPress) {
onPress({ message, poll });
return;
}

toggleVote();
}, [message, onPress, poll, toggleVote]);
toggleVote(option.id);
}, [message, onPress, option.id, poll, toggleVote]);

const hasVote = !!ownVotesByOptionId[option.id];
const accessibilityState = hasVote
Expand Down
Loading
Loading