diff --git a/package/src/components/ui/Avatar/AvatarGroup.tsx b/package/src/components/ui/Avatar/AvatarGroup.tsx
new file mode 100644
index 000000000..81d338f4e
--- /dev/null
+++ b/package/src/components/ui/Avatar/AvatarGroup.tsx
@@ -0,0 +1,233 @@
+import React, { useCallback, useMemo } from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import { UserResponse } from 'stream-chat';
+
+import { Avatar } from './Avatar';
+
+import { iconSizes } from './constants';
+
+import { UserAvatar } from './UserAvatar';
+
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { PeopleIcon } from '../../../icons/PeopleIcon';
+import { primitives } from '../../../theme';
+import { BadgeCount } from '../BadgeCount';
+import { OnlineIndicator } from '../OnlineIndicator';
+
+export type AvatarGroupProps = {
+ /**
+ * The size of the avatar group.
+ */
+ size: 'lg' | 'xl';
+ /**
+ * The items to display in the avatar group.
+ */
+ items: React.ReactNode[];
+};
+
+const sizes = {
+ lg: {
+ width: 40,
+ height: 40,
+ },
+ xl: {
+ width: 64,
+ height: 64,
+ },
+};
+
+const buildForTwo = (items: React.ReactNode[]) => {
+ return (
+ <>
+ {items[0]}
+ {items[1]}
+ >
+ );
+};
+
+const buildForThree = (items: React.ReactNode[]) => {
+ return (
+ <>
+ {items[0]}
+ {items[1]}
+ {items[2]}
+ >
+ );
+};
+
+const buildForFour = (items: React.ReactNode[]) => {
+ return (
+ <>
+ {items[0]}
+ {items[1]}
+ {items[2]}
+ {items[3]}
+ >
+ );
+};
+
+export const AvatarGroup = (props: AvatarGroupProps) => {
+ const { size, items = [] } = props;
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ const avatarSize = size === 'lg' ? 'sm' : 'lg';
+ const badgeCountSize = size === 'lg' ? 'xs' : 'md';
+
+ const buildForOne = useCallback(
+ (item: React.ReactNode) => {
+ return buildForTwo([
+
+ }
+ size={avatarSize}
+ />,
+ item,
+ ]);
+ },
+ [semantics.avatarTextDefault, avatarSize],
+ );
+
+ const buildForMore = useCallback(
+ (items: React.ReactNode[]) => {
+ const remainingItems = items.length - 2;
+ return (
+ <>
+ {items[0]}
+ {items[1]}
+
+
+
+ >
+ );
+ },
+ [badgeCountSize],
+ );
+
+ const renderItems = useMemo(() => {
+ const length = items.length;
+ if (length === 1) {
+ return buildForOne(items[0]);
+ }
+ if (length === 2) {
+ return buildForTwo(items);
+ }
+ if (length === 3) {
+ return buildForThree(items);
+ }
+ if (length === 4) {
+ return buildForFour(items);
+ }
+ return buildForMore(items);
+ }, [buildForMore, buildForOne, items]);
+
+ return (
+
+ {renderItems}
+
+ );
+};
+
+export type UserAvatarGroupProps = Pick & {
+ /**
+ * The users to display in the avatar group.
+ */
+ users: UserResponse[];
+ /**
+ * Whether to show the online indicator.
+ */
+ showOnlineIndicator?: boolean;
+};
+
+export const UserAvatarGroup = ({
+ users,
+ showOnlineIndicator = true,
+ size,
+}: UserAvatarGroupProps) => {
+ const styles = useUserAvatarGroupStyles();
+ const userAvatarSize = size === 'lg' ? 'sm' : 'lg';
+ const onlineIndicatorSize = size === 'xl' ? 'xl' : 'lg';
+ return (
+
+ (
+
+
+
+ ))}
+ />
+ {showOnlineIndicator ? (
+
+
+
+ ) : null}
+
+ );
+};
+
+const useUserAvatarGroupStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ userAvatarWrapper: {
+ borderWidth: 2,
+ borderColor: semantics.borderCoreOnAccent,
+ borderRadius: primitives.radiusMax,
+ },
+ onlineIndicatorWrapper: {
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ }),
+ [semantics],
+ );
+};
+
+// TODO V9: Add theming support here.
+const styles = StyleSheet.create({
+ container: {
+ padding: 2,
+ },
+ topStart: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ },
+ bottomEnd: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ },
+ topEnd: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ },
+ bottomStart: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ },
+ topCenter: {
+ alignItems: 'center',
+ },
+ bottomCenter: {
+ position: 'absolute',
+ bottom: 0,
+ alignSelf: 'center',
+ },
+});
diff --git a/package/src/components/ui/Avatar/ChannelAvatar.tsx b/package/src/components/ui/Avatar/ChannelAvatar.tsx
index 9755528ba..335e22c84 100644
--- a/package/src/components/ui/Avatar/ChannelAvatar.tsx
+++ b/package/src/components/ui/Avatar/ChannelAvatar.tsx
@@ -1,17 +1,13 @@
import React, { useMemo } from 'react';
-import { Channel } from 'stream-chat';
+import { Channel, UserResponse } from 'stream-chat';
import { Avatar } from './Avatar';
-import { iconSizes } from './constants';
-
-import { UserAvatar } from './UserAvatar';
+import { UserAvatarGroup } from './AvatarGroup';
import { useChannelPreviewDisplayPresence } from '../../../components/ChannelPreview/hooks/useChannelPreviewDisplayPresence';
-import { useChatContext } from '../../../contexts/chatContext/ChatContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { GroupIcon } from '../../../icons/GroupIcon';
import { hashStringToNumber } from '../../../utils/utils';
export type ChannelAvatarProps = {
@@ -22,10 +18,9 @@ export type ChannelAvatarProps = {
};
export const ChannelAvatar = (props: ChannelAvatarProps) => {
- const { client } = useChatContext();
const { channel } = props;
- const members = Object.values(channel.state.members);
const online = useChannelPreviewDisplayPresence(channel);
+ const { showOnlineIndicator = online, size, showBorder = true } = props;
const {
theme: { semantics },
@@ -34,38 +29,24 @@ export const ChannelAvatar = (props: ChannelAvatarProps) => {
const hashedValue = hashStringToNumber(channel.cid);
const index = ((hashedValue % 5) + 1) as 1 | 2 | 3 | 4 | 5;
const avatarBackgroundColor = semantics[`avatarPaletteBg${index}`];
- const avatarTextColor = semantics[`avatarPaletteText${index}`];
-
- const { size, showBorder = true, showOnlineIndicator = online } = props;
const channelImage = channel.data?.image;
- const placeholder = useMemo(() => {
- return ;
- }, [size, avatarTextColor]);
+ const usersForGroup = useMemo(
+ () => Object.values(channel.state.members).map((member) => member.user as UserResponse),
+ [channel.state.members],
+ );
if (!channelImage) {
- const otherMembers = members.filter((member) => member.user?.id !== client?.user?.id);
- const otherUser = otherMembers?.[0]?.user;
-
- const user = members.length === 1 ? client.user : members.length === 2 ? otherUser : null;
- if (user) {
- return (
-
- );
- }
+ return (
+
+ );
}
return (
diff --git a/package/src/components/ui/BadgeCount.tsx b/package/src/components/ui/BadgeCount.tsx
index f96653ab4..202e7f487 100644
--- a/package/src/components/ui/BadgeCount.tsx
+++ b/package/src/components/ui/BadgeCount.tsx
@@ -1,32 +1,59 @@
import React, { useMemo } from 'react';
-import { StyleSheet, Text } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { primitives } from '../../theme';
export type BadgeCountProps = {
- count: number;
- size: 'sm' | 'xs';
+ count: string | number;
+ size: 'sm' | 'xs' | 'md';
};
const sizes = {
+ md: {
+ minWidth: 32,
+ height: 32,
+ },
sm: {
- borderRadius: 12,
minWidth: 24,
- lineHeight: 22,
+ height: 24,
},
xs: {
- borderRadius: 10,
minWidth: 20,
- lineHeight: 18,
+ height: 20,
+ },
+};
+
+const textStyles = {
+ md: {
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightBold,
+ lineHeight: 14,
+ },
+ sm: {
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightBold,
+ lineHeight: 14,
+ },
+ xs: {
+ fontSize: primitives.typographyFontSizeXxs,
+ fontWeight: primitives.typographyFontWeightBold,
+ lineHeight: 10,
},
};
export const BadgeCount = (props: BadgeCountProps) => {
const { count, size = 'sm' } = props;
const styles = useStyles();
+ const paddingHorizontal = size === 'xs' ? primitives.spacingXxs : primitives.spacingXs;
- return {count};
+ return (
+
+ {count}
+
+ );
};
const useStyles = () => {
@@ -34,22 +61,24 @@ const useStyles = () => {
theme: { semantics },
} = useTheme();
- const { badgeBgInverse, badgeTextInverse, borderCoreSubtle } = semantics;
+ const { badgeBgDefault, badgeTextInverse, borderCoreSubtle } = semantics;
return useMemo(
() =>
StyleSheet.create({
- text: {
- backgroundColor: badgeBgInverse,
+ container: {
+ backgroundColor: badgeBgDefault,
borderColor: borderCoreSubtle,
borderWidth: 1,
+ borderRadius: primitives.radiusMax,
+ justifyContent: 'center',
+ },
+ text: {
color: badgeTextInverse,
- fontSize: primitives.typographyFontSizeXs,
- fontWeight: primitives.typographyFontWeightBold,
includeFontPadding: false,
textAlign: 'center',
},
}),
- [badgeBgInverse, badgeTextInverse, borderCoreSubtle],
+ [badgeBgDefault, badgeTextInverse, borderCoreSubtle],
);
};
diff --git a/package/src/components/ui/BadgeNotification.tsx b/package/src/components/ui/BadgeNotification.tsx
index 28227262e..cfd2632c6 100644
--- a/package/src/components/ui/BadgeNotification.tsx
+++ b/package/src/components/ui/BadgeNotification.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { StyleSheet, Text } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { primitives } from '../../theme';
@@ -13,19 +13,30 @@ export type BadgeNotificationProps = {
const sizes = {
md: {
- fontSize: 12,
- lineHeight: 16,
+ height: 20,
minWidth: 20,
borderWidth: 2,
},
sm: {
- fontSize: 10,
- lineHeight: 14,
+ height: 16,
minWidth: 16,
borderWidth: 1,
},
};
+const textStyles = {
+ md: {
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightBold,
+ lineHeight: 14,
+ },
+ sm: {
+ fontSize: primitives.typographyFontSizeXxs,
+ fontWeight: primitives.typographyFontWeightBold,
+ lineHeight: 10,
+ },
+};
+
export const BadgeNotification = (props: BadgeNotificationProps) => {
const { type, count, size = 'md', testID } = props;
const styles = useStyles();
@@ -40,9 +51,11 @@ export const BadgeNotification = (props: BadgeNotificationProps) => {
};
return (
-
- {count}
-
+
+
+ {count}
+
+
);
};
@@ -56,14 +69,16 @@ const useStyles = () => {
return useMemo(
() =>
StyleSheet.create({
+ container: {
+ paddingHorizontal: primitives.spacingXxs,
+ borderColor: badgeBorder,
+ borderRadius: primitives.radiusMax,
+ justifyContent: 'center',
+ },
text: {
color: badgeText,
- fontWeight: primitives.typographyFontWeightBold,
includeFontPadding: false,
textAlign: 'center',
- paddingHorizontal: primitives.spacingXxs,
- borderColor: badgeBorder,
- borderRadius: primitives.radiusMax,
},
}),
[badgeText, badgeBorder],
diff --git a/package/src/components/ui/OnlineIndicator.tsx b/package/src/components/ui/OnlineIndicator.tsx
index 129aee3dc..7f493d817 100644
--- a/package/src/components/ui/OnlineIndicator.tsx
+++ b/package/src/components/ui/OnlineIndicator.tsx
@@ -6,10 +6,15 @@ import { primitives } from '../../theme';
export type OnlineIndicatorProps = {
online: boolean;
- size: 'lg' | 'sm' | 'md';
+ size: 'xl' | 'lg' | 'sm' | 'md';
};
const sizes = {
+ xl: {
+ borderWidth: 2,
+ height: 16,
+ width: 16,
+ },
lg: {
borderWidth: 2,
height: 14,