├── .npmrc ├── src ├── theme │ ├── default │ │ └── index.ts │ ├── index.ts │ ├── tailwind.ts │ └── colors │ │ ├── blackA.ts │ │ └── whiteA.ts ├── components-next │ ├── common │ │ ├── icon │ │ │ ├── index.ts │ │ │ └── Icon.tsx │ │ ├── slider │ │ │ └── index.ts │ │ ├── search │ │ │ ├── index.ts │ │ │ └── SearchBar.stories.tsx │ │ ├── spinner │ │ │ └── index.ts │ │ ├── swipeable │ │ │ └── index.ts │ │ ├── filters │ │ │ ├── index.ts │ │ │ └── stories │ │ │ │ ├── FilterBarMockData.ts │ │ │ │ ├── FilterBar.stories.tsx │ │ │ │ └── FilterButton.stories.tsx │ │ ├── avatar │ │ │ └── index.ts │ │ ├── bottomsheet │ │ │ ├── index.ts │ │ │ ├── BottomSheetWrapper.tsx │ │ │ └── BottomSheetHeader.tsx │ │ └── index.ts │ ├── spinner │ │ └── index.ts │ ├── action-tabs │ │ └── index.ts │ ├── no-network │ │ └── index.ts │ ├── label-section │ │ └── index.ts │ ├── button │ │ └── index.ts │ ├── sheet-components │ │ └── index.ts │ ├── list-components │ │ ├── index.ts │ │ ├── ChannelIndicator.tsx │ │ └── PriorityIndicator.tsx │ ├── native-components │ │ ├── NText.tsx │ │ ├── NView.tsx │ │ └── index.ts │ └── index.ts ├── screens │ ├── chat-screen │ │ ├── index.ts │ │ ├── components │ │ │ ├── message-item │ │ │ │ └── index.ts │ │ │ ├── message-menu │ │ │ │ └── index.ts │ │ │ ├── audio-recorder │ │ │ │ └── index.ts │ │ │ ├── chat-header │ │ │ │ ├── index.ts │ │ │ │ └── SLAEventItem.tsx │ │ │ ├── reply-box │ │ │ │ ├── index.ts │ │ │ │ ├── mentions-input │ │ │ │ │ ├── types │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── utils │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── buttons │ │ │ │ │ ├── VoiceRecordButton.tsx │ │ │ │ │ └── PhotosCommandButton.tsx │ │ │ │ ├── TypingIndicator.tsx │ │ │ │ └── MentionUser.tsx │ │ │ ├── message-list │ │ │ │ ├── index.tsx │ │ │ │ └── stories │ │ │ │ │ └── mock-data │ │ │ │ │ └── helper.ts │ │ │ ├── index.ts │ │ │ └── message-components │ │ │ │ ├── UnsupportedBubble.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── ActivityBubble.tsx │ │ │ │ ├── ActivityTextCell.tsx │ │ │ │ ├── ErrorInformation.tsx │ │ │ │ ├── ImageBubble.ios.tsx │ │ │ │ └── TextBubble.tsx │ │ └── conversation-actions │ │ │ ├── index.ts │ │ │ └── components │ │ │ └── index.ts │ ├── conversations │ │ └── components │ │ │ ├── conversation-item │ │ │ ├── index.ts │ │ │ ├── ConversationId.tsx │ │ │ ├── TypingMessage.tsx │ │ │ ├── UnreadIndicator.tsx │ │ │ ├── ConversationSelect.tsx │ │ │ └── ConversationAvatar.tsx │ │ │ ├── conversation-header │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── conversation-actions │ │ │ └── index.ts │ │ │ └── conversation-filters │ │ │ └── index.ts │ ├── inbox │ │ └── components │ │ │ └── index.ts │ ├── contact-details │ │ └── components │ │ │ ├── index.ts │ │ │ └── ContactMetaInformation.tsx │ └── settings │ │ └── SettingsHeader.tsx ├── svg-icons │ ├── sla-icons │ │ └── index.ts │ ├── check │ │ ├── index.ts │ │ ├── Unchecked.tsx │ │ └── Checked.tsx │ ├── conversation-icons │ │ ├── index.ts │ │ ├── NoPriority.tsx │ │ └── Team.tsx │ ├── tabs │ │ ├── index.ts │ │ └── ConversationIcon.tsx │ ├── priority-icons │ │ ├── index.tsx │ │ ├── Low.tsx │ │ ├── High.tsx │ │ └── Medium.tsx │ ├── menu-icons │ │ ├── index.ts │ │ ├── Delete.tsx │ │ ├── CannedResponse.tsx │ │ ├── Link.tsx │ │ └── Copy.tsx │ ├── swipe-actions │ │ ├── index.ts │ │ ├── StatusIcon.tsx │ │ └── AssignIcon.tsx │ ├── status-icons │ │ ├── index.ts │ │ └── Open.tsx │ ├── channels │ │ ├── index.ts │ │ └── Mail.tsx │ ├── attachments │ │ ├── index.ts │ │ ├── VoiceNote.tsx │ │ └── PrivateNote.tsx │ ├── list-icons │ │ ├── index.ts │ │ ├── Switch.tsx │ │ ├── User.tsx │ │ ├── Translate.tsx │ │ ├── Notification.tsx │ │ └── Macro.tsx │ ├── index.ts │ └── common │ │ ├── SendIcon.tsx │ │ ├── Close.tsx │ │ ├── CaretRight.tsx │ │ ├── ChevronLeft.tsx │ │ ├── Tick.tsx │ │ ├── CheckIcon.tsx │ │ ├── Add.tsx │ │ ├── Chatwoot.tsx │ │ ├── Search.tsx │ │ ├── Loading.tsx │ │ ├── Priority.tsx │ │ ├── Email.tsx │ │ ├── Company.tsx │ │ ├── CaretBottomSmall.tsx │ │ ├── MessageType.tsx │ │ ├── Trash.tsx │ │ ├── Warning.tsx │ │ ├── SelfAssign.tsx │ │ ├── Error.tsx │ │ ├── Filter.tsx │ │ ├── SocialIcons.tsx │ │ ├── Empty.tsx │ │ ├── Mail.tsx │ │ ├── Lock.tsx │ │ ├── Overflow.tsx │ │ ├── Clear.tsx │ │ ├── Linked.tsx │ │ └── index.ts ├── types │ ├── common │ │ ├── UnixTimestamp.ts │ │ ├── Theme.ts │ │ ├── UserRole.ts │ │ ├── ConversationPriority.ts │ │ ├── Label.ts │ │ ├── CannedResponse.ts │ │ ├── Dashboard.ts │ │ ├── AvailabilityStatus.ts │ │ ├── CustomAttribute.ts │ │ ├── index.ts │ │ ├── SLA.ts │ │ ├── ConversationTyping.ts │ │ ├── Channel.ts │ │ └── ConversationStatus.ts │ ├── fonts.d.ts │ ├── Team.ts │ ├── Macro.ts │ ├── Account.ts │ ├── Inbox.ts │ ├── AgentBot.ts │ ├── react-native-action-cable.d.ts │ ├── Agent.ts │ ├── User.ts │ └── Contact.ts ├── utils │ ├── cx.ts │ ├── fileUtils.ts │ ├── common.ts │ ├── audioConverter.android.ts │ ├── toastUtils.ts │ ├── messageFormatterUtils.ts │ ├── priorityIcon.tsx │ ├── statusTypeIcon.tsx │ ├── isMarkdown.ts │ ├── index.ts │ ├── urlUtils.ts │ ├── useAppKeyboardAnimation.ts │ ├── permissionUtils.ts │ ├── navigationUtils.ts │ ├── specs │ │ ├── serverUtils.spec.ts │ │ └── messageFormatterUtils.spec.ts │ ├── typingUtils.ts │ └── errorUtils.ts ├── assets │ ├── images │ │ └── logo.png │ ├── local │ │ ├── logo.png │ │ ├── typing.gif │ │ ├── PlayIcon.png │ │ ├── bot-avatar.png │ │ ├── avatars-small │ │ │ ├── avatar.png │ │ │ ├── avatar1.png │ │ │ ├── avatar2.png │ │ │ ├── avatar3.png │ │ │ ├── avatar4.png │ │ │ ├── avatar5.png │ │ │ └── avatar6.png │ │ ├── avatars │ │ │ ├── avatar-image.png │ │ │ ├── avatar-image-1.png │ │ │ ├── avatar-image-2.png │ │ │ ├── avatar-image-3.png │ │ │ ├── avatar-image-4.png │ │ │ └── avatar-image-5.png │ │ └── ImageCellTimeStampOverlay.png │ └── fonts │ │ ├── Inter-400-20.ttf │ │ ├── Inter-420-20.ttf │ │ ├── Inter-500-24.ttf │ │ ├── Inter-580-24.ttf │ │ └── Inter-600-20.ttf ├── store │ ├── label │ │ ├── labelTypes.ts │ │ ├── specs │ │ │ ├── labelSlice.spec.ts │ │ │ ├── labelMockData.ts │ │ │ ├── labelSelectors.spec.ts │ │ │ ├── labelService.spec.ts │ │ │ └── labelActions.spec.ts │ │ ├── labelService.ts │ │ ├── labelActions.ts │ │ ├── labelSelectors.ts │ │ └── labelSlice.ts │ ├── macro │ │ ├── macroTypes.ts │ │ ├── macroSelectors.ts │ │ ├── specs │ │ │ ├── macroMockData.ts │ │ │ ├── macroSlice.spec.ts │ │ │ ├── macroSelectors.spec.ts │ │ │ └── macroService.spec.ts │ │ ├── macroService.ts │ │ ├── macroSlice.ts │ │ └── macroActions.ts │ ├── inbox │ │ ├── inboxTypes.ts │ │ ├── specs │ │ │ ├── inboxSlice.spec.ts │ │ │ ├── inboxMockData.ts │ │ │ ├── inboxSelectors.spec.ts │ │ │ ├── inboxServices.spec.ts │ │ │ └── inboxActions.spec.ts │ │ ├── inboxSelectors.ts │ │ ├── inboxService.ts │ │ ├── inboxActions.ts │ │ └── inboxSlice.ts │ ├── dashboard-app │ │ ├── dashboardAppTypes.ts │ │ ├── dashboardAppService.ts │ │ └── dashboardAppActions.ts │ ├── canned-response │ │ ├── cannedResponseTypes.ts │ │ ├── cannedResponseService.ts │ │ └── cannedResponseActions.ts │ ├── custom-attribute │ │ ├── customAttributeTypes.ts │ │ ├── customAttributeService.ts │ │ └── customAttributeActions.ts │ ├── assignable-agent │ │ ├── specs │ │ │ ├── assignableAgentMockData.ts │ │ │ ├── assignableAgentSlice.spec.ts │ │ │ ├── assignableAgentActions.spec.ts │ │ │ ├── assignableAgentSelectors.spec.ts │ │ │ └── assignableAgentService.spec.ts │ │ ├── assignableAgentTypes.ts │ │ ├── assignableAgentService.ts │ │ └── assignableAgentActions.ts │ ├── storeAccessor.ts │ ├── auth │ │ ├── specs │ │ │ └── authMockData.ts │ │ └── authUtils.ts │ ├── team │ │ ├── teamService.ts │ │ ├── teamActions.ts │ │ ├── teamSelectors.ts │ │ └── teamSlice.ts │ ├── contact │ │ ├── contactTypes.ts │ │ ├── specs │ │ │ ├── contactMockData.ts │ │ │ ├── contactSelectors.spec.ts │ │ │ ├── contactActions.spec.ts │ │ │ └── contactService.spec.ts │ │ ├── contactSelectors.ts │ │ ├── contactActions.ts │ │ ├── contactConversationActions.ts │ │ ├── contactConversationSlice.ts │ │ └── contactLabelSlice.ts │ ├── conversation-participant │ │ ├── conversationParticipantTypes.ts │ │ └── conversationParticipantSelectors.ts │ ├── conversation │ │ ├── audioPlayerSlice.ts │ │ ├── localRecordedAudioCacheSlice.ts │ │ └── conversationActionSlice.ts │ ├── settings │ │ └── settingsUtils.ts │ └── notification │ │ ├── notificationFilterSlice.ts │ │ └── notificationTypes.ts ├── navigation │ └── stack │ │ ├── index.ts │ │ ├── InboxStack.tsx │ │ ├── SettingsStack.tsx │ │ └── ConversationStack.tsx ├── context │ ├── index.ts │ └── InboxListContext.tsx ├── constants │ ├── permissions.ts │ └── url.js ├── hooks.ts ├── hooks │ └── useHeaderAnimation.ts └── app.tsx ├── __mocks__ ├── reactotron-redux.js ├── reactotron-react-native.js ├── react-native-snackbar.js ├── react-native-config.js ├── @notifee │ └── react-native.js ├── @sentry │ └── react-native.js ├── @react-native-async-storage │ └── async-storage.js ├── react-native-device-info.js ├── @react-native-firebase │ └── messaging.js └── @react-native-community │ └── push-notification-ios.js ├── .eslintignore ├── assets ├── icon.png ├── splash.png └── adaptive-icon.png ├── crowdin.yml ├── firebase.json ├── .aidigestignore ├── .prettierrc ├── reanimatedConfig.js ├── .storybook ├── main.ts ├── index.ts └── preview.tsx ├── babel.config.js ├── tsconfig.json ├── wdyr.js ├── jest.config.js ├── react-native.config.js ├── .env.example ├── eas.json ├── .eslintrc.js ├── .gitignore ├── ReactotronConfig.js ├── metro.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── enhancement_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .circleci └── config.yml ├── LICENSE └── App.tsx /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /src/theme/default/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar'; 2 | -------------------------------------------------------------------------------- /__mocks__/reactotron-redux.js: -------------------------------------------------------------------------------- 1 | jest.mock('reactotron-redux'); -------------------------------------------------------------------------------- /src/components-next/common/icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Icon'; 2 | -------------------------------------------------------------------------------- /src/components-next/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Spinner'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ChatScreen'; 2 | -------------------------------------------------------------------------------- /src/svg-icons/sla-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SlaMissed'; 2 | -------------------------------------------------------------------------------- /src/components-next/common/slider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Slider'; 2 | -------------------------------------------------------------------------------- /src/types/common/UnixTimestamp.ts: -------------------------------------------------------------------------------- 1 | export type UnixTimestamp = number; 2 | -------------------------------------------------------------------------------- /__mocks__/reactotron-react-native.js: -------------------------------------------------------------------------------- 1 | jest.mock('reactotron-react-native'); -------------------------------------------------------------------------------- /src/components-next/action-tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionTabs'; 2 | -------------------------------------------------------------------------------- /src/components-next/common/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SearchBar'; 2 | -------------------------------------------------------------------------------- /src/components-next/common/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Spinner'; 2 | -------------------------------------------------------------------------------- /src/components-next/common/swipeable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Swipeable'; 2 | -------------------------------------------------------------------------------- /src/types/common/Theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = 'system' | 'light' | 'dark'; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Add JS and JSX files here 3 | *.js 4 | *.jsx 5 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './default'; 2 | export * from './tailwind'; 3 | -------------------------------------------------------------------------------- /src/types/common/UserRole.ts: -------------------------------------------------------------------------------- 1 | export type UserRole = 'administrator' | 'agent'; 2 | -------------------------------------------------------------------------------- /__mocks__/react-native-snackbar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | show: jest.fn(), 3 | }; 4 | -------------------------------------------------------------------------------- /src/components-next/no-network/index.ts: -------------------------------------------------------------------------------- 1 | export { NoNetworkBar } from './NoNetwork'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-item/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MessageItem'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MessageMenu'; 2 | -------------------------------------------------------------------------------- /src/svg-icons/check/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checked'; 2 | export * from './Unchecked'; 3 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/assets/splash.png -------------------------------------------------------------------------------- /src/screens/chat-screen/components/audio-recorder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AudioManager'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/chat-header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ChatHeaderContainer'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ReplyBoxContainer'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/conversation-actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationActions'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-list/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './MessagesListContainer'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/mentions-input/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/mentions-input/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/i18n/en.json 3 | translation: /src/i18n/%two_letters_code%.json -------------------------------------------------------------------------------- /src/components-next/label-section/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LabelCell'; 2 | export * from './LabelItem'; 3 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /src/components-next/common/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FilterBar'; 2 | export * from './FilterButton'; 3 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-item/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationItemContainer'; 2 | -------------------------------------------------------------------------------- /src/utils/cx.ts: -------------------------------------------------------------------------------- 1 | export const cx = (...classNames: unknown[]): string => classNames.filter(Boolean).join(' '); 2 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/local/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/logo.png -------------------------------------------------------------------------------- /src/assets/local/typing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/typing.gif -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/mentions-input/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mention-input'; 2 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationHeaderContainer'; 2 | -------------------------------------------------------------------------------- /src/screens/inbox/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InboxHeader'; 2 | export * from './InboxItemContainer'; 3 | -------------------------------------------------------------------------------- /src/types/fonts.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.ttf' { 2 | const content: number; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /__mocks__/react-native-config.js: -------------------------------------------------------------------------------- 1 | // __mocks__/react-native-config.js 2 | export default { 3 | FOO_BAR: 'baz', 4 | }; 5 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "react-native": { 3 | "messaging_ios_auto_register_for_remote_messages": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/local/PlayIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/PlayIcon.png -------------------------------------------------------------------------------- /__mocks__/@notifee/react-native.js: -------------------------------------------------------------------------------- 1 | jest.mock('@notifee/react-native', () => require('@notifee/react-native/jest-mock')); 2 | -------------------------------------------------------------------------------- /__mocks__/@sentry/react-native.js: -------------------------------------------------------------------------------- 1 | jest.mock('@sentry/react-native', () => ({ 2 | captureException: jest.fn(), 3 | })); 4 | -------------------------------------------------------------------------------- /src/assets/local/bot-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/bot-avatar.png -------------------------------------------------------------------------------- /src/types/common/ConversationPriority.ts: -------------------------------------------------------------------------------- 1 | export type ConversationPriority = null | 'low' | 'medium' | 'high' | 'urgent'; 2 | -------------------------------------------------------------------------------- /src/assets/fonts/Inter-400-20.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/fonts/Inter-400-20.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-420-20.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/fonts/Inter-420-20.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-500-24.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/fonts/Inter-500-24.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-580-24.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/fonts/Inter-580-24.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Inter-600-20.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/fonts/Inter-600-20.ttf -------------------------------------------------------------------------------- /src/components-next/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | export * from './IconButton'; 3 | export * from './AuthButton'; 4 | -------------------------------------------------------------------------------- /.aidigestignore: -------------------------------------------------------------------------------- 1 | expo-env.d.ts 2 | node_modules/ 3 | .env 4 | android/ 5 | ios/ 6 | .expo 7 | src/storybook 8 | src/svg-icons 9 | src/assets -------------------------------------------------------------------------------- /src/svg-icons/conversation-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Unassigned'; 2 | export * from './Team'; 3 | export * from './NoPriority'; 4 | -------------------------------------------------------------------------------- /src/svg-icons/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationIcon'; 2 | export * from './InboxIcon'; 3 | export * from './SettingsIcon'; 4 | -------------------------------------------------------------------------------- /src/components-next/common/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Avatar'; 2 | export * from './AvatarImage'; 3 | export * from './AvatarStatus'; 4 | -------------------------------------------------------------------------------- /src/store/label/labelTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Label } from '@/types'; 2 | 3 | export interface LabelResponse { 4 | payload: Label[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/store/macro/macroTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Macro } from '@/types'; 2 | 3 | export interface MacroResponse { 4 | payload: Macro[]; 5 | } 6 | -------------------------------------------------------------------------------- /__mocks__/@react-native-async-storage/async-storage.js: -------------------------------------------------------------------------------- 1 | export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'; 2 | -------------------------------------------------------------------------------- /__mocks__/react-native-device-info.js: -------------------------------------------------------------------------------- 1 | jest.mock('react-native-device-info', () => { 2 | return { 3 | getVersion: () => 4, 4 | }; 5 | }); 6 | -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar1.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar2.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar3.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar4.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar5.png -------------------------------------------------------------------------------- /src/assets/local/avatars-small/avatar6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars-small/avatar6.png -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image.png -------------------------------------------------------------------------------- /src/screens/chat-screen/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat-header'; 2 | export * from './message-list'; 3 | export * from './reply-box'; 4 | -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image-1.png -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image-2.png -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image-3.png -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image-4.png -------------------------------------------------------------------------------- /src/assets/local/avatars/avatar-image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/avatars/avatar-image-5.png -------------------------------------------------------------------------------- /src/store/inbox/inboxTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Inbox } from '@/types/Inbox'; 2 | 3 | export interface InboxResponse { 4 | payload: Inbox[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/svg-icons/priority-icons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Urgent'; 2 | export * from './High'; 3 | export * from './Medium'; 4 | export * from './Low'; 5 | -------------------------------------------------------------------------------- /src/assets/local/ImageCellTimeStampOverlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatwoot/chatwoot-mobile-app/HEAD/src/assets/local/ImageCellTimeStampOverlay.png -------------------------------------------------------------------------------- /src/svg-icons/menu-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CannedResponse'; 2 | export * from './Copy'; 3 | export * from './Delete'; 4 | export * from './Link'; 5 | -------------------------------------------------------------------------------- /src/theme/tailwind.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'twrnc'; 2 | 3 | import { twConfig } from './tailwind.config'; 4 | 5 | export const tailwind = create(twConfig); 6 | -------------------------------------------------------------------------------- /src/components-next/common/bottomsheet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BottomSheetBackdrop'; 2 | export * from './BottomSheetHeader'; 3 | export * from './BottomSheetWrapper'; 4 | -------------------------------------------------------------------------------- /src/utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | export const findFileSize = (bytes: number) => { 2 | if (bytes === 0) { 3 | return 0; 4 | } 5 | return bytes / (1024 * 1024); 6 | }; 7 | -------------------------------------------------------------------------------- /src/components-next/sheet-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AvailabilityStatusList'; 2 | export * from './NotificationPreferences'; 3 | export * from './SwitchAccount'; 4 | -------------------------------------------------------------------------------- /src/navigation/stack/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthStack'; 2 | export * from './ConversationStack'; 3 | export * from './InboxStack'; 4 | export * from './SettingsStack'; 5 | -------------------------------------------------------------------------------- /src/svg-icons/swipe-actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AssignIcon'; 2 | export * from './MarkAsRead'; 3 | export * from './MarkAsUnRead'; 4 | export * from './StatusIcon'; 5 | -------------------------------------------------------------------------------- /src/types/common/Label.ts: -------------------------------------------------------------------------------- 1 | export interface Label { 2 | id: number; 3 | title: string; 4 | description: string; 5 | color: string; 6 | showOnSidebar: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/store/dashboard-app/dashboardAppTypes.ts: -------------------------------------------------------------------------------- 1 | import type { DashboardApp } from '@/types'; 2 | 3 | export interface DashboardAppResponse { 4 | payload: DashboardApp[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RefsContext'; 2 | export * from './ConversationListContext'; 3 | export * from './InboxListContext'; 4 | export * from './useChatWindowContext'; 5 | -------------------------------------------------------------------------------- /src/screens/contact-details/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContactDetailsScreenHeader'; 2 | export * from './ContactBasicActions'; 3 | export * from './ContactMetaInformation'; 4 | -------------------------------------------------------------------------------- /src/svg-icons/status-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Mute'; 2 | export * from './Open'; 3 | export * from './Pending'; 4 | export * from './Resolved'; 5 | export * from './Snoozed'; 6 | -------------------------------------------------------------------------------- /src/store/canned-response/cannedResponseTypes.ts: -------------------------------------------------------------------------------- 1 | import type { CannedResponse } from '@/types'; 2 | 3 | export interface CannedResponseResponse { 4 | payload: CannedResponse[]; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /src/store/custom-attribute/customAttributeTypes.ts: -------------------------------------------------------------------------------- 1 | import type { CustomAttribute } from '@/types'; 2 | 3 | export interface CustomAttributeResponse { 4 | payload: CustomAttribute[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/Team.ts: -------------------------------------------------------------------------------- 1 | export interface Team { 2 | id: number; 3 | name: string; 4 | description: string | null; 5 | allowAutoAssign: boolean; 6 | accountId: number; 7 | isMember: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/svg-icons/channels/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Facebook'; 2 | export * from './Telegram'; 3 | export * from './Website'; 4 | export * from './WhatsApp'; 5 | export * from './X'; 6 | export * from './Mail'; 7 | -------------------------------------------------------------------------------- /src/screens/conversations/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './conversation-item'; 2 | export * from './conversation-header'; 3 | export * from './conversation-filters'; 4 | export * from './conversation-actions'; 5 | -------------------------------------------------------------------------------- /src/types/common/CannedResponse.ts: -------------------------------------------------------------------------------- 1 | export interface CannedResponse { 2 | id: number; 3 | shortCode: string; 4 | content: string; 5 | accountId: number; 6 | createdAt: string; 7 | updatedAt: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/svg-icons/attachments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AIAssisst'; 2 | export * from './Photos'; 3 | export * from './Player'; 4 | export * from './PrivateNote'; 5 | export * from './VideoCall'; 6 | export * from './VoiceNote'; 7 | -------------------------------------------------------------------------------- /__mocks__/@react-native-firebase/messaging.js: -------------------------------------------------------------------------------- 1 | jest.mock('@react-native-firebase/messaging', () => () => { 2 | return { 3 | getToken: jest.fn(() => Promise.resolve('fd79y-tiw4t-9ygv2-4fiw4-yghqw-4t79f')), 4 | }; 5 | }); 6 | -------------------------------------------------------------------------------- /src/constants/permissions.ts: -------------------------------------------------------------------------------- 1 | export const CONVERSATION_PERMISSIONS = [ 2 | 'administrator', 3 | 'agent', 4 | 'conversation_manage', 5 | 'conversation_unassigned_manage', 6 | 'conversation_participating_manage', 7 | ]; 8 | -------------------------------------------------------------------------------- /reanimatedConfig.js: -------------------------------------------------------------------------------- 1 | import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated'; 2 | 3 | configureReanimatedLogger({ 4 | level: ReanimatedLogLevel.warn, 5 | strict: false, // Disable strict mode 6 | }); 7 | -------------------------------------------------------------------------------- /src/types/common/Dashboard.ts: -------------------------------------------------------------------------------- 1 | export interface DashboardApp { 2 | id: number; 3 | title: string; 4 | content: Content[]; 5 | createdAt: string; 6 | } 7 | interface Content { 8 | url: string; 9 | type: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/svg-icons/list-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Info'; 2 | export * from './Macro'; 3 | export * from './Notification'; 4 | export * from './Play'; 5 | export * from './Switch'; 6 | export * from './Translate'; 7 | export * from './User'; 8 | -------------------------------------------------------------------------------- /src/components-next/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar'; 2 | export * from './bottomsheet'; 3 | export * from './filters'; 4 | export * from './icon'; 5 | export * from './search'; 6 | export * from './slider'; 7 | export * from './swipeable'; 8 | -------------------------------------------------------------------------------- /src/components-next/list-components/index.ts: -------------------------------------------------------------------------------- 1 | export { SettingsList } from './SettingsList'; 2 | export * from './LanguageList'; 3 | export * from './AttributeList'; 4 | export * from './PriorityIndicator'; 5 | export * from './ChannelIndicator'; 6 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | import { TAB_BAR_HEIGHT } from '@/constants'; 4 | 5 | export const useTabBarHeight = () => { 6 | return Platform.OS === 'ios' ? TAB_BAR_HEIGHT : TAB_BAR_HEIGHT - 21; 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/assignable-agent/specs/assignableAgentMockData.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '@/types'; 2 | 3 | export const agent: Agent = { 4 | id: 1, 5 | name: 'Test Agent', 6 | }; 7 | 8 | export const mockInboxAgentsResponse = { data: { payload: [agent] } }; 9 | -------------------------------------------------------------------------------- /src/utils/audioConverter.android.ts: -------------------------------------------------------------------------------- 1 | export const convertOggToWav = async (oggUrl: string): Promise => { 2 | return ''; 3 | }; 4 | 5 | export const convertAacToWav = async (inputPath: string): Promise => { 6 | return ''; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/Macro.ts: -------------------------------------------------------------------------------- 1 | export interface Macro { 2 | id: number; 3 | name: string; 4 | hasChevron: boolean; 5 | actions: MacroAction[]; 6 | } 7 | 8 | export interface MacroAction { 9 | actionName: string; 10 | actionParams: (string | number)[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/screens/chat-screen/conversation-actions/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConversationBasicActions'; 2 | export * from './ConversationSettingsPanel'; 3 | export * from './ConversationLabelActions'; 4 | export * from './AddParticipantList'; 5 | export * from './UpdateParticipant'; 6 | -------------------------------------------------------------------------------- /src/store/macro/macroSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '@/store'; 2 | import { macroAdapter } from './macroSlice'; 3 | 4 | export const selectMacrosState = (state: RootState) => state.macros; 5 | 6 | export const { selectAll: selectAllMacros } = macroAdapter.getSelectors(selectMacrosState); 7 | -------------------------------------------------------------------------------- /src/store/macro/specs/macroMockData.ts: -------------------------------------------------------------------------------- 1 | import type { Macro } from '@/types'; 2 | 3 | export const macro: Macro = { 4 | id: 1, 5 | name: 'Macro 1', 6 | hasChevron: true, 7 | actions: [], 8 | }; 9 | 10 | export const mockMacrosResponse = { 11 | data: { payload: [macro] }, 12 | }; 13 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-native'; 2 | 3 | const main: StorybookConfig = { 4 | stories: ['../src/**/*.stories.?(ts|tsx|js|jsx)'], 5 | addons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'], 6 | }; 7 | 8 | export default main; 9 | -------------------------------------------------------------------------------- /__mocks__/@react-native-community/push-notification-ios.js: -------------------------------------------------------------------------------- 1 | jest.mock('@react-native-community/push-notification-ios', () => { 2 | return { 3 | setApplicationIconBadgeNumber: jest.fn(() => Promise.resolve()), 4 | removeAllDeliveredNotifications: jest.fn(() => Promise.resolve()), 5 | }; 6 | }); 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | [ 6 | 'babel-preset-expo', 7 | { 8 | jsxImportSource: '@welldone-software/why-did-you-render', 9 | }, 10 | ], 11 | ], 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/toastUtils.ts: -------------------------------------------------------------------------------- 1 | import Snackbar from 'react-native-snackbar'; 2 | 3 | interface ToastParams { 4 | message: string; 5 | } 6 | 7 | export const showToast = ({ message }: ToastParams): void => { 8 | Snackbar.show({ 9 | text: message, 10 | duration: Snackbar.LENGTH_SHORT, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-actions/index.ts: -------------------------------------------------------------------------------- 1 | export { UpdateStatus } from './UpdateStatus'; 2 | export { UpdateLabels } from './UpdateLabels'; 3 | export { UpdateAssignee } from './UpdateAssignee'; 4 | export { UpdateTeam } from './UpdateTeam'; 5 | export { UpdatePriority } from './UpdatePriority'; 6 | -------------------------------------------------------------------------------- /src/store/storeAccessor.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@reduxjs/toolkit'; 2 | 3 | let store: Store; 4 | 5 | export const setStore = (s: Store) => { 6 | store = s; 7 | }; 8 | 9 | export const getStore = () => { 10 | if (!store) { 11 | throw new Error('Store not initialized'); 12 | } 13 | return store; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components-next/native-components/NText.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 2 | export const NativeText = require('react-native/Libraries/Text/TextNativeComponent'); 3 | 4 | // export const AnimatedNativeText = Animated.createAnimatedComponent(NativeText); 5 | -------------------------------------------------------------------------------- /src/store/auth/specs/authMockData.ts: -------------------------------------------------------------------------------- 1 | export const mockUser = { 2 | id: 1, 3 | email: 'test@example.com', 4 | name: 'Test User', 5 | account_id: 123, 6 | type: 'user', 7 | }; 8 | 9 | export const mockHeaders = { 10 | 'access-token': 'SxsseweDSEWESDSSSSFDFDf', 11 | uid: 'uid', 12 | client: 'client', 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": [ 8 | "src/*" 9 | ], 10 | "*": ["src/*"], 11 | }, 12 | }, 13 | "include": [ 14 | "**/*.ts", 15 | "**/*.tsx" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /wdyr.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | if (__DEV__) { 4 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 5 | whyDidYouRender(React, { 6 | trackAllPureComponents: true, 7 | exclude: [/^BottomTabBar$/, /^Pressable$/, /TabItem/, /TabBarIcons/, /PureComponentWrapper/], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/components-next/common/bottomsheet/BottomSheetWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { BottomSheetView } from '@gorhom/bottom-sheet'; 3 | 4 | export const BottomSheetWrapper = (props: PropsWithChildren) => { 5 | const { children } = props; 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/label/specs/labelSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../labelSlice'; 2 | 3 | describe('labelSlice', () => { 4 | it('should return the initial state', () => { 5 | expect(reducer(undefined, { type: undefined })).toEqual({ 6 | ids: [], 7 | entities: {}, 8 | isLoading: false, 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/store/macro/specs/macroSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../macroSlice'; 2 | 3 | describe('macroSlice', () => { 4 | it('should return the initial state', () => { 5 | expect(reducer(undefined, { type: undefined })).toEqual({ 6 | ids: [], 7 | entities: {}, 8 | isLoading: false, 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components-next/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | export * from './common'; 3 | export * from './label-section'; 4 | export * from './list-components'; 5 | export * from './sheet-components'; 6 | export * from './spinner'; 7 | export * from './action-tabs'; 8 | export * from './no-network'; 9 | export * from './verification-code'; 10 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-filters/index.ts: -------------------------------------------------------------------------------- 1 | export { ConversationFilterBar } from './ConversationFilterBar'; 2 | export { StatusFilters } from './StatusFilters'; 3 | export { SortByFilters } from './SortByFilters'; 4 | export { InboxFilters } from './InboxFilters'; 5 | export { AssigneeTypeFilters } from './AssigneeTypeFilters'; 6 | -------------------------------------------------------------------------------- /src/store/inbox/specs/inboxSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../inboxSlice'; 2 | 3 | describe('inbox reducer', () => { 4 | it('should return the initial state', () => { 5 | expect(reducer(undefined, { type: undefined })).toEqual({ 6 | ids: [], 7 | entities: {}, 8 | isLoading: false, 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/store/label/specs/labelMockData.ts: -------------------------------------------------------------------------------- 1 | import type { Label } from '@/types'; 2 | 3 | export const label1: Label = { 4 | id: 1, 5 | title: 'bug', 6 | color: '#ff0000', 7 | description: 'This is a bug label', 8 | showOnSidebar: true, 9 | }; 10 | 11 | export const mockLabelsResponse = { 12 | data: { payload: [label1] }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/types/Account.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | active_at: string; 3 | auto_offline: boolean; 4 | availability: string; 5 | availability_status: string; 6 | custom_role?: string; 7 | custom_role_id?: string; 8 | id: number; 9 | name: string; 10 | permissions: string[]; 11 | role: string; 12 | status: string; 13 | } 14 | -------------------------------------------------------------------------------- /.storybook/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import { view } from './storybook.requires'; 3 | 4 | const StorybookUIRoot = view.getStorybookUI({ 5 | storage: { 6 | getItem: AsyncStorage.getItem, 7 | setItem: AsyncStorage.setItem, 8 | }, 9 | }); 10 | 11 | export default StorybookUIRoot; 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | moduleDirectories: ['node_modules', 'src'], 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/src/$1', 6 | }, 7 | transformIgnorePatterns: [ 8 | 'node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation|@reduxjs|immer)', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/mentions-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | 3 | export type { Suggestion, Part, MentionSuggestionsProps, PartType } from './types'; 4 | 5 | export { 6 | mentionRegEx, 7 | isMentionPartType, 8 | getMentionValue, 9 | parseValue, 10 | replaceMentionValues, 11 | } from './utils'; 12 | -------------------------------------------------------------------------------- /src/store/assignable-agent/assignableAgentTypes.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '@/types'; 2 | 3 | export interface AssignableAgentAPIResponse { 4 | payload: Agent[]; 5 | } 6 | 7 | export interface AssignableAgentResponse { 8 | agents: Agent[]; 9 | inboxIds: number[]; 10 | } 11 | 12 | export interface AssignableAgentPayload { 13 | inboxIds: number[]; 14 | } 15 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependencies: { 3 | 'ffmpeg-kit-react-native': { 4 | platforms: { 5 | android: null, // 👈 prevents Android autolinking 6 | }, 7 | }, 8 | '@notifee/react-native': { 9 | platforms: { 10 | android: null, // 👈 prevents Android autolinking 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/types/Inbox.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from './common/Channel'; 2 | 3 | export type Inbox = { 4 | id: number; 5 | avatarUrl: string; 6 | channelId: number; 7 | name: string; 8 | channelType: Channel; 9 | phoneNumber: string; 10 | medium: string; 11 | additionalAttributes?: { 12 | agentReplyTimeWindowMessage?: string; 13 | }; 14 | provider: string; 15 | }; 16 | -------------------------------------------------------------------------------- /src/store/assignable-agent/specs/assignableAgentSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../assignableAgentSlice'; 2 | 3 | describe('assignableAgentSlice', () => { 4 | it('should return the initial state', () => { 5 | expect(reducer(undefined, { type: undefined })).toEqual({ 6 | records: {}, 7 | uiFlags: { 8 | isLoading: false, 9 | }, 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/types/AgentBot.ts: -------------------------------------------------------------------------------- 1 | type AgentBotType = 'webhook' | 'csml'; 2 | 3 | export interface AgentBot { 4 | id: number; 5 | name: string | null; 6 | description: string | null; 7 | outgoingUrl: string | null; 8 | botType: AgentBotType; 9 | botConfig: object | null; 10 | accountId: number | null; 11 | accessToken: string | null; 12 | thumbnail?: string | null; 13 | type: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/messageFormatterUtils.ts: -------------------------------------------------------------------------------- 1 | import markdownToTxt from '@chatwoot/markdown-to-txt'; 2 | 3 | export const getPlainText = (message: string) => { 4 | try { 5 | const lastMessageContent = message ? markdownToTxt(message) : ''; 6 | return lastMessageContent; 7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | } catch (e) { 9 | return message; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/store/inbox/specs/inboxMockData.ts: -------------------------------------------------------------------------------- 1 | import type { Inbox } from '@/types/Inbox'; 2 | 3 | export const inbox: Inbox = { 4 | id: 1, 5 | avatarUrl: 'https://example.com/avatar.png', 6 | channelId: 1, 7 | name: 'Test Inbox', 8 | channelType: 'Channel::WebWidget', 9 | phoneNumber: '+1234567890', 10 | medium: 'web', 11 | }; 12 | 13 | export const mockInboxesResponse = { data: { payload: [inbox] } }; 14 | -------------------------------------------------------------------------------- /src/types/common/AvailabilityStatus.ts: -------------------------------------------------------------------------------- 1 | export type AvailabilityStatus = 'online' | 'offline' | 'busy' | 'typing'; 2 | 3 | export type AvailabilityStatusListItemType = { 4 | status: AvailabilityStatus; 5 | statusColor: string; 6 | }; 7 | 8 | export type PresenceUpdateData = { 9 | account_id: number; 10 | contacts: Record; 11 | users: Record; 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/team/teamService.ts: -------------------------------------------------------------------------------- 1 | import type { Team } from '@/types'; 2 | import { apiService } from '@/services/APIService'; 3 | import { transformTeam } from '@/utils/camelCaseKeys'; 4 | 5 | export class TeamService { 6 | static async getTeams(): Promise { 7 | const response = await apiService.get('teams'); 8 | const teams = response.data.map(transformTeam); 9 | return teams; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/svg-icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attachments'; 2 | export * from './channels'; 3 | export * from './check'; 4 | export * from './common'; 5 | export * from './list-icons'; 6 | export * from './menu-icons'; 7 | export * from './status-icons'; 8 | export * from './swipe-actions'; 9 | export * from './tabs'; 10 | export * from './priority-icons'; 11 | export * from './sla-icons'; 12 | export * from './conversation-icons'; 13 | -------------------------------------------------------------------------------- /src/types/react-native-action-cable.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '@kesha-antonov/react-native-action-cable' { 3 | export class Cable { 4 | constructor(options: any); 5 | setChannel(name: string, subscription: any): any; 6 | channel(name: string): any; 7 | } 8 | 9 | export class ActionCable { 10 | static createConsumer(url: string): any; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/common/CustomAttribute.ts: -------------------------------------------------------------------------------- 1 | export interface CustomAttribute { 2 | id: number; 3 | attributeDisplayName: string; 4 | attributeDisplayType: string; 5 | attributeDescription: string; 6 | attributeKey: string; 7 | regexPattern: string; 8 | regexCue: string; 9 | attributeValues: string[]; 10 | attributeModel: string; 11 | defaultValue: string; 12 | createdAt: string; 13 | updatedAt: string; 14 | value: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/svg-icons/check/Unchecked.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Circle, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const UncheckedIcon = (props: IconProps) => { 7 | const { stroke = '#C7C7C7' } = props; 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/store/inbox/inboxSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '@/store'; 2 | import { inboxAdapter } from './inboxSlice'; 3 | 4 | export const selectInboxesState = (state: RootState) => state.inboxes; 5 | 6 | export const { selectAll: selectAllInboxes } = 7 | inboxAdapter.getSelectors(selectInboxesState); 8 | 9 | export const selectInboxById = (state: RootState, inboxId: number) => 10 | selectAllInboxes(state).find(inbox => inbox.id === inboxId); 11 | -------------------------------------------------------------------------------- /src/components-next/native-components/NView.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import Animated from 'react-native-reanimated'; 3 | 4 | export const NativeView = 5 | // eslint-disable-next-line @typescript-eslint/no-require-imports 6 | require('react-native/Libraries/Components/View/ViewNativeComponent').default; 7 | 8 | export const AnimatedNativeView = Animated.createAnimatedComponent( 9 | NativeView, 10 | ) as unknown as typeof NativeView; 11 | -------------------------------------------------------------------------------- /src/types/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AvailabilityStatus'; 2 | export * from './Channel'; 3 | export * from './ConversationPriority'; 4 | export * from './ConversationStatus'; 5 | export * from './UnixTimestamp'; 6 | export * from './UserRole'; 7 | export * from './Theme'; 8 | export * from './ConversationTyping'; 9 | export * from './Label'; 10 | export * from './SLA'; 11 | export * from './Dashboard'; 12 | export * from './CustomAttribute'; 13 | export * from './CannedResponse'; 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_CHATWOOT_WEBSITE_TOKEN= 2 | EXPO_PUBLIC_CHATWOOT_BASE_URL=https://app.chatwoot.com 3 | EXPO_PUBLIC_JUNE_SDK_KEY= 4 | EXPO_PUBLIC_MINIMUM_CHATWOOT_VERSION=4.1.0 5 | EXPO_PUBLIC_SENTRY_DSN= 6 | EXPO_PUBLIC_PROJECT_ID= 7 | EXPO_PUBLIC_APP_SLUG= 8 | EXPO_PUBLIC_SENTRY_PROJECT_NAME= 9 | EXPO_PUBLIC_SENTRY_ORG_NAME= 10 | EXPO_PUBLIC_IOS_GOOGLE_SERVICES_FILE= 11 | EXPO_PUBLIC_ANDROID_GOOGLE_SERVICES_FILE= 12 | EXPO_APPLE_ID= 13 | EXPO_APPLE_TEAM_ID= 14 | EXPO_STORYBOOK_ENABLED=false -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 12.3.0", 4 | "appVersionSource": "remote" 5 | }, 6 | "build": { 7 | "development": { 8 | "developmentClient": true, 9 | "distribution": "internal" 10 | }, 11 | "production": { 12 | "autoIncrement": true 13 | } 14 | }, 15 | "submit": { 16 | "production": { 17 | "android": { 18 | "track": "internal" // (enum: production, beta, alpha, internal) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/reply-box/types.ts: -------------------------------------------------------------------------------- 1 | import { PressableProps } from 'react-native'; 2 | import { SharedValue } from 'react-native-reanimated'; 3 | 4 | export type SendMessageButtonProps = PressableProps & {}; 5 | 6 | export type AddCommandButtonProps = PressableProps & { 7 | derivedAddMenuOptionStateValue: SharedValue; 8 | }; 9 | 10 | export type PhotosCommandButtonProps = PressableProps & {}; 11 | 12 | export type VoiceRecordButtonProps = PressableProps & {}; 13 | -------------------------------------------------------------------------------- /src/store/label/labelService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { LabelResponse } from './labelTypes'; 3 | import { transformLabel } from '@/utils/camelCaseKeys'; 4 | 5 | export class LabelService { 6 | static async index(): Promise { 7 | const response = await apiService.get('labels'); 8 | const labels = response.data.payload.map(transformLabel); 9 | return { 10 | payload: labels, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/inbox/inboxService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { InboxResponse } from './inboxTypes'; 3 | import { transformInbox } from '@/utils/camelCaseKeys'; 4 | 5 | export class InboxService { 6 | static async index(): Promise { 7 | const response = await apiService.get('inboxes'); 8 | const inboxes = response.data.payload.map(transformInbox); 9 | return { 10 | payload: inboxes, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['expo', 'prettier'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'prettier/prettier': 'error', 6 | }, 7 | overrides: [ 8 | { 9 | files: ['*.ts', '*.tsx'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | extends: ['plugin:@typescript-eslint/recommended'], 13 | parserOptions: { 14 | warnOnUnsupportedTypeScriptVersion: false, 15 | }, 16 | }, 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /src/svg-icons/common/SendIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const SendIcon = () => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/store/macro/specs/macroSelectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '@/store'; 2 | import { selectAllMacros } from '../macroSelectors'; 3 | import { macro } from './macroMockData'; 4 | 5 | describe('Macro Selectors', () => { 6 | const mockState = { 7 | macros: { 8 | ids: [macro.id], 9 | entities: { [macro.id]: macro }, 10 | }, 11 | } as RootState; 12 | 13 | it('should select all macros', () => { 14 | expect(selectAllMacros(mockState)).toEqual([macro]); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/store/label/specs/labelSelectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '@/store'; 2 | import { label1 } from './labelMockData'; 3 | import { selectAllLabels } from '../labelSelectors'; 4 | 5 | describe('Label Selectors', () => { 6 | const mockState = { 7 | labels: { 8 | ids: [label1.id], 9 | entities: { [label1.id]: label1 }, 10 | }, 11 | } as RootState; 12 | 13 | it('should select all labels', () => { 14 | expect(selectAllLabels(mockState)).toEqual([label1]); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/store/contact/contactTypes.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from '@/types'; 2 | export interface ContactLabelsAPIResponse { 3 | payload: string[]; 4 | } 5 | 6 | export interface ContactLabelsPayload { 7 | contactId: number; 8 | } 9 | 10 | export interface UpdateContactLabelsPayload { 11 | contactId: number; 12 | labels: string[]; 13 | } 14 | 15 | export interface ContactConversationPayload { 16 | contactId: number; 17 | } 18 | 19 | export interface ContactConversationAPIResponse { 20 | payload: Conversation[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/theme/colors/blackA.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | blackA: { 3 | A1: 'hsla(0, 0%, 0%, 0.012)', 4 | A2: 'hsla(0, 0%, 0%, 0.024)', 5 | A3: 'hsla(0, 0%, 0%, 0.055)', 6 | A4: 'hsla(0, 0%, 0%, 0.078)', 7 | A5: 'hsla(0, 0%, 0%, 0.106)', 8 | A6: 'hsla(0, 0%, 0%, 0.133)', 9 | A7: 'hsla(0, 0%, 0%, 0.169)', 10 | A8: 'hsla(0, 0%, 0%, 0.267)', 11 | A9: 'hsla(0, 0%, 0%, 0.447)', 12 | A10: 'hsla(0, 0%, 0%, 0.498)', 13 | A11: 'hsla(0, 0%, 0%, 0.608)', 14 | A12: 'hsla(0, 0%, 0%, 0.875)', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/types/Agent.ts: -------------------------------------------------------------------------------- 1 | import { type AvailabilityStatus } from './common/AvailabilityStatus'; 2 | import { type UserRole } from './common/UserRole'; 3 | 4 | export interface Agent { 5 | id: number; 6 | accountId?: number | null; 7 | availabilityStatus?: AvailabilityStatus; 8 | autoOffline?: boolean; 9 | confirmed?: boolean; 10 | email?: string; 11 | availableName?: string | null; 12 | customAttributes?: object; 13 | name?: string | null; 14 | role?: UserRole; 15 | thumbnail?: string | null; 16 | type?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/priorityIcon.tsx: -------------------------------------------------------------------------------- 1 | import { HighIcon, MediumIcon, LowIcon, UrgentIcon } from '@/svg-icons/priority-icons'; 2 | import { ConversationPriority } from '@/types'; 3 | 4 | export const getPriorityIcon = (priority: ConversationPriority) => { 5 | switch (priority) { 6 | case 'high': 7 | return ; 8 | case 'medium': 9 | return ; 10 | case 'low': 11 | return ; 12 | case 'urgent': 13 | return ; 14 | default: 15 | return null; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/conversation-participant/conversationParticipantTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from '@/types'; 2 | 3 | export interface ConversationParticipantAPIResponse { 4 | data: Agent[]; 5 | } 6 | 7 | export interface ConversationParticipantPayload { 8 | conversationId: number; 9 | } 10 | 11 | export interface ConversationParticipantResponse { 12 | participants: Agent[]; 13 | conversationId: number; 14 | } 15 | 16 | export interface UpdateConversationParticipantPayload { 17 | conversationId: number; 18 | userIds: number[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/svg-icons/check/Checked.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Circle, Path } from 'react-native-svg'; 3 | 4 | export const CheckedIcon = () => { 5 | return ( 6 | 7 | 8 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/svg-icons/common/Close.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const CloseIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/theme/colors/whiteA.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | whiteA: { 3 | A1: 'hsla(0, 0%, 0%, 0)', 4 | A2: 'hsla(0, 0%, 100%, 0.013)', 5 | A3: 'hsla(0, 0%, 100%, 0.069)', 6 | A4: 'hsla(0, 0%, 100%, 0.104)', 7 | A5: 'hsla(0, 0%, 100%, 0.134)', 8 | A6: 'hsla(0, 0%, 100%, 0.169)', 9 | A7: 'hsla(0, 0%, 100%, 0.216)', 10 | A8: 'hsla(0, 0%, 100%, 0.312)', 11 | A9: 'hsla(0, 0%, 100%, 0.372)', 12 | A10: 'hsla(0, 0%, 100%, 0.455)', 13 | A11: 'hsla(0, 0%, 100%, 0.662)', 14 | A12: 'hsla(0, 0%, 100%, 0.926)', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/svg-icons/common/CaretRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const CaretRight = ({ stroke = '#6F6F6F' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/svg-icons/common/ChevronLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const ChevronLeft = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/constants/url.js: -------------------------------------------------------------------------------- 1 | export const URL_TYPE = 'https://'; 2 | 3 | export const API_URL = 'api/v1/'; 4 | 5 | export const HELP_URL = 'https://www.chatwoot.com/help-center'; 6 | 7 | export const GRAVATAR_URL = 'https://www.gravatar.com/avatar/'; 8 | 9 | export const REPLY_POLICY = { 10 | FACEBOOK: 'https://developers.facebook.com/docs/messenger-platform/policy/policy-overview/', 11 | TWILIO_WHATSAPP: 12 | 'https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates#sending-non-template-messages-within-a-24-hour-session', 13 | }; 14 | -------------------------------------------------------------------------------- /src/svg-icons/common/Tick.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const TickIcon = ({ stroke = '#0081F1' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/svg-icons/common/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const CheckIcon = ({ stroke = '#0081F1' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components-next/common/filters/stories/FilterBarMockData.ts: -------------------------------------------------------------------------------- 1 | import { BaseFilterOption } from '../FilterBar'; 2 | import { AssigneeOptions, SortOptions, StatusOptions } from '@/types'; 3 | 4 | export const ConversationFilterOptions: BaseFilterOption[] = [ 5 | { 6 | type: 'assignee_type', 7 | options: AssigneeOptions, 8 | defaultFilter: 'All', 9 | }, 10 | { 11 | type: 'status', 12 | options: StatusOptions, 13 | defaultFilter: 'Open', 14 | }, 15 | { 16 | type: 'sort_by', 17 | options: SortOptions, 18 | defaultFilter: 'Latest', 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /src/svg-icons/common/Add.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const AddIcon = ({ stroke = 'black' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/statusTypeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { GridIcon, OpenIcon, PendingIcon, ResolvedIcon, SnoozedIcon } from '@/svg-icons'; 4 | import { AllStatusTypes } from '@/types'; 5 | 6 | export const getStatusTypeIcon = (type: AllStatusTypes) => { 7 | switch (type) { 8 | case 'all': 9 | return ; 10 | case 'open': 11 | return ; 12 | case 'pending': 13 | return ; 14 | case 'resolved': 15 | return ; 16 | case 'snoozed': 17 | return ; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-components/UnsupportedBubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import { tailwind } from '@/theme'; 4 | import i18n from '@/i18n'; 5 | 6 | export const UnsupportedBubble = () => { 7 | return ( 8 | 12 | 13 | {i18n.t('CONVERSATION.UNSUPPORTED_MESSAGE')} 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/svg-icons/list-icons/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const SwitchIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/store/team/teamActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { TeamService } from './teamService'; 3 | import type { Team } from '@/types'; 4 | 5 | export const teamActions = { 6 | fetchTeams: createAsyncThunk('teams/fetchTeams', async (_, { rejectWithValue }) => { 7 | try { 8 | const teams = await TeamService.getTeams(); 9 | return teams; 10 | } catch (error) { 11 | console.error(error); 12 | const message = error instanceof Error ? error.message : ''; 13 | return rejectWithValue(message); 14 | } 15 | }), 16 | }; 17 | -------------------------------------------------------------------------------- /src/types/User.ts: -------------------------------------------------------------------------------- 1 | import type { Account } from './Account'; 2 | import type { AvailabilityStatus } from './common/AvailabilityStatus'; 3 | 4 | export type UserRole = 'administrator' | 'agent'; 5 | 6 | export type User = { 7 | id: number; 8 | name: string; 9 | account_id: number; 10 | accounts: Account[]; 11 | email: string; 12 | pubsub_token: string; 13 | avatar_url: string; 14 | available_name: string; 15 | role: UserRole; 16 | identifier_hash: string; 17 | availability: string; 18 | thumbnail: string; 19 | availability_status: AvailabilityStatus; 20 | type: string; 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/dashboard-app/dashboardAppService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { DashboardAppResponse } from './dashboardAppTypes'; 3 | import { transformDashboardApp } from '@/utils/camelCaseKeys'; 4 | import { DashboardApp } from '@/types'; 5 | 6 | export class DashboardAppService { 7 | static async index(): Promise { 8 | const response = await apiService.get('dashboard_apps'); 9 | const dashboardApps = response.data.map(transformDashboardApp); 10 | return { 11 | payload: dashboardApps, 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/navigation/stack/InboxStack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 3 | 4 | import InboxScreen from '@/screens/inbox/InboxScreen'; 5 | 6 | export type InboxStackParamList = { 7 | InboxScreen: undefined; 8 | }; 9 | 10 | const Stack = createNativeStackNavigator(); 11 | 12 | export const InboxStack = () => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/svg-icons/common/Chatwoot.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const ChatwootIcon = ({ stroke = '#858585', strokeWidth = 1.5 }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/isMarkdown.ts: -------------------------------------------------------------------------------- 1 | export const isMarkdown = (text: string) => { 2 | // Regular expressions for common Markdown patterns 3 | const markdownPatterns = [ 4 | /\*\*.*?\*\*/, // Bold (double asterisks) 5 | /__.*?__/, // Bold (double underscores) 6 | /\*.*?\*/, // Italic (asterisks) 7 | /_.*?_/, // Italic (underscores) 8 | /\[.*?\]\(.*?\)/, // Links ([text](url)) 9 | /`.*?`/, // Inline code (backticks) 10 | // Add more Markdown patterns as needed 11 | ]; 12 | 13 | // Check if the text contains any Markdown pattern 14 | return markdownPatterns.some(pattern => pattern.test(text)); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cx'; 2 | export * from './statusTypeIcon'; 3 | export * from './styleAdapter'; 4 | export * from './useHaptic'; 5 | export * from './useScaleAnimation'; 6 | export * from './common'; 7 | export * from './withAnchorPoint'; 8 | export * from './dateTimeUtils'; 9 | export * from './conversationUtils'; 10 | export * from './camelCaseKeys'; 11 | export * from './getChannelIcon'; 12 | export * from './typingUtils'; 13 | export * from './useAppKeyboardAnimation'; 14 | export * from './inboxUtils'; 15 | export * from './isMarkdown'; 16 | export * from './customAnimations'; 17 | export * from './priorityIcon'; 18 | -------------------------------------------------------------------------------- /src/store/contact/specs/contactMockData.ts: -------------------------------------------------------------------------------- 1 | import type { Contact } from '@/types'; 2 | 3 | export const contact: Contact = { 4 | id: 1, 5 | availabilityStatus: 'online', 6 | email: null, 7 | name: 'Leo Das', 8 | phoneNumber: '+2323242242', 9 | identifier: null, 10 | thumbnail: null, 11 | customAttributes: {}, 12 | additionalAttributes: {}, 13 | lastActivityAt: 1732159896, 14 | createdAt: 1728033836, 15 | type: 'user', 16 | }; 17 | 18 | export const conversation = { 19 | meta: { 20 | sender: contact, 21 | }, 22 | }; 23 | 24 | export const mockContactLabelsResponse = { data: { payload: ['label1', 'label2'] } }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | ios/ 38 | android/ 39 | 40 | google-services.json 41 | GoogleService-Info.plist 42 | .env 43 | .env.local 44 | -------------------------------------------------------------------------------- /src/store/custom-attribute/customAttributeService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { CustomAttributeResponse } from './customAttributeTypes'; 3 | import { transformCustomAttribute } from '@/utils/camelCaseKeys'; 4 | import { CustomAttribute } from '@/types'; 5 | 6 | export class CustomAttributeService { 7 | static async index(): Promise { 8 | const response = await apiService.get('custom_attribute_definitions'); 9 | const customAttributes = response.data.map(transformCustomAttribute); 10 | return { 11 | payload: customAttributes, 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/store/inbox/specs/inboxSelectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { selectAllInboxes, selectInboxById } from '../inboxSelectors'; 2 | import { RootState } from '@/store'; 3 | import { inbox } from './inboxMockData'; 4 | 5 | describe('Inbox Selectors', () => { 6 | const mockState = { 7 | inboxes: { 8 | ids: [inbox.id], 9 | entities: { [inbox.id]: inbox }, 10 | }, 11 | } as RootState; 12 | 13 | it('should select all inboxes', () => { 14 | expect(selectAllInboxes(mockState)).toEqual([inbox]); 15 | }); 16 | 17 | it('should select inbox by id', () => { 18 | expect(selectInboxById(mockState, inbox.id)).toEqual(inbox); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /ReactotronConfig.js: -------------------------------------------------------------------------------- 1 | import Reactotron from 'reactotron-react-native'; 2 | // Don't remove this import, this is used by reactotron-redux only in dev mode 3 | import { reactotronRedux } from 'reactotron-redux'; 4 | 5 | // For viewing the state you need to select State tab on the side, 6 | // press CMD + N and press enter. This will create a new subscription to your app state. 7 | // Once you reload the app the entire state will show up along with actions in timeline. 8 | const reactotron = Reactotron.useReactNative() // add all built-in react native plugins 9 | .use(reactotronRedux()) 10 | .connect(); //Don't forget about me! 11 | 12 | export default reactotron; 13 | -------------------------------------------------------------------------------- /src/svg-icons/common/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const SearchIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/store/label/labelActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { LabelService } from './labelService'; 3 | import type { LabelResponse } from './labelTypes'; 4 | 5 | export const labelActions = { 6 | fetchLabels: createAsyncThunk( 7 | 'labels/fetchLabels', 8 | async (_, { rejectWithValue }) => { 9 | try { 10 | const response = await LabelService.index(); 11 | return response; 12 | } catch (error) { 13 | console.error(error); 14 | const message = error instanceof Error ? error.message : ''; 15 | return rejectWithValue(message); 16 | } 17 | }, 18 | ), 19 | }; 20 | -------------------------------------------------------------------------------- /src/store/inbox/inboxActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { InboxService } from './inboxService'; 3 | import type { InboxResponse } from './inboxTypes'; 4 | 5 | export const inboxActions = { 6 | fetchInboxes: createAsyncThunk( 7 | 'inboxes/fetchInboxes', 8 | async (_, { rejectWithValue }) => { 9 | try { 10 | const response = await InboxService.index(); 11 | return response; 12 | } catch (error) { 13 | console.error(error); 14 | const message = error instanceof Error ? error.message : ''; 15 | return rejectWithValue(message); 16 | } 17 | }, 18 | ), 19 | }; 20 | -------------------------------------------------------------------------------- /src/svg-icons/common/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const LoadingIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/store/contact/specs/contactSelectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { selectAllContacts, selectContactById } from '../contactSelectors'; 2 | import { RootState } from '@/store'; 3 | import { contact } from './contactMockData'; 4 | 5 | describe('Contact Selectors', () => { 6 | const mockState = { 7 | contacts: { 8 | ids: [contact.id], 9 | entities: { [contact.id]: contact }, 10 | }, 11 | } as RootState; 12 | 13 | it('should select all contacts', () => { 14 | expect(selectAllContacts(mockState)).toEqual([contact]); 15 | }); 16 | 17 | it('should select contact by id', () => { 18 | expect(selectContactById(mockState, contact.id)).toEqual(contact); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/store/conversation-participant/conversationParticipantSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '@/store'; 2 | 3 | import { createSelector } from '@reduxjs/toolkit'; 4 | 5 | export const selectConversationParticipantsState = (state: RootState) => 6 | state.conversationParticipants; 7 | 8 | export const selectConversationParticipants = createSelector( 9 | [selectConversationParticipantsState], 10 | state => state.records, 11 | ); 12 | 13 | export const selectConversationParticipantsByConversationId = createSelector( 14 | [selectConversationParticipants, (_state: RootState, conversationId: number) => conversationId], 15 | (state, conversationId) => state[conversationId], 16 | ); 17 | -------------------------------------------------------------------------------- /src/store/label/labelSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '@/store'; 2 | import { labelAdapter } from './labelSlice'; 3 | import { createSelector } from '@reduxjs/toolkit'; 4 | 5 | export const selectLabelsState = (state: RootState) => state.labels; 6 | 7 | export const { selectAll: selectAllLabels } = 8 | labelAdapter.getSelectors(selectLabelsState); 9 | 10 | export const filterLabels = createSelector( 11 | [selectAllLabels, (state: RootState, searchTerm: string) => searchTerm], 12 | (labels, searchTerm) => { 13 | return searchTerm 14 | ? labels.filter(label => label?.title?.toLowerCase().includes(searchTerm.toLowerCase())) 15 | : labels; 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/types/common/SLA.ts: -------------------------------------------------------------------------------- 1 | export interface SLA { 2 | id: number; 3 | slaId: number; 4 | slaStatus: string; 5 | createdAt: number; 6 | updatedAt: number; 7 | slaDescription: string; 8 | slaName: string; 9 | slaFirstResponseTimeThreshold: number; 10 | slaNextResponseTimeThreshold: number; 11 | slaOnlyDuringBusinessHours: boolean; 12 | slaResolutionTimeThreshold: number; 13 | } 14 | 15 | export interface SLAStatus { 16 | type: string; 17 | threshold: string; 18 | icon: string; 19 | isSlaMissed: boolean; 20 | } 21 | 22 | export interface SLAEvent { 23 | id: number; 24 | meta: object; 25 | eventType: string; 26 | createdAt: number; 27 | updatedAt: number; 28 | } 29 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | const { getSentryExpoConfig } = require('@sentry/react-native/metro'); 4 | const withStorybook = require('@storybook/react-native/metro/withStorybook'); 5 | 6 | /** @type {import('expo/metro-config').MetroConfig} */ 7 | const defaultConfig = getDefaultConfig(__dirname); 8 | const sentryConfig = getSentryExpoConfig(__dirname); 9 | 10 | // Merge Sentry config with default config 11 | const config = { 12 | ...defaultConfig, 13 | ...sentryConfig, 14 | }; 15 | 16 | module.exports = withStorybook(config, { 17 | enabled: true, 18 | configPath: path.resolve(__dirname, './.storybook'), 19 | }); 20 | -------------------------------------------------------------------------------- /src/navigation/stack/SettingsStack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 3 | 4 | import SettingsScreen from '@/screens/settings/SettingsScreen'; 5 | 6 | export type SettingsStackParamList = { 7 | SettingsScreen: undefined; 8 | }; 9 | 10 | const Stack = createNativeStackNavigator(); 11 | 12 | export const SettingsStack = () => { 13 | return ( 14 | 15 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/urlUtils.ts: -------------------------------------------------------------------------------- 1 | import { Linking } from 'react-native'; 2 | import * as WebBrowser from 'expo-web-browser'; 3 | 4 | interface URLParams { 5 | URL: string; 6 | } 7 | 8 | interface PhoneParams { 9 | phoneNumber: string; 10 | } 11 | 12 | interface EmailParams { 13 | email: string; 14 | } 15 | 16 | export const openURL = ({ URL }: URLParams): void => { 17 | if (!URL) { 18 | return; 19 | } 20 | WebBrowser.openBrowserAsync(URL); 21 | }; 22 | 23 | export const openNumber = ({ phoneNumber }: PhoneParams): void => { 24 | Linking.openURL(`tel:${phoneNumber}`); 25 | }; 26 | 27 | export const openEmail = ({ email }: EmailParams): void => { 28 | Linking.openURL(`mailto:${email}`); 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/components-next/common/search/SearchBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { SearchBar } from './SearchBar'; 3 | import { CloseIcon } from '@/svg-icons'; 4 | 5 | const meta = { 6 | title: 'Search Bar', 7 | component: SearchBar, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | type Story = StoryObj; 13 | 14 | export const Default: Story = {}; 15 | 16 | // Search with loading 17 | export const WithLoading: Story = { 18 | args: { 19 | isLoading: true, 20 | }, 21 | }; 22 | 23 | // Search with prefix icon 24 | export const WithPrefixIcon: Story = { 25 | args: { 26 | prefix: , 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/store/macro/macroService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { MacroResponse } from './macroTypes'; 3 | import { transformMacro } from '@/utils/camelCaseKeys'; 4 | 5 | export class MacroService { 6 | static async index(): Promise { 7 | const response = await apiService.get('macros'); 8 | const macros = response.data.payload.map(transformMacro); 9 | return { 10 | payload: macros, 11 | }; 12 | } 13 | 14 | static async executeMacro(macroId: number, conversationIds: number[]): Promise { 15 | await apiService.post(`macros/${macroId}/execute`, { 16 | conversation_ids: conversationIds, 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Suggest any enhancements for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your enhancement request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the enhancement request here. 21 | -------------------------------------------------------------------------------- /src/store/canned-response/cannedResponseService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { CannedResponseResponse } from './cannedResponseTypes'; 3 | import { transformCannedResponse } from '@/utils/camelCaseKeys'; 4 | import { CannedResponse } from '@/types'; 5 | 6 | export class CannedResponseService { 7 | static async index(searchKey: string): Promise { 8 | const url = searchKey ? `canned_responses?search=${searchKey}` : 'canned_responses'; 9 | const response = await apiService.get(url); 10 | const cannedResponses = response.data.map(transformCannedResponse); 11 | return { 12 | payload: cannedResponses, 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/svg-icons/menu-icons/Delete.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const DeleteIcon = () => { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/svg-icons/list-icons/User.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const UserIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components-next/common/bottomsheet/BottomSheetHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { tailwind } from '@/theme'; 5 | 6 | type BottomSheetHeaderProps = { 7 | headerText: string; 8 | }; 9 | 10 | export const BottomSheetHeader = (props: BottomSheetHeaderProps) => { 11 | const { headerText } = props; 12 | return ( 13 | 14 | 18 | {headerText} 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/svg-icons/common/Priority.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { G, Mask, Rect } from 'react-native-svg'; 3 | 4 | export const PriorityIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/common/ConversationTyping.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from '@/types'; 2 | 3 | export interface TypingData { 4 | account_id: number; 5 | conversation: Conversation; 6 | user: TypingUser; 7 | } 8 | 9 | export type TypingUser = UserTyping | ContactTyping; 10 | 11 | export type TypingUserType = 'user' | 'contact'; 12 | 13 | interface UserTyping { 14 | availabilityStatus: string; 15 | availableName: string; 16 | avatarUrl: string; 17 | id: number; 18 | name: string; 19 | thumbnail: string; 20 | type: TypingUserType; 21 | } 22 | 23 | interface ContactTyping { 24 | email: string | null; 25 | id: number; 26 | name: string; 27 | phoneNumber: string | null; 28 | thumbnail: string; 29 | type: TypingUserType; 30 | } 31 | -------------------------------------------------------------------------------- /src/navigation/stack/ConversationStack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 3 | 4 | import ConversationScreen from '@/screens/conversations/ConversationScreen'; 5 | 6 | export type ConversationStackParamList = { 7 | ConversationScreen: undefined; 8 | }; 9 | 10 | const Stack = createNativeStackNavigator(); 11 | 12 | export const ConversationStack = () => { 13 | return ( 14 | 15 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/store/contact/contactSelectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import type { RootState } from '@/store'; 3 | import { contactAdapter } from './contactSlice'; 4 | 5 | export const selectContactsState = (state: RootState) => state.contacts; 6 | 7 | export const { selectAll: selectAllContacts } = 8 | contactAdapter.getSelectors(selectContactsState); 9 | 10 | export const selectContactById = createSelector( 11 | [selectContactsState, (_state: RootState, contactId: number) => contactId], 12 | (contactsState, contactId) => contactsState.entities[contactId], 13 | ); 14 | 15 | export const selectContacts = createSelector(selectContactsState, contactsState => 16 | Object.values(contactsState.entities), 17 | ); 18 | -------------------------------------------------------------------------------- /src/screens/settings/SettingsHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import i18n from 'i18n'; 5 | import { tailwind } from '@/theme'; 6 | 7 | export const SettingsHeader = () => { 8 | return ( 9 | 10 | 11 | 12 | 14 | {i18n.t('SETTINGS.HEADER_TITLE')} 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/svg-icons/common/Email.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | export const EmailIcon = (): JSX.Element => { 5 | return ( 6 | 7 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/assignable-agent/assignableAgentService.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import type { AssignableAgentAPIResponse, AssignableAgentResponse } from './assignableAgentTypes'; 3 | import { transformInboxAgent } from '@/utils/camelCaseKeys'; 4 | 5 | export class AssignableAgentService { 6 | static async getAgents(inboxIds: number[]): Promise { 7 | const response = await apiService.get('assignable_agents', { 8 | params: { 9 | 'inbox_ids[]': inboxIds, 10 | }, 11 | }); 12 | 13 | const inboxesAgents = response.data.payload.map(transformInboxAgent); 14 | return { 15 | agents: inboxesAgents, 16 | inboxIds, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/svg-icons/conversation-icons/NoPriority.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { G, Mask, Rect } from 'react-native-svg'; 3 | 4 | export const NoPriorityIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/dashboard-app/dashboardAppActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { DashboardAppService } from './dashboardAppService'; 3 | import type { DashboardAppResponse } from './dashboardAppTypes'; 4 | 5 | export const dashboardAppActions = { 6 | index: createAsyncThunk( 7 | 'dashboardApps/index', 8 | async (_, { rejectWithValue }) => { 9 | try { 10 | const response = await DashboardAppService.index(); 11 | return response; 12 | } catch (error) { 13 | if (error instanceof Error) { 14 | return rejectWithValue({ message: error.message }); 15 | } 16 | return rejectWithValue({ message: 'An unknown error occurred' }); 17 | } 18 | }, 19 | ), 20 | }; 21 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-item/ConversationId.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | import { tailwind } from '@/theme'; 5 | import { NativeView } from '@/components-next/native-components'; 6 | 7 | type ConversationIdProps = { 8 | id: number; 9 | }; 10 | 11 | export const ConversationId = (props: ConversationIdProps) => { 12 | const { id } = props; 13 | return ( 14 | 15 | # 16 | {id} 17 | 18 | ); 19 | }; 20 | 21 | ConversationId.displayName = 'ConversationId'; 22 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-item/TypingMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | import { tailwind } from '@/theme'; 5 | import { NativeView } from '@/components-next/native-components'; 6 | 7 | type TypingMessageProps = { 8 | typingText: string; 9 | }; 10 | 11 | export const TypingMessage = (props: TypingMessageProps) => { 12 | const { typingText } = props; 13 | return ( 14 | 15 | 20 | {typingText} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/store/assignable-agent/assignableAgentActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { AssignableAgentService } from './assignableAgentService'; 3 | import type { AssignableAgentPayload, AssignableAgentResponse } from './assignableAgentTypes'; 4 | 5 | export const assignableAgentActions = { 6 | fetchAgents: createAsyncThunk( 7 | 'assignableAgents/fetchAgents', 8 | async (payload, { rejectWithValue }) => { 9 | try { 10 | const response = await AssignableAgentService.getAgents(payload.inboxIds); 11 | return response; 12 | } catch (error) { 13 | const message = error instanceof Error ? error.message : ''; 14 | return rejectWithValue(message); 15 | } 16 | }, 17 | ), 18 | }; 19 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-item/UnreadIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | import { tailwind } from '@/theme'; 5 | import { NativeView } from '@/components-next/native-components'; 6 | 7 | type UnreadIndicatorProps = { 8 | count: number; 9 | }; 10 | 11 | export const UnreadIndicator = (props: UnreadIndicatorProps) => { 12 | const { count } = props; 13 | return ( 14 | 16 | 20 | {count > 9 ? '9+' : count} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/store/custom-attribute/customAttributeActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { CustomAttributeService } from './customAttributeService'; 3 | import type { CustomAttributeResponse } from './customAttributeTypes'; 4 | 5 | export const customAttributeActions = { 6 | index: createAsyncThunk( 7 | 'customAttributes/index', 8 | async (_, { rejectWithValue }) => { 9 | try { 10 | const response = await CustomAttributeService.index(); 11 | return response; 12 | } catch (error) { 13 | if (error instanceof Error) { 14 | return rejectWithValue({ message: error.message }); 15 | } 16 | return rejectWithValue({ message: 'An unknown error occurred' }); 17 | } 18 | }, 19 | ), 20 | }; 21 | -------------------------------------------------------------------------------- /src/svg-icons/common/Company.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const CompanyIcon = (): JSX.Element => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-list/stories/mock-data/helper.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | import { Message } from '@/types'; 3 | import { getGroupedMessages } from '@/utils'; 4 | import { flatMap } from 'lodash'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export const getAllGroupedMessages = (messages: any[]) => { 8 | const MESSAGES_LIST_MOCKDATA = [...messages].reverse(); 9 | 10 | const updatedMessages = MESSAGES_LIST_MOCKDATA.map( 11 | value => camelcaseKeys(value, { deep: true }) as unknown as Message, 12 | ); 13 | 14 | const groupedMessages = getGroupedMessages(updatedMessages); 15 | 16 | const allMessages = flatMap(groupedMessages, section => [ 17 | ...section.data, 18 | { date: section.date }, 19 | ]); 20 | 21 | return allMessages; 22 | }; 23 | -------------------------------------------------------------------------------- /src/store/contact/contactActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { ContactService } from './contactService'; 3 | 4 | import { ContactLabelsPayload } from './contactTypes'; 5 | 6 | export const contactActions = { 7 | getContactLabels: createAsyncThunk< 8 | { 9 | contactId: number; 10 | labels: string[]; 11 | }, 12 | ContactLabelsPayload 13 | >('contact/getContactLabels', async (payload, { rejectWithValue }) => { 14 | try { 15 | const response = await ContactService.getContactLabels(payload); 16 | const { payload: labels } = response.data; 17 | return { contactId: payload.contactId, labels }; 18 | } catch (error) { 19 | const message = error instanceof Error ? error.message : ''; 20 | return rejectWithValue(message); 21 | } 22 | }), 23 | }; 24 | -------------------------------------------------------------------------------- /src/store/canned-response/cannedResponseActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { CannedResponseService } from './cannedResponseService'; 3 | import type { CannedResponseResponse } from './cannedResponseTypes'; 4 | 5 | export const cannedResponseActions = { 6 | index: createAsyncThunk( 7 | 'cannedResponses/index', 8 | async (payload, { rejectWithValue }) => { 9 | try { 10 | const response = await CannedResponseService.index(payload.searchKey); 11 | return response; 12 | } catch (error) { 13 | if (error instanceof Error) { 14 | return rejectWithValue({ message: error.message }); 15 | } 16 | return rejectWithValue({ message: 'An unknown error occurred' }); 17 | } 18 | }, 19 | ), 20 | }; 21 | -------------------------------------------------------------------------------- /src/svg-icons/common/CaretBottomSmall.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const CaretBottomSmall = ({ fill = '#303030' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/svg-icons/priority-icons/Low.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Mask, G, Rect } from 'react-native-svg'; 3 | 4 | export const LowIcon = () => { 5 | return ( 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/svg-icons/priority-icons/High.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Mask, G, Rect } from 'react-native-svg'; 3 | 4 | export const HighIcon = () => { 5 | return ( 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/svg-icons/priority-icons/Medium.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Mask, G, Rect } from 'react-native-svg'; 3 | 4 | export const MediumIcon = () => { 5 | return ( 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/useAppKeyboardAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useKeyboardHandler } from 'react-native-keyboard-controller'; 2 | import { useSharedValue, withSpring } from 'react-native-reanimated'; 3 | 4 | const ANIMATION_CONFIGS_IOS = { 5 | damping: 500, 6 | stiffness: 1000, 7 | mass: 3, 8 | overshootClamping: true, 9 | restDisplacementThreshold: 10, 10 | restSpeedThreshold: 10, 11 | }; 12 | 13 | const ANIMATION_CONFIGS = ANIMATION_CONFIGS_IOS; 14 | 15 | export const useAppKeyboardAnimation = () => { 16 | const progress = useSharedValue(0); 17 | const height = useSharedValue(0); 18 | useKeyboardHandler({ 19 | onStart: e => { 20 | 'worklet'; 21 | progress.value = withSpring(e.progress, ANIMATION_CONFIGS); 22 | height.value = withSpring(e.height, ANIMATION_CONFIGS); 23 | }, 24 | }); 25 | 26 | return { height, progress }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useHeaderAnimation.ts: -------------------------------------------------------------------------------- 1 | import { withDelay, withSpring } from 'react-native-reanimated'; 2 | 3 | export const useHeaderAnimation = () => { 4 | const entering = () => { 5 | 'worklet'; 6 | return { 7 | initialValues: { 8 | opacity: 0, 9 | transform: [{ scale: 0.95 }], 10 | }, 11 | animations: { 12 | opacity: withDelay(200, withSpring(1)), 13 | transform: [{ scale: withDelay(200, withSpring(1)) }], 14 | }, 15 | }; 16 | }; 17 | 18 | const exiting = () => { 19 | 'worklet'; 20 | return { 21 | initialValues: { 22 | opacity: 1, 23 | transform: [{ scale: 1 }], 24 | }, 25 | animations: { 26 | opacity: withSpring(0), 27 | transform: [{ scale: withSpring(0.95) }], 28 | }, 29 | }; 30 | }; 31 | 32 | return { entering, exiting }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-components/index.ts: -------------------------------------------------------------------------------- 1 | // Deprecated components 2 | export * from './AudioCell'; 3 | export * from './ComposedCell'; 4 | export * from './FileCell'; 5 | export * from './VideoCell'; 6 | export * from './TextMessageCell'; 7 | export * from './MarkdownDisplay'; 8 | export * from './PrivateTextCell'; 9 | export * from './ReplyMessageCell'; 10 | export * from './DeliveryStatus'; 11 | export * from './EmailMessageCell'; 12 | 13 | // New components 14 | export * from './TextBubble'; 15 | export * from './ActivityBubble'; 16 | export * from './MarkdownBubble'; 17 | export * from './ComposedBubble'; 18 | export * from './FileBubble'; 19 | export * from './AudioBubble'; 20 | export * from './LocationBubble'; 21 | export * from './ImageBubble'; 22 | export * from './VideoBubble'; 23 | export * from './EmailBubble'; 24 | export * from './UnsupportedBubble'; 25 | -------------------------------------------------------------------------------- /src/svg-icons/common/MessageType.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { G, Path, Rect } from 'react-native-svg'; 3 | 4 | export const PrivateNoteIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | export const OutgoingIcon = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/svg-icons/common/Trash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const Trash = () => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/types/Contact.ts: -------------------------------------------------------------------------------- 1 | import { type AvailabilityStatus } from './common/AvailabilityStatus'; 2 | import { type UnixTimestamp } from './common/UnixTimestamp'; 3 | 4 | export interface Contact { 5 | additionalAttributes: { 6 | location?: string; 7 | companyName?: string; 8 | city?: string; 9 | country?: string; 10 | description?: string; 11 | createdAtIp?: string; 12 | socialProfiles?: Record; 13 | twitterScreenName?: string; 14 | telegramUsername?: string; 15 | }; 16 | availabilityStatus?: AvailabilityStatus; 17 | createdAt: UnixTimestamp; 18 | customAttributes: Record; 19 | email: string | null; 20 | id: number; 21 | identifier: string | null; 22 | lastActivityAt: UnixTimestamp | null; 23 | name: string | null; 24 | phoneNumber: string | null; 25 | thumbnail: string | null; 26 | type: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/screens/contact-details/components/ContactMetaInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { CustomAttribute, AttributeListType } from '@/types'; 5 | import { AttributeList } from '@/components-next'; 6 | import i18n from '@/i18n'; 7 | 8 | export const ContactMetaInformation = ({ attributes }: { attributes: CustomAttribute[] }) => { 9 | const processedAttributes = attributes.map(attribute => ({ 10 | title: attribute.attributeDisplayName, 11 | subtitle: attribute.value, 12 | subtitleType: 'dark', 13 | type: attribute.attributeDisplayType, 14 | })); 15 | return ( 16 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-components/ActivityBubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { tailwind } from '@/theme'; 5 | import { unixTimestampToReadableTime } from '@/utils/dateTimeUtils'; 6 | 7 | type ActivityBubbleProps = { 8 | text: string; 9 | timeStamp: number; 10 | }; 11 | 12 | export const ActivityBubble = (props: ActivityBubbleProps) => { 13 | const { text, timeStamp } = props; 14 | return ( 15 | 16 | 20 | {text} {unixTimestampToReadableTime(timeStamp)} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/store/conversation/audioPlayerSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '@/store'; 3 | 4 | interface AudioPlayerState { 5 | currentPlayingAudioSrc: string; 6 | } 7 | 8 | const initialState: AudioPlayerState = { 9 | currentPlayingAudioSrc: '', 10 | }; 11 | 12 | const audioPlayerSlice = createSlice({ 13 | name: 'audioPlayer', 14 | initialState, 15 | reducers: { 16 | setCurrentPlayingAudioSrc: (state, action: PayloadAction) => { 17 | state.currentPlayingAudioSrc = action.payload; 18 | }, 19 | }, 20 | }); 21 | 22 | // Selector 23 | export const selectCurrentPlayingAudioSrc = (state: RootState) => { 24 | return state.audioPlayer?.currentPlayingAudioSrc; 25 | }; 26 | 27 | // Actions 28 | export const { setCurrentPlayingAudioSrc } = audioPlayerSlice.actions; 29 | 30 | export default audioPlayerSlice.reducer; 31 | -------------------------------------------------------------------------------- /src/svg-icons/conversation-icons/Team.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const TeamIcon = () => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-components/ActivityTextCell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import { tailwind } from '@/theme'; 5 | import { unixTimestampToReadableTime } from '@/utils/dateTimeUtils'; 6 | 7 | type ActivityTextCellProps = { 8 | text: string; 9 | timeStamp: number; 10 | }; 11 | 12 | export const ActivityTextCell = (props: ActivityTextCellProps) => { 13 | const { text, timeStamp } = props; 14 | return ( 15 | 16 | 20 | {text} {unixTimestampToReadableTime(timeStamp)} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/svg-icons/menu-icons/CannedResponse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const CannedResponseIcon = () => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/svg-icons/status-icons/Open.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const OpenIcon = ({ stroke = '#858585', strokeWidth = '1.5' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/svg-icons/common/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | type WarningIconProps = IconProps & { 7 | renderSecondTick?: boolean; 8 | }; 9 | 10 | export const WarningIcon = ({ 11 | stroke = '#800', 12 | renderSecondTick = true, 13 | }: WarningIconProps): JSX.Element => { 14 | return ( 15 | 16 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/screens/chat-screen/components/message-components/ErrorInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, Animated } from 'react-native'; 3 | import { tailwind } from '@/theme'; 4 | 5 | interface ErrorInformationProps { 6 | errorCode?: string; 7 | errorMessage: string; 8 | } 9 | 10 | export const ErrorInformation = ({ errorCode, errorMessage }: ErrorInformationProps) => ( 11 | 12 | {errorCode && ( 13 | 17 | {errorCode} 18 | 19 | )} 20 | 24 | {errorMessage} 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/utils/permissionUtils.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react-native'; 2 | import { User } from '@/types/User'; 3 | import { Account } from '@/types/Account'; 4 | 5 | export const getCurrentAccount = ( 6 | user: User = {} as User, 7 | accountId: number | null = null, 8 | ): Account | undefined => { 9 | const accounts = user.accounts || []; 10 | return accounts.find(account => Number(account.id) === Number(accountId)); 11 | }; 12 | 13 | export const getUserPermissions = (user: User, accountId: number | null): string[] => { 14 | try { 15 | const currentAccount = getCurrentAccount(user, accountId) || {}; 16 | return (currentAccount as Account).permissions || []; 17 | } catch (error) { 18 | Sentry.captureException(error, { 19 | extra: { 20 | user, 21 | accountId, 22 | functionName: 'getUserPermissions', 23 | }, 24 | }); 25 | return []; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/svg-icons/common/SelfAssign.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Path, Svg } from 'react-native-svg'; 3 | 4 | export const SelfAssign = (): JSX.Element => { 5 | return ( 6 | 7 | 14 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/svg-icons/swipe-actions/StatusIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const StatusIcon = () => { 5 | return ( 6 | 7 | 11 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components-next/common/filters/stories/FilterBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FilterBar as FilterBarComponent } from '../FilterBar'; 5 | import { ConversationFilterOptions } from './FilterBarMockData'; 6 | 7 | const meta = { 8 | title: 'Filters', 9 | component: FilterBarComponent, 10 | decorators: [ 11 | Story => ( 12 | 13 | 14 | 15 | ), 16 | ], 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const FilterBar: Story = { 24 | args: { 25 | allFilters: ConversationFilterOptions, 26 | selectedFilters: { 27 | assignee_type: 'me', 28 | status: 'open', 29 | sort_by: 'latest', 30 | }, 31 | onFilterPress: () => {}, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/svg-icons/swipe-actions/AssignIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const AssignIcon = () => { 5 | return ( 6 | 7 | 13 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/contact/contactConversationActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { ContactService } from './contactService'; 3 | 4 | import { ContactConversationPayload } from './contactTypes'; 5 | import { Conversation } from '@/types'; 6 | export const contactConversationActions = { 7 | getContactConversations: createAsyncThunk< 8 | { 9 | contactId: number; 10 | conversations: Conversation[]; 11 | }, 12 | ContactConversationPayload 13 | >('contact/getContactConversations', async (payload, { rejectWithValue }) => { 14 | try { 15 | const response = await ContactService.getContactConversations(payload); 16 | return { 17 | contactId: payload.contactId, 18 | conversations: response.payload, 19 | }; 20 | } catch (error) { 21 | const message = error instanceof Error ? error.message : ''; 22 | return rejectWithValue(message); 23 | } 24 | }), 25 | }; 26 | -------------------------------------------------------------------------------- /src/svg-icons/common/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const ErrorIcon = () => { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components-next/common/filters/stories/FilterButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import { FilterButton as FilterButtonComponent } from '../FilterButton'; 5 | import { ConversationFilterOptions } from './FilterBarMockData'; 6 | 7 | const meta = { 8 | title: 'Filters', 9 | component: FilterButtonComponent, 10 | decorators: [ 11 | Story => ( 12 | 13 | 14 | 15 | ), 16 | ], 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const FilterButton: Story = { 24 | args: { 25 | allFilters: ConversationFilterOptions[0], 26 | selectedFilters: { 27 | assignee_type: 'me', 28 | status: 'open', 29 | sort_by: 'latest', 30 | }, 31 | handleOnPress: () => {}, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/conversation/localRecordedAudioCacheSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '@/store'; 3 | 4 | interface LocalRecordedAudioCacheState { 5 | localRecordedAudioCacheFilePaths: string[]; 6 | } 7 | 8 | export const initialState: LocalRecordedAudioCacheState = { 9 | localRecordedAudioCacheFilePaths: [], 10 | }; 11 | 12 | const localRecordedAudioCacheSlice = createSlice({ 13 | name: 'localRecordedAudioCache', 14 | initialState, 15 | reducers: { 16 | addNewCachePath: (state, action: PayloadAction) => { 17 | state.localRecordedAudioCacheFilePaths.push(action.payload); 18 | }, 19 | }, 20 | }); 21 | 22 | export const selectLocalRecordedAudioCacheFilePaths = (state: RootState) => 23 | state.localRecordedAudioCache.localRecordedAudioCacheFilePaths; 24 | 25 | export const { addNewCachePath } = localRecordedAudioCacheSlice.actions; 26 | export default localRecordedAudioCacheSlice.reducer; 27 | -------------------------------------------------------------------------------- /src/store/team/teamSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '@/store'; 2 | import { teamAdapter } from './teamSlice'; 3 | import { createSelector } from '@reduxjs/toolkit'; 4 | 5 | export const selectTeamsState = (state: RootState) => state.teams; 6 | 7 | export const selectLoading = createSelector([selectTeamsState], state => state.isLoading); 8 | 9 | export const { selectAll: selectAllTeams } = teamAdapter.getSelectors(selectTeamsState); 10 | 11 | export const filterTeams = createSelector( 12 | [selectAllTeams, (state: RootState, searchTerm: string) => searchTerm], 13 | (teams, searchTerm) => { 14 | const teamsList = [ 15 | { 16 | id: '0', 17 | name: 'None', 18 | description: null, 19 | allowAutoAssign: false, 20 | accountId: 0, 21 | isMember: false, 22 | }, 23 | ...teams, 24 | ]; 25 | 26 | return searchTerm ? teamsList.filter(team => team?.name?.includes(searchTerm)) : teamsList; 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 4 | import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; 5 | import { RefsProvider } from '../src/context/RefsContext'; 6 | 7 | const preview = { 8 | parameters: { 9 | controls: { 10 | matchers: { 11 | color: /(background|color)$/i, 12 | date: /Date$/, 13 | }, 14 | }, 15 | }, 16 | 17 | decorators: [ 18 | (Story, { parameters }) => ( 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | ), 32 | ], 33 | }; 34 | 35 | export default preview; 36 | -------------------------------------------------------------------------------- /src/store/settings/settingsUtils.ts: -------------------------------------------------------------------------------- 1 | import { showToast } from '@/utils/toastUtils'; 2 | import I18n from '@/i18n'; 3 | 4 | export const handleApiError = (error: unknown, customErrorMsg?: string) => { 5 | const errorMessage = error instanceof Error ? error.message : I18n.t('CONFIGURE_URL.ERROR'); 6 | showToast({ message: errorMessage }); 7 | return errorMessage; 8 | }; 9 | 10 | export const extractDomain = ({ url }: { url: string }) => { 11 | const isValidUrl = checkValidUrl({ url }); 12 | 13 | if (!isValidUrl) { 14 | return url; 15 | } 16 | const domain = url.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/i); 17 | if ( 18 | domain != null && 19 | domain.length > 2 && 20 | typeof domain[2] === 'string' && 21 | domain[2].length > 0 22 | ) { 23 | return domain[2]; 24 | } 25 | return url; 26 | }; 27 | 28 | export const checkValidUrl = ({ url }: { url: string }) => { 29 | try { 30 | return Boolean(new URL(url)); 31 | } catch (e) { 32 | return e; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/svg-icons/common/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const FilterIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | 20 | export const InboxFilterIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 21 | return ( 22 | 23 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/navigationUtils.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StackActions, NavigationContainerRef } from '@react-navigation/native'; 3 | 4 | export type RootStackParamList = { 5 | [key: string]: object | undefined; 6 | }; 7 | 8 | // Define the navigation ref with proper typing 9 | export const navigationRef = React.createRef>(); 10 | 11 | // Add type definitions for the navigation functions 12 | export function navigate(name: string, params?: object, key?: string): void { 13 | navigationRef.current?.navigate({ name, key, params }); 14 | } 15 | 16 | export function pop(n: number): void { 17 | navigationRef.current?.dispatch(StackActions.pop(n)); 18 | } 19 | 20 | export function getCurrentRouteName(): string | undefined { 21 | return navigationRef.current?.getCurrentRoute()?.name; 22 | } 23 | 24 | export function replace(name: string, params?: object): void { 25 | navigationRef.current?.dispatch(StackActions.replace(name, params)); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/notification/notificationFilterSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '@/store'; 3 | 4 | export type SortTypes = 'asc' | 'desc'; 5 | 6 | export type FilterState = { 7 | sortOrder: SortTypes; 8 | }; 9 | 10 | export const defaultFilterState: FilterState = { 11 | sortOrder: 'desc', 12 | }; 13 | 14 | const notificationFilterSlice = createSlice({ 15 | name: 'notificationFilter', 16 | initialState: defaultFilterState, 17 | reducers: { 18 | setFilters: (state, action: PayloadAction<{ key: SortTypes }>) => { 19 | state.sortOrder = action.payload.key; 20 | }, 21 | resetFilters: state => { 22 | state.sortOrder = defaultFilterState.sortOrder; 23 | }, 24 | }, 25 | }); 26 | 27 | export const { setFilters, resetFilters } = notificationFilterSlice.actions; 28 | 29 | export const selectSortOrder = (state: RootState) => state.notificationFilter.sortOrder; 30 | 31 | export default notificationFilterSlice.reducer; 32 | -------------------------------------------------------------------------------- /src/svg-icons/common/SocialIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | export const FacebookChannelIcon = () => { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/svg-icons/list-icons/Translate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const TranslateIcon = ({ stroke = '#858585' }: IconProps): JSX.Element => { 7 | return ( 8 | 9 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components-next/list-components/ChannelIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { tailwind } from '@/theme'; 4 | import { NativeView } from '@/components-next/native-components'; 5 | import { Icon } from '@/components-next/common/icon'; 6 | import { getChannelIcon } from '@/utils'; 7 | import { Inbox } from '@/types/Inbox'; 8 | import { ConversationAdditionalAttributes } from '@/types/Conversation'; 9 | import { Channel } from '@/types'; 10 | 11 | type ChannelIndicatorProps = { 12 | inbox: Inbox; 13 | additionalAttributes?: ConversationAdditionalAttributes; 14 | }; 15 | 16 | export const ChannelIndicator = (props: ChannelIndicatorProps) => { 17 | const { channelType = '', medium = '' } = props.inbox || {}; 18 | const { type = '' } = props.additionalAttributes || {}; 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/store/contact/specs/contactActions.spec.ts: -------------------------------------------------------------------------------- 1 | import { contactActions } from '../contactActions'; 2 | import { ContactService } from '../contactService'; 3 | import { mockContactLabelsResponse } from './contactMockData'; 4 | 5 | jest.mock('@/i18n', () => ({ 6 | t: (key: string) => key, 7 | })); 8 | 9 | jest.mock('@/utils/toastUtils', () => ({ 10 | showToast: jest.fn(), 11 | })); 12 | 13 | jest.mock('../contactService', () => ({ 14 | ContactService: { 15 | getContactLabels: jest.fn(), 16 | }, 17 | })); 18 | 19 | describe('contactActions', () => { 20 | it('should return the getContactLabels action', async () => { 21 | (ContactService.getContactLabels as jest.Mock).mockResolvedValue(mockContactLabelsResponse); 22 | const dispatch = jest.fn(); 23 | const getState = jest.fn(); 24 | const payload = { contactId: 1 }; 25 | 26 | await contactActions.getContactLabels(payload)(dispatch, getState, undefined); 27 | expect(ContactService.getContactLabels).toHaveBeenCalledWith(payload); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | orbs: 3 | node: circleci/node@5.1.0 4 | 5 | jobs: 6 | build-and-test: 7 | docker: 8 | - image: cimg/node:20.9.0 9 | steps: 10 | - checkout 11 | - run: 12 | name: Setup Working Directory 13 | command: | 14 | ls -la 15 | pwd 16 | - restore_cache: 17 | name: Restore Yarn Package Cache 18 | keys: 19 | - yarn-packages-v1-{{ checksum "package.json" }} 20 | - yarn-packages-v1- 21 | - run: 22 | name: Install Dependencies 23 | command: yarn install 24 | - save_cache: 25 | name: Save Yarn Package Cache 26 | key: yarn-packages-v1-{{ checksum "package.json" }} 27 | paths: 28 | - .yarn/cache 29 | - .yarn/unplugged 30 | - node_modules 31 | - run: 32 | command: yarn run test 33 | name: Run YARN tests 34 | 35 | workflows: 36 | test_my_app: 37 | jobs: 38 | - build-and-test -------------------------------------------------------------------------------- /src/store/label/specs/labelService.spec.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import { LabelService } from '../labelService'; 3 | import { mockLabelsResponse } from './labelMockData'; 4 | 5 | jest.mock('@sentry/react-native', () => ({ 6 | captureException: jest.fn(), 7 | })); 8 | 9 | jest.mock('@/i18n', () => ({ 10 | t: (key: string) => key, 11 | })); 12 | 13 | jest.mock('@/utils/toastUtils', () => ({ 14 | showToast: jest.fn(), 15 | })); 16 | 17 | jest.mock('@/services/APIService', () => ({ 18 | apiService: { 19 | get: jest.fn(), 20 | post: jest.fn(), 21 | put: jest.fn(), 22 | delete: jest.fn(), 23 | }, 24 | })); 25 | 26 | describe('LabelService', () => { 27 | it('should fetch labels successfully', async () => { 28 | (apiService.get as jest.Mock).mockResolvedValueOnce(mockLabelsResponse); 29 | 30 | const response = await LabelService.index(); 31 | expect(apiService.get).toHaveBeenCalledWith('labels'); 32 | expect(response).toEqual(mockLabelsResponse.data); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/store/macro/specs/macroService.spec.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/APIService'; 2 | import { MacroService } from '../macroService'; 3 | import { mockMacrosResponse } from './macroMockData'; 4 | 5 | jest.mock('@sentry/react-native', () => ({ 6 | captureException: jest.fn(), 7 | })); 8 | 9 | jest.mock('@/i18n', () => ({ 10 | t: (key: string) => key, 11 | })); 12 | 13 | jest.mock('@/utils/toastUtils', () => ({ 14 | showToast: jest.fn(), 15 | })); 16 | 17 | jest.mock('@/services/APIService', () => ({ 18 | apiService: { 19 | get: jest.fn(), 20 | post: jest.fn(), 21 | put: jest.fn(), 22 | delete: jest.fn(), 23 | }, 24 | })); 25 | 26 | describe('MacroService', () => { 27 | it('should fetch macros successfully', async () => { 28 | (apiService.get as jest.Mock).mockResolvedValueOnce(mockMacrosResponse); 29 | 30 | const response = await MacroService.index(); 31 | expect(apiService.get).toHaveBeenCalledWith('macros'); 32 | expect(response).toEqual(mockMacrosResponse.data); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/screens/conversations/components/conversation-item/ConversationSelect.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React, { memo } from 'react'; 3 | import { LinearTransition } from 'react-native-reanimated'; 4 | 5 | import { Icon } from '@/components-next/common'; 6 | import { AnimatedNativeView } from '@/components-next/native-components'; 7 | import { CheckedIcon, UncheckedIcon } from '@/svg-icons'; 8 | import { tailwind } from '@/theme'; 9 | 10 | type ConversationSelectProps = { 11 | isSelected: boolean; 12 | currentState: string; 13 | }; 14 | 15 | export const ConversationSelect = memo((props: ConversationSelectProps) => { 16 | const { isSelected, currentState } = props; 17 | 18 | return currentState === 'Select' ? ( 19 | 22 | : } size={20} /> 23 | 24 | ) : null; 25 | }); 26 | -------------------------------------------------------------------------------- /src/store/contact/contactConversationSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { RootState } from '@/store'; 3 | import { contactConversationActions } from './contactConversationActions'; 4 | import { Conversation } from '@/types'; 5 | interface ContactConversationState { 6 | records: { [key: number]: Conversation[] }; 7 | } 8 | 9 | const initialState: ContactConversationState = { 10 | records: {}, 11 | }; 12 | 13 | const contactConversationSlice = createSlice({ 14 | name: 'contactConversation', 15 | initialState, 16 | reducers: {}, 17 | extraReducers: builder => { 18 | builder.addCase( 19 | contactConversationActions.getContactConversations.fulfilled, 20 | (state, action) => { 21 | const { contactId, conversations } = action.payload; 22 | state.records[contactId] = conversations; 23 | }, 24 | ); 25 | }, 26 | }); 27 | 28 | export const selectContactConversations = (state: RootState) => state.contactConversations.records; 29 | 30 | export default contactConversationSlice.reducer; 31 | -------------------------------------------------------------------------------- /src/store/conversation/conversationActionSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '@/store'; 3 | 4 | type ActionType = 'Assign' | 'Status' | 'Label' | 'TeamAssign' | 'Priority' | null; 5 | 6 | export interface ActionState { 7 | currentActionState: ActionType; 8 | } 9 | 10 | const initialState: ActionState = { 11 | currentActionState: null, 12 | }; 13 | 14 | const conversationActionSlice = createSlice({ 15 | name: 'conversationAction', 16 | initialState, 17 | reducers: { 18 | setActionState: (state, action: PayloadAction) => { 19 | state.currentActionState = action.payload; 20 | }, 21 | resetActionState: state => { 22 | state.currentActionState = null; 23 | }, 24 | }, 25 | }); 26 | 27 | export const selectCurrentActionState = (state: RootState) => 28 | state.conversationAction.currentActionState; 29 | 30 | export const { setActionState, resetActionState } = conversationActionSlice.actions; 31 | export default conversationActionSlice.reducer; 32 | -------------------------------------------------------------------------------- /src/components-next/common/icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import React from 'react'; 3 | import { View, ViewStyle } from 'react-native'; 4 | 5 | import { tailwind } from '@/theme'; 6 | import { RenderPropType } from '@/types'; 7 | 8 | export interface IconComponentProps { 9 | /** 10 | * Svg Icon 11 | */ 12 | icon: RenderPropType; 13 | /** 14 | * Bounding Box style for Icon 15 | */ 16 | style?: ViewStyle; 17 | /** 18 | * Icon Size 19 | */ 20 | size?: 10 | 12 | 16 | 20 | 24 | 32 | number | string; 21 | } 22 | 23 | // Please take icons from https://icones.js.org/collection/fluent 24 | export const Icon: React.FC> = props => { 25 | const { icon, style, size } = props; 26 | const iconAspectRatio = 1; 27 | const sizer = typeof size === 'number' ? `w-[${size}px]` : typeof size === 'string' ? size : ''; 28 | return ( 29 | 30 | {/* @ts-ignore */} 31 | {icon} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/team/teamSlice.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; 2 | import { teamActions } from './teamActions'; 3 | import { Team } from '@/types'; 4 | 5 | export const teamAdapter = createEntityAdapter(); 6 | 7 | interface TeamState { 8 | isLoading: boolean; 9 | } 10 | 11 | const initialState = teamAdapter.getInitialState({ 12 | isLoading: false, 13 | }); 14 | 15 | const teamSlice = createSlice({ 16 | name: 'team', 17 | initialState, 18 | reducers: {}, 19 | extraReducers: builder => { 20 | builder 21 | .addCase(teamActions.fetchTeams.pending, state => { 22 | state.isLoading = true; 23 | }) 24 | .addCase(teamActions.fetchTeams.fulfilled, (state, action) => { 25 | state.isLoading = false; 26 | const { payload: teams } = action; 27 | teamAdapter.setAll(state, teams); 28 | }) 29 | .addCase(teamActions.fetchTeams.rejected, (state, action) => { 30 | state.isLoading = false; 31 | }); 32 | }, 33 | }); 34 | 35 | export default teamSlice.reducer; 36 | -------------------------------------------------------------------------------- /src/types/common/Channel.ts: -------------------------------------------------------------------------------- 1 | export type Channel = 2 | | 'Channel::Whatsapp' 3 | | 'Channel::WebWidget' 4 | | 'Channel::TwitterProfile' 5 | | 'Channel::TwilioSms' 6 | | 'Channel::Telegram' 7 | | 'Channel::Sms' 8 | | 'Channel::Line' 9 | | 'Channel::FacebookPage' 10 | | 'Channel::Email' 11 | | 'Channel::Api' 12 | | 'Channel::All'; 13 | 14 | export type AllChannels = Channel | 'All'; 15 | 16 | export type ChannelCollection = { 17 | name: string; 18 | type: AllChannels; 19 | icon: React.ReactNode; 20 | }; 21 | 22 | export const InboxTypes = { 23 | WEB: 'Channel::WebWidget', 24 | FB: 'Channel::FacebookPage', 25 | TWITTER: 'Channel::TwitterProfile', 26 | TWILIO: 'Channel::TwilioSms', 27 | WHATSAPP: 'Channel::Whatsapp', 28 | API: 'Channel::Api', 29 | EMAIL: 'Channel::Email', 30 | TELEGRAM: 'Channel::Telegram', 31 | LINE: 'Channel::Line', 32 | SMS: 'Channel::Sms', 33 | }; 34 | 35 | export const getRandomChannel = () => { 36 | const channels = Object.values(InboxTypes); 37 | return channels[Math.floor(Math.random() * channels.length)]; 38 | }; 39 | -------------------------------------------------------------------------------- /src/store/contact/specs/contactService.spec.ts: -------------------------------------------------------------------------------- 1 | import { ContactService } from '../contactService'; 2 | import { apiService } from '@/services/APIService'; 3 | import { mockContactLabelsResponse } from './contactMockData'; 4 | 5 | jest.mock('@sentry/react-native', () => ({ 6 | captureException: jest.fn(), 7 | })); 8 | 9 | jest.mock('@/i18n', () => ({ 10 | t: (key: string) => key, 11 | })); 12 | 13 | jest.mock('@/utils/toastUtils', () => ({ 14 | showToast: jest.fn(), 15 | })); 16 | 17 | jest.mock('@/services/APIService', () => ({ 18 | apiService: { 19 | get: jest.fn(), 20 | post: jest.fn(), 21 | put: jest.fn(), 22 | delete: jest.fn(), 23 | }, 24 | })); 25 | 26 | describe('ContactService', () => { 27 | it('should get contact labels', async () => { 28 | (apiService.get as jest.Mock).mockResolvedValueOnce(mockContactLabelsResponse); 29 | 30 | const result = await ContactService.getContactLabels({ contactId: 1 }); 31 | expect(apiService.get).toHaveBeenCalledWith('contacts/1/labels'); 32 | expect(result).toEqual(mockContactLabelsResponse.data); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/svg-icons/attachments/VoiceNote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../../types'; 5 | 6 | export const VoiceNote = (props: IconProps) => { 7 | const { stroke = 'black', strokeOpacity = '0.565' } = props; 8 | return ( 9 | 10 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/specs/serverUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkShouldShowServerUpgradeWarning } from '../serverUtils'; 2 | 3 | describe('ServerHelper', () => { 4 | it('returns true if installed version is less than minimum version', () => { 5 | expect( 6 | checkShouldShowServerUpgradeWarning({ installedVersion: '3.3.0', minimumVersion: '3.4.0' }), 7 | ).toBe(true); 8 | }); 9 | 10 | it('returns false if installed version is greater than minimum version', () => { 11 | expect( 12 | checkShouldShowServerUpgradeWarning({ installedVersion: '3.4.0', minimumVersion: '3.3.0' }), 13 | ).toBe(false); 14 | }); 15 | 16 | it('returns false if installed version is equal to minimum version', () => { 17 | expect( 18 | checkShouldShowServerUpgradeWarning({ installedVersion: '3.4.0', minimumVersion: '3.4.0' }), 19 | ).toBe(false); 20 | }); 21 | 22 | it('returns false if installed version is not a valid semver', () => { 23 | expect( 24 | checkShouldShowServerUpgradeWarning({ installedVersion: 'invalid', minimumVersion: '3.4.0' }), 25 | ).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/store/macro/macroSlice.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; 2 | import { macroActions } from './macroActions'; 3 | import { Macro } from '@/types'; 4 | 5 | export const macroAdapter = createEntityAdapter(); 6 | 7 | export interface MacroState { 8 | isLoading: boolean; 9 | } 10 | 11 | const initialState = macroAdapter.getInitialState({ 12 | isLoading: false, 13 | }); 14 | 15 | export const macroSlice = createSlice({ 16 | name: 'macro', 17 | initialState: initialState, 18 | reducers: {}, 19 | extraReducers: builder => { 20 | builder.addCase(macroActions.fetchMacros.pending, state => { 21 | state.isLoading = true; 22 | }); 23 | builder.addCase(macroActions.fetchMacros.fulfilled, (state, action) => { 24 | const { payload: macros } = action.payload; 25 | macroAdapter.setAll(state, macros); 26 | state.isLoading = false; 27 | }); 28 | builder.addCase(macroActions.fetchMacros.rejected, (state, action) => { 29 | state.isLoading = false; 30 | }); 31 | }, 32 | }); 33 | 34 | export default macroSlice.reducer; 35 | -------------------------------------------------------------------------------- /src/store/label/labelSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'; 2 | import type { Label } from '@/types'; 3 | import { labelActions } from './labelActions'; 4 | 5 | export const labelAdapter = createEntityAdapter