diff --git a/package/src/components/ChannelList/__tests__/ChannelList.test.js b/package/src/components/ChannelList/__tests__/ChannelList.test.js
index 2b1228ae99..d166544fa7 100644
--- a/package/src/components/ChannelList/__tests__/ChannelList.test.js
+++ b/package/src/components/ChannelList/__tests__/ChannelList.test.js
@@ -20,6 +20,7 @@ import dispatchChannelDeletedEvent from '../../../mock-builders/event/channelDel
import dispatchChannelHiddenEvent from '../../../mock-builders/event/channelHidden';
import dispatchChannelTruncatedEvent from '../../../mock-builders/event/channelTruncated';
import dispatchChannelUpdatedEvent from '../../../mock-builders/event/channelUpdated';
+import dispatchConnectionChangedEvent from '../../../mock-builders/event/connectionChanged';
import dispatchConnectionRecoveredEvent from '../../../mock-builders/event/connectionRecovered';
import dispatchMessageNewEvent from '../../../mock-builders/event/messageNew';
import dispatchNotificationAddedToChannelEvent from '../../../mock-builders/event/notificationAddedToChannel';
@@ -75,6 +76,11 @@ const ChannelListSwipeActionsProbe = () => {
return {`${swipeActionsEnabled}`};
};
+const ChannelListRefreshingProbe = () => {
+ const { refreshing } = useChannelsContext();
+ return {`${refreshing}`};
+};
+
const ChannelPreviewContent = ({ unread }) => {`${unread}`};
const ChannelListWithChannelPreview = () => {
@@ -805,6 +811,43 @@ describe('ChannelList', () => {
});
});
+ describe('connection.changed', () => {
+ it('should keep background reconnection refreshes debounced and out of pull-to-refresh UI', async () => {
+ useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);
+ const deferredPromise = new DeferredPromise();
+ const dateNowSpy = jest.spyOn(Date, 'now');
+ dateNowSpy.mockReturnValueOnce(0);
+ dateNowSpy.mockReturnValue(6000);
+
+ render(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('refreshing').children[0]).toBe('false');
+ });
+
+ chatClient.queryChannels = jest.fn(() => deferredPromise.promise);
+
+ act(() => dispatchConnectionChangedEvent(chatClient, false));
+ act(() => dispatchConnectionChangedEvent(chatClient, true));
+
+ await waitFor(() => {
+ expect(chatClient.queryChannels).toHaveBeenCalled();
+ });
+
+ act(() => dispatchConnectionChangedEvent(chatClient, true));
+
+ expect(chatClient.queryChannels).toHaveBeenCalledTimes(1);
+ expect(screen.getByTestId('refreshing').children[0]).toBe('false');
+
+ deferredPromise.resolve([testChannel1]);
+ dateNowSpy.mockRestore();
+ });
+ });
+
describe('channel.truncated', () => {
it('should call the `onChannelTruncated` function prop, if provided', async () => {
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);
diff --git a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
index 27af07468f..7965d278f0 100644
--- a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
+++ b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
@@ -24,7 +24,7 @@ type Parameters = {
const RETRY_INTERVAL_IN_MS = 5000;
-type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels';
+type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels' | 'backgroundRefresh';
export type QueryChannels = (queryType?: QueryType, retryCount?: number) => Promise;
@@ -68,6 +68,7 @@ export const usePaginatedChannels = ({
const hasUpdatedData =
queryType === 'loadChannels' ||
queryType === 'refresh' ||
+ queryType === 'backgroundRefresh' ||
[
JSON.stringify(filtersRef.current) !== JSON.stringify(filters),
JSON.stringify(sortRef.current) !== JSON.stringify(sort),
@@ -129,7 +130,7 @@ export const usePaginatedChannels = ({
setActiveQueryType(null);
};
- const refreshList = async () => {
+ const refreshList = async ({ isBackground = false }: { isBackground?: boolean } = {}) => {
const now = Date.now();
// Only allow pull-to-refresh 5 seconds after last successful refresh.
if (now - lastRefresh.current < RETRY_INTERVAL_IN_MS && error === undefined) {
@@ -137,7 +138,7 @@ export const usePaginatedChannels = ({
}
lastRefresh.current = Date.now();
- await queryChannels('refresh');
+ await queryChannels(isBackground ? 'backgroundRefresh' : 'refresh');
};
const reloadList = async () => {
@@ -167,7 +168,9 @@ export const usePaginatedChannels = ({
'connection.changed',
async (event) => {
if (event.online) {
- await refreshList();
+ // Reconnection refreshes should stay silent, but still share the same debounce
+ // path as pull-to-refresh.
+ await refreshList({ isBackground: true });
}
},
);
@@ -195,7 +198,7 @@ export const usePaginatedChannels = ({
loadingNextPage: pagination?.isLoadingNext,
loadNextPage: channelManager.loadNext,
refreshing: activeQueryType === 'refresh',
- refreshList,
+ refreshList: () => refreshList(),
reloadList,
staticChannelsActive,
};
diff --git a/package/src/components/MessageMenu/MessageUserReactions.tsx b/package/src/components/MessageMenu/MessageUserReactions.tsx
index affcf9c444..a606374a1c 100644
--- a/package/src/components/MessageMenu/MessageUserReactions.tsx
+++ b/package/src/components/MessageMenu/MessageUserReactions.tsx
@@ -291,6 +291,7 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => {
{
const [showAddCommentDialog, setShowAddCommentDialog] = useState(false);
const { onPress } = props;
+ const styles = useStyles();
+
const onPressHandler = useCallback(() => {
if (onPress) {
onPress({ message, poll });
@@ -37,10 +40,10 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => {
}, [message, onPress, poll]);
return (
- <>
+
);
};
@@ -64,6 +67,7 @@ export type PollAnswersListProps = PollContextValue & {
};
export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
+ const { client } = useChatContext();
const { t, tDateTimeParser } = useTranslationContext();
const { votingVisibility } = usePollState();
@@ -87,25 +91,32 @@ export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
[answer.updated_at, t, tDateTimeParser],
);
+ const isMyAnswer = client.userID === answer.user?.id;
+
const isAnonymous = useMemo(
- () => votingVisibility === VotingVisibility.anonymous,
- [votingVisibility],
+ () => votingVisibility === VotingVisibility.anonymous && !isMyAnswer,
+ [votingVisibility, isMyAnswer],
);
+ const answerAuthorName = isMyAnswer ? t('You') : answer.user?.name;
+
return (
-
- {answer.answer_text}
-
-
- {!isAnonymous && answer.user?.image ? (
-
- ) : null}
-
- {isAnonymous ? t('Anonymous') : answer.user?.name}
-
+
+
+ {answer.answer_text}
+
+
+ {!isAnonymous && answer.user?.image ? (
+
+ ) : null}
+
+ {isAnonymous ? t('Anonymous') : answerAuthorName}
+
+ {dateString}
+
- {dateString}
+ {isMyAnswer ? : null}
);
};
@@ -137,7 +148,6 @@ export const PollAnswersListContent = ({
renderItem={renderPollAnswerListItem}
{...additionalFlatListProps}
/>
-
);
};
@@ -164,14 +174,7 @@ const useStyles = () => {
return useMemo(
() =>
StyleSheet.create({
- addCommentButtonContainer: {
- alignItems: 'center',
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 18,
- },
contentContainer: { gap: primitives.spacingMd },
- addCommentButtonText: { fontSize: 16 },
container: {
flex: 1,
padding: primitives.spacingMd,
@@ -179,31 +182,41 @@ const useStyles = () => {
},
listItemAnswerText: {
fontSize: primitives.typographyFontSizeMd,
- lineHeight: primitives.typographyLineHeightRelaxed,
- fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
color: semantics.textPrimary,
},
- listItemContainer: {
+ listItemWrapper: {
borderRadius: primitives.radiusLg,
- padding: primitives.spacingMd,
backgroundColor: semantics.backgroundCoreSurfaceCard,
},
+ listItemContainer: {
+ padding: primitives.spacingMd,
+ gap: primitives.spacingXs,
+ },
listItemInfoContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
- marginTop: 24,
},
listItemInfoUserName: {
- color: semantics.textPrimary,
+ color: semantics.chatTextUsername,
fontSize: primitives.typographyFontSizeSm,
- marginLeft: primitives.spacingXxs,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
},
listItemInfoDate: {
fontSize: primitives.typographyFontSizeSm,
color: semantics.textTertiary,
},
- listItemUserInfoContainer: { alignItems: 'center', flexDirection: 'row' },
+ listItemUserInfoContainer: {
+ gap: primitives.spacingXs,
+ alignItems: 'center',
+ flexDirection: 'row',
+ },
+ inlineButton: {
+ borderColor: semantics.borderCoreDefault,
+ borderTopWidth: 1,
+ },
}),
[semantics],
);
diff --git a/package/src/components/Poll/components/PollOption.tsx b/package/src/components/Poll/components/PollOption.tsx
index 8e72aad7ea..835457834b 100644
--- a/package/src/components/Poll/components/PollOption.tsx
+++ b/package/src/components/Poll/components/PollOption.tsx
@@ -14,6 +14,7 @@ import {
useOwnCapabilitiesContext,
usePollContext,
useTheme,
+ useTranslationContext,
} from '../../../contexts';
import { Check } from '../../../icons';
@@ -26,6 +27,7 @@ import { usePollState } from '../hooks/usePollState';
export type PollOptionProps = {
option: PollOptionClass;
showProgressBar?: boolean;
+ forceIncoming?: boolean;
};
export type PollAllOptionsContentProps = PollContextValue & {
@@ -36,6 +38,7 @@ export type PollAllOptionsContentProps = PollContextValue & {
export const PollAllOptionsContent = ({
additionalScrollViewProps,
}: Pick) => {
+ const { t } = useTranslationContext();
const { name, options } = usePollState();
const {
@@ -50,12 +53,13 @@ export const PollAllOptionsContent = ({
return (
+ {t('Question')}
{name}
{options?.map((option: PollOptionClass) => (
-
+
))}
@@ -78,19 +82,15 @@ export const PollAllOptions = ({
);
-export const PollOption = ({ option, showProgressBar = true }: PollOptionProps) => {
- const { latestVotesByOption, maxVotedOptionIds, voteCountsByOption } = usePollState();
+export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => {
+ const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState();
const styles = useStyles();
const relevantVotes = useMemo(
() => latestVotesByOption?.[option.id] || [],
[latestVotesByOption, option.id],
);
- const maxVotes = useMemo(
- () =>
- maxVotedOptionIds?.[0] && voteCountsByOption ? voteCountsByOption[maxVotedOptionIds[0]] : 0,
- [maxVotedOptionIds, voteCountsByOption],
- );
+
const votes = voteCountsByOption[option.id] || 0;
const {
@@ -105,13 +105,15 @@ export const PollOption = ({ option, showProgressBar = true }: PollOptionProps)
} = useTheme();
const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
- const unFilledColor = isPollCreatedByClient
- ? semantics.chatPollProgressTrackOutgoing
- : semantics.chatPollProgressTrackIncoming;
+ const unFilledColor =
+ isPollCreatedByClient && !forceIncoming
+ ? semantics.chatPollProgressTrackOutgoing
+ : semantics.chatPollProgressTrackIncoming;
- const filledColor = isPollCreatedByClient
- ? semantics.chatPollProgressFillOutgoing
- : semantics.chatPollProgressFillIncoming;
+ const filledColor =
+ isPollCreatedByClient && !forceIncoming
+ ? semantics.chatPollProgressFillOutgoing
+ : semantics.chatPollProgressFillIncoming;
return (
@@ -133,7 +135,7 @@ export const PollOption = ({ option, showProgressBar = true }: PollOptionProps)
{showProgressBar ? (
@@ -273,6 +275,13 @@ const useAllOptionStyles = () => {
lineHeight: primitives.typographyLineHeightRelaxed,
fontWeight: primitives.typographyFontWeightSemiBold,
color: semantics.textPrimary,
+ paddingTop: primitives.spacingXs,
+ },
+ allOptionsTitleMeta: {
+ fontSize: primitives.typographyFontSizeSm,
+ color: semantics.textTertiary,
+ lineHeight: primitives.typographyLineHeightNormal,
+ fontWeight: primitives.typographyFontWeightMedium,
},
allOptionsWrapper: {
flex: 1,
diff --git a/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx
index 372af40a76..9f2d436359 100644
--- a/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx
+++ b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx
@@ -112,7 +112,9 @@ const useStyles = () => {
backgroundColor: semantics.backgroundCoreSurfaceCard,
borderRadius: primitives.radiusLg,
marginBottom: primitives.spacingMd,
- padding: primitives.spacingMd,
+ paddingHorizontal: primitives.spacingMd,
+ paddingTop: primitives.spacingMd,
+ paddingBottom: primitives.spacingXs,
},
headerContainer: {
flexDirection: 'row',
@@ -133,6 +135,7 @@ const useStyles = () => {
lineHeight: primitives.typographyLineHeightNormal,
fontWeight: primitives.typographyFontWeightSemiBold,
color: semantics.textPrimary,
+ paddingTop: primitives.spacingXs,
marginLeft: primitives.spacingMd,
},
}),
diff --git a/package/src/components/Poll/components/PollResults/PollResultItem.tsx b/package/src/components/Poll/components/PollResults/PollResultItem.tsx
index f6a3cb830a..702fd3eb83 100644
--- a/package/src/components/Poll/components/PollResults/PollResultItem.tsx
+++ b/package/src/components/Poll/components/PollResults/PollResultItem.tsx
@@ -57,7 +57,9 @@ export const ShowAllVotesButton = (props: ShowAllVotesButtonProps) => {
{ownCapabilities.queryPollVotes &&
voteCountsByOption &&
voteCountsByOption?.[option.id] > 5 ? (
-
+
+
+
) : null}
{showAllVotes ? (
{
return (
-
- {t('Option {{count}}', { count: index + 1 })}
-
-
- {option.text}
-
- {t('{{count}} votes', { count: voteCountsByOption[option.id] ?? 0 })}
+
+
+ {t('Option {{count}}', { count: index + 1 })}
+
+ {option.text}
+
+ {t('{{count}} votes', { count: voteCountsByOption[option.id] ?? 0 })}
+
+
- {latestVotesByOption?.[option.id]?.length > 0
- ? (latestVotesByOption?.[option.id] ?? []).slice(0, 5).map(PollResultsVoteItem)
- : null}
+ {latestVotesByOption?.[option.id]?.length > 0 ? (
+
+ {(latestVotesByOption?.[option.id] ?? []).slice(0, 5).map(PollResultsVoteItem)}
+
+ ) : (
+
+ )}
);
@@ -127,11 +135,21 @@ const useStyles = () => {
return useMemo(
() =>
StyleSheet.create({
+ spacer: {
+ paddingBottom: primitives.spacingXs,
+ },
container: {
backgroundColor: semantics.backgroundCoreSurfaceCard,
borderRadius: primitives.radiusLg,
marginBottom: primitives.spacingMd,
- padding: primitives.spacingMd,
+ },
+ metaContainer: {
+ paddingTop: primitives.spacingMd,
+ paddingHorizontal: primitives.spacingMd,
+ },
+ votesContainer: {
+ paddingHorizontal: primitives.spacingMd,
+ paddingVertical: primitives.spacingXs,
},
headerContainer: {
flexDirection: 'row',
@@ -167,6 +185,12 @@ const useStyles = () => {
backgroundColor: semantics.backgroundCoreElevation1,
flex: 1,
},
+ inlineButton: {
+ borderColor: semantics.borderCoreDefault,
+ borderTopWidth: 1,
+ paddingHorizontal: primitives.spacingMd,
+ paddingVertical: primitives.spacingXs,
+ },
}),
[semantics],
);
diff --git a/package/src/components/Poll/hooks/usePollState.ts b/package/src/components/Poll/hooks/usePollState.ts
index 64ec01068c..bf019c8fd9 100644
--- a/package/src/components/Poll/hooks/usePollState.ts
+++ b/package/src/components/Poll/hooks/usePollState.ts
@@ -32,6 +32,7 @@ export type UsePollStateSelectorReturnType = {
ownVotesByOptionId: Record;
voteCountsByOption: Record;
votingVisibility: VotingVisibility | undefined;
+ voteCount: number;
};
export type UsePollStateReturnType = UsePollStateSelectorReturnType & {
@@ -56,6 +57,7 @@ const selector = (nextValue: PollState): UsePollStateSelectorReturnType => ({
ownVotesByOptionId: nextValue.ownVotesByOptionId,
voteCountsByOption: nextValue.vote_counts_by_option,
votingVisibility: nextValue.voting_visibility,
+ voteCount: nextValue.vote_count,
});
export const usePollState = (): UsePollStateReturnType => {
@@ -76,6 +78,7 @@ export const usePollState = (): UsePollStateReturnType => {
ownVotesByOptionId,
voteCountsByOption,
votingVisibility,
+ voteCount,
} = usePollStateStore(selector);
const addOption = useCallback(
@@ -109,5 +112,6 @@ export const usePollState = (): UsePollStateReturnType => {
ownVotesByOptionId,
voteCountsByOption,
votingVisibility,
+ voteCount,
};
};
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index ac9f9e5584..cdfe66b10c 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -822,6 +822,7 @@ export type Theme = {
container: ViewStyle;
infoContainer: ViewStyle;
userInfoContainer: ViewStyle;
+ wrapper: ViewStyle;
};
};
button: { container: ViewStyle; text: TextStyle };
@@ -1731,6 +1732,7 @@ export const defaultTheme: Theme = {
container: {},
infoContainer: {},
userInfoContainer: {},
+ wrapper: {},
},
},
button: { container: {}, text: {} },