diff --git a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
index 8c62fffa57..4733e48b26 100644
--- a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
+++ b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
@@ -19,114 +19,101 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
}
>
-
-
-
-
-
+ >
+
+
+
+
-
-
-
-
-
+ >
+
+
+
+
-
-
-
-
-
+ >
+
+
+
+
-
-
-
-
-
+ >
+
+
+
+
-
-
-
-
-
+ >
+
+
+
+
{
NativeHandlers.triggerHaptic('impactMedium');
stopVoiceRecordingHandler();
};
+ const StopIcon = useCallback(
+ (props: IconProps) => ,
+ [semantics.buttonDestructiveBg],
+ );
+
return (
-
);
};
@@ -77,11 +82,12 @@ const UploadRecording = ({
};
return (
-
);
@@ -97,12 +103,13 @@ const DeleteRecording = ({
deleteVoiceRecordingHandler();
};
return (
-
);
};
diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
index 6950f4370c..82f95e7385 100644
--- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
+++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
@@ -11,7 +11,6 @@ import Animated, {
withSpring,
} from 'react-native-reanimated';
-import { IconButton } from '../../../../components/ui/IconButton';
import { useActiveAudioPlayer } from '../../../../contexts/audioPlayerContext/AudioPlayerContext';
import {
MessageInputContextValue,
@@ -24,6 +23,7 @@ import { useStateStore } from '../../../../hooks/useStateStore';
import { NewMic } from '../../../../icons/NewMic';
import { NativeHandlers } from '../../../../native';
import { AudioRecorderManagerState } from '../../../../state-store/audio-recorder-manager';
+import { Button } from '../../../ui';
export type AudioRecordingButtonPropsWithContext = Pick<
MessageInputContextValue,
@@ -228,13 +228,17 @@ export const AudioRecordingButtonWithContext = (props: AudioRecordingButtonProps
return (
-
diff --git a/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx b/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx
index 9f8640be40..cce437b1e1 100644
--- a/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx
+++ b/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx
@@ -9,9 +9,8 @@ import {
MessageInputContextValue,
useMessageInputContext,
} from '../../../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { NewPlus } from '../../../../icons/NewPlus';
-import { IconButton } from '../../../ui/IconButton';
+import { Button } from '../../../ui/';
import { NativeAttachmentPicker } from '../NativeAttachmentPicker';
type AttachButtonPropsWithContext = Pick<
@@ -35,11 +34,6 @@ const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => {
selectedPicker,
toggleAttachmentPicker,
} = props;
- const {
- theme: {
- messageInput: { attachButton },
- },
- } = useTheme();
const onAttachButtonLayout = (event: LayoutChangeEvent) => {
const layout = event.nativeEvent.layout;
@@ -82,15 +76,17 @@ const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => {
return (
<>
-
{showAttachButtonPicker ? (
{
const seconds = useCooldownRemaining();
- const styles = useStyles();
- const {
- theme: {
- messageInput: {
- cooldownTimer: { text },
- },
- },
- } = useTheme();
-
- const icon = useCallback(() => {
- return (
-
- {seconds}
-
- );
- }, [seconds, text, styles]);
return (
-
);
};
-
-const useStyles = () => {
- const {
- theme: { semantics },
- } = useTheme();
- return useMemo(
- () =>
- StyleSheet.create({
- text: {
- color: semantics.textDisabled,
- fontSize: primitives.typographyFontSizeMd,
- fontWeight: primitives.typographyFontWeightSemiBold,
- lineHeight: primitives.typographyLineHeightNormal,
- },
- }),
- [semantics.textDisabled],
- );
-};
diff --git a/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx b/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx
index 48adc924f5..fbae9135b6 100644
--- a/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx
+++ b/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx
@@ -4,9 +4,8 @@ import {
MessageInputContextValue,
useMessageInputContext,
} from '../../../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { NewTick } from '../../../../icons/NewTick';
-import { IconButton } from '../../../ui/IconButton';
+import { Button } from '../../../ui';
export type EditButtonProps = Partial> & {
/** Disables the button */
@@ -18,12 +17,6 @@ export const EditButton = (props: EditButtonProps) => {
const { sendMessage: sendMessageFromContext } = useMessageInputContext();
const sendMessage = propsSendMessage || sendMessageFromContext;
- const {
- theme: {
- messageInput: { editButton },
- },
- } = useTheme();
-
const onPressHandler = useCallback(() => {
if (disabled) {
return;
@@ -32,13 +25,14 @@ export const EditButton = (props: EditButtonProps) => {
}, [disabled, sendMessage]);
return (
-
);
diff --git a/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx b/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx
index b0ccabad26..3a9f6962b9 100644
--- a/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx
+++ b/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx
@@ -4,9 +4,8 @@ import {
MessageInputContextValue,
useMessageInputContext,
} from '../../../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { SendRight } from '../../../../icons/SendRight';
-import { IconButton } from '../../../ui/IconButton';
+import { Button } from '../../../ui';
export type SendButtonProps = Partial> & {
/** Disables the button */
@@ -18,12 +17,6 @@ export const SendButton = (props: SendButtonProps) => {
const { sendMessage: sendMessageFromContext } = useMessageInputContext();
const sendMessage = propsSendMessage || sendMessageFromContext;
- const {
- theme: {
- messageInput: { sendButton },
- },
- } = useTheme();
-
const onPressHandler = useCallback(() => {
if (disabled) {
return;
@@ -32,14 +25,15 @@ export const SendButton = (props: SendButtonProps) => {
}, [disabled, sendMessage]);
return (
-
);
};
diff --git a/package/src/components/MessageList/ScrollToBottomButton.tsx b/package/src/components/MessageList/ScrollToBottomButton.tsx
index c8614c1656..3c867275d1 100644
--- a/package/src/components/MessageList/ScrollToBottomButton.tsx
+++ b/package/src/components/MessageList/ScrollToBottomButton.tsx
@@ -1,23 +1,13 @@
import React from 'react';
-import { StyleSheet, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
import Animated, { ZoomIn, ZoomOut } from 'react-native-reanimated';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { NewDown } from '../../icons/NewDown';
-import { BadgeNotification } from '../ui/BadgeNotification';
-import { IconButton } from '../ui/IconButton';
-
-const styles = StyleSheet.create({
- unreadCountNotificationContainer: {
- position: 'absolute',
- right: 0,
- top: 0,
- },
- container: {
- padding: 4,
- },
-});
+import { primitives } from '../../theme';
+import { BadgeNotification } from '../ui';
+import { Button } from '../ui/Button';
export type ScrollToBottomButtonProps = {
/** onPress handler */
@@ -29,13 +19,8 @@ export type ScrollToBottomButtonProps = {
export const ScrollToBottomButton = (props: ScrollToBottomButtonProps) => {
const { onPress, showNotification = true, unreadCount } = props;
-
const {
- theme: {
- messageList: {
- scrollToBottomButton: { container },
- },
- },
+ theme: { semantics },
} = useTheme();
if (!showNotification) {
@@ -43,28 +28,51 @@ export const ScrollToBottomButton = (props: ScrollToBottomButtonProps) => {
}
return (
-
-
+
-
-
+
+
{unreadCount ? (
) : null}
-
+
);
};
+const styles = StyleSheet.create({
+ unreadCountNotificationContainer: {
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ floatingButtonContainer: {
+ borderRadius: primitives.radiusMax,
+ },
+ container: {
+ padding: primitives.spacingXxs,
+ },
+});
+
ScrollToBottomButton.displayName = 'ScrollToBottomButton{messageList{scrollToBottomButton}}';
diff --git a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
index ea4ebf392b..c18d5de3c1 100644
--- a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
+++ b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
@@ -2,6 +2,8 @@
exports[`ScrollToBottomButton should render the message notification and match snapshot 1`] = `
-
+
-
-
-
-
-
+ >
+
+
+
+
-
-
-
-
-
+ >
+
+
+
+
@@ -2174,137 +2180,143 @@ exports[`Thread should match thread snapshot 1`] = `
style={{}}
>
-
-
-
-
-
+ >
+
+
+
+
diff --git a/package/src/components/ui/Avatar/index.ts b/package/src/components/ui/Avatar/index.ts
index 17e92462dd..d4bd64f670 100644
--- a/package/src/components/ui/Avatar/index.ts
+++ b/package/src/components/ui/Avatar/index.ts
@@ -2,3 +2,4 @@ export * from './Avatar';
export * from './ChannelAvatar';
export * from './UserAvatar';
export * from './AvatarStack';
+export * from './AvatarGroup';
diff --git a/package/src/components/ui/Button/Button.tsx b/package/src/components/ui/Button/Button.tsx
new file mode 100644
index 0000000000..c9051c3ed6
--- /dev/null
+++ b/package/src/components/ui/Button/Button.tsx
@@ -0,0 +1,168 @@
+import React, { useMemo } from 'react';
+import { I18nManager, Pressable, PressableProps, StyleSheet, Text, View } from 'react-native';
+
+import { buttonPadding, buttonSizes } from './constants';
+import { useButtonStyles } from './hooks/useButtonStyles';
+
+import { useTheme } from '../../../contexts/themeContext/ThemeContext';
+import { IconProps } from '../../../icons/utils/base';
+import { primitives } from '../../../theme';
+
+type IconRenderer = (props: IconProps) => React.ReactNode;
+
+export type ButtonProps = PressableProps & {
+ /**
+ * The style of the button.
+ */
+ variant: 'primary' | 'secondary' | 'destructive';
+ /**
+ * The type of the button.
+ */
+ type: 'solid' | 'outline' | 'ghost' | 'liquidGlass';
+ /**
+ * The size of the button.
+ */
+ size?: 'sm' | 'md' | 'lg';
+ /**
+ * Whether the button is selected.
+ */
+ selected?: boolean;
+ /**
+ * The icon to display on the leading side of the button.
+ */
+ LeadingIcon?: IconRenderer;
+ /**
+ * The content to display on the center of the button.
+ */
+ label?: React.ReactNode;
+ /**
+ * The icon to display on the trailing side of the button.
+ */
+ TrailingIcon?: IconRenderer;
+ /**
+ * Whether the button is only an icon.
+ */
+ iconOnly?: boolean;
+};
+
+export const Button = ({
+ variant,
+ type,
+ selected = false,
+ size = 'md',
+ LeadingIcon,
+ TrailingIcon,
+ iconOnly = false,
+ label,
+ onLayout,
+ disabled = false,
+ ...rest
+}: ButtonProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const buttonStyles = useButtonStyles({ variant, type });
+ const styles = useStyles();
+
+ const isRTL = I18nManager.isRTL;
+
+ const LeftIcon = isRTL ? TrailingIcon : LeadingIcon;
+ const RightIcon = isRTL ? LeadingIcon : TrailingIcon;
+
+ return (
+
+ [
+ {
+ backgroundColor: pressed
+ ? semantics.backgroundCorePressed
+ : selected
+ ? semantics.backgroundCoreSelected
+ : 'transparent',
+ },
+ styles.container,
+ { paddingHorizontal: buttonPadding[size] },
+ ]}
+ {...rest}
+ >
+ {LeftIcon ? (
+
+ ) : null}
+ {!iconOnly ? (
+ <>
+ {typeof label === 'string' ? (
+
+ {label}
+
+ ) : (
+ label
+ )}
+ {RightIcon ? (
+
+ ) : null}
+ >
+ ) : null}
+
+
+ );
+};
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ wrapper: {
+ borderRadius: primitives.radiusMax,
+ },
+ container: {
+ borderRadius: primitives.radiusMax,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flex: 1,
+ gap: primitives.spacingXs,
+ },
+ label: {
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ },
+ }),
+ [],
+ );
+};
diff --git a/package/src/components/ui/Button/constants.ts b/package/src/components/ui/Button/constants.ts
new file mode 100644
index 0000000000..718b576046
--- /dev/null
+++ b/package/src/components/ui/Button/constants.ts
@@ -0,0 +1,22 @@
+import { primitives } from '../../../theme';
+
+export const buttonSizes = {
+ sm: {
+ height: 32,
+ width: 32,
+ },
+ md: {
+ height: 40,
+ width: 40,
+ },
+ lg: {
+ height: 48,
+ width: 48,
+ },
+};
+
+export const buttonPadding = {
+ sm: primitives.spacingSm,
+ md: primitives.spacingMd,
+ lg: primitives.spacingLg,
+};
diff --git a/package/src/components/ui/Button/hooks/useButtonStyles.ts b/package/src/components/ui/Button/hooks/useButtonStyles.ts
new file mode 100644
index 0000000000..86f3bbb32c
--- /dev/null
+++ b/package/src/components/ui/Button/hooks/useButtonStyles.ts
@@ -0,0 +1,118 @@
+import { useMemo } from 'react';
+import { ColorValue } from 'react-native';
+
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { ButtonProps } from '../Button';
+
+export type ButtonStyleCategory =
+ | 'primarySolid'
+ | 'primaryOutline'
+ | 'primaryGhost'
+ | 'secondarySolid'
+ | 'secondaryOutline'
+ | 'secondaryGhost'
+ | 'destructiveSolid'
+ | 'destructiveOutline'
+ | 'destructiveGhost';
+
+export type ButtonStyle = {
+ foregroundColor?: ColorValue;
+ backgroundColor?: ColorValue;
+ borderColor?: ColorValue;
+ disabledForegroundColor?: ColorValue;
+ disabledBackgroundColor?: ColorValue;
+ disabledBorderColor?: ColorValue;
+};
+
+/**
+ * Returns the styles for the button based on the button style and type.
+ * @param buttonStyle - The style of the button.
+ * @param type - The type of the button.
+ * @returns The styles for the button.
+ */
+export const useButtonStyles = ({ variant, type }: Pick) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ const category = variant.concat(type[0].toUpperCase() + type.slice(1)) as ButtonStyleCategory;
+
+ const defaultButtonStyles: Record = useMemo(() => {
+ return {
+ primarySolid: {
+ foregroundColor: semantics.buttonPrimaryTextOnAccent,
+ backgroundColor: semantics.buttonPrimaryBg,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: semantics.backgroundCoreDisabled,
+ disabledBorderColor: undefined,
+ },
+ primaryOutline: {
+ foregroundColor: semantics.buttonPrimaryText,
+ backgroundColor: undefined,
+ borderColor: semantics.buttonPrimaryBorder,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: semantics.borderUtilityDisabled,
+ },
+ primaryGhost: {
+ foregroundColor: semantics.buttonPrimaryText,
+ backgroundColor: undefined,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: undefined,
+ },
+ secondarySolid: {
+ foregroundColor: semantics.buttonSecondaryTextOnAccent,
+ backgroundColor: semantics.buttonSecondaryBg,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: semantics.backgroundCoreDisabled,
+ disabledBorderColor: undefined,
+ },
+ secondaryOutline: {
+ foregroundColor: semantics.buttonSecondaryText,
+ backgroundColor: undefined,
+ borderColor: semantics.buttonSecondaryBorder,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: semantics.buttonSecondaryBorder,
+ },
+ secondaryGhost: {
+ foregroundColor: semantics.buttonSecondaryText,
+ backgroundColor: undefined,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: undefined,
+ },
+ destructiveSolid: {
+ foregroundColor: semantics.buttonDestructiveTextOnAccent,
+ backgroundColor: semantics.buttonDestructiveBg,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: semantics.backgroundCoreDisabled,
+ disabledBorderColor: undefined,
+ },
+ destructiveOutline: {
+ foregroundColor: semantics.buttonDestructiveText,
+ backgroundColor: undefined,
+ borderColor: semantics.buttonDestructiveBorder,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: semantics.borderUtilityDisabled,
+ },
+ destructiveGhost: {
+ foregroundColor: semantics.buttonDestructiveText,
+ backgroundColor: undefined,
+ borderColor: undefined,
+ disabledForegroundColor: semantics.textDisabled,
+ disabledBackgroundColor: undefined,
+ disabledBorderColor: undefined,
+ },
+ };
+ }, [semantics]);
+
+ return defaultButtonStyles[category];
+};
diff --git a/package/src/components/ui/Button/index.ts b/package/src/components/ui/Button/index.ts
new file mode 100644
index 0000000000..8b166a86e4
--- /dev/null
+++ b/package/src/components/ui/Button/index.ts
@@ -0,0 +1 @@
+export * from './Button';
diff --git a/package/src/components/ui/IconButton.tsx b/package/src/components/ui/IconButton.tsx
deleted file mode 100644
index dd6d9a6888..0000000000
--- a/package/src/components/ui/IconButton.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import React from 'react';
-import {
- ColorValue,
- Pressable,
- PressableProps,
- StyleProp,
- StyleSheet,
- View,
- ViewStyle,
-} from 'react-native';
-
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { IconProps } from '../../icons/utils/base';
-
-export type IconButtonProps = PressableProps & {
- Icon: React.FC | React.ReactNode;
- iconColor?: ColorValue;
- onPress?: () => void;
- size?: 'sm' | 'md' | 'lg';
- status?: 'disabled' | 'pressed' | 'selected' | 'enabled';
- type?: 'primary' | 'secondary' | 'destructive';
- category?: 'ghost' | 'filled' | 'outline';
-};
-
-const sizes = {
- lg: { borderRadius: 24, height: 48, width: 48 },
- md: { borderRadius: 20, height: 40, width: 40 },
- sm: {
- borderRadius: 16,
- height: 32,
- width: 32,
- },
-};
-
-const getBackgroundColor = ({
- type,
- status,
-}: {
- type: IconButtonProps['type'];
- status: IconButtonProps['status'];
-}) => {
- if (type === 'primary') {
- if (status === 'disabled') {
- return '#E2E6EA';
- } else {
- return '#005FFF';
- }
- } else if (type === 'secondary') {
- return '#FFFFFF';
- }
- return {
- destructive: '#D92F26',
- primary: '#005FFF',
- secondary: '#FFFFFF',
- }[type ?? 'primary'];
-};
-
-export const IconButton = (props: IconButtonProps) => {
- const {
- category = 'filled',
- status = 'enabled',
- Icon,
- iconColor,
- onPress,
- size = 'md',
- style,
- type = 'primary',
- ...rest
- } = props;
- const {
- theme: {
- colors: { selected: selectedColor },
- },
- } = useTheme();
- return (
- [
- styles.container,
- sizes[size],
- {
- backgroundColor:
- status === 'selected'
- ? selectedColor
- : pressed
- ? '#F5F6F7'
- : category === 'outline'
- ? 'none'
- : getBackgroundColor({ status, type }),
- borderColor: type === 'destructive' ? '#D92F26' : '#E2E6EA',
- borderWidth: category === 'outline' || category === 'filled' ? 1 : 0,
- },
- style as StyleProp,
- ]}
- {...rest}
- >
- {typeof Icon === 'function' ? (
-
- ) : (
- {Icon}
- )}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- },
-});
diff --git a/package/src/components/ui/index.ts b/package/src/components/ui/index.ts
index 6c1291a195..d9d379b842 100644
--- a/package/src/components/ui/index.ts
+++ b/package/src/components/ui/index.ts
@@ -1,6 +1,6 @@
export * from './Avatar';
export * from './BadgeCount';
export * from './BadgeNotification';
-export * from './IconButton';
export * from './OnlineIndicator';
export * from './VideoPlayIndicator';
+export * from './Button';