├── api
├── Procfile
├── src
│ ├── controllers
│ │ └── v1
│ │ │ ├── config
│ │ │ ├── index.js
│ │ │ └── get.action.js
│ │ │ ├── health
│ │ │ ├── index.js
│ │ │ └── get.action.js
│ │ │ ├── upload
│ │ │ ├── index.js
│ │ │ └── post.action.js
│ │ │ ├── auth
│ │ │ ├── password
│ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ ├── post.action.js
│ │ │ └── login.action.js
│ │ │ ├── transcript
│ │ │ ├── index.js
│ │ │ └── get.action.js
│ │ │ ├── plugin
│ │ │ ├── integration
│ │ │ │ ├── index.js
│ │ │ │ ├── clearbit
│ │ │ │ │ └── enrich.action.js
│ │ │ │ └── blaze_verify
│ │ │ │ │ └── verify.action.js
│ │ │ ├── index.js
│ │ │ ├── list.action.js
│ │ │ ├── post.action.js
│ │ │ ├── destroy.action.js
│ │ │ ├── get.action.js
│ │ │ └── update.action.js
│ │ │ ├── faq
│ │ │ ├── index.js
│ │ │ ├── post.action.js
│ │ │ ├── get.action.js
│ │ │ ├── list.action.js
│ │ │ ├── update.action.js
│ │ │ └── destroy.action.js
│ │ │ ├── tag
│ │ │ ├── index.js
│ │ │ ├── list.action.js
│ │ │ ├── post.action.js
│ │ │ ├── destroy.action.js
│ │ │ ├── get.action.js
│ │ │ └── update.action.js
│ │ │ ├── agent
│ │ │ ├── index.js
│ │ │ ├── post.action.js
│ │ │ ├── list.action.js
│ │ │ ├── update.action.js
│ │ │ ├── get.action.js
│ │ │ └── destroy.action.js
│ │ │ ├── invite
│ │ │ ├── index.js
│ │ │ ├── get.action.js
│ │ │ ├── update.action.js
│ │ │ ├── destroy.action.js
│ │ │ └── list.action.js
│ │ │ ├── response
│ │ │ ├── index.js
│ │ │ ├── list.action.js
│ │ │ ├── post.action.js
│ │ │ ├── get.action.js
│ │ │ ├── destroy.action.js
│ │ │ └── update.action.js
│ │ │ ├── user
│ │ │ ├── index.js
│ │ │ ├── get.action.js
│ │ │ ├── update.action.js
│ │ │ ├── list.action.js
│ │ │ ├── destroy.action.js
│ │ │ └── post.action.js
│ │ │ ├── organization
│ │ │ ├── index.js
│ │ │ ├── post.action.js
│ │ │ ├── list.action.js
│ │ │ ├── get.action.js
│ │ │ ├── destroy.action.js
│ │ │ └── update.action.js
│ │ │ └── chat
│ │ │ ├── index.js
│ │ │ ├── get.action.js
│ │ │ ├── update.action.js
│ │ │ ├── list.action.js
│ │ │ ├── exchange.action.js
│ │ │ └── destroy.action.js
│ ├── routes
│ │ ├── config.js
│ │ ├── health.js
│ │ ├── transcript.js
│ │ ├── upload.js
│ │ ├── auth.js
│ │ ├── faq.js
│ │ ├── tag.js
│ │ ├── chat.js
│ │ ├── user.js
│ │ ├── agent.js
│ │ ├── invite.js
│ │ ├── response.js
│ │ ├── organization.js
│ │ └── plugin.js
│ ├── utils
│ │ ├── controllers.js
│ │ ├── email
│ │ │ └── transcript.ejs
│ │ ├── db
│ │ │ └── index.js
│ │ ├── stream
│ │ │ └── index.js
│ │ └── auth
│ │ │ └── whitelist.js
│ ├── models
│ │ ├── tag.js
│ │ ├── response.js
│ │ ├── faq.js
│ │ ├── plugin.js
│ │ └── invite.js
│ └── index.js
├── jsconfig.json
├── swaggerDef.js
├── .editorconfig
├── ecosystem.dev.config.js
├── ecosystem.prod.config.js
├── .env.example
├── .env.gitpod
├── .eslintrc.json
├── docs
│ └── API.md
└── .gitignore
├── dashboard
├── public
│ ├── _redirects
│ ├── robots.txt
│ ├── favicon.ico
│ ├── favicon-196x196.png
│ └── manifest.json
├── src
│ ├── screens
│ │ ├── Auth
│ │ │ ├── index.js
│ │ │ ├── forms
│ │ │ │ ├── LoginForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── validationSchema.js
│ │ │ │ ├── SignUpForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── validationSchema.js
│ │ │ │ └── InvitationForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── validationSchema.js
│ │ │ ├── views
│ │ │ │ ├── Invite.js
│ │ │ │ └── Login.js
│ │ │ └── Auth.js
│ │ ├── Dashboard
│ │ │ ├── index.js
│ │ │ ├── views
│ │ │ │ ├── Agents
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── views
│ │ │ │ │ │ ├── AgentDetail
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── widgets
│ │ │ │ │ │ │ ├── TotalThreadsWidget.js
│ │ │ │ │ │ │ └── ChatActivityWidget.js
│ │ │ │ │ │ └── InviteAgents
│ │ │ │ │ │ └── index.js
│ │ │ │ ├── Inbox
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── views
│ │ │ │ │ │ ├── Threads
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ └── Threads.js
│ │ │ │ │ │ ├── SideDrawer
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ │ └── InfoDrawer
│ │ │ │ │ │ │ │ │ └── index.js
│ │ │ │ │ │ │ └── SideDrawer.js
│ │ │ │ │ │ └── MessageThread
│ │ │ │ │ │ │ └── index.js
│ │ │ │ │ └── Inbox.js
│ │ │ │ ├── Plugins
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── Plugins.js
│ │ │ │ └── Settings
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── views
│ │ │ │ │ ├── SettingsList
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── SettingsDetail
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── views
│ │ │ │ │ │ ├── AppSettings
│ │ │ │ │ │ └── index.js
│ │ │ │ │ │ ├── UserSettings
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── UserSettings.js
│ │ │ │ │ │ └── OrganizationSettings
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── OrganizationSettings.js
│ │ │ │ │ └── Settings.js
│ │ │ ├── forms
│ │ │ │ ├── PluginForm
│ │ │ │ │ └── index.js
│ │ │ │ ├── UserSettings
│ │ │ │ │ ├── UserProfileForm
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── validationSchema.js
│ │ │ │ │ ├── UserAvailabilityForm
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── validationSchema.js
│ │ │ │ │ └── index.js
│ │ │ │ └── OrganizationSettings
│ │ │ │ │ ├── OrganizationProfileForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── validationSchema.js
│ │ │ │ │ ├── OrganizationChatSettingsForm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── validationSchema.js
│ │ │ │ │ └── index.js
│ │ │ ├── Dashboard.js
│ │ │ └── routes.js
│ │ └── Welcome
│ │ │ ├── index.js
│ │ │ └── Welcome.js
│ ├── components
│ │ ├── Tabs
│ │ │ ├── index.js
│ │ │ ├── Tabs.js
│ │ │ └── Tab.js
│ │ ├── PageSheet
│ │ │ ├── index.js
│ │ │ ├── PageSheet.js
│ │ │ └── SearchHeader.js
│ │ ├── Shell
│ │ │ ├── Drawer
│ │ │ │ ├── index.js
│ │ │ │ └── Drawer.js
│ │ │ ├── Sidenav
│ │ │ │ ├── index.js
│ │ │ │ └── SidenavItem.js
│ │ │ └── Helmet.js
│ │ ├── ListDetailView
│ │ │ ├── index.js
│ │ │ └── ListDetailView.js
│ │ ├── AvailabilityField
│ │ │ ├── dayMap.js
│ │ │ ├── index.js
│ │ │ ├── Availability.js
│ │ │ └── AvailabilityDay.js
│ │ ├── OrgProtectedRoute.js
│ │ ├── AuthedRoute.js
│ │ ├── UnauthedRoute.js
│ │ ├── SettingsListItem.js
│ │ ├── OrganizationCard.js
│ │ ├── MobileHeader.js
│ │ └── AgentItem.js
│ ├── shared
│ │ ├── Modal
│ │ │ ├── index.js
│ │ │ └── Undersheet.js
│ │ ├── InputField.js
│ │ ├── ColorField.js
│ │ ├── AvatarField.js
│ │ ├── ScreenRoot.js
│ │ ├── Logo.js
│ │ ├── SectionTitle.js
│ │ ├── InfoWidget.js
│ │ ├── SearchInput.js
│ │ ├── MenuButton.js
│ │ ├── UserBlock.js
│ │ ├── IconLabel.js
│ │ ├── StreamLogo.js
│ │ └── ListItem.js
│ ├── widgets
│ │ ├── ChatWidget
│ │ │ ├── index.js
│ │ │ └── ChatWidget.js
│ │ ├── Clearbit
│ │ │ └── EnrichmentWidget
│ │ │ │ └── index.js
│ │ ├── BlazeVerify
│ │ │ └── EmailVerificationWidget
│ │ │ │ └── index.js
│ │ └── Combase
│ │ │ └── ChatDataWidget.js
│ ├── utils
│ │ ├── delay.js
│ │ ├── history.js
│ │ ├── ResizeObserver.js
│ │ ├── getNestedProperty.js
│ │ ├── upload.js
│ │ ├── request.js
│ │ └── GetRef.js
│ ├── contexts
│ │ ├── Agents
│ │ │ └── index.js
│ │ ├── Auth
│ │ │ └── index.js
│ │ ├── Plugins
│ │ │ └── index.js
│ │ ├── Shell
│ │ │ └── index.js
│ │ ├── ThemeSwitcher
│ │ │ ├── index.js
│ │ │ └── ThemeSwitcher.js
│ │ └── Snackbar
│ │ │ ├── index.js
│ │ │ └── useSnackbar.js
│ ├── sounds
│ │ ├── new_message.wav
│ │ └── new_conversation.wav
│ ├── styles
│ │ ├── fonts
│ │ │ └── cerebri
│ │ │ │ ├── CerebriSansPro-Bold.eot
│ │ │ │ ├── CerebriSansPro-Book.eot
│ │ │ │ ├── CerebriSansPro-Bold.woff
│ │ │ │ ├── CerebriSansPro-Bold.woff2
│ │ │ │ ├── CerebriSansPro-Book.woff
│ │ │ │ ├── CerebriSansPro-Book.woff2
│ │ │ │ ├── CerebriSansPro-Heavy.eot
│ │ │ │ ├── CerebriSansPro-Heavy.woff
│ │ │ │ ├── CerebriSansPro-Heavy.woff2
│ │ │ │ ├── CerebriSansPro-Italic.eot
│ │ │ │ ├── CerebriSansPro-Italic.woff
│ │ │ │ ├── CerebriSansPro-Light.eot
│ │ │ │ ├── CerebriSansPro-Light.woff
│ │ │ │ ├── CerebriSansPro-Light.woff2
│ │ │ │ ├── CerebriSansPro-Medium.eot
│ │ │ │ ├── CerebriSansPro-Medium.woff
│ │ │ │ ├── CerebriSansPro-Regular.eot
│ │ │ │ ├── CerebriSansPro-ExtraBold.eot
│ │ │ │ ├── CerebriSansPro-Italic.woff2
│ │ │ │ ├── CerebriSansPro-Medium.woff2
│ │ │ │ ├── CerebriSansPro-Regular.woff
│ │ │ │ ├── CerebriSansPro-Regular.woff2
│ │ │ │ ├── CerebriSansPro-SemiBold.eot
│ │ │ │ ├── CerebriSansPro-SemiBold.woff
│ │ │ │ ├── CerebriSansPro-BoldItalic.eot
│ │ │ │ ├── CerebriSansPro-BoldItalic.woff
│ │ │ │ ├── CerebriSansPro-BoldItalic.woff2
│ │ │ │ ├── CerebriSansPro-BookItalic.eot
│ │ │ │ ├── CerebriSansPro-BookItalic.woff
│ │ │ │ ├── CerebriSansPro-BookItalic.woff2
│ │ │ │ ├── CerebriSansPro-ExtraBold.woff
│ │ │ │ ├── CerebriSansPro-ExtraBold.woff2
│ │ │ │ ├── CerebriSansPro-HeavyItalic.eot
│ │ │ │ ├── CerebriSansPro-HeavyItalic.woff
│ │ │ │ ├── CerebriSansPro-LightItalic.eot
│ │ │ │ ├── CerebriSansPro-LightItalic.woff
│ │ │ │ ├── CerebriSansPro-MediumItalic.eot
│ │ │ │ ├── CerebriSansPro-SemiBold.woff2
│ │ │ │ ├── CerebriSansPro-HeavyItalic.woff2
│ │ │ │ ├── CerebriSansPro-LightItalic.woff2
│ │ │ │ ├── CerebriSansPro-MediumItalic.woff
│ │ │ │ ├── CerebriSansPro-MediumItalic.woff2
│ │ │ │ ├── CerebriSansPro-SemiBoldItalic.eot
│ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.eot
│ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.woff
│ │ │ │ ├── CerebriSansPro-ExtraBoldItalic.woff2
│ │ │ │ ├── CerebriSansPro-SemiBoldItalic.woff
│ │ │ │ └── CerebriSansPro-SemiBoldItalic.woff2
│ │ ├── theme
│ │ │ ├── breakpoints.js
│ │ │ ├── colors.js
│ │ │ ├── base.js
│ │ │ └── index.js
│ │ ├── global.js
│ │ └── css
│ │ │ ├── pageCard.js
│ │ │ ├── baseInputStyle.js
│ │ │ └── listItemInteractions.js
│ ├── hooks
│ │ ├── usePrevious.js
│ │ ├── useLayoutProvider.js
│ │ ├── useLiveMoment.js
│ │ ├── useAuth.js
│ │ ├── useAgent.js
│ │ ├── useConfig.js
│ │ ├── useLayout.js
│ │ ├── useMedia.js
│ │ ├── usePluginEndpoint.js
│ │ ├── useNotifications.js
│ │ ├── useInvite.js
│ │ ├── usePlugin.js
│ │ ├── useActivePlugins.js
│ │ ├── useChats.js
│ │ ├── useAgents.js
│ │ └── usePageSheet.js
│ ├── hocs
│ │ ├── withAuth.js
│ │ ├── asField.js
│ │ └── withLayout.js
│ ├── setupTests.js
│ ├── index.js
│ └── App.js
├── .vscode
│ └── settings.json
├── jsconfig.json
└── .gitignore
├── widget
├── src
│ ├── screens
│ │ ├── Home
│ │ │ ├── index.js
│ │ │ └── Home.js
│ │ └── Thread
│ │ │ ├── index.js
│ │ │ ├── views
│ │ │ └── MessageThread
│ │ │ │ ├── index.js
│ │ │ │ └── MessageThread.js
│ │ │ └── Thread.js
│ ├── components
│ │ ├── ListDetailView
│ │ │ ├── index.js
│ │ │ └── ListDetailView.js
│ │ ├── Home
│ │ │ ├── ConversationsWidget
│ │ │ │ └── index.js
│ │ │ └── KnowledgeBaseWidget
│ │ │ │ ├── index.js
│ │ │ │ └── KnowledgeBaseWidget.js
│ │ ├── CardFooter.js
│ │ └── CardHeader.js
│ ├── contexts
│ │ ├── Auth
│ │ │ └── index.js
│ │ └── ScrollAnimation
│ │ │ ├── useScrollAnim.js
│ │ │ ├── index.js
│ │ │ └── Provider.js
│ ├── hocs
│ │ └── withAuth.js
│ ├── setupTests.js
│ ├── index.js
│ ├── hooks
│ │ ├── useAuth.js
│ │ └── useChats.js
│ ├── Root.js
│ ├── utils
│ │ └── request.js
│ └── App.js
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ └── manifest.json
├── jsconfig.json
├── .gitignore
└── package.json
├── .vscode
└── settings.json
├── netlify.toml
├── .gitpod.Dockerfile
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── app.json
/api/Procfile:
--------------------------------------------------------------------------------
1 | web: yarn start --only api
--------------------------------------------------------------------------------
/dashboard/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/widget/src/screens/Home/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Home';
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Auth';
2 |
--------------------------------------------------------------------------------
/widget/src/screens/Thread/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Thread';
--------------------------------------------------------------------------------
/api/src/controllers/v1/config/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/health/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 |
--------------------------------------------------------------------------------
/dashboard/src/components/Tabs/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Tabs';
2 |
--------------------------------------------------------------------------------
/dashboard/src/shared/Modal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Modal";
2 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/upload/index.js:
--------------------------------------------------------------------------------
1 | export { post } from './post.action';
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Dashboard";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Welcome/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Welcome';
2 |
--------------------------------------------------------------------------------
/dashboard/src/components/PageSheet/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PageSheet';
2 |
--------------------------------------------------------------------------------
/dashboard/src/components/Shell/Drawer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Drawer";
2 |
--------------------------------------------------------------------------------
/dashboard/src/components/Shell/Sidenav/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Sidenav";
2 |
--------------------------------------------------------------------------------
/dashboard/src/widgets/ChatWidget/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ChatWidget";
2 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/auth/password/index.js:
--------------------------------------------------------------------------------
1 | export { reset } from './reset.action';
2 |
--------------------------------------------------------------------------------
/dashboard/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Agents/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Agents";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Inbox";
2 |
--------------------------------------------------------------------------------
/widget/src/components/ListDetailView/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListDetailView";
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "recyclerlistview"
4 | ]
5 | }
--------------------------------------------------------------------------------
/dashboard/src/components/ListDetailView/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./ListDetailView";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/LoginForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './LoginForm';
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/SignUpForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SignUpForm';
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Plugins/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Plugins";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Settings";
2 |
--------------------------------------------------------------------------------
/widget/src/screens/Thread/views/MessageThread/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './MessageThread';
--------------------------------------------------------------------------------
/dashboard/src/components/AvailabilityField/dayMap.js:
--------------------------------------------------------------------------------
1 | export default ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/PluginForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PluginForm';
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/Threads/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Threads';
--------------------------------------------------------------------------------
/widget/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/widget/src/components/Home/ConversationsWidget/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ConversationsWidget';
--------------------------------------------------------------------------------
/widget/src/components/Home/KnowledgeBaseWidget/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './KnowledgeBaseWidget';
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/InvitationForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './InvitationForm';
2 |
--------------------------------------------------------------------------------
/dashboard/src/utils/delay.js:
--------------------------------------------------------------------------------
1 | export default time => new Promise(res => setTimeout(() => res(), time));
2 |
--------------------------------------------------------------------------------
/dashboard/src/widgets/Clearbit/EnrichmentWidget/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./EnrichmentWidget";
2 |
--------------------------------------------------------------------------------
/widget/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/favicon.ico
--------------------------------------------------------------------------------
/widget/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/logo192.png
--------------------------------------------------------------------------------
/widget/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/widget/public/logo512.png
--------------------------------------------------------------------------------
/dashboard/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/public/favicon.ico
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SideDrawer';
2 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Agents/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | export default createContext();
3 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Auth/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './AgentDetail';
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Agents/views/InviteAgents/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./InviteAgents";
2 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/auth/index.js:
--------------------------------------------------------------------------------
1 | export { login } from './login.action';
2 | export { post } from './post.action';
3 |
--------------------------------------------------------------------------------
/dashboard/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "clearbit",
4 | "refetch"
5 | ]
6 | }
--------------------------------------------------------------------------------
/dashboard/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
7 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Plugins/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
4 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Shell/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export default createContext();
4 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/UserSettings/UserProfileForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UserProfileForm';
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/MessageThread/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./MessageThread";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsList/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SettingsList";
2 |
--------------------------------------------------------------------------------
/dashboard/src/widgets/BlazeVerify/EmailVerificationWidget/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./EmailVerificationWidget";
2 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/transcript/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { post } from './post.action';
3 |
--------------------------------------------------------------------------------
/dashboard/public/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/public/favicon-196x196.png
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./SettingsDetail";
2 |
--------------------------------------------------------------------------------
/dashboard/src/sounds/new_message.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/sounds/new_message.wav
--------------------------------------------------------------------------------
/api/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "*": ["src/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/UserSettings/UserAvailabilityForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './UserAvailabilityForm';
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/views/InfoDrawer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './InfoDrawer';
2 |
--------------------------------------------------------------------------------
/dashboard/src/sounds/new_conversation.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/sounds/new_conversation.wav
--------------------------------------------------------------------------------
/api/src/controllers/v1/health/get.action.js:
--------------------------------------------------------------------------------
1 | exports.get = (req, res) => {
2 | res.status(200).json({ code: 200, status: 'OK' });
3 | };
4 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationProfileForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './OrganizationProfileForm';
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/AppSettings/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./AppSettings";
2 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/UserSettings/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./UserSettings";
2 |
--------------------------------------------------------------------------------
/widget/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": [
6 | "src"
7 | ]
8 | }
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationChatSettingsForm/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './OrganizationChatSettingsForm';
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/OrganizationSettings/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./OrganizationSettings";
2 |
--------------------------------------------------------------------------------
/dashboard/src/utils/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from "history";
2 | const history = createBrowserHistory();
3 | export default history;
4 |
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Bold.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Book.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Heavy.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Light.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.eot
--------------------------------------------------------------------------------
/widget/src/contexts/Auth/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
4 | export { default as AuthProvider } from './AuthProvider';
--------------------------------------------------------------------------------
/dashboard/src/components/AvailabilityField/index.js:
--------------------------------------------------------------------------------
1 | import Availability from './Availability';
2 | import asField from 'hocs/asField';
3 |
4 | export default asField(Availability);
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/UserSettings/UserAvailabilityForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | export default Yup.object().shape({
4 |
5 | });
6 |
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Italic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Medium.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-Regular.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff
--------------------------------------------------------------------------------
/api/swaggerDef.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | info: {
3 | title: 'Combase API',
4 | version: '1.0.0',
5 | description: 'API for https://comba.se'
6 | },
7 | basePath: '/src'
8 | };
9 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/ThemeSwitcher/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
4 | export { default as ThemeSwitcher } from './ThemeSwitcher';
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BoldItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-BookItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBold.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBold.woff2
--------------------------------------------------------------------------------
/dashboard/src/utils/ResizeObserver.js:
--------------------------------------------------------------------------------
1 | import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
2 | const ResizeObserver = Polyfill;
3 |
4 | export default ResizeObserver;
5 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/integration/index.js:
--------------------------------------------------------------------------------
1 | export { blazeVerifyExecVerify } from './blaze_verify/verify.action';
2 | export { clearbitExecEnrich } from './clearbit/enrich.action';
3 |
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-HeavyItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-LightItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-MediumItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.eot
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff
--------------------------------------------------------------------------------
/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/combase-v1/HEAD/dashboard/src/styles/fonts/cerebri/CerebriSansPro-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/widget/src/contexts/ScrollAnimation/useScrollAnim.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import Context from './index';
3 |
4 | export default () => {
5 | return useContext(Context)
6 | }
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/UserSettings/index.js:
--------------------------------------------------------------------------------
1 | export { default as UserProfileForm } from './UserProfileForm';
2 | export { default as UserAvailabilityForm } from './UserAvailabilityForm';
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | base = "dashboard"
3 | command = "yarn build"
4 | publish = "build"
5 | ignore = "git diff --quiet HEAD^ HEAD api/"
6 |
7 | [template.environment]
8 | SASS_PATH = "./node_modules"
--------------------------------------------------------------------------------
/dashboard/src/styles/theme/breakpoints.js:
--------------------------------------------------------------------------------
1 | export default {
2 | xs: 600, // tablet_portrait_up
3 | sm: 900, // tablet_landscape_up
4 | md: 1200, // desktop_up
5 | lg: 1800, // lg_desktop_up,
6 | xl: 2200
7 | };
8 |
--------------------------------------------------------------------------------
/api/src/routes/config.js:
--------------------------------------------------------------------------------
1 | import { get } from '../controllers/v1/config';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/configs').get(wrapAsync(get));
7 | };
8 |
--------------------------------------------------------------------------------
/api/src/routes/health.js:
--------------------------------------------------------------------------------
1 | import { get } from '../controllers/v1/health';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/health').get(wrapAsync(get));
7 | };
8 |
--------------------------------------------------------------------------------
/api/src/utils/controllers.js:
--------------------------------------------------------------------------------
1 | function wrapAsync(fn) {
2 | return function(req, res, next) {
3 | fn(req, res, next).catch(e => {
4 | next(e);
5 | });
6 | };
7 | }
8 |
9 | exports.wrapAsync = wrapAsync;
10 |
--------------------------------------------------------------------------------
/dashboard/src/shared/InputField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import asField from 'hocs/asField';
3 | import { Input } from '@comba.se/ui';
4 |
5 | export default asField(Input);
6 |
--------------------------------------------------------------------------------
/api/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/dashboard/src/shared/ColorField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import asField from 'hocs/asField';
3 | import { ColorInput } from '@comba.se/ui';
4 |
5 | export default asField(ColorInput);
6 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/OrganizationSettings/index.js:
--------------------------------------------------------------------------------
1 | export { default as OrganizationProfileForm } from './OrganizationProfileForm';
2 | export { default as OrganizationChatSettingsForm } from './OrganizationChatSettingsForm';
--------------------------------------------------------------------------------
/dashboard/src/shared/AvatarField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import asField from 'hocs/asField';
3 | import AvatarInput from 'shared/AvatarInput';
4 |
5 | export default asField(AvatarInput);
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/ecosystem.dev.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: 'api',
5 | script: 'src/index.js',
6 | interpreter: 'babel-node',
7 | watch: true,
8 | ignore_watch: [ '.git', 'node_modules' ]
9 | }
10 | ]
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { update } from './update.action';
4 | export { post } from './post.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/invite/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/widget/src/contexts/ScrollAnimation/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export default createContext();
4 | export { default as ScrollAnimationProvider } from './Provider';
5 | export { default as useScrollAnim } from './useScrollAnim';
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/index.js:
--------------------------------------------------------------------------------
1 | export { list } from './list.action';
2 | export { get } from './get.action';
3 | export { update } from './update.action';
4 | export { post } from './post.action';
5 | export { destroy } from './destroy.action';
6 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Snackbar/index.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export default createContext();
4 |
5 | export { default as SnackbarProvider } from './SnackbarProvider';
6 | export { default as useSnackbar } from './useSnackbar';
7 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/usePrevious.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export default value => {
4 | const ref = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | };
10 |
--------------------------------------------------------------------------------
/dashboard/src/utils/getNestedProperty.js:
--------------------------------------------------------------------------------
1 | export default (nestedObj, path) => {
2 | const pathArr = path.split('.');
3 | return pathArr.reduce(
4 | (obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined),
5 | nestedObj
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/dashboard/src/hocs/withAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useAuth from 'hooks/useAuth';
3 |
4 | export default WrappedComponent => props => {
5 | const [{ organization, user }] = useAuth();
6 | return
7 | }
--------------------------------------------------------------------------------
/widget/src/hocs/withAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useAuth from 'hooks/useAuth';
3 |
4 | export default WrappedComponent => props => {
5 | const [{ organization, user }] = useAuth();
6 | return
7 | }
--------------------------------------------------------------------------------
/api/ecosystem.prod.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: 'api',
5 | script: 'dist/index.js',
6 | instances: process.env.WEB_CONCURRENCY || 1,
7 | exec_mode: 'cluster',
8 | autorestart: true,
9 | watch: false
10 | }
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/Snackbar/useSnackbar.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import SnackbarContext from './index';
3 |
4 | export default () => {
5 | const { addSnackbar, queueSnackbar } = useContext(SnackbarContext);
6 | return { addSnackbar, queueSnackbar };
7 | };
8 |
--------------------------------------------------------------------------------
/dashboard/src/hocs/asField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'formik';
3 |
4 | export default WrappedInput => props => {
5 | const [field, meta] = useField(props);
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/dashboard/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/widget/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/index.js:
--------------------------------------------------------------------------------
1 | export { get } from './get.action';
2 | export { list } from './list.action';
3 | export { post } from './post.action';
4 | export { update } from './update.action';
5 | export { destroy } from './destroy.action';
6 | export { exchange } from './exchange.action';
7 |
--------------------------------------------------------------------------------
/api/src/routes/transcript.js:
--------------------------------------------------------------------------------
1 | import { get, post } from '../controllers/v1/transcript';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = (api) => {
6 | api.route('/v1/transcripts').get(wrapAsync(get));
7 | api.route('/v1/transcripts').post(wrapAsync(post));
8 | };
9 |
--------------------------------------------------------------------------------
/api/src/routes/upload.js:
--------------------------------------------------------------------------------
1 | import multer from 'multer';
2 |
3 | import { post } from '../controllers/v1/upload';
4 |
5 | import { wrapAsync } from '../utils/controllers';
6 |
7 | const upload = multer();
8 |
9 | module.exports = api => {
10 | api.route('/v1/uploads').post(upload.single('file'), post);
11 | };
12 |
--------------------------------------------------------------------------------
/dashboard/src/styles/global.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import fonts from './fonts';
3 |
4 | /* eslint no-unused-expressions: 0 */
5 | export default createGlobalStyle`
6 | ${fonts}
7 |
8 | body {
9 | background-color: ${({ theme }) => theme.color.surface};
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/dashboard/src/styles/css/pageCard.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | export default css`
4 | overflow: hidden;
5 | box-shadow: -4px 0px 24px rgba(0, 0, 0, 0.12);
6 | border-top-left-radius: ${({ theme }) => theme.borderRadius}px;
7 | border-bottom-left-radius: ${({ theme }) => theme.borderRadius}px;
8 | `;
9 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationChatSettingsForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | export default Yup.object().shape({
4 | welcome: Yup.object().shape({
5 | enabled: Yup.bool(),
6 | message: Yup.string()
7 | }),
8 | response: Yup.string(),
9 | });
10 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/list.action.js:
--------------------------------------------------------------------------------
1 | import Tag from 'models/tag';
2 |
3 | exports.list = async (req, res) => {
4 | try {
5 | const data = req.query;
6 |
7 | const tags = await Tag.apiQuery(data);
8 | res.status(200).json(tags);
9 | } catch (error) {
10 | console.log(error);
11 | res.status(500).json({ error: error.message });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/api/src/utils/email/transcript.ejs:
--------------------------------------------------------------------------------
1 | <% if (transcript.length) { %>
2 |
3 | <% transcript.forEach((msg) => { %>
4 | -
5 | » <%= msg.name %>
7 | <%= msg.timestamp %>
8 | <%= msg.text %>
9 |
10 | <% }) %>
11 |
12 | <% } %>
13 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/post.action.js:
--------------------------------------------------------------------------------
1 | import Tag from 'models/tag';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const tag = await Tag.create(data);
8 | res.status(200).json(tag);
9 | } catch (error) {
10 | console.error(error);
11 | res.status(500).json({ error: error.message });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/post.action.js:
--------------------------------------------------------------------------------
1 | import Faq from 'models/faq';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const faq = await Faq.create(data);
8 |
9 | res.status(200).json(faq);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Tag from 'models/tag';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | await Tag.findByIdAndRemove(data.tag);
8 |
9 | res.sendStatus(204);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/get.action.js:
--------------------------------------------------------------------------------
1 | import Faq from 'models/faq';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 |
7 | const faq = await Faq.findById(data.faq).lean({ autopopulate: true });
8 | res.status(200).json(faq);
9 | } catch (error) {
10 | console.error(error);
11 | res.status(500).json({ error: error.message });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/get.action.js:
--------------------------------------------------------------------------------
1 | import Tag from 'models/tag';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 |
7 | const tag = await Tag.findById(data.tag).lean({ autopopulate: true });
8 | res.status(200).json(tag);
9 | } catch (error) {
10 | console.error(error);
11 | res.status(500).json({ error: error.message });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/LoginForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | const requiredError = 'This is required.';
4 | const emailError = 'Invalid Email Address.';
5 |
6 | export default Yup.object().shape({
7 | email: Yup.string()
8 | .email(emailError)
9 | .required(requiredError),
10 | password: Yup.string().required(requiredError),
11 | });
12 |
--------------------------------------------------------------------------------
/.gitpod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-mongodb
2 |
3 | # Install custom tools, runtimes, etc.
4 | # For example "bastet", a command-line tetris clone:
5 | # RUN brew install bastet
6 | #
7 | # More information: https://www.gitpod.io/docs/config-docker/
8 |
9 | # Install Redis.
10 | RUN sudo apt-get update \
11 | && sudo apt-get install -y \
12 | redis-server \
13 | && sudo rm -rf /var/lib/apt/lists/*
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/list.action.js:
--------------------------------------------------------------------------------
1 | import Response from 'models/response';
2 |
3 | exports.list = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const responses = await Response.apiQuery(data);
8 | res.status(200).json(responses);
9 | } catch (error) {
10 | console.log(error);
11 | res.status(500).json({ error: error.message });
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/api/src/routes/auth.js:
--------------------------------------------------------------------------------
1 | import { login, post } from '../controllers/v1/auth';
2 | import { reset } from '../controllers/v1/auth/password';
3 |
4 | import { wrapAsync } from '../utils/controllers';
5 |
6 | module.exports = (api) => {
7 | api.route('/v1/auth/login').post(wrapAsync(login));
8 | api.route('/v1/auth').post(wrapAsync(post));
9 | api.route('/v1/auth/password-reset').post(wrapAsync(reset));
10 | };
11 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/list.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import Faq from 'models/faq';
4 |
5 | exports.list = async (req, res) => {
6 | try {
7 | const data = { ...req.body, ...req.params };
8 |
9 | const faqs = await Faq.apiQuery(data);
10 | res.status(200).json(faqs);
11 | } catch (error) {
12 | console.log(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/post.action.js:
--------------------------------------------------------------------------------
1 | import Response from 'models/response';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const response = await Response.create(data);
8 |
9 | res.status(200).json(response);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/get.action.js:
--------------------------------------------------------------------------------
1 | import User from 'models/user';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 |
7 | const user = await User.findById(data.user).lean({
8 | autopopulate: true,
9 | });
10 | res.status(200).json(user);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/post.action.js:
--------------------------------------------------------------------------------
1 | import Agent from 'models/agent';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = req.body;
6 |
7 | const agent = await Agent.findOneOrCreate({ email: data.email.toLowerCase() }, data);
8 |
9 | res.status(200).json(agent);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/invite/get.action.js:
--------------------------------------------------------------------------------
1 | import Invite from 'models/invite';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 | const invite = await Invite.findById(data.invite).lean({
7 | autopopulate: true,
8 | });
9 | res.status(200).json(invite);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/dashboard/src/components/Shell/Helmet.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import Helmet from 'react-helmet';
3 | import { ThemeContext } from 'styled-components';
4 |
5 | export default () => {
6 | const theme = useContext(ThemeContext);
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/widget/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/tag/update.action.js:
--------------------------------------------------------------------------------
1 | import Tag from 'models/tag';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const tag = await Tag.updateOne({ _id: params.tag }, { $set: data });
9 | res.status(200).json(tag);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/dashboard/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/get.action.js:
--------------------------------------------------------------------------------
1 | import Chat from 'models/chat';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const chat = await Chat.findById(data.chat).lean({
8 | autopopulate: true,
9 | });
10 | res.status(200).json(chat);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/update.action.js:
--------------------------------------------------------------------------------
1 | import Faq from 'models/faq';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const faq = await Faq.updateOne({ _id: params.faq }, { $set: data });
9 |
10 | res.status(200).json(faq);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/post.action.js:
--------------------------------------------------------------------------------
1 | import Organization from 'models/organization';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const organization = await Organization.create(data);
8 |
9 | res.status(200).json(organization);
10 | } catch (error) {
11 | console.error(error);
12 | res.status(500).json({ error: error.message });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/dashboard/src/styles/theme/colors.js:
--------------------------------------------------------------------------------
1 | // TODO: need more colors
2 | // for dark theme.
3 | export default {
4 | yellow: "#FFD47D",
5 | blue: '#4D7CFE',
6 | red: '#FF5B5B',
7 | green: '#43FC99',
8 | white: '#ffffff',
9 | slate: '#606264',
10 | light_gray: '#EFF1F5',
11 | gray: '#C6C6C8',
12 | off_white: '#FCFCFC',
13 | black: '#302F32',
14 | true_black: '#000000',
15 | stream: '#4D7CFE',
16 | };
17 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/update.action.js:
--------------------------------------------------------------------------------
1 | import Chat from 'models/chat';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const chat = await Chat.updateOne({ _id: params.chat }, { $set: data });
9 |
10 | res.status(200).json(chat);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/get.action.js:
--------------------------------------------------------------------------------
1 | import Response from 'models/response';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 |
7 | const response = await Response.findById(data.response).lean({
8 | autopopulate: true,
9 | });
10 | res.status(200).json(response);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/update.action.js:
--------------------------------------------------------------------------------
1 | import User from 'models/user';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const user = await User.updateOne({ _id: params.user }, { $set: data });
9 |
10 | res.status(200).json(user);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/list.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import Organization from 'models/organization';
4 |
5 | exports.list = async (req, res) => {
6 | try {
7 | const data = req.query;
8 |
9 | const organizations = await Organization.apiQuery(data);
10 | res.status(200).json(organizations);
11 | } catch (error) {
12 | console.log(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/invite/update.action.js:
--------------------------------------------------------------------------------
1 | import Invite from 'models/invite';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const invite = await Invite.updateOne({ _id: params.invite }, { $set: data });
9 |
10 | res.status(200).json(invite);
11 | } catch (error) {
12 | console.error(error);
13 | res.status(500).json({ error: error.message });
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/dashboard/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Stream",
3 | "short_name": "Stream",
4 | "url": "https://getstream.io",
5 | "icons": [
6 | {
7 | "src": "foo",
8 | "sizes": "16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "bar",
13 | "type": "image/png",
14 | "sizes": "512x512"
15 | }
16 | ],
17 | "start_url": ".",
18 | "display": "standalone",
19 | "theme_color": "baz",
20 | "background_color": "bingo"
21 | }
22 |
--------------------------------------------------------------------------------
/widget/src/components/CardFooter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | // Components //
5 | const Root = styled.div`
6 | padding: 16px;
7 | flex-direction: row;
8 | justify-content: space-between;
9 | align-items: center;
10 | `;
11 |
12 | const CardFooter = ({ children }) => (
13 |
14 |
15 | {children}
16 |
17 | );
18 |
19 | export default CardFooter;
--------------------------------------------------------------------------------
/api/src/routes/faq.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, put, destroy } from '../controllers/v1/faq';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/faqs').get(wrapAsync(list));
7 | api.route('/v1/faqs/:faq').get(wrapAsync(get));
8 | api.route('/v1/faqs/:faq').put(wrapAsync(put));
9 | api.route('/v1/faqs').post(wrapAsync(post));
10 | api.route('/v1/faqs/:faq').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/routes/tag.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, put, destroy } from '../controllers/v1/tag';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/tags').get(wrapAsync(list));
7 | api.route('/v1/tags/:tag').get(wrapAsync(get));
8 | api.route('/v1/tags/:tag').put(wrapAsync(put));
9 | api.route('/v1/tags').post(wrapAsync(post));
10 | api.route('/v1/tags/:tag').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/routes/chat.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, put, destroy } from '../controllers/v1/chat';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/chats').get(wrapAsync(list));
7 | api.route('/v1/chats/:chat').get(wrapAsync(get));
8 | api.route('/v1/chats/:chat').put(wrapAsync(put));
9 | api.route('/v1/chats').post(wrapAsync(post));
10 | api.route('/v1/chats/:chat').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/routes/user.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, put, destroy } from '../controllers/v1/user';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/users').get(wrapAsync(list));
7 | api.route('/v1/users/:user').get(wrapAsync(get));
8 | api.route('/v1/users/:user').put(wrapAsync(put));
9 | api.route('/v1/users').post(wrapAsync(post));
10 | api.route('/v1/users/:user').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Response from 'models/response';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 |
7 | const response = await Response.updateOne(
8 | { _id: data.faq },
9 | { $set: data }
10 | );
11 | res.status(200).json(response);
12 | } catch (error) {
13 | console.error(error);
14 | res.status(500).json({ error: error.message });
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/api/src/routes/agent.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, update, destroy } from '../controllers/v1/agent';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/agents').get(wrapAsync(list));
7 | api.route('/v1/agents/:agent').get(wrapAsync(get));
8 | api.route('/v1/agents/:agent').put(wrapAsync(update));
9 | api.route('/v1/agents').post(wrapAsync(post));
10 | api.route('/v1/agents/:agent').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/widget/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import * as serviceWorker from './serviceWorker';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
8 | // If you want your app to work offline and load faster, you can change
9 | // unregister() to register() below. Note this comes with some pitfalls.
10 | // Learn more about service workers: https://bit.ly/CRA-PWA
11 | serviceWorker.unregister();
12 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/response/update.action.js:
--------------------------------------------------------------------------------
1 | import Response from 'models/response';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 |
8 | const response = await Response.updateOne(
9 | { _id: params.response },
10 | { $set: data }
11 | );
12 |
13 | res.status(200).json(response);
14 | } catch (error) {
15 | console.error(error);
16 | res.status(500).json({ error: error.message });
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/list.action.js:
--------------------------------------------------------------------------------
1 | import User from 'models/user';
2 |
3 | exports.list = async (req, res) => {
4 | try {
5 | const data = req.query;
6 |
7 | const users = await User.apiQuery(data);
8 |
9 | const sanitized = users.map((user) => {
10 | user.password = undefined;
11 | return user;
12 | });
13 |
14 | res.status(200).json(sanitized);
15 | } catch (error) {
16 | console.log(error);
17 | res.status(500).json({ error: error.message });
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/api/src/routes/invite.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, update, destroy } from '../controllers/v1/invite';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/invites').get(wrapAsync(list));
7 | api.route('/v1/invites/:invite').get(wrapAsync(get));
8 | api.route('/v1/invites/:invite').put(wrapAsync(update));
9 | api.route('/v1/invites').post(wrapAsync(post));
10 | api.route('/v1/invites/:invite').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/api/src/routes/response.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, put, destroy } from '../controllers/v1/response';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/responses').get(wrapAsync(list));
7 | api.route('/v1/responses/:response').get(wrapAsync(get));
8 | api.route('/v1/responses/:response').put(wrapAsync(put));
9 | api.route('/v1/responses').post(wrapAsync(post));
10 | api.route('/v1/responses/:response').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useLayoutProvider.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default (LayoutUtil, itemHeight) => {
4 | const [{ width }, onResize] = useState({ width: 0 });
5 | const [layoutProvider, setLayoutProvider] = useState(
6 | LayoutUtil.getLayoutProvider(width, itemHeight)
7 | );
8 | useEffect(() => {
9 | setLayoutProvider(LayoutUtil.getLayoutProvider(width, itemHeight));
10 | }, [width]);
11 | return [layoutProvider, onResize];
12 | };
13 |
--------------------------------------------------------------------------------
/widget/src/screens/Thread/Thread.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | // Views //
5 | import MessageThread from './views/MessageThread';
6 |
7 | // Components //
8 | const Root = styled.div`
9 | flex: 1;
10 | `
11 |
12 | const Thread = ({ match, ...props }) => {
13 | return (
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Thread;
--------------------------------------------------------------------------------
/dashboard/src/index.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import App from './App';
6 | import * as serviceWorker from './serviceWorker';
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 |
10 | // If you want your app to work offline and load faster, you can change
11 | // unregister() to register() below. Note this comes with some pitfalls.
12 | // Learn more about service workers: https://bit.ly/CRA-PWA
13 | serviceWorker.unregister();
14 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/list.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import Agent from 'models/agent';
4 |
5 | exports.list = async (req, res) => {
6 | try {
7 | const data = req.query;
8 |
9 | const agents = await Agent.apiQuery(data);
10 |
11 | const sanitized = agents.map(agent => {
12 | agent.password = undefined;
13 | return agent;
14 | });
15 |
16 | res.status(200).json(sanitized);
17 | } catch (error) {
18 | console.error(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useLiveMoment.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import moment from "moment";
3 |
4 | export default (time = Date.now(), interval = 10000) => {
5 | const [momentValue, setMoment] = useState(moment(time));
6 | const tick = useCallback(() => {
7 | setMoment(moment(time));
8 | }, [time]);
9 |
10 | useEffect(() => {
11 | const timer = setInterval(tick, interval);
12 | return () => clearInterval(timer);
13 | }, [tick, interval]);
14 |
15 | return momentValue;
16 | };
17 |
--------------------------------------------------------------------------------
/dashboard/src/shared/ScreenRoot.js:
--------------------------------------------------------------------------------
1 | import React from "react"; // eslint-disable-line no-unused-vars
2 | import styled from "styled-components";
3 |
4 | // CSS //
5 | import pageCard from "styles/css/pageCard";
6 |
7 | const ScreenRoot = styled.div`
8 | z-index: ${({ theme }) => theme.z.page};
9 |
10 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
11 | position: absolute;
12 | top: 0;
13 | right: 0;
14 | bottom: 0;
15 | left: 96px;
16 | ${pageCard}
17 | }
18 | `;
19 |
20 | export default ScreenRoot;
21 |
--------------------------------------------------------------------------------
/api/src/routes/organization.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, update, destroy } from '../controllers/v1/organization';
2 |
3 | import { wrapAsync } from '../utils/controllers';
4 |
5 | module.exports = api => {
6 | api.route('/v1/organizations').get(wrapAsync(list));
7 | api.route('/v1/organizations/:organization').get(wrapAsync(get));
8 | api.route('/v1/organizations/:organization').put(wrapAsync(update));
9 | api.route('/v1/organizations').post(wrapAsync(post));
10 | api.route('/v1/organizations/:organization').delete(wrapAsync(destroy));
11 | };
12 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/UserSettings/UserProfileForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | const requiredError = 'This is required.';
4 | const emailError = 'Invalid email address.';
5 |
6 | export default Yup.object().shape({
7 | name: Yup.object().shape({
8 | first: Yup.string().required(requiredError),
9 | last: Yup.string().required(requiredError),
10 | }),
11 | email: Yup.string().email(emailError).required(requiredError),
12 | title: Yup.string().required(requiredError),
13 | });
14 |
--------------------------------------------------------------------------------
/api/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | NODE_ENV=development
3 |
4 | APP_BASE=api/
5 |
6 | AUTH_SECRET=YOUR_AUTH_SECRET
7 |
8 | ALGOLIASEARCH_API_KEY=YOUR_ALGOLIA_API_KEY
9 | ALGOLIASEARCH_API_KEY_SEARCH=YOUR_ALGOLIA_SEARCH_API_KEY
10 | ALGOLIASEARCH_APPLICATION_ID=YOUR_ALGOLIA_APP_ID
11 |
12 | MONGODB_URI=YOUR_MONGODB_URI
13 | REDIS_URL=YOUR_REDIS_URL
14 |
15 | STREAM_URL=YOUR_STREAM_URL
16 | CLOUDINARY_URL=YOUR_CLOUDINARY_URL
17 | PAPERTRAIL_API_TOKEN=YOUR_PAPERTRAIL_API_TOKEN
18 |
19 | MAILGUN_API_KEY=YOUR_MAILGUN_API_KEY
20 | MAILGUN_DOMAIN=YOUR_MAILGUN_DOMAIN
--------------------------------------------------------------------------------
/dashboard/src/styles/theme/base.js:
--------------------------------------------------------------------------------
1 | import breakpoints from "./breakpoints";
2 | import * as colorUtils from "./colorUtils";
3 |
4 | export default {
5 | borderRadius: 8,
6 | breakpoints,
7 | colorUtils,
8 | easing: {
9 | accelerate: [0.4, 0.0, 1, 1],
10 | deccelerate: [0.0, 0.0, 0.2, 1],
11 | standard: [0.4, 0.0, 0.2, 1],
12 | css: easing => `cubic-bezier(${easing.join(",")})`
13 | },
14 | gutter: 16,
15 | z: {
16 | page: 0,
17 | sidenav: 1,
18 | modal: 5,
19 | snackbar: 6,
20 | tooltip: 7
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/config/get.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import StreamClient from 'utils/stream';
4 |
5 | exports.get = async (req, res) => {
6 | try {
7 | const { key } = await StreamClient();
8 |
9 | res.status(200).json({
10 | stream: {
11 | key
12 | },
13 | algolia: {
14 | id: process.env.ALGOLIASEARCH_APPLICATION_ID,
15 | key: process.env.ALGOLIASEARCH_API_KEY_SEARCH
16 | }
17 | });
18 | } catch (error) {
19 | console.error(error);
20 | res.status(500).json({ error: error.message });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/widgets/TotalThreadsWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from '@comba.se/ui';
3 |
4 | // Components //
5 | import InfoWidget from 'shared/InfoWidget';
6 |
7 | const ThreadCount = () => {
8 | return (
9 |
10 | 12
11 |
12 | );
13 | };
14 |
15 | const TotalThreadsWidget = () => {
16 | return ;
17 | };
18 |
19 | export default TotalThreadsWidget;
20 |
--------------------------------------------------------------------------------
/api/.env.gitpod:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | NODE_ENV=development
3 |
4 | APP_BASE=api/
5 |
6 | AUTH_SECRET=YOUR_AUTH_SECRET
7 |
8 | ALGOLIASEARCH_API_KEY=YOUR_ALGOLIA_API_KEY
9 | ALGOLIASEARCH_API_KEY_SEARCH=YOUR_ALGOLIA_SEARCH_API_KEY
10 | ALGOLIASEARCH_APPLICATION_ID=YOUR_ALGOLIA_APP_ID
11 |
12 | MONGODB_URI=mongodb://localhost:27017/combase
13 | REDIS_URL=localhost:6379
14 |
15 | STREAM_URL=YOUR_STREAM_URL
16 | CLOUDINARY_URL=YOUR_CLOUDINARY_URL
17 | PAPERTRAIL_API_TOKEN=YOUR_PAPERTRAIL_API_TOKEN
18 |
19 | MAILGUN_API_KEY=YOUR_MAILGUN_API_KEY
20 | MAILGUN_DOMAIN=YOUR_MAILGUN_DOMAIN
21 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import AuthContext from "contexts/Auth";
3 |
4 | export default () => {
5 | const {
6 | user,
7 | organization,
8 | organizations,
9 | loading,
10 | error,
11 | login,
12 | logout,
13 | setCurrentOrganization,
14 | refetchCurrentOrg,
15 | refetchUser,
16 | } = useContext(AuthContext);
17 | return [
18 | { organization, organizations, user },
19 | { loading, error, login, logout, setCurrentOrganization, refetchCurrentOrg, refetchUser }
20 | ];
21 | };
22 |
--------------------------------------------------------------------------------
/dashboard/src/styles/css/baseInputStyle.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | export default css`
4 | color: ${({ theme }) => theme.color.alt_text};
5 | font-size: 14px;
6 | padding: 12px;
7 |
8 | &:-webkit-autofill,
9 | &:-webkit-autofill:hover,
10 | &:-webkit-autofill:focus,
11 | &:-webkit-autofill:active {
12 | -webkit-text-fill-color: ${({ theme }) =>
13 | theme.color.alt_text} !important;
14 | -webkit-box-shadow: 0 0 0 30px ${({ theme }) => theme.color.surface}
15 | inset !important;
16 | }
17 | `;
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/get.action.js:
--------------------------------------------------------------------------------
1 | import Organization from 'models/organization';
2 |
3 | import StreamClient from 'utils/stream';
4 |
5 | exports.get = async (req, res) => {
6 | try {
7 | const data = req.params;
8 |
9 | const { key } = await StreamClient();
10 |
11 | const organization = await Organization.findById(
12 | data.organization
13 | ).lean({ autopopulate: true });
14 |
15 | res.status(200).json({ stream: { key }, ...organization });
16 | } catch (error) {
17 | console.error(error);
18 | res.status(500).json({ error: error.message });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/list.action.js:
--------------------------------------------------------------------------------
1 | import Plugin from 'models/plugin';
2 |
3 | exports.list = async (req, res) => {
4 | try {
5 | const data = req.query;
6 | const { serialized } = req;
7 |
8 | // if (serialized.role !== 'admin') {
9 | // return res.status(403).json({
10 | // status: 'Invalid permissions to view or modify this resource.',
11 | // });
12 | // }
13 |
14 | const plugins = await Plugin.apiQuery(data);
15 | res.status(200).json(plugins);
16 | } catch (error) {
17 | console.log(error);
18 | res.status(500).json({ error: error.message });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/list.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import Chat from 'models/chat';
4 |
5 | exports.list = async (req, res) => {
6 | try {
7 | const { serialized, query: data } = req;
8 |
9 | // if (serialized.role !== 'admin') {
10 | // return res.status(403).json({
11 | // status: 'Invalid permissions to view or modify this resource.',
12 | // });
13 | // }
14 |
15 | const chats = await Chat.apiQuery(data);
16 | res.status(200).json(chats);
17 | } catch (error) {
18 | console.log(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/post.action.js:
--------------------------------------------------------------------------------
1 | import Plugin from 'models/plugin';
2 |
3 | exports.post = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 | const { serialized } = req;
7 |
8 | if (serialized.role !== 'admin') {
9 | return res.status(403).json({
10 | status: 'Invalid permissions to view or modify this resource.',
11 | });
12 | }
13 |
14 | const plugin = await Plugin.create(data);
15 | res.status(200).json(plugin);
16 | } catch (error) {
17 | console.error(error);
18 | res.status(500).json({ error: error.message });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/widget/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import AuthContext from "contexts/Auth";
3 |
4 | export default () => {
5 | const {
6 | isFirstVisit,
7 | user,
8 | organization,
9 | organizations,
10 | loading,
11 | error,
12 | login,
13 | logout,
14 | setCurrentOrganization,
15 | refetchCurrentOrg,
16 | refetchUser,
17 | } = useContext(AuthContext);
18 | return [
19 | { organization, organizations, user },
20 | { loading, error, login, logout, setCurrentOrganization, refetchCurrentOrg, refetchUser, isFirstVisit }
21 | ];
22 | };
23 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Plugin from 'models/plugin';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 | const { serialized } = req;
7 |
8 | if (serialized.role !== 'admin') {
9 | return res.status(403).json({
10 | status: 'Invalid permissions to view or modify this resource.',
11 | });
12 | }
13 |
14 | await Plugin.findByIdAndRemove(data.plugin);
15 |
16 | res.sendStatus(204);
17 | } catch (error) {
18 | console.error(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/dashboard/src/widgets/ChatWidget/ChatWidget.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from "react";
2 | import { FAB } from "@comba.se/ui";
3 | import { ChatIcon } from "@comba.se/ui/Icons";
4 |
5 | // Components //
6 | import Widget from "./Widget";
7 |
8 | const ChatWidget = () => {
9 | const [open, setOpen] = useState(false);
10 | const toggleChatWidget = useCallback(() => {
11 | setOpen(!open);
12 | }, [open]);
13 | return (
14 | <>
15 |
16 |
17 | >
18 | );
19 | };
20 |
21 | export default ChatWidget;
22 |
--------------------------------------------------------------------------------
/widget/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/invite/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Invite from 'models/invite';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 | const { serialized } = req;
7 |
8 | if (serialized.role !== 'admin') {
9 | return res.status(403).json({
10 | status: 'Invalid permissions to view or modify this resource.'
11 | });
12 | }
13 |
14 | const invite = await Invite.findByIdAndRemove(data.invite);
15 |
16 | res.sendStatus(204);
17 | } catch (error) {
18 | console.error(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/invite/list.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import Invite from 'models/invite';
4 |
5 | exports.list = async (req, res) => {
6 | try {
7 | const data = req.query;
8 | const { serialized } = req;
9 |
10 | if (serialized.role !== 'admin') {
11 | return res.status(403).json({
12 | status: 'Invalid permissions to view or modify this resource.',
13 | });
14 | }
15 |
16 | const invites = await Invite.apiQuery(data);
17 |
18 | res.status(200).json(invites);
19 | } catch (error) {
20 | console.error(error);
21 | res.status(500).json({ error: error.message });
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/faq/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Faq from 'models/faq';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 | const { serialized } = req;
7 |
8 | if (serialized.role !== 'admin') {
9 | return res.status(403).json({
10 | status: 'Invalid permissions to view or modify this resource.'
11 | });
12 | }
13 |
14 | const faq = await Faq.updateOne({ _id: data.faq }, { $set: data });
15 |
16 | res.status(200).json(faq);
17 | } catch (error) {
18 | console.error(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Organization from 'models/organization';
2 |
3 | exports.destroy = async (req, res) => {
4 | try {
5 | const data = { ...req.body, ...req.params };
6 | const { serialized } = req;
7 |
8 | if (serialized.role !== 'admin') {
9 | return res.status(403).json({
10 | status: 'Invalid permissions to view or modify this resource.'
11 | });
12 | }
13 |
14 | await Organization.findByIdAndRemove(data.organization);
15 |
16 | res.sendStatus(204);
17 | } catch (error) {
18 | console.error(error);
19 | res.status(500).json({ error: error.message });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/get.action.js:
--------------------------------------------------------------------------------
1 | import Plugin from 'models/plugin';
2 |
3 | exports.get = async (req, res) => {
4 | try {
5 | const data = req.params;
6 | const { serialized } = req;
7 |
8 | // if (serialized.role !== 'admin') {
9 | // return res.status(403).json({
10 | // status: 'Invalid permissions to view or modify this resource.',
11 | // });
12 | // }
13 |
14 | const plugin = await Plugin.findById(data.plugin).lean({
15 | autopopulate: true,
16 | });
17 | res.status(200).json(plugin);
18 | } catch (error) {
19 | console.error(error);
20 | res.status(500).json({ error: error.message });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/InvitationForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | const requiredError = 'This is required.';
4 | const emailError = 'Invalid Email Address.';
5 |
6 |
7 | export default Yup.object().shape({
8 | invitations: Yup.array().of(
9 | Yup.object().shape({
10 | email: Yup.string()
11 | .email(emailError)
12 | .required(requiredError),
13 | name: Yup.object().shape({
14 | first: Yup.string().required(requiredError),
15 | last: Yup.string().required(requiredError),
16 | })
17 | })
18 | ),
19 | })
20 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/update.action.js:
--------------------------------------------------------------------------------
1 | import Plugin from 'models/plugin';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 | const { serialized } = req;
8 |
9 | if (serialized.role !== 'admin') {
10 | return res.status(403).json({
11 | status: 'Invalid permissions to view or modify this resource.',
12 | });
13 | }
14 |
15 | const plugin = await Plugin.updateOne(
16 | { _id: params.plugin },
17 | { $set: data }
18 | );
19 | res.status(200).json(plugin);
20 | } catch (error) {
21 | console.error(error);
22 | res.status(500).json({ error: error.message });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/dashboard/src/shared/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import { withTheme } from 'styled-components';
3 | import { Icon } from '@comba.se/ui';
4 |
5 | const Logo = ({ theme, ...props }) => (
6 |
7 |
8 |
17 |
18 |
19 | );
20 |
21 | export default withTheme(Logo);
22 |
--------------------------------------------------------------------------------
/api/src/utils/db/index.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import mongoose from 'mongoose';
3 |
4 | const db = mongoose.connection;
5 |
6 | mongoose.promise = global.Promise;
7 | mongoose.connect(process.env.MONGODB_URI, {
8 | useNewUrlParser: true,
9 | useUnifiedTopology: true,
10 | });
11 | mongoose.set('useCreateIndex', true);
12 | mongoose.set('useFindAndModify', false);
13 |
14 | db.on('error', err => {
15 | console.error(err);
16 | });
17 |
18 | db.on('disconnected', () => {
19 | console.info('Database disconnected!');
20 | });
21 |
22 | process.on('SIGINT', () => {
23 | mongoose.connection.close(() => {
24 | process.exit(0);
25 | });
26 | });
27 |
28 | export default db;
29 |
--------------------------------------------------------------------------------
/dashboard/src/shared/Modal/Undersheet.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import styled from 'styled-components';
3 | import Animated from 'animated/lib/targets/react-dom';
4 |
5 | const Undersheet = styled(Animated.div)`
6 | position: fixed;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 | background-color: ${({ show, theme }) =>
12 | show ? theme.color.undersheet : 'transparent'};
13 | ${''}
14 | z-index: ${({ theme }) => theme.z.modal};
15 | visibility: ${({ open }) => (open ? 'visible' : 'hidden')};
16 | ${({ show }) => (show ? 'backdrop-filter: blur(5px);' : null)}
17 | `;
18 |
19 | export default Undersheet;
20 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/update.action.js:
--------------------------------------------------------------------------------
1 | import Agent from 'models/agent';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 | const { serialized } = req;
8 |
9 | // if (serialized.role !== 'admin') {
10 | // return res.status(403).json({
11 | // status: 'Invalid permissions to view or modify this resource.'
12 | // });
13 | // }
14 |
15 | const agent = await Agent.findOneAndUpdate({ _id: params.agent }, { $set: data }, { new: true });
16 |
17 | res.status(200).json(agent);
18 | } catch (error) {
19 | console.error(error);
20 | res.status(500).json({ error: error.message });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useAgent.js:
--------------------------------------------------------------------------------
1 | import { useContext, useMemo } from 'react';
2 |
3 | // Hooks //
4 | import usePrevious from 'hooks/usePrevious';
5 |
6 | // Contexts //
7 | import AgentsContext from 'contexts/Agents';
8 |
9 | export default agentId => {
10 | const [agents, tabs, { refetchAgents }] = useContext(AgentsContext); // eslint-disable-line no-unused-vars
11 | const prevAgentId = usePrevious(agentId);
12 | const agent = useMemo(() => {
13 | if (!agentId && !prevAgentId) {
14 | return null;
15 | }
16 | return agents.find(({ _id }) => (agentId || prevAgentId) === _id);
17 | }, [agents, agentId, prevAgentId]);
18 | return [agent, { refetchAgents }];
19 | };
20 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/destroy.action.js:
--------------------------------------------------------------------------------
1 | import User from 'models/user';
2 | import Chat from 'models/chat';
3 |
4 | exports.destroy = async (req, res) => {
5 | try {
6 | const data = { ...req.body, ...req.params };
7 | const { serialized } = req;
8 |
9 | if (serialized.role !== 'admin') {
10 | return res.status(403).json({
11 | status: 'Invalid permissions to view or modify this resource.'
12 | });
13 | }
14 |
15 | await User.findByIdAndRemove(data.user);
16 | await Chat.remove({
17 | 'refs.organization': data.organization
18 | });
19 |
20 | res.sendStatus(204);
21 | } catch (error) {
22 | console.error(error);
23 | res.status(500).json({ error: error.message });
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/organization/update.action.js:
--------------------------------------------------------------------------------
1 | import Organization from 'models/organization';
2 |
3 | exports.update = async (req, res) => {
4 | try {
5 | const data = req.body;
6 | const params = req.params;
7 | const { serialized } = req;
8 |
9 | // if (serialized.role !== 'admin') {
10 | // return res.status(403).json({
11 | // status: 'Invalid permissions to view or modify this resource.'
12 | // });
13 | // }
14 |
15 | const organization = await Organization.findOneAndUpdate({ _id: params.organization }, { $set: data }, { new: true });
16 |
17 | res.status(200).json(organization);
18 | } catch (error) {
19 | console.error(error);
20 | res.status(500).json({ error: error.message });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Switch } from "react-router-dom";
3 |
4 | // Data //
5 | import routes from "./routes";
6 |
7 | // Hooks //
8 | import useNotifications from 'hooks/useNotifications';
9 |
10 | // HOCs //
11 | import withShell from "hocs/withShell";
12 |
13 | const renderRoutes = match =>
14 | routes.map(({ slug, component, isExact }, key) => (
15 |
16 | ));
17 |
18 | const Dashboard = ({ match }) => {
19 | useNotifications(); // Triggers Notification Sounds & Snackbars.
20 | return {renderRoutes(match)};
21 | };
22 |
23 | export default withShell(Dashboard, routes);
24 |
--------------------------------------------------------------------------------
/widget/src/Root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import compose from 'lodash.flowright';
3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
4 |
5 | // HOCs //
6 | import withAuth from 'hocs/withAuth';
7 | import withChannels from '@comba.se/chat/hocs/withChannels';
8 |
9 | // Screens //
10 | import Home from 'screens/Home';
11 | import Thread from 'screens/Thread';
12 |
13 | const Root = () => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default compose(withAuth, withChannels)(Root);
--------------------------------------------------------------------------------
/dashboard/src/utils/upload.js:
--------------------------------------------------------------------------------
1 | const parse = response => response.json();
2 | export default async (file, token) => {
3 | console.log(file);
4 | const body = new FormData();
5 | body.append("file", file);
6 |
7 | try {
8 | const response = await fetch(
9 | `${process.env.REACT_APP_API_ENDPOINT}v1/uploads`,
10 | {
11 | method: "POST",
12 | headers: {
13 | Authorization: `Bearer ${token ||
14 | process.env.REACT_APP_API_KEY}`,
15 | },
16 | body,
17 | }
18 | ).then(parse);
19 | return response;
20 | } catch (error) {
21 | throw new Error(error);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/widget/src/utils/request.js:
--------------------------------------------------------------------------------
1 | const parseJSON = response => response.json();
2 | export default async (path, method = 'get', options = {}, token) => {
3 | try {
4 | const response = await fetch(
5 | `${process.env.REACT_APP_API_ENDPOINT}${path}`,
6 | {
7 | method: method.toUpperCase(),
8 | headers: {
9 | Authorization: `Bearer ${token ||
10 | process.env.REACT_APP_API_KEY}`,
11 | 'Content-Type': 'application/json',
12 | },
13 | ...options,
14 | }
15 | ).then(parseJSON);
16 | return response;
17 | } catch (error) {
18 | throw new Error(error);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/user/post.action.js:
--------------------------------------------------------------------------------
1 | import { StreamChat } from 'stream-chat';
2 |
3 | import User from 'models/user';
4 | import StreamClient from 'utils/stream';
5 |
6 | exports.post = async (req, res) => {
7 | try {
8 | const data = req.body;
9 |
10 | const user = await User.findOneOrCreate({ email: data.email }, data);
11 |
12 | const { key, secret } = await StreamClient();
13 | const client = new StreamChat(key, secret);
14 |
15 | await client.updateUser({
16 | id: user._id.toString(),
17 | name: `${user.name.first} ${user.name.last}`,
18 | role: 'user'
19 | });
20 |
21 | res.status(200).json(user);
22 | } catch (error) {
23 | console.error(error);
24 | res.status(500).json({ error: error.message });
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/dashboard/src/utils/request.js:
--------------------------------------------------------------------------------
1 | const parseJSON = response => response.json();
2 | export default async (path, method = 'get', options = {}, token) => {
3 | try {
4 | const response = await fetch(
5 | `${process.env.REACT_APP_API_ENDPOINT}${path}`,
6 | {
7 | method: method.toUpperCase(),
8 | headers: {
9 | Authorization: `Bearer ${token ||
10 | process.env.REACT_APP_API_KEY}`,
11 | 'Content-Type': 'application/json',
12 | },
13 | ...options,
14 | }
15 | ).then(parseJSON);
16 | return response;
17 | } catch (error) {
18 | throw new Error(error);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/dashboard/src/widgets/Combase/ChatDataWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import moment from 'moment';
5 | import { ListItem } from '@comba.se/ui';
6 |
7 | // Components //
8 | const Root = styled.div`
9 |
10 | `
11 |
12 | const ChatDataWidget = ({ createdAt, partnerId }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | )
19 | };
20 |
21 | ChatDataWidget.propTypes = {
22 | createdAt: PropTypes.string,
23 | partnerId: PropTypes.string,
24 | }
25 |
26 | export default ChatDataWidget;
--------------------------------------------------------------------------------
/widget/src/components/CardHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Text } from "@comba.se/ui";
5 |
6 | // Components //
7 | const Root = styled.div`
8 | flex-direction: row;
9 | align-items: center;
10 | padding: 20px 24px 12px 24px;
11 |
12 | & ${Text} {
13 | margin-left: 8px;
14 | }
15 | `;
16 |
17 | const CardHeader = ({ icon: Icon, title }) => {
18 | return (
19 |
20 |
21 | {title}
22 |
23 | );
24 | };
25 |
26 | CardHeader.propTypes = {
27 | icon: PropTypes.func,
28 | text: PropTypes.string,
29 | }
30 |
31 | export default CardHeader;
--------------------------------------------------------------------------------
/dashboard/src/components/OrgProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import { LoadingState } from "@comba.se/ui";
4 | import useAuth from 'hooks/useAuth';
5 |
6 | export default ({ component: Component, ...rest }) => {
7 | const [{ organization }, { loading }] = useAuth();
8 | return (
9 | {
12 | if (loading) {
13 | return ;
14 | } else if (organization) {
15 | return ;
16 | }
17 | return ;
18 | }}
19 | />
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/api/src/routes/plugin.js:
--------------------------------------------------------------------------------
1 | import { get, list, post, update, destroy } from '../controllers/v1/plugin';
2 | import { blazeVerifyExecVerify, clearbitExecEnrich } from '../controllers/v1/plugin/integration';
3 |
4 | import { wrapAsync } from '../utils/controllers';
5 |
6 | module.exports = (api) => {
7 | api.route('/v1/plugins').get(wrapAsync(list));
8 | api.route('/v1/plugins/:plugin').get(wrapAsync(get));
9 | api.route('/v1/plugins/:plugin').put(wrapAsync(update));
10 | api.route('/v1/plugins').post(wrapAsync(post));
11 | api.route('/v1/plugins/:plugin').delete(wrapAsync(destroy));
12 |
13 | // integrations
14 | api.route('/v1/plugins/blaze_verify/verify').post(wrapAsync(blazeVerifyExecVerify));
15 | api.route('/v1/plugins/clearbit/enrich').post(wrapAsync(clearbitExecEnrich));
16 | };
17 |
--------------------------------------------------------------------------------
/dashboard/src/shared/SectionTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Text } from '@comba.se/ui';
4 |
5 | // Components //
6 | const Root = styled.div`
7 | flex-direction: row;
8 | padding-bottom: 8px;
9 | padding-left: 8px;
10 | padding-right: 8px;
11 | border-bottom: 1px solid ${({ theme }) => theme.color.border};
12 |
13 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px){
14 | padding-left: 16px;
15 | padding-right: 16px;
16 | }
17 | `;
18 |
19 | const SectionTitle = ({ className, title }) => (
20 |
21 |
22 | {title}
23 |
24 |
25 | );
26 |
27 | export default SectionTitle;
28 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/forms/SignUpForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | const requiredError = 'This is required.';
4 | const emailError = 'Invalid Email Address.';
5 |
6 | export default Yup.object().shape({
7 | name: Yup.object().shape({
8 | first: Yup.string().required(requiredError),
9 | last: Yup.string().required(requiredError),
10 | }),
11 | email: Yup.string()
12 | .email(emailError)
13 | .required(requiredError),
14 | password: Yup.string().required(requiredError),
15 | confirm: Yup.string().when('password', {
16 | is: val => val && val.length > 0,
17 | then: Yup.string()
18 | .oneOf([Yup.ref('password')], 'Both passwords need to be the same')
19 | .required(requiredError),
20 | }),
21 | });
22 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/Settings.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route } from "react-router-dom";
3 |
4 | // Views //
5 | import SettingsDetail from "./views/SettingsDetail";
6 | import SettingsList from "./views/SettingsList";
7 |
8 | // Components //
9 | import ListDetailView from "components/ListDetailView";
10 | import ScreenRoot from "shared/ScreenRoot";
11 |
12 | const renderSettingsDetail = props => ;
13 | const renderSettingsList = props => ;
14 |
15 | export default props => (
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useConfig.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import request from 'utils/request';
3 |
4 | export default () => {
5 | const [config, setConfig] = useState({});
6 | const [loading, setLoading] = useState(false);
7 | const [error, setError] = useState(null);
8 |
9 | const getConfig = useCallback(async () => {
10 | try {
11 | setLoading(true);
12 | const data = await request('v1/configs');
13 | setConfig(data);
14 | setLoading(false);
15 | } catch (error) {
16 | setLoading(false);
17 | setError(error);
18 | }
19 | }, []);
20 |
21 | useEffect(() => {
22 | getConfig();
23 | }, [getConfig]);
24 |
25 | return [config, { loading, error }];
26 | };
27 |
--------------------------------------------------------------------------------
/widget/src/components/Home/KnowledgeBaseWidget/KnowledgeBaseWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Card, Input } from "@comba.se/ui";
4 | import { KnowledgeBaseIcon } from "@comba.se/ui/Icons";
5 |
6 | // Components //
7 | import CardHeader from 'components/CardHeader';
8 |
9 | const Root = styled(Card)`
10 | width: 100%;
11 | `;
12 |
13 | const List = styled.div`
14 | padding: 16px 24px;
15 | `
16 |
17 | const KnowledgeBaseWidget = ({ className }) => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default KnowledgeBaseWidget;
--------------------------------------------------------------------------------
/dashboard/src/components/AvailabilityField/Availability.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | // Components //
5 | import AvailabilityDay from './AvailabilityDay';
6 |
7 | const Root = styled.div`
8 | flex-direction: row;
9 | align-items: center;
10 | justify-content: space-between;
11 |
12 | @media (min-width: ${ ({ theme }) => theme.breakpoints.sm}px) {
13 | padding: 0px 16px;
14 | }
15 | `;
16 |
17 | const renderDays = (days, onChange) => Object.entries(days).map(([day, data], key) => ());
18 |
19 | const Availability = ({ value, onChange }) => {
20 | return (
21 |
22 | {renderDays(value, onChange)}
23 |
24 | );
25 | };
26 |
27 | export default Availability;
--------------------------------------------------------------------------------
/dashboard/src/components/AuthedRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Redirect } from "react-router-dom";
3 | import { LoadingState } from "@comba.se/ui";
4 | import useAuth from "hooks/useAuth";
5 |
6 | export default ({ component: Component, ...rest }) => {
7 | const [{ user }, { loading }] = useAuth();
8 | return (
9 | {
12 | if (loading) {
13 | return ;
14 | } else if (!!user) {
15 | return ;
16 | }
17 | return (
18 |
25 | );
26 | }}
27 | />
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useLayout.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import ResizeObserver from 'utils/ResizeObserver';
3 |
4 | export default () => {
5 | const [layout, setLayout] = useState(null);
6 | const [el, setEl] = useState(null);
7 |
8 | const setRef = useCallback(el => {
9 | console.log('setRef');
10 | setEl(el);
11 | }, []);
12 |
13 | const handleResize = useCallback(entries => {
14 | console.log(entries);
15 | const [entry] = entries;
16 | setLayout(entry.contentRect);
17 | }, []);
18 |
19 | useEffect(() => {
20 | const observer = new ResizeObserver(handleResize);
21 | if (el) {
22 | observer.observe(el);
23 | return observer.disconnect();
24 | }
25 | }, [el]);
26 |
27 | return [layout, setRef];
28 | };
29 |
--------------------------------------------------------------------------------
/api/src/models/tag.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import query from 'mongoose-string-query';
3 | import timestamps from 'mongoose-timestamp';
4 | import autopopulate from 'mongoose-autopopulate';
5 |
6 | export const TagSchema = new Schema(
7 | {
8 | name: {
9 | type: String,
10 | trim: true,
11 | required: true,
12 | },
13 | colors: {
14 | primary: {
15 | type: String,
16 | trim: true,
17 | default: '#4D7CFE',
18 | },
19 | secondary: {
20 | type: String,
21 | trim: true,
22 | default: '#ffffff',
23 | },
24 | },
25 | },
26 | {
27 | collection: 'tags',
28 | }
29 | );
30 |
31 | TagSchema.plugin(timestamps);
32 | TagSchema.plugin(query);
33 | TagSchema.plugin(autopopulate);
34 |
35 | TagSchema.index({ createdAt: 1, updatedAt: 1 });
36 |
37 | module.exports = exports = mongoose.model('Tag', TagSchema);
38 |
--------------------------------------------------------------------------------
/dashboard/src/components/UnauthedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import { LoadingState } from "@comba.se/ui";
4 | import useAuth from 'hooks/useAuth';
5 |
6 | export default ({ component: Component, ...rest }) => {
7 | const [{ organization, user }, { loading }] = useAuth();
8 | return (
9 | {
12 | if (loading) {
13 | return ;
14 | } else if (!organization) {
15 | return ;
16 | } else if (!!user) {
17 | return ;
18 | }
19 | return ;
20 | }}
21 | />
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/api/src/utils/stream/index.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { StreamChat } from 'stream-chat';
3 |
4 | export default async () => {
5 | try {
6 | let apiKey;
7 | let apiSecret;
8 |
9 | // if this env is found, it's assumed that the api is running on heroku
10 | if (process.env.STREAM_URL) {
11 | // extract the key and secret from the environment variable
12 | [apiKey, apiSecret] = process.env.STREAM_URL.substr(8)
13 | .split('@')[0]
14 | .split(':');
15 | } else {
16 | // api key and secret were provided from a .env file
17 | apiKey = process.env.STREAM_API_KEY;
18 | apiSecret = process.env.STREAM_API_SECRET;
19 | }
20 |
21 | const client = {
22 | key: apiKey,
23 | secret: apiSecret,
24 | client: new StreamChat(apiKey, apiSecret),
25 | };
26 |
27 | return client;
28 | } catch (error) {
29 | return new Error(Error);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/views/Invite.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Container, Text } from '@comba.se/ui';
4 |
5 | // Forms //
6 | import InvitationForm from '../forms/InvitationForm';
7 |
8 | // Components //
9 | const Root = styled(Container)`
10 | flex: 1;
11 | justify-content: center;
12 | `;
13 |
14 | const Header = styled.div`
15 | padding: 32px 0px;
16 | justify-content: center;
17 | align-items: center;
18 | text-align: center;
19 | `;
20 |
21 | const Invite = () => {
22 | return (
23 |
24 |
25 |
26 | Invite your Team
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Invite;
35 |
--------------------------------------------------------------------------------
/dashboard/src/shared/InfoWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Text } from '@comba.se/ui';
5 |
6 | // Components //
7 | const Root = styled.div`
8 | flex: 1;
9 | justify-content: flex-start;
10 | `;
11 |
12 | const Label = styled(Text)`
13 | margin-top: 8px;
14 | text-transform: uppercase;
15 | letter-spacing: 1px;
16 | `;
17 |
18 | const InfoWidget = ({ label, widget: Widget }) => {
19 | return (
20 |
21 |
22 |
25 |
26 | );
27 | };
28 |
29 | InfoWidget.propTypes = {
30 | widget: PropTypes.any.isRequired,
31 | label: PropTypes.string.isRequired,
32 | };
33 |
34 | export default InfoWidget;
35 |
--------------------------------------------------------------------------------
/api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["babel"],
3 | "extends": ["eslint:recommended"],
4 | "rules": {
5 | "no-console": 0,
6 | "no-mixed-spaces-and-tabs": 1,
7 | "comma-dangle": 0,
8 | "no-unused-vars": 1,
9 | "eqeqeq": [2, "smart"],
10 | "no-useless-concat": 2,
11 | "default-case": 2,
12 | "no-self-compare": 2,
13 | "prefer-const": 2,
14 | "no-underscore-dangle": 0,
15 | "object-shorthand": 1,
16 | "babel/no-invalid-this": 2,
17 | "array-callback-return": 2,
18 | "valid-typeof": 2,
19 | "arrow-body-style": 2,
20 | "require-await": 2,
21 | "react/prop-types": 0,
22 | "no-var": 2,
23 | "linebreak-style": [2, "unix"],
24 | "semi": [1, "always"]
25 | },
26 | "env": {
27 | "node": true
28 | },
29 | "parser": "babel-eslint",
30 | "parserOptions": {
31 | "sourceType": "module",
32 | "ecmaVersion": 2018,
33 | "ecmaFeatures": {
34 | "modules": true
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/Threads/Threads.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { ThreadList } from "@comba.se/chat";
4 | import useChats from 'hooks/useChats';
5 |
6 | // Components //
7 | import MenuButton from 'shared/MenuButton';
8 |
9 | const Root = styled.div`
10 | flex: 1;
11 | order: -1;
12 | height: 100%;
13 | background-color: ${({ theme }) => theme.color.background};
14 |
15 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
16 | width: 375px;
17 | position: fixed;
18 | top: 0;
19 | left: 96px;
20 | right: 0;
21 | bottom: 0;
22 | }
23 | `;
24 |
25 | const Threads = () => {
26 | const [chats, { error, loading }] = useChats();
27 | return
28 |
29 |
30 | }
31 |
32 | export default Threads;
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/routes.js:
--------------------------------------------------------------------------------
1 | import { AgentsIcon, InboxIcon, PluginsIcon, SettingsIcon } from "@comba.se/ui/Icons";
2 |
3 | import Agents from "./views/Agents";
4 | import Inbox from "./views/Inbox";
5 | import Settings from "./views/Settings";
6 | import Plugins from './views/Plugins';
7 |
8 | export default [
9 | {
10 | label: "Inbox",
11 | slug: "inbox",
12 | component: Inbox,
13 | isExact: false,
14 | icon: InboxIcon
15 | },
16 | {
17 | label: "Agents",
18 | slug: "agents",
19 | component: Agents,
20 | isExact: false,
21 | icon: AgentsIcon
22 | },
23 | {
24 | label: "Plugins",
25 | slug: "plugins",
26 | component: Plugins,
27 | isExact: false,
28 | icon: PluginsIcon
29 | },
30 | {
31 | label: "Settings",
32 | slug: "settings",
33 | component: Settings,
34 | isExact: false,
35 | icon: SettingsIcon
36 | },
37 | ];
38 |
--------------------------------------------------------------------------------
/dashboard/src/components/Shell/Sidenav/SidenavItem.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import styled from 'styled-components';
3 | import { Link, Route } from 'react-router-dom';
4 |
5 | // Components //
6 | const Root = styled.div`
7 | justify-content: center;
8 | align-items: center;
9 | padding: 24px 0px;
10 | `;
11 |
12 | const NavLink = styled(Link)`
13 | pointer-events: ${({ active }) => (active ? 'none' : 'auto')};
14 | `;
15 |
16 | export default memo(({ icon: Icon, isExact, path }) => {
17 | return (
18 | (
21 |
22 |
23 |
24 |
25 |
26 | )}
27 | />
28 | );
29 | });
30 |
--------------------------------------------------------------------------------
/dashboard/src/styles/css/listItemInteractions.js:
--------------------------------------------------------------------------------
1 | import { css } from "styled-components";
2 |
3 | /*
4 | * Used for hover & active states of a list item component
5 | * Falls back to the theme's primary color
6 | * Optionally pass an 'activeColor' prop with the key name of a color from your theme to customize
7 | */
8 |
9 | export default css`
10 | background-color: ${({ active, activeColor = "text", theme }) =>
11 | theme.colorUtils.fade(theme.color[activeColor], active ? 0.04 : 0)};
12 | transition: 0.24s background-color
13 | ${({ theme }) => theme.easing.css(theme.easing.standard)};
14 |
15 | &:hover {
16 | background-color: ${({ activeColor = "text", theme }) =>
17 | theme.colorUtils.fade(theme.color[activeColor], 0.04)};
18 | }
19 |
20 | &:active {
21 | background-color: ${({ activeColor = "text", theme }) =>
22 | theme.colorUtils.fade(theme.color[activeColor], 0.08)};
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useMedia.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react'; // eslint-disable-line no-unused-vars
2 | import { ThemeContext } from 'styled-components';
3 |
4 | export default (breakpoint, minmax = 'min') => {
5 | const [matches, setMatch] = useState(false);
6 |
7 | if (!breakpoint) {
8 | throw new Error('Must provide a breakpoint');
9 | }
10 |
11 | const theme = useContext(ThemeContext);
12 |
13 | const query = window.matchMedia(
14 | `(${minmax}-width: ${theme.breakpoints[breakpoint] -
15 | (minmax === 'max' ? 1 : 0)}px)`
16 | );
17 |
18 | useEffect(() => setMatch(!query.matches), [query.matches]);
19 |
20 | const handleChange = ({ matches: isMatched }) => setMatch(!isMatched);
21 |
22 | useEffect(() => {
23 | query.onchange = handleChange;
24 | return query.removeListener(handleChange);
25 | }, [query]);
26 |
27 | return matches;
28 | };
29 |
--------------------------------------------------------------------------------
/dashboard/src/shared/SearchInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { SearchIcon } from "@comba.se/ui/Icons";
4 |
5 | // Components //
6 | const Root = styled.div`
7 | border-radius: ${({ theme }) => theme.borderRadius * 1.5}px;
8 | background-color: ${({ theme }) => theme.color.border};
9 | height: 40px;
10 | flex-direction: row;
11 | overflow: hidden;
12 | `;
13 |
14 | const IconWrapper = styled.div`
15 | padding: 0px 12px;
16 | justify-content: center;
17 | align-items: center;
18 | `;
19 |
20 | const Input = styled.input`
21 | flex: 1;
22 | padding-right: 12px;
23 | `;
24 |
25 | const SearchInput = () => {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default SearchInput;
--------------------------------------------------------------------------------
/widget/src/contexts/ScrollAnimation/Provider.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo } from 'react';
2 | import { useSpring } from 'react-spring';
3 | import { window } from 'browser-monads';
4 | import Context from './index';
5 |
6 | export default ({ children, target = window }) => {
7 | const [anim, set] = useSpring(() => ({ value: 0, config: { mass: 1, tension: 500, friction: 30 } }));
8 | const onScroll = useCallback(({ target: eventTarget }) => set({ value: eventTarget.scrollTop }), [set])
9 |
10 | useEffect(() => {
11 | if (!!target) {
12 | target.addEventListener('scroll', onScroll);
13 | return () => target.removeEventListener('scroll', onScroll);
14 | }
15 | }, [onScroll, target]);
16 |
17 | const value = useMemo(() => ({
18 | anim,
19 | }), [anim]);
20 |
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | }
--------------------------------------------------------------------------------
/dashboard/src/shared/MenuButton.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { IconButton } from '@comba.se/ui';
5 | import { MenuIcon } from "@comba.se/ui/Icons";
6 |
7 | // Contexts //
8 | import ShellContext from 'contexts/Shell';
9 |
10 | // Components //
11 | const Root = styled(IconButton)`
12 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
13 | display: none;
14 | }
15 | `;
16 |
17 | const MenuButton = props => {
18 | const { drawer } = useContext(ShellContext);
19 | return (
20 |
27 | );
28 | };
29 |
30 | MenuButton.propTypes = {
31 | color: PropTypes.string,
32 | };
33 |
34 | MenuButton.defaultProps = {
35 | color: 'text',
36 | };
37 |
38 | export default MenuButton;
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/api/src/utils/auth/whitelist.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: 'agents',
4 | method: 'GET',
5 | auth: true,
6 | },
7 | {
8 | path: 'agents',
9 | method: 'POST',
10 | auth: true,
11 | },
12 | {
13 | path: 'auth',
14 | method: 'POST',
15 | auth: true,
16 | },
17 | {
18 | path: 'chats',
19 | method: 'POST',
20 | auth: true,
21 | },
22 | {
23 | path: 'configs',
24 | method: 'GET',
25 | auth: true,
26 | },
27 | {
28 | path: 'invites',
29 | method: 'GET',
30 | auth: true,
31 | },
32 | {
33 | path: 'invites',
34 | method: 'PUT',
35 | auth: true,
36 | },
37 | {
38 | path: 'password-reset',
39 | method: 'POST',
40 | auth: true,
41 | },
42 | {
43 | path: 'organizations',
44 | method: 'POST',
45 | auth: true,
46 | },
47 | {
48 | path: 'organizations',
49 | method: 'GET',
50 | auth: true,
51 | },
52 | {
53 | path: 'users',
54 | method: 'GET',
55 | auth: true,
56 | },
57 | {
58 | path: 'users',
59 | method: 'POST',
60 | auth: true,
61 | },
62 | ];
63 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/views/SideDrawer/SideDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Switch, Route } from "react-router-dom";
4 |
5 | // Views //
6 | import InfoDrawer from "./views/InfoDrawer";
7 |
8 | // Components //
9 | const Root = styled.div`
10 | position: absolute;
11 | top: 0;
12 | right: 0;
13 | bottom: 0;
14 | width: 376px;
15 | z-index: 3;
16 | background-color: ${({ theme }) => theme.color.surface};
17 | border-left: 1px solid ${({ theme }) => theme.color.border};
18 | transform: translateX(${({ open }) => (open ? 0 : 100)}%);
19 | `;
20 |
21 | const SideDrawer = ({ match, open, partner }) => (
22 |
23 |
24 | } />
25 | "transfer"} />
26 |
27 |
28 | );
29 |
30 | export default SideDrawer;
31 |
--------------------------------------------------------------------------------
/api/src/index.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import express from 'express';
5 | import bodyParser from 'body-parser';
6 | import cors from 'cors';
7 | import helmet from 'helmet';
8 | import compression from 'compression';
9 |
10 | import 'utils/db';
11 | import auth from 'utils/auth';
12 |
13 | const api = express();
14 |
15 | api.use(cors());
16 | api.use(compression());
17 | api.use(helmet());
18 | api.use(bodyParser.urlencoded({ extended: true }));
19 | api.use(bodyParser.json());
20 | //api.use(auth);
21 |
22 | api.listen(process.env.PORT, (err) => {
23 | if (err) {
24 | console.warn('Database connection error.', new Error(err));
25 | process.exit(1);
26 | }
27 |
28 | // eslint-disable-next-line array-callback-return
29 | fs.readdirSync(path.join(__dirname, 'routes')).map((file) => {
30 | require('./routes/' + file)(api);
31 | });
32 |
33 | console.info(`API is now running on port ${process.env.PORT} in ${process.env.NODE_ENV} mode. 👨🚀`);
34 | });
35 |
36 | module.exports = api;
37 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/forms/OrganizationSettings/OrganizationProfileForm/validationSchema.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | const requiredError = 'This is required.';
4 | const emailError = 'Invalid email address.';
5 |
6 | export default Yup.object().shape({
7 | name: Yup.string().required(requiredError),
8 | meta: Yup.object().shape({
9 | tagline: Yup.string().required(requiredError),
10 | branding: Yup.object().shape({
11 | logo: Yup.string(),
12 | color: Yup.object().shape({
13 | primary: Yup.string(),
14 | })
15 | }),
16 | }),
17 | phone: Yup.object().shape({
18 | display: Yup.bool(),
19 | number: Yup.string().required(requiredError)
20 | }),
21 | website: Yup.object().shape({
22 | display: Yup.bool(),
23 | url: Yup.string().required(requiredError)
24 | }),
25 | email: Yup.object().shape({
26 | display: Yup.bool(),
27 | address: Yup.string().email(emailError).required(requiredError)
28 | }),
29 | });
30 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/UserSettings/UserSettings.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Container, ListHeader } from '@comba.se/ui';
4 | import { UserSettingsIcon } from "@comba.se/ui/Icons";
5 |
6 | // Hooks //
7 | import useMedia from 'hooks/useMedia';
8 |
9 | // Forms //
10 | import { UserAvailabilityForm, UserProfileForm } from 'screens/Dashboard/forms/UserSettings';
11 |
12 | // Components //
13 | import MenuButton from 'shared/MenuButton';
14 |
15 | const Root = styled.div`
16 | flex: 1;
17 | `;
18 |
19 | const UserSettings = () => {
20 | const isMobile = useMedia('sm');
21 | return (
22 |
23 |
24 | {!isMobile ? : null}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default UserSettings;
33 |
--------------------------------------------------------------------------------
/api/docs/API.md:
--------------------------------------------------------------------------------
1 | # Combase API
2 | API for https://comba.se
3 |
4 | ## Version: 1.0.0
5 |
6 |
7 | ### DELETE /v1/agents/:id
8 |
9 | #### DELETE
10 | ##### Description:
11 |
12 | Deletes an agent
13 |
14 | ##### Parameters
15 |
16 | | Name | Located in | Description | Required | Schema |
17 | | ---- | ---------- | ----------- | -------- | ---- |
18 | | ID | controllers/v1/agent/destroy.action.js | UUID of agent to delete | Yes | string (uuid) |
19 |
20 | ##### Responses
21 |
22 | | Code | Description |
23 | | ---- | ----------- |
24 | | 204 | No response |
25 |
26 | ### GET /v1/agents:id
27 |
28 | #### GET
29 | ##### Description:
30 |
31 | Get a specific agent
32 |
33 | ##### Parameters
34 |
35 | | Name | Located in | Description | Required | Schema |
36 | | ---- | ---------- | ----------- | -------- | ---- |
37 | | ID | controllers/v1/agent/get.action.js | UUID of the agent to retrieve | Yes | string (uuid) |
38 |
39 | ##### Responses
40 |
41 | | Code | Description | Schema |
42 | | ---- | ----------- | ------ |
43 | | 200 | JSON representation of the agent | object |
44 |
45 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/usePluginEndpoint.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import useAuth from "hooks/useAuth";
3 | import request from "utils/request";
4 |
5 | export default (plugin, endpoint, reqBody) => {
6 | const [{ user }] = useAuth();
7 | const [data, setData] = useState({});
8 | const [loading, setLoading] = useState(true);
9 | const [error, setError] = useState(false);
10 |
11 | const getData = useCallback(async () => {
12 | try {
13 | setLoading(true);
14 | const enriched = await request(
15 | `v1/plugins/${plugin}/${endpoint}`,
16 | "post",
17 | {
18 | body: JSON.stringify(reqBody)
19 | },
20 | user.tokens.api
21 | );
22 | setData(enriched);
23 | setLoading(false);
24 | } catch (error) {
25 | setLoading(false);
26 | setError(true);
27 | }
28 | }, [endpoint, plugin, reqBody, user.tokens.api]);
29 |
30 | useEffect(() => {
31 | getData();
32 | }, [getData]);
33 |
34 | return [data, { loading, error, refetch: getData }];
35 | };
36 |
--------------------------------------------------------------------------------
/api/src/models/response.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import query from 'mongoose-string-query';
3 | import timestamps from 'mongoose-timestamp';
4 | import autopopulate from 'mongoose-autopopulate';
5 |
6 | export const ResponseSchema = new Schema(
7 | {
8 | title: {
9 | type: String,
10 | trim: true,
11 | required: true,
12 | },
13 | message: {
14 | type: String,
15 | trim: true,
16 | required: true,
17 | },
18 | refs: {
19 | organization: {
20 | type: Schema.Types.ObjectId,
21 | ref: 'Organization',
22 | required: true,
23 | autopopulate: true,
24 | },
25 | agent: {
26 | type: Schema.Types.ObjectId,
27 | ref: 'Agent',
28 | required: true,
29 | autopopulate: {
30 | select: ['name', 'email'],
31 | },
32 | },
33 | },
34 | },
35 | {
36 | collection: 'responses',
37 | }
38 | );
39 |
40 | ResponseSchema.plugin(timestamps);
41 | ResponseSchema.plugin(query);
42 | ResponseSchema.plugin(autopopulate);
43 |
44 | ResponseSchema.index({ createdAt: 1, updatedAt: 1 });
45 |
46 | module.exports = exports = mongoose.model('Response', ResponseSchema);
47 |
--------------------------------------------------------------------------------
/api/src/models/faq.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import query from 'mongoose-string-query';
3 | import timestamps from 'mongoose-timestamp';
4 | import autopopulate from 'mongoose-autopopulate';
5 |
6 | export const FaqSchema = new Schema(
7 | {
8 | meta: {
9 | question: {
10 | type: String,
11 | trim: true,
12 | required: true,
13 | unique: true,
14 | },
15 | answer: {
16 | type: String,
17 | trim: true,
18 | required: true,
19 | },
20 | },
21 | refs: {
22 | organization: {
23 | type: Schema.Types.ObjectId,
24 | ref: 'Organization',
25 | required: true,
26 | autopopulate: true,
27 | },
28 | agent: {
29 | type: Schema.Types.ObjectId,
30 | ref: 'Agent',
31 | required: true,
32 | autopopulate: {
33 | select: ['name', 'email'],
34 | },
35 | },
36 | },
37 | },
38 | {
39 | collection: 'faqs',
40 | }
41 | );
42 |
43 | FaqSchema.plugin(timestamps);
44 | FaqSchema.plugin(query);
45 | FaqSchema.plugin(autopopulate);
46 |
47 | FaqSchema.index({ createdAt: 1, updatedAt: 1 });
48 |
49 | module.exports = exports = mongoose.model('Faq', FaqSchema);
50 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Inbox/Inbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router-dom';
3 | import compose from 'lodash.flowright';
4 |
5 | // Views //
6 | import Threads from './views/Threads';
7 | import MessageThread from './views/MessageThread';
8 |
9 | // HOCs //
10 | import withAuth from 'hocs/withAuth';
11 | import withChannels from '@comba.se/chat/hocs/withChannels';
12 |
13 | // Components ///
14 | import ListDetailView from 'components/ListDetailView';
15 | import ScreenRoot from 'shared/ScreenRoot';
16 |
17 | const renderThreadList = props => ;
18 | const renderMessageThread = props => (
19 |
23 | );
24 |
25 | const Inbox = props => (
26 |
27 |
31 |
32 |
33 | );
34 |
35 | export default compose(withAuth, withChannels)(Inbox);
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Settings/views/SettingsDetail/views/OrganizationSettings/OrganizationSettings.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Container, ListHeader } from '@comba.se/ui';
4 | import { OrganizationSettingsIcon } from "@comba.se/ui/Icons";
5 |
6 | // Forms //
7 | import { OrganizationProfileForm, OrganizationChatSettingsForm } from 'screens/Dashboard/forms/OrganizationSettings';
8 |
9 | // Hooks //
10 | import useMedia from 'hooks/useMedia';
11 |
12 | // Components //
13 | import MenuButton from 'shared/MenuButton';
14 |
15 | const Root = styled.div`
16 | flex: 1;
17 | `;
18 |
19 | const OrganizationSettings = () => {
20 | const isMobile = useMedia('sm');
21 | return (
22 |
23 |
24 | {!isMobile ? : null}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default OrganizationSettings;
33 |
--------------------------------------------------------------------------------
/dashboard/src/utils/GetRef.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import PropTypes from "prop-types";
4 |
5 | class GetRef extends React.Component {
6 | componentDidMount() {
7 | this.ref = ReactDOM.findDOMNode(this);
8 | this.props.rootRef(this.ref);
9 | }
10 |
11 | componentDidUpdate(prevProps) {
12 | const ref = ReactDOM.findDOMNode(this);
13 |
14 | if (prevProps.rootRef !== this.props.rootRef || this.ref !== ref) {
15 | if (prevProps.rootRef !== this.props.rootRef) {
16 | prevProps.rootRef(null);
17 | }
18 |
19 | this.ref = ref;
20 | this.props.rootRef(this.ref);
21 | }
22 | }
23 |
24 | componentWillUnmount() {
25 | this.ref = null;
26 | this.props.rootRef(null);
27 | }
28 |
29 | render() {
30 | return this.props.children;
31 | }
32 | }
33 |
34 | GetRef.propTypes = {
35 | children: PropTypes.element,
36 | // Provide a way to access the DOM node of the wrapped element.
37 | // You can provide a callback ref or a `React.createRef()` ref.
38 | rootRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired
39 | };
40 |
41 | export default GetRef;
42 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/integration/clearbit/enrich.action.js:
--------------------------------------------------------------------------------
1 | import { Client } from 'clearbit';
2 |
3 | import Plugin from 'models/plugin';
4 |
5 | exports.clearbitExecEnrich = async (req, res) => {
6 | try {
7 | const { email } = req.body;
8 |
9 | if (!email) {
10 | console.error('An email is required for enrichment.');
11 | return res
12 | .status(400)
13 | .json({ error: 'An email is required for enrichment' });
14 | }
15 |
16 | const {
17 | keys: [{ value: api_key }],
18 | } = await Plugin.findOne({
19 | name: 'clearbit',
20 | keys: { $elemMatch: { name: 'api_key' } },
21 | })
22 | .select('keys')
23 | .lean({ autopopulate: false });
24 |
25 | if (!api_key) {
26 | console.error('Clearbit has not been initialized.');
27 | return res
28 | .status(400)
29 | .json({ error: 'Clearbit has not been initialized.' });
30 | }
31 |
32 | const client = new Client({ key: api_key });
33 |
34 | const { person: data } = await client.Enrichment.find({ email });
35 |
36 | res.status(200).json(data);
37 | } catch (error) {
38 | console.error(error);
39 | res.status(500).json({ error: error.message });
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/get.action.js:
--------------------------------------------------------------------------------
1 | import Agent from 'models/agent';
2 |
3 | /**
4 | * @swagger
5 | * GET /v1/agents:id:
6 | * get:
7 | * description: Get a specific agent
8 | * tags: [Agents]
9 | * produces:
10 | * - application/json
11 | * parameters:
12 | * - in: 'controllers/v1/agent/get.action.js'
13 | * name: ID
14 | * schema:
15 | * type: string
16 | * format: uuid
17 | * required: true
18 | * description: UUID of the agent to retrieve
19 | * responses:
20 | * 200:
21 | * description: JSON representation of the agent
22 | * schema:
23 | * type: object
24 | * properties:
25 | * message:
26 | * type: string
27 | */
28 | exports.get = async (req, res) => {
29 | try {
30 | const data = { ...req.body, ...req.params };
31 |
32 | const agent = await Agent.findById(data.agent);
33 |
34 | res.status(200).json(agent);
35 | } catch (error) {
36 | console.error(error);
37 | res.status(500).json({ error: error.message });
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/widget/src/App.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | import React from 'react';
4 | import { StreamChatProvider } from 'stream-chat-hooks';
5 |
6 | // Styles //
7 | import { ThemeProvider } from 'styled-components';
8 | import { light as theme } from '@comba.se/ui/styles/theme';
9 | import GlobalStyles from '@comba.se/ui/styles/global';
10 |
11 | // Contexts //
12 | import { AuthProvider } from 'contexts/Auth';
13 | import { SnackbarProvider } from "@comba.se/ui/Snackbar";
14 |
15 | import Root from './Root';
16 |
17 | const apiKey = "pyst6tqux4vf";
18 | const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNWU1NTUyOTYxZTdmNzAwYWJkMGRmZjIzIn0.Z1WIshH9NZ54eVbcGeNOcfVSNGjUEOtLJ2FDuTfbtVI";
19 | const user = {
20 | id: '5e5552961e7f700abd0dff23',
21 | name: 'Josh Tilton'
22 | };
23 |
24 | function App() {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export default App;
40 |
--------------------------------------------------------------------------------
/api/src/models/plugin.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import query from 'mongoose-string-query';
3 | import timestamps from 'mongoose-timestamp';
4 | import autopopulate from 'mongoose-autopopulate';
5 |
6 | export const PluginSchema = new Schema(
7 | {
8 | name: {
9 | type: String,
10 | trim: true,
11 | required: true
12 | },
13 | keys: [
14 | {
15 | name: {
16 | type: String,
17 | trim: true
18 | },
19 | value: {
20 | type: String,
21 | trim: true
22 | }
23 | }
24 | ],
25 | refs: {
26 | organization: {
27 | type: Schema.Types.ObjectId,
28 | ref: 'Organization',
29 | required: true,
30 | autopopulate: {
31 | select: [ 'name' ]
32 | }
33 | }
34 | },
35 | enabled: {
36 | type: Boolean,
37 | default: false
38 | }
39 | },
40 | {
41 | collection: 'plugins'
42 | }
43 | );
44 |
45 | PluginSchema.plugin(timestamps);
46 | PluginSchema.plugin(query);
47 | PluginSchema.plugin(autopopulate);
48 |
49 | PluginSchema.index({ createdAt: 1, updatedAt: 1 });
50 | PluginSchema.index({ name: 1, 'refs.organization': 1 }, { unique: true });
51 |
52 | module.exports = exports = mongoose.model('Plugin', PluginSchema);
53 |
--------------------------------------------------------------------------------
/dashboard/src/components/Tabs/Tabs.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 |
5 | // Components //
6 | import Tab from './Tab';
7 |
8 | const Root = styled.div`
9 | flex: 1;
10 | flex-direction: row;
11 | align-items: center;
12 | padding: 24px 16px;
13 | overflow-x: scroll;
14 |
15 | & > * + * {
16 | margin-left: 16px;
17 | }
18 |
19 | &:last-child {
20 | margin-right: 16px;
21 | }
22 |
23 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
24 | padding: 24px 40px;
25 |
26 | & > * + * {
27 | margin-left: 24px;
28 | }
29 | }
30 | `;
31 |
32 | const renderTabs = (tabs, activeTab, onClick) =>
33 | tabs.map((tab, key) => (
34 |
35 | ));
36 |
37 | const Tabs = ({ active, onTabClick, tabs }) => {
38 | return {renderTabs(tabs, active, onTabClick)};
39 | };
40 |
41 | Tabs.propTypes = {
42 | active: PropTypes.any,
43 | onTabClick: PropTypes.func,
44 | tabs: PropTypes.array,
45 | };
46 |
47 | export default Tabs;
48 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | # computed
64 | dist/
65 |
66 | # osx
67 | .DS_Store
--------------------------------------------------------------------------------
/api/src/controllers/v1/agent/destroy.action.js:
--------------------------------------------------------------------------------
1 | import Agent from 'models/agent';
2 |
3 | /**
4 | * @swagger
5 | * DELETE /v1/agents/:id:
6 | * delete:
7 | * description: Deletes an agent
8 | * tags: [Agents]
9 | * produces:
10 | * - application/json
11 | * parameters:
12 | * - in: 'controllers/v1/agent/destroy.action.js'
13 | * name: ID
14 | * schema:
15 | * type: string
16 | * format: uuid
17 | * required: true
18 | * description: UUID of agent to delete
19 | * responses:
20 | * 204:
21 | * description: No response
22 | */
23 | exports.destroy = async (req, res) => {
24 | try {
25 | const { agent } = req.params;
26 | const { serialized } = req;
27 |
28 | if (serialized.role !== 'admin') {
29 | return res.status(403).json({
30 | status: 'Invalid permissions to view or modify this resource.'
31 | });
32 | }
33 |
34 | const { password, ...removed } = await Agent.findByIdAndRemove(agent);
35 |
36 | res.sendStatus(204);
37 | } catch (error) {
38 | console.error(error);
39 | res.status(500).json({ error: error.message });
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useNotifications.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useEffect } from 'react';
2 | import { useChatClient } from 'stream-chat-hooks';
3 | import UIfx from 'uifx';
4 |
5 | // Contexts //
6 | import ShellContext from 'contexts/Shell';
7 |
8 | // Sounds //
9 | import newMessageSound from 'sounds/new_message.wav';
10 | import newConversationSound from 'sounds/new_conversation.wav';
11 |
12 | const messageChime = new UIfx(newMessageSound);
13 | const conversationChime = new UIfx(newConversationSound);
14 |
15 | export default () => {
16 | const client = useChatClient();
17 | const { sounds } = useContext(ShellContext);
18 |
19 | const handleNewMessage = useCallback(() => {
20 | messageChime.play();
21 | }, []);
22 |
23 | const handleNewConversation = useCallback(() => {
24 | conversationChime.play();
25 | }, []);
26 |
27 | useEffect(() => {
28 | if (sounds.enabled) {
29 | client.on('notification.message_new', handleNewMessage);
30 | client.on('notification.added_to_channel', handleNewConversation);
31 | }
32 | return () => sounds.enabled ? client.off('message.new', handleNewMessage) : null;
33 | }, [client, handleNewConversation, handleNewMessage, sounds.enabled]);
34 | }
--------------------------------------------------------------------------------
/dashboard/src/components/SettingsListItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 | import { IconBubble, Text } from '@comba.se/ui';
5 |
6 | // Components //
7 | const Root = styled.div`
8 | padding: 16px 0px;
9 | flex-direction: row;
10 | align-items: center;
11 | justify-content: space-between;
12 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
13 | padding: 16px;
14 | }
15 | `;
16 |
17 | const TitleBlock = styled.div`
18 | flex-direction: row;
19 | align-items: center;
20 | `;
21 |
22 | const Content = styled.div`
23 | margin-left: 24px;
24 | `;
25 |
26 | const SettingsListItem = ({ children, color, icon, text, title }) => (
27 |
28 |
29 |
30 |
31 | {title}
32 |
33 | {text}
34 |
35 |
36 |
37 | {children}
38 |
39 | );
40 |
41 | SettingsListItem.propTypes = {
42 | color: PropTypes.string,
43 | icon: PropTypes.func,
44 | text: PropTypes.string,
45 | title: PropTypes.string
46 | };
47 |
48 | export default SettingsListItem;
49 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/views/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Avatar, Container, Text } from '@comba.se/ui';
4 |
5 | // Hooks //
6 | import useAuth from 'hooks/useAuth';
7 |
8 | // Forms //
9 | import LoginForm from '../forms/LoginForm';
10 |
11 | // Components //
12 | const Root = styled(Container)`
13 | flex: 1;
14 | justify-content: center;
15 | `;
16 |
17 | const Header = styled.div`
18 | padding: 32px 0px;
19 | justify-content: center;
20 | align-items: center;
21 | text-align: center;
22 | `;
23 |
24 | const Login = () => {
25 | const [{ organization }] = useAuth();
26 | return (
27 |
28 |
29 |
35 |
36 | {organization.name}
37 |
38 | Customer Support Chat
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default Login;
46 |
--------------------------------------------------------------------------------
/api/src/models/invite.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import query from 'mongoose-string-query';
3 | import timestamps from 'mongoose-timestamp';
4 | import autopopulate from 'mongoose-autopopulate';
5 | import moment from 'moment';
6 |
7 | export const InviteSchema = new Schema(
8 | {
9 | name: {
10 | first: {
11 | type: String,
12 | trim: true,
13 | required: true
14 | },
15 | last: {
16 | type: String,
17 | trim: true,
18 | required: true
19 | }
20 | },
21 | email: {
22 | type: String,
23 | lowercase: true,
24 | trim: true,
25 | required: true
26 | },
27 | refs: {
28 | organization: {
29 | type: Schema.Types.ObjectId,
30 | ref: 'Organization',
31 | required: true,
32 | autopopulate: true
33 | }
34 | },
35 | expiration: {
36 | type: Date,
37 | default: moment().add('48', 'hours').toISOString()
38 | },
39 | accepted: {
40 | type: Boolean,
41 | default: false
42 | }
43 | },
44 | {
45 | collection: 'invites'
46 | }
47 | );
48 |
49 | InviteSchema.plugin(timestamps);
50 | InviteSchema.plugin(query);
51 | InviteSchema.plugin(autopopulate);
52 |
53 | InviteSchema.index({ createdAt: 1, updatedAt: 1 });
54 |
55 | module.exports = exports = mongoose.model('Invite', InviteSchema);
56 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/exchange.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { StreamChat } from 'stream-chat';
3 |
4 | import Agent from 'models/agent';
5 | import Chat from 'models/chat';
6 | import StreamClient from 'utils/stream';
7 |
8 | exports.exchange = async (req, res) => {
9 | try {
10 | const data = req.body;
11 |
12 | const { key, secret } = await StreamClient();
13 | const client = new StreamChat(key, secret);
14 |
15 | // get the new agent
16 | const invitee = await Agent.findById(data.invitee).lean();
17 | const current = await Agent.findById(data.current).lean();
18 |
19 | // initialize the channel
20 | const channel = client.channel('messaging', data.chat);
21 |
22 | // add the new agent and remove the old agent
23 | await channel.addMembers([ invitee._id ]);
24 | await channel.removeMembers([ current._id ]);
25 |
26 | // update database with user exchange for agent trail (timestamp is automatically generated)
27 | const chat = await Chat.findByIdAndUpdate(data.chat, {
28 | $set: {
29 | refs: {
30 | agents: {
31 | assignee: invitee._id
32 | }
33 | }
34 | }
35 | });
36 |
37 | res.status(200).json(chat);
38 | } catch (error) {
39 | console.error(error);
40 | res.status(500).json({ error: error.message });
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/dashboard/src/shared/UserBlock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Avatar, Text } from '@comba.se/ui';
5 |
6 | // Components //
7 | const Root = styled.div`
8 | flex-direction: row;
9 | align-items: center;
10 | `;
11 |
12 | const Content = styled.div`
13 | margin-left: 24px;
14 | `;
15 |
16 | const UserBlock = ({ avatar, avatarSize, meta, metaSize, name, textSize }) => (
17 |
18 |
19 |
20 |
21 | {name}
22 |
23 |
24 | {meta}
25 |
26 |
27 |
28 | );
29 |
30 | UserBlock.propTypes = {
31 | avatar: PropTypes.string.isRequired,
32 | avatarSize: PropTypes.number.isRequired,
33 | meta: PropTypes.string.isRequired,
34 | metaSize: PropTypes.number,
35 | name: PropTypes.string.isRequired,
36 | textSize: PropTypes.number.isRequired,
37 | };
38 |
39 | UserBlock.defaultProps = {
40 | avatarSize: 48,
41 | textSize: 16,
42 | };
43 |
44 | export default UserBlock;
45 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/plugin/integration/blaze_verify/verify.action.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import Plugin from 'models/plugin';
4 |
5 | exports.blazeVerifyExecVerify = async (req, res) => {
6 | try {
7 | const { email } = req.body;
8 |
9 | if (!email) {
10 | console.error('An email is required for verification.');
11 | return res
12 | .status(400)
13 | .json({ error: 'An email is required for verification' });
14 | }
15 |
16 | const {
17 | keys: [{ value: api_key }],
18 | } = await Plugin.findOne({
19 | name: 'blaze_verify',
20 | keys: { $elemMatch: { name: 'api_key' } },
21 | })
22 | .select('keys')
23 | .lean({ autopopulate: false });
24 |
25 | if (!api_key) {
26 | console.error('Blaze Verify has not been initialized.');
27 | return res
28 | .status(400)
29 | .json({ error: 'Blaze Verify has not been initialized.' });
30 | }
31 |
32 | const {
33 | data: { disposable, state, domain, score, gender },
34 | } = await axios.get(
35 | `https://api.blazeverify.com/v1/verify?email=${email.toLowerCase()}&api_key=${api_key}`
36 | );
37 |
38 | res.status(200).json({ disposable, state, domain, score, gender });
39 | } catch (error) {
40 | console.error(error);
41 | res.status(500).json({ error: error.message });
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/dashboard/src/components/ListDetailView/ListDetailView.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled, { withTheme } from "styled-components";
3 | import Animated from "animated/lib/targets/react-dom";
4 | import { Switch } from "react-router-dom";
5 |
6 | // Hooks //
7 | import useMedia from "hooks/useMedia";
8 |
9 | // Components //
10 | import ListDetailTransition from "./ListDetailTransition";
11 |
12 | const Root = styled.div`
13 | flex: 1;
14 | flex-direction: row;
15 | height: 100%;
16 | overflow: hidden;
17 | `;
18 |
19 | const anim = new Animated.Value(0);
20 | const ListDetailView = ({
21 | children,
22 | location,
23 | match,
24 | rootAs,
25 | transitionAnim = anim
26 | }) => {
27 | const [animating, setAnimating] = useState(false);
28 | const useStack = useMedia("sm");
29 | if (useStack) {
30 | return (
31 |
32 |
37 | {children}
38 |
39 |
40 | );
41 | }
42 | return {children};
43 | };
44 |
45 | export default withTheme(ListDetailView);
46 |
--------------------------------------------------------------------------------
/dashboard/src/components/PageSheet/PageSheet.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Card, LoadingState } from '@comba.se/ui';
4 |
5 | // Components //
6 | import Tabs from 'components/Tabs';
7 | import SearchHeader from './SearchHeader';
8 |
9 | const PageSheet = ({
10 | activeTab,
11 | children,
12 | loading,
13 | onQueryChange,
14 | setActiveTab,
15 | showSearch,
16 | tabs,
17 | ...rest
18 | }) => {
19 | return (
20 |
21 | {showSearch ? : null}
22 | {tabs && tabs.length ? (
23 |
28 | ) : null}
29 | {loading ? : children}
30 |
31 | );
32 | };
33 |
34 | PageSheet.propTypes = {
35 | activeTab: PropTypes.string,
36 | loading: PropTypes.bool,
37 | onQueryChange: PropTypes.func,
38 | setActiveTab: PropTypes.func,
39 | showSearch: PropTypes.bool,
40 | tabs: PropTypes.array,
41 | };
42 |
43 | PageSheet.defaultProps = {
44 | showSearch: true,
45 | };
46 |
47 | export default PageSheet;
48 |
--------------------------------------------------------------------------------
/widget/src/components/ListDetailView/ListDetailView.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled, { withTheme } from "styled-components";
3 | import Animated from "animated/lib/targets/react-dom";
4 | import { Switch } from "react-router-dom";
5 |
6 | // Hooks //
7 | import useMedia from "hooks/useMedia";
8 |
9 | // Components //
10 | import ListDetailTransition from "./ListDetailTransition";
11 |
12 | const Root = styled.div`
13 | flex: 1;
14 | flex-direction: row;
15 | height: 100%;
16 | overflow: hidden;
17 | `;
18 |
19 | const anim = new Animated.Value(0);
20 | const ListDetailView = ({
21 | children,
22 | location,
23 | match,
24 | rootAs,
25 | transitionAnim = anim
26 | }) => {
27 | const [animating, setAnimating] = useState(false);
28 | const useStack = useMedia("sm");
29 | if (useStack) {
30 | return (
31 |
32 |
37 | {children}
38 |
39 |
40 | );
41 | }
42 | return {children};
43 | };
44 |
45 | export default withTheme(ListDetailView);
46 |
--------------------------------------------------------------------------------
/dashboard/src/shared/IconLabel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { PlaceholderIcon, Text } from '@comba.se/ui';
5 |
6 | // Components //
7 | const Root = styled.div`
8 | flex: 1;
9 | padding: 8px;
10 | justify-content: center;
11 | align-items: center;
12 |
13 | & > ${Text} {
14 | margin-top: 8px;
15 | }
16 | `;
17 |
18 | const IconLabel = ({ icon: Icon, iconColor, iconSize, label, labelColor, labelSize, showPlaceholder }) => {
19 | return (
20 |
21 | {showPlaceholder ? : }
22 | {label}
23 |
24 | );
25 | };
26 |
27 | IconLabel.propTypes = {
28 | icon: PropTypes.func.isRequired,
29 | iconColor: PropTypes.string,
30 | iconSize: PropTypes.number,
31 | label: PropTypes.string.isRequired,
32 | labelColor: PropTypes.string,
33 | labelSize: PropTypes.number,
34 | showPlaceholder: PropTypes.bool,
35 | };
36 |
37 | IconLabel.defaultProps = {
38 | iconColor: 'primary',
39 | iconSize: 32,
40 | labelColor: 'alt_text',
41 | labelSize: 12,
42 | showPlaceholder: false,
43 | };
44 |
45 | export default IconLabel;
--------------------------------------------------------------------------------
/dashboard/src/components/PageSheet/SearchHeader.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, memo } from 'react';
2 | import styled from 'styled-components';
3 | import { SearchIcon } from "@comba.se/ui/Icons";
4 |
5 | // Components //
6 | const Root = styled.div`
7 | height: 64px;
8 | flex-direction: row;
9 | border-bottom: 1px solid ${({ theme }) => theme.color.border};
10 | padding: 0px 12px;
11 | `;
12 |
13 | const IconWrapper = styled.div`
14 | padding: 0px 12px;
15 | justify-content: center;
16 | align-items: center;
17 | `;
18 |
19 | const Input = styled.input`
20 | flex: 1;
21 | border: 0;
22 | font-size: 16px;
23 | font-weight: 500;
24 | color: ${({ theme }) => theme.color.text};
25 | &::placeholder {
26 | color: ${({ theme }) => theme.color.gray};
27 | }
28 | `;
29 |
30 | const SearchHeader = ({ onChange }) => {
31 | const handleChange = useCallback(
32 | ({ target: { value } }) => {
33 | onChange(value);
34 | },
35 | [onChange]
36 | );
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default memo(SearchHeader);
48 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useInvite.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import moment from "moment";
3 |
4 | // Utils //
5 | import request from "utils/request";
6 |
7 | export default inviteId => {
8 | const [invite, setInvite] = useState(null);
9 | const [loading, setLoading] = useState(true);
10 | const [error, setError] = useState(false);
11 | const [expired, setExpired] = useState(false);
12 |
13 | const fetchInvite = useCallback(async () => {
14 | if (inviteId) {
15 | try {
16 | setLoading(true);
17 | const invite = await request(`v1/invites/${inviteId}`, "get");
18 | setLoading(false);
19 | if (invite.error) {
20 | throw new Error("Something went wrong");
21 | }
22 |
23 | setInvite(invite);
24 | if (invite.accepted || moment(invite.expiration).isBefore(moment())) {
25 | throw new Error("Invitation Expired");
26 | }
27 | } catch (error) {
28 | setLoading(false);
29 | if (error.message === "Invitation Expired") {
30 | setExpired(true);
31 | } else {
32 | setError(true);
33 | }
34 | }
35 | }
36 | }, [inviteId]);
37 |
38 | useEffect(() => {
39 | fetchInvite();
40 | }, [fetchInvite]);
41 |
42 | return [invite, { loading, error, expired }];
43 | };
44 |
--------------------------------------------------------------------------------
/dashboard/src/components/Tabs/Tab.js:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react';
2 | import styled from 'styled-components';
3 | import { Text } from '@comba.se/ui';
4 |
5 | // Components //
6 | const Root = styled.div`
7 | padding: 4px 8px;
8 | border-radius: 9999px;
9 | background-color: ${({ active, theme }) =>
10 | active ? theme.color.primary : 'transparent'};
11 | cursor: pointer;
12 | transition: 0.24s background-color
13 | ${({ theme }) => theme.easing.css(theme.easing.standard)};
14 |
15 | & > ${Text} {
16 | text-transform: capitalize;
17 | user-select: none;
18 | }
19 |
20 | &:hover {
21 | background-color: ${({ active, theme }) =>
22 | !active ? theme.colorUtils.fade(theme.color.gray, 0.24) : null};
23 | }
24 |
25 | &:active {
26 | background-color: ${({ active, theme }) =>
27 | !active ? theme.colorUtils.fade(theme.color.gray, 0.48) : null};
28 | }
29 | `;
30 |
31 | export default memo(({ active, label, onClick }) => {
32 | const handleClick = useCallback(() => {
33 | return onClick ? onClick(label) : null;
34 | }, [label, onClick]);
35 |
36 | return (
37 |
38 | {label}
39 |
40 | );
41 | });
42 |
--------------------------------------------------------------------------------
/dashboard/src/components/OrganizationCard.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import styled from 'styled-components';
3 | import { Card, Text } from '@comba.se/ui';
4 |
5 | // Hooks //
6 | import useAuth from 'hooks/useAuth';
7 |
8 | // Components //
9 | const Root = styled(Card)`
10 | padding: 4px;
11 | cursor: pointer;
12 | `;
13 |
14 | const Logo = styled.div`
15 | width: 100%;
16 | padding-top: 100%;
17 | border-radius: ${({ theme }) => theme.borderRadius}px;
18 | background-image: ${({ src }) => `url(${src})`};
19 | background-size: cover;
20 | background-position: center;
21 | overflow: hidden;
22 | `;
23 |
24 | const Meta = styled.div`
25 | padding: 8px;
26 | `;
27 |
28 | const OrganizationCard = ({ id, logo, name, tagline }) => {
29 | const [_, { setCurrentOrganization }] = useAuth(); // eslint-disable-line no-unused-vars
30 | const onClick = useCallback(() => {
31 | setCurrentOrganization(id);
32 | }, [id, setCurrentOrganization]);
33 | return (
34 |
35 |
36 |
37 | {name}
38 |
39 | {tagline}
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default OrganizationCard;
47 |
--------------------------------------------------------------------------------
/dashboard/src/components/MobileHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { IconButton, Text } from '@comba.se/ui';
5 | import { ArrowBackIcon } from "@comba.se/ui/Icons";
6 |
7 | // Components //
8 | import MenuButton from "shared/MenuButton";
9 |
10 | const Root = styled.div`
11 | flex: 0 0 64px;
12 | flex-direction: row;
13 | align-items: center;
14 | padding: 0px 16px;
15 | & ${Text} {
16 | margin-left: 16px;
17 | }
18 | `;
19 |
20 | const MobileHeader = ({ color, showBackBtn, onBackClick, title }) => {
21 | return (
22 |
23 | {showBackBtn ? (
24 |
25 | ) : (
26 |
27 | )}
28 | {title ? (
29 |
30 | {title}
31 |
32 | ) : null}
33 |
34 | );
35 | };
36 |
37 | MobileHeader.propTypes = {
38 | color: PropTypes.string,
39 | showBackBtn: PropTypes.bool,
40 | onBackClick: PropTypes.func,
41 | title: PropTypes.string,
42 | };
43 |
44 | MobileHeader.defaultProps = {
45 | color: 'text',
46 | showBackBtn: false,
47 | }
48 |
49 | export default MobileHeader;
--------------------------------------------------------------------------------
/dashboard/src/components/AgentItem.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useState } from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { Text } from '@comba.se/ui';
5 |
6 | // Styles //
7 | import listItemInteractions from "styles/css/listItemInteractions";
8 |
9 | // Components //
10 | import UserBlock from "shared/UserBlock";
11 |
12 | const Root = styled.div`
13 | padding: 0px 16px;
14 | flex-direction: row;
15 | align-items: center;
16 | height: 80px;
17 | cursor: pointer;
18 | ${listItemInteractions}
19 | & ${Text} {
20 | user-select: none;
21 | }
22 |
23 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
24 | padding: 0px 40px;
25 | }
26 |
27 | `;
28 | const AgentItem = ({ _id, email, image, name }) => {
29 | const [dims, setDims] = useState(null);
30 | const rootRef = useCallback(el => {
31 | if (el) {
32 | setDims(el.getBoundingClientRect());
33 | }
34 | }, []);
35 | const to = useMemo(
36 | () => ({
37 | pathname: `/agents/${_id}`,
38 | startDims: dims
39 | }),
40 | [dims, _id]
41 | );
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default AgentItem;
52 |
--------------------------------------------------------------------------------
/widget/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "widget",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@comba.se/chat": "^0.0.31",
7 | "@comba.se/ui": "^0.0.79",
8 | "@juggle/resize-observer": "^3.1.1",
9 | "@testing-library/jest-dom": "^5.1.1",
10 | "@testing-library/react": "^9.5.0",
11 | "@testing-library/user-event": "^10.0.0",
12 | "animated": "^0.2.2",
13 | "lodash.flowright": "^3.5.0",
14 | "moment": "^2.24.0",
15 | "react": "^16.13.0",
16 | "react-dom": "^16.13.0",
17 | "react-lottie": "^1.2.3",
18 | "react-router-dom": "^5.1.2",
19 | "react-scripts": "3.4.0",
20 | "react-spring": "^8.0.27",
21 | "recyclerlistview": "^3.0.0",
22 | "stream-chat": "^1.4.0",
23 | "stream-chat-hooks": "^0.0.9",
24 | "styled-components": "^5.0.1",
25 | "whatwg-fetch": "^3.0.0"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": "react-app"
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/usePlugin.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useMemo } from "react";
2 | import useAuth from "hooks/useAuth";
3 | import PluginsContext from "contexts/Plugins";
4 | import { useSnackbar } from "contexts/Snackbar";
5 | import request from "utils/request";
6 |
7 | export default slug => {
8 | const [{ user }] = useAuth();
9 | const { queueSnackbar } = useSnackbar();
10 | const [activePlugins, { refetch }] = useContext(PluginsContext);
11 | const plugin = useMemo(() => {
12 | if (!slug || !activePlugins) {
13 | return null;
14 | }
15 | return activePlugins[slug];
16 | }, [slug, activePlugins]);
17 |
18 | const togglePlugin = useCallback(
19 | async ({ target: { checked } }) => {
20 | try {
21 | await request(
22 | `v1/plugins/${plugin._id}`,
23 | "put",
24 | {
25 | body: JSON.stringify({
26 | ...plugin,
27 | enabled: checked
28 | })
29 | },
30 | user.tokens.api
31 | );
32 | await refetch();
33 | } catch (error) {
34 | queueSnackbar({
35 | replace: true,
36 | isError: true,
37 | text: error.message
38 | });
39 | console.log(error);
40 | }
41 | },
42 | [plugin, user.tokens.api, refetch, queueSnackbar]
43 | );
44 |
45 | return [plugin, refetch, togglePlugin];
46 | };
47 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect, Route, Switch } from "react-router-dom";
3 | import styled from "styled-components";
4 | import { Text } from '@comba.se/ui';
5 |
6 | // Views //
7 | import Invite from "./views/Invite";
8 | import Login from "./views/Login";
9 | import SignUp from "./views/SignUp";
10 |
11 | // Components //
12 | import StreamLogo from "shared/StreamLogo";
13 |
14 | const Root = styled.div`
15 | flex: 1;
16 | overflow-y: scroll;
17 | `;
18 |
19 | const Credit = styled.div`
20 | padding: 40px 0px;
21 | justify-content: center;
22 | align-items: center;
23 | text-align: center;
24 | `;
25 |
26 | const redirectToLogin = () => ;
27 |
28 | const Auth = ({ match }) => {
29 | return (
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Powered by Stream.
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default Auth;
52 |
--------------------------------------------------------------------------------
/widget/src/screens/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { Container } from '@comba.se/ui';
4 |
5 | // Contexts //
6 | import { ScrollAnimationProvider } from 'contexts/ScrollAnimation';
7 |
8 | // Components //
9 | import Header from 'components/Home/Header';
10 | import ConversationsWidget from 'components/Home/ConversationsWidget';
11 | import KnowledgeBaseWidget from 'components/Home/KnowledgeBaseWidget';
12 |
13 | const Root = styled.div`
14 | flex: 1;
15 | width: 100%;
16 | overflow: scroll;
17 | `;
18 |
19 | const Content = styled(Container)`
20 | z-index: 2;
21 | padding-bottom: 40px;
22 | margin-top: 280px;
23 |
24 | & > * + * {
25 | margin-top: 24px;
26 | }
27 | `;
28 |
29 | const Home = () => {
30 | const [rootRef, setRootRef] = useState();
31 |
32 | const ref = useCallback((el) =>{
33 | if (el && !rootRef) {
34 | setRootRef(el);
35 | }
36 | }, [rootRef]);
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default Home;
--------------------------------------------------------------------------------
/api/src/controllers/v1/upload/post.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import DataURI from 'datauri';
3 | import path from 'path';
4 | import { v2 as cloudinary } from 'cloudinary';
5 |
6 | exports.post = (req, res) => {
7 | try {
8 | let cloudinaryKey;
9 | let cloudinarySecret;
10 |
11 | // if this env is found, it's assumed that the api is running on heroku
12 | if (process.env.CLOUDINARY_URL) {
13 | // extract the key and secret from the environment variable
14 | [
15 | cloudinaryKey,
16 | cloudinarySecret,
17 | ] = process.env.CLOUDINARY_URL.substr(13)
18 | .split('@')[0]
19 | .split(':');
20 | } else {
21 | // api key and secret were provided from a .env file
22 | cloudinaryKey = process.env.CLOUDINARY_API_KEY;
23 | cloudinarySecret = process.env.CLOUDINARY_API_SECRET;
24 | }
25 |
26 | const datauri = new DataURI();
27 | const file = datauri.format(
28 | path.extname(req.file.originalname).toString(),
29 | req.file.buffer
30 | );
31 |
32 | cloudinary.uploader.upload(file.content, (error, data) => {
33 | if (error) {
34 | console.error(error);
35 | return res.sendStatus(500);
36 | }
37 |
38 | res.status(200).json({
39 | dimensions: {
40 | height: data.height,
41 | width: data.width,
42 | },
43 | format: data.format,
44 | url: data.url,
45 | });
46 | });
47 | } catch (error) {
48 | console.error(error);
49 | res.status(500).json({ error: error.message });
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Combase",
3 | "description": "Open Source & White Label Customer Support Chat – Powered by Stream",
4 | "keywords": [ "Customer Support", "Stream", "Open Source", "Chat", "White Label" ],
5 | "website": "https://comba.se/",
6 | "repository": "https://github.com/GetStream/combase",
7 | "logo": "https://i.imgur.com/jwPM769.png",
8 | "success_url": "https://github.com/GetStream/combase",
9 | "formation": {
10 | "web": {
11 | "quantity": 1,
12 | "size": "free"
13 | }
14 | },
15 | "image": "heroku/nodejs",
16 | "addons": [
17 | {
18 | "plan": "stream:chat-trial",
19 | "as": "STREAM"
20 | },
21 | {
22 | "plan": "mongolab:sandbox",
23 | "as": "MONGODB"
24 | },
25 | {
26 | "plan": "rediscloud:30",
27 | "as": "REDIS"
28 | },
29 | {
30 | "plan": "algoliasearch:free",
31 | "as": "ALGOLIASEARCH"
32 | },
33 | {
34 | "plan": "timber-logging:free",
35 | "as": "TIMBER_LOGGING"
36 | },
37 | {
38 | "plan": "cloudinary:starter",
39 | "as": "CLOUDINARY"
40 | }
41 | ],
42 | "buildpacks": [
43 | {
44 | "url": "https://github.com/lstoll/heroku-buildpack-monorepo"
45 | },
46 | {
47 | "url": "heroku/nodejs"
48 | }
49 | ],
50 | "env": {
51 | "AUTH_SECRET": {
52 | "description": "A secret key for verifying the integrity of signed JWTs.",
53 | "generator": "secret"
54 | },
55 | "APP_BASE": {
56 | "description": "Absolute path to API in monorepo (default = /api).",
57 | "value": "api/"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/dashboard/src/shared/StreamLogo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-unused-vars
2 | import { Icon } from '@comba.se/ui';
3 |
4 | const StreamLogo = props => (
5 |
6 |
7 |
8 | );
9 |
10 | export default StreamLogo;
11 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useActivePlugins.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 |
3 | // Hooks //
4 | import useAuth from "hooks/useAuth";
5 | import { useSnackbar } from "contexts/Snackbar";
6 |
7 | // Utils //
8 | import request from "utils/request";
9 |
10 | export default () => {
11 | const { queueSnackbar } = useSnackbar();
12 | const [loading, setLoading] = useState(false);
13 | const [plugins, setPlugins] = useState(
14 | JSON.parse(localStorage.getItem("plugins")) || null
15 | );
16 | const [{ user }] = useAuth();
17 |
18 | const fetchPlugins = useCallback(async () => {
19 | try {
20 | setLoading(true);
21 | const data = await request(
22 | `v1/plugins?refs.organization._id=${user.refs.organization._id}`,
23 | "get",
24 | null,
25 | user.tokens.api
26 | );
27 | let pluginData = {};
28 | data.forEach(plugin => {
29 | pluginData[plugin.name] = plugin;
30 | });
31 | setPlugins(pluginData);
32 | localStorage.setItem("plugins", JSON.stringify(pluginData));
33 | setLoading(false);
34 | } catch (error) {
35 | setLoading(false);
36 | queueSnackbar({
37 | replace: true,
38 | isError: true,
39 | text: error.message
40 | });
41 | }
42 | }, [user, queueSnackbar]); // eslint-disable-line react-hooks/exhaustive-deps
43 | useEffect(() => {
44 | fetchPlugins();
45 | }, [fetchPlugins]);
46 | return [plugins, { refetch: fetchPlugins, loading }];
47 | };
48 |
--------------------------------------------------------------------------------
/dashboard/src/styles/theme/index.js:
--------------------------------------------------------------------------------
1 | import colors from "./colors";
2 | import baseTheme from "./base";
3 |
4 | export const light = (overrides) => ({
5 | ...baseTheme,
6 | color: {
7 | ...colors,
8 | primary: colors.blue,
9 | background: colors.off_white,
10 | surface: colors.white,
11 | error: colors.red,
12 | disabled: colors.gray,
13 | light_text: colors.light_gray,
14 | shadow: colors.black,
15 | border: colors.light_gray,
16 | text: colors.black,
17 | alt_text: colors.slate,
18 | undersheet: baseTheme.colorUtils.fade(colors.black, 0.64),
19 | placeholder: colors.light_gray,
20 | placeholder_shimmer: baseTheme.colorUtils.lighten(colors.light_gray, .64),
21 | ...overrides,
22 | }
23 | });
24 |
25 | export const dark = overrides => ({
26 | ...baseTheme,
27 | color: {
28 | ...colors,
29 | primary: colors.blue,
30 | background: baseTheme.colorUtils.darken(colors.black, 0.2),
31 | surface: colors.black,
32 | error: colors.red,
33 | disabled: colors.gray,
34 | light_text: baseTheme.colorUtils.fade(colors.white, 0.32),
35 | shadow: colors.black,
36 | border: baseTheme.colorUtils.fade(colors.white, 0.05),
37 | text: colors.white,
38 | alt_text: baseTheme.colorUtils.fade(colors.white, 0.8),
39 | undersheet: baseTheme.colorUtils.fade(colors.black, 0.64),
40 | placeholder: baseTheme.colorUtils.darken(colors.black, .2),
41 | placeholder_shimmer: baseTheme.colorUtils.darken(colors.black, .1),
42 | ...overrides,
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/transcript/get.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import fs from 'fs';
3 | import ejs from 'ejs';
4 | import moment from 'moment';
5 | import { StreamChat } from 'stream-chat';
6 |
7 | import Chat from 'models/chat';
8 | import StreamClient from 'utils/stream';
9 |
10 | exports.get = async (req, res) => {
11 | try {
12 | const data = req.query;
13 |
14 | if (!process.env.POSTMARK_KEY) {
15 | return res.status(400).json({
16 | error:
17 | 'Missing POSTMARK_KEY environment variable. Please add this to your .env file or Heroku settings.'
18 | });
19 | }
20 |
21 | const chat = await Chat.findById(data.chat);
22 |
23 | const { key, secret } = await StreamClient();
24 | const client = new StreamChat(key, secret);
25 |
26 | const channel = client.channel('commerce', data.chat);
27 | const { messages } = await channel.query({
28 | messages: { limit: 1000, offset: 0 },
29 | members: { limit: 2, offset: 0 }
30 | });
31 |
32 | const transcript = messages.map((message) => {
33 | return {
34 | name: message.user.name.split(' ')[0] + ' ' + message.user.name.split(' ')[1].charAt(0) + '.',
35 | text: message.text,
36 | timestamp: moment(message.created_at).format('LLLL'),
37 | attachments: message.attachments.map((attachment) => {
38 | return {
39 | url: attachment.asset_url
40 | };
41 | })
42 | };
43 | });
44 |
45 | res.status(200).json(transcript);
46 | } catch (error) {
47 | console.error(error);
48 | res.status(500).json({ error: error.message });
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Welcome/Welcome.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Container, Text } from '@comba.se/ui';
4 |
5 | // Hooks //
6 | import useAuth from 'hooks/useAuth';
7 |
8 | // Components //
9 | import OrganizationCard from 'components/OrganizationCard';
10 |
11 | const Root = styled.div`
12 | flex: 1;
13 | justify-content: center;
14 | & > ${Container} {
15 | flex: 1;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 | `;
20 |
21 | const Header = styled.div`
22 | padding: 32px 0px;
23 | justify-content: center;
24 | align-items: center;
25 | text-align: center;
26 | `;
27 |
28 | const renderOrganizations = (org, key) => {
29 | return (
30 |
36 | );
37 | };
38 |
39 | const Welcome = () => {
40 | const [{ organizations }] = useAuth();
41 | return (
42 |
43 |
44 |
45 | Combase
46 |
47 |
48 | Choose an Organization
49 |
50 |
51 |
52 | {organizations.map(renderOrganizations)}
53 |
54 |
55 | );
56 | };
57 |
58 | export default Welcome;
59 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/auth/post.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import bcrypt from 'bcrypt';
3 |
4 | import Agent from 'models/agent';
5 |
6 | exports.post = async (req, res) => {
7 | try {
8 | // extract json from body
9 | const data = req.body;
10 |
11 | // if the agent does not exist, create a new agent
12 | const agent = await Agent.findOneOrCreate(
13 | { email: data.email.toLowerCase() }, // lowercase email to avoid lookup issues
14 | {
15 | name: {
16 | first: data.name.first,
17 | last: data.name.last,
18 | },
19 | email: data.email, // email is set to lowercase automatically by mongoose via model
20 | password: data.password, // password is hashed using bcrypt automatically by mongoose plugin
21 | refs: data.refs,
22 | }
23 | );
24 |
25 | // if the agent does not exist
26 | if (!agent) {
27 | // sanitize / remove password
28 | delete agent.password;
29 |
30 | // return the response
31 | return res.json(agent);
32 | }
33 |
34 | // validate that the provided password matches the hashed password stored in the database
35 | const match = await bcrypt.compare(data.password, agent.password);
36 |
37 | // if the password does not match, throw a 403 forbidden error status code
38 | if (!match) {
39 | return res.sendStatus(403);
40 | }
41 |
42 | // sanitize / remove password
43 | delete agent.password;
44 |
45 | // return the response with agent data, token, and api key
46 | return res.json(agent);
47 | } catch (error) {
48 | console.error(error);
49 | res.status(500).json({ error: error.message });
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/dashboard/src/hocs/withLayout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ResizeObserver from 'utils/ResizeObserver';
3 |
4 | const defaultOptions = {
5 | box: 'border-box',
6 | };
7 |
8 | export default (WrappedComponent, observerOptions = defaultOptions) => {
9 | return class withResize extends Component {
10 | ref = React.createRef();
11 |
12 | state = {
13 | layout: {},
14 | };
15 |
16 | componentDidMount() {
17 | this.observer = new ResizeObserver(this.handleResize);
18 | this.observer.observe(this.ref.current, observerOptions);
19 | }
20 |
21 | componentWillUnmount() {
22 | this.observer.disconnect();
23 | }
24 |
25 | handleResize = entries => {
26 | const { onResize } = this.props;
27 |
28 | const [entry] = entries;
29 |
30 | const {
31 | inlineSize: width,
32 | blockSize: height,
33 | } = entry.borderBoxSize[0]; // use for backwards compatibility
34 | this.setState({
35 | layout: { width, height },
36 | });
37 |
38 | if (onResize) {
39 | onResize({ width, height });
40 | }
41 | };
42 | render() {
43 | const { layout } = this.state;
44 | return (
45 |
50 | );
51 | }
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/dashboard/src/contexts/ThemeSwitcher/ThemeSwitcher.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useState } from "react";
2 | import { ThemeProvider } from "styled-components";
3 | import ThemeSwitcherContext from "./index";
4 |
5 | import * as themes from "styles/theme";
6 |
7 | export default ({ children }) => {
8 | const [currentTheme, changeTheme] = useState(
9 | localStorage.getItem("theme") || "light"
10 | );
11 |
12 | const [overrides, setOverrides] = useState(JSON.parse(localStorage.getItem('themeOverrides')) || {});
13 |
14 | const toggleTheme = useCallback(() => {
15 | const newTheme = currentTheme === "light" ? "dark" : "light";
16 | localStorage.setItem("theme", newTheme);
17 | changeTheme(newTheme);
18 | }, [currentTheme]);
19 |
20 | const setTheme = useCallback((themeName) => {
21 | changeTheme(themeName);
22 | }, []);
23 |
24 | const updateOverrides = useCallback(values => {
25 | localStorage.setItem('themeOverrides', JSON.stringify(values));
26 | console.log('setting overrides', values);
27 | setOverrides(values);
28 | }, []);
29 |
30 | const value = useMemo(
31 | () => ({
32 | isDarkMode: currentTheme === "dark",
33 | toggleTheme,
34 | updateOverrides,
35 | setTheme,
36 | }),
37 | [toggleTheme, currentTheme, updateOverrides, setTheme]
38 | );
39 |
40 | const theme = useMemo(() => themes[currentTheme](overrides), [currentTheme, overrides]);
41 |
42 | return (
43 |
44 | {children}
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Agents/views/AgentDetail/widgets/ChatActivityWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withTheme } from 'styled-components';
3 | import { Sparklines, SparklinesLine } from 'react-sparklines';
4 |
5 | // Components //
6 | import InfoWidget from 'shared/InfoWidget';
7 |
8 | function boxMullerRandom() {
9 | let phase = false,
10 | x1,
11 | x2,
12 | w;
13 |
14 | return (function() {
15 | if ((phase = !phase)) {
16 | do {
17 | x1 = 2.0 * Math.random() - 1.0;
18 | x2 = 2.0 * Math.random() - 1.0;
19 | w = x1 * x1 + x2 * x2;
20 | } while (w >= 1.0);
21 |
22 | w = Math.sqrt((-2.0 * Math.log(w)) / w);
23 | return x1 * w;
24 | } else {
25 | return x2 * w;
26 | }
27 | })();
28 | }
29 |
30 | function randomData(n = 30) {
31 | return Array.apply(0, Array(n)).map(boxMullerRandom);
32 | }
33 |
34 | const sampleData = randomData(30);
35 |
36 | const SparklineWidget = withTheme(({ theme }) => {
37 | return (
38 |
39 |
46 |
47 | );
48 | });
49 |
50 | const ChatActivityWidget = () => {
51 | return (
52 |
53 | );
54 | };
55 |
56 | export default ChatActivityWidget;
57 |
--------------------------------------------------------------------------------
/dashboard/src/shared/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled, { withTheme } from "styled-components";
4 | import ContentLoader from 'react-content-loader';
5 | import { Text } from '@comba.se/ui';
6 |
7 | // Components //
8 | const Root = styled.div`
9 | flex-direction: row;
10 | align-items: center;
11 | padding: 12px 16px;
12 | `;
13 |
14 | const Content = styled.div`
15 | margin-left: ${({ hasIcon }) => (hasIcon ? 16 : 0)}px;
16 | `;
17 |
18 | const ListItem = ({ icon: Icon, theme, title, value }) => {
19 | if (!title || !value) {
20 | return (
21 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 | return (
36 |
37 | {Icon ? : null}
38 |
39 |
40 | {title}
41 |
42 | {value}
43 |
44 |
45 | );
46 | };
47 |
48 | ListItem.propTypes = {
49 | icon: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
50 | title: PropTypes.string,
51 | value: PropTypes.string
52 | };
53 |
54 | export default withTheme(ListItem);
55 |
--------------------------------------------------------------------------------
/dashboard/src/components/Shell/Drawer/Drawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Route } from "react-router-dom";
4 | import Animated from "animated/lib/targets/react-dom";
5 |
6 | // Components //
7 | import Modal from "shared/Modal";
8 | import DrawerItem from "./DrawerItem";
9 |
10 | const Root = styled(Animated.div)`
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | bottom: 0;
15 | background-color: ${({ theme }) => theme.color.surface};
16 | width: 280px;
17 | z-index: ${({ theme }) => theme.z.modal};
18 | `;
19 |
20 | const renderRoutes = (routes, match, onClose) =>
21 | routes.map((route, key) => (
22 | (
26 |
33 | )}
34 | />
35 | ));
36 |
37 | const Drawer = ({ anim, match, onClose, open, routes }) => {
38 | const style = {
39 | transform: [
40 | {
41 | translateX: anim.interpolate({
42 | inputRange: [0, 1],
43 | outputRange: ["-100%", "-0%"]
44 | })
45 | }
46 | ]
47 | };
48 | return (
49 |
50 | {renderRoutes(routes, match, onClose)}
51 |
52 | );
53 | };
54 |
55 | Drawer.defaultProps = {
56 | anim: new Animated.Value(0)
57 | };
58 |
59 | export default Drawer;
60 |
--------------------------------------------------------------------------------
/widget/src/hooks/useChats.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useState } from "react";
2 | import { useChannels } from "stream-chat-hooks";
3 | import useAuth from "hooks/useAuth";
4 | import request from "utils/request";
5 |
6 | export default () => {
7 | const [{ organization, user }] = useAuth();
8 | const [loading, setLoading] = useState(true);
9 | const [error, setError] = useState(false);
10 | const filter = useMemo(() => ({ organization: organization._id, members: { $in: [user._id] } }), [organization, user]);
11 | const [channels, { loading: channelsLoading }] = useChannels(user._id, filter);
12 | const [chats, setChats] = useState(
13 | JSON.parse(localStorage.getItem("chats")) || []
14 | );
15 |
16 | const getChats = useCallback(async () => {
17 | if (channelsLoading) {
18 | return;
19 | }
20 |
21 | if (channels.length) {
22 | try {
23 | setLoading(true);
24 | const data = await request(
25 | `v1/chats?refs.user._id=${user._id}`,
26 | "get",
27 | null,
28 | process.env.REACT_APP_API_KEY
29 | );
30 |
31 | const chatData = data.map(chat => ({
32 | ...chat,
33 | channel: channels.find(({ id }) => id === chat._id)
34 | }));
35 |
36 | setChats(chatData);
37 | setLoading(false);
38 | } catch (error) {
39 | setError(true);
40 | setLoading(false);
41 | }
42 | } else {
43 | setLoading(false);
44 | }
45 | }, [user._id, channels, channelsLoading]);
46 | useEffect(() => {
47 | getChats();
48 | }, [getChats]);
49 | return [chats, { loading, error }];
50 | };
51 |
--------------------------------------------------------------------------------
/widget/src/screens/Thread/views/MessageThread/MessageThread.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { LoadingState } from '@comba.se/ui';
3 | import Chat from '@comba.se/chat';
4 |
5 | // Hooks //
6 | import useAuth from 'hooks/useAuth';
7 |
8 | // HOCs //
9 | import withChat from '@comba.se/chat/hocs/withChat';
10 |
11 | const MessageThread = (props) => {
12 | const {
13 | channel,
14 | history,
15 | isPartnerTyping,
16 | match,
17 | loading,
18 | loadMoreMessages,
19 | messages,
20 | partner,
21 | read
22 | } = props;
23 | const [{ user }] = useAuth();
24 | const markRead = useCallback(async () => {
25 | if (channel) {
26 | await channel.markRead();
27 | }
28 | }, [channel]);
29 |
30 | useEffect(() => {
31 | if (match && match.params.channel) {
32 | markRead();
33 | }
34 | }, [channel, match, markRead]);
35 |
36 | const onSend = useCallback(
37 | messages => {
38 | channel.sendMessage(messages[0]);
39 | },
40 | [channel]
41 | );
42 |
43 | if (loading) {
44 | return
45 | }
46 |
47 | return (
48 |
60 | );
61 | };
62 |
63 | export default withChat(MessageThread);
--------------------------------------------------------------------------------
/dashboard/src/components/AvailabilityField/AvailabilityDay.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Text } from '@comba.se/ui';
5 |
6 | // Components //
7 | const Root = styled.div`
8 | width: 40px;
9 | height: 40px;
10 | border-radius: 50%;
11 | background-color: ${({ enabled, theme }) => enabled ? theme.color.primary : 'transparent'};
12 | border: 1px solid ${({ enabled, theme }) => enabled ? theme.color.primary : theme.color.border};
13 | justify-content: center;
14 | align-items: center;
15 | cursor: pointer;
16 |
17 | & > ${Text} {
18 | user-select: none;
19 | text-transform: uppercase;
20 | }
21 | `;
22 |
23 | const AvailabilityDay = ({ enabled, day, days, onChange }) => {
24 | const onClick = useCallback(() => {
25 | onChange({
26 | target: {
27 | name: 'availability',
28 | value: {
29 | ...days,
30 | [day]: {
31 | ...days[day],
32 | enabled: !enabled
33 | }
34 | }
35 | }
36 | });
37 | }, [days, day, enabled, onChange]);
38 | return (
39 |
40 | {day.charAt(0)}
41 |
42 | );
43 | }
44 |
45 | AvailabilityDay.propTypes = {
46 | enabled: PropTypes.bool,
47 | day: PropTypes.string,
48 | days: PropTypes.object,
49 | onChange: PropTypes.func,
50 | };
51 |
52 | export default AvailabilityDay;
--------------------------------------------------------------------------------
/dashboard/src/hooks/useChats.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useState } from "react";
2 | import { useChannels } from "stream-chat-hooks";
3 | import useAuth from "hooks/useAuth";
4 | import request from "utils/request";
5 |
6 | export default () => {
7 | const [{ organization, user }] = useAuth();
8 | const [loading, setLoading] = useState(true);
9 | const [error, setError] = useState(false);
10 | const filter = useMemo(() => ({ organization: organization._id, members: { $in: [user._id] } }), [organization, user]);
11 | const [channels, { loading: channelsLoading }] = useChannels(user._id, filter);
12 | const [chats, setChats] = useState(
13 | JSON.parse(localStorage.getItem("chats")) || []
14 | );
15 |
16 | const getChats = useCallback(async () => {
17 | if (channelsLoading) {
18 | return;
19 | }
20 |
21 | if (channels.length) {
22 | try {
23 | setLoading(true);
24 | const data = await request(
25 | `v1/chats?refs.agents.assignee.agent._id=${user._id}`,
26 | "get",
27 | null,
28 | user.tokens.api
29 | );
30 |
31 | const chatData = data.map(chat => ({
32 | ...chat,
33 | channel: channels.find(({ id }) => id === chat._id)
34 | }));
35 |
36 | setChats(chatData);
37 | setLoading(false);
38 | } catch (error) {
39 | setError(true);
40 | setLoading(false);
41 | }
42 | } else {
43 | setLoading(false);
44 | }
45 | }, [user._id, user.tokens.api, channels, channelsLoading]);
46 | useEffect(() => {
47 | getChats();
48 | }, [getChats]);
49 | return [chats, { loading, error }];
50 | };
51 |
--------------------------------------------------------------------------------
/dashboard/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import moment from "moment";
3 |
4 | // Router //
5 | import { Router, Switch } from "react-router-dom";
6 | import history from "utils/history";
7 |
8 | // Styles //
9 | import { ThemeSwitcher } from 'contexts/ThemeSwitcher';
10 | import GlobalStyles from "styles/global";
11 |
12 | // Context //
13 | import AuthProvider from "contexts/Auth/AuthProvider";
14 | import SnackbarProvider from "contexts/Snackbar/SnackbarProvider";
15 |
16 | // Screens //
17 | import Auth from "screens/Auth";
18 | import Dashboard from "screens/Dashboard";
19 | import Welcome from "screens/Welcome";
20 |
21 | // Components //
22 | import AuthedRoute from "components/AuthedRoute";
23 | import OrgProtectedRoute from "components/OrgProtectedRoute";
24 | import UnauthedRoute from "components/UnauthedRoute";
25 |
26 | moment.updateLocale("en", {
27 | calendar: {
28 | lastDay: "[Yesterday at] h:mma",
29 | sameDay: "h:mma",
30 | nextDay: "[Tomorrow at] h:mma",
31 | lastWeek: "MMMM Do YYYY",
32 | nextWeek: "MMMM Do YYYY",
33 | sameElse: "L"
34 | }
35 | });
36 |
37 | function App() {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/chat/destroy.action.js:
--------------------------------------------------------------------------------
1 | // import Chat from 'models/chat';
2 |
3 | // exports.destroy = async (req, res) => {
4 | // try {
5 | // const data = { ...req.body, ...req.params };
6 | // const { serialized } = req;
7 |
8 | // if (serialized.role !== 'admin') {
9 | // return res.status(403).json({
10 | // status: 'Invalid permissions to view or modify this resource.'
11 | // });
12 | // }
13 |
14 | // await Chat.updateOne({ _id: data.chat }, { $push: { status: { type: 'Archived', timestamp: Date.now() } } });
15 |
16 | // res.sendStatus(204);
17 | // } catch (error) {
18 | // console.error(error);
19 | // res.status(500).json({ error: error.message });
20 | // }
21 | // };
22 |
23 |
24 | //
25 | //
26 | // Permanently Delete the Chat data from Mongo AND Stream Chat
27 | //
28 | //
29 |
30 | import Chat from 'models/chat';
31 | import StreamClient from 'utils/stream';
32 |
33 | exports.destroy = async (req, res) => {
34 | try {
35 | const data = { ...req.body, ...req.params };
36 | const { serialized } = req;
37 |
38 | // if (serialized.role !== 'admin') {
39 | // return res.status(403).json({
40 | // status: 'Invalid permissions to view or modify this resource.'
41 | // });
42 | // }
43 | console.log('chat', data.chat)
44 | const { client } = await StreamClient();
45 | await Chat.findByIdAndRemove(data.chat);
46 | const channel = client.channel('commerce', data.chat);
47 | await channel.delete();
48 | // await Chat.updateOne({ _id: data.chat }, { $push: { status: { type: 'Archived', timestamp: Date.now() } } });
49 |
50 | res.sendStatus(204);
51 | } catch (error) {
52 | console.error(error);
53 | res.status(500).json({ error: error.message });
54 | }
55 | };
56 |
57 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/useAgents.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 |
3 | // Utils //
4 | import request from "utils/request";
5 |
6 | // Hooks //
7 | import useAuth from "hooks/useAuth";
8 |
9 | export default () => {
10 | const [loading, setLoading] = useState(false);
11 | const [tabs, setTabs] = useState(["All"]);
12 | const [agents, setAgents] = useState(
13 | JSON.parse(localStorage.getItem("agents")) || []
14 | );
15 | const [{ user }] = useAuth();
16 |
17 | const refetchAgents = useCallback(async () => {
18 | const agents = await request(`v1/agents?refs.organization._id=${user.refs.organization._id}`, "get", null, user.tokens.api);
19 | localStorage.setItem("agents", JSON.stringify(agents));
20 | setAgents(agents);
21 | }, [user]);
22 |
23 | const getAgents = useCallback(async () => {
24 | try {
25 | setLoading(true);
26 | const agents = await request(
27 | `v1/agents?refs.organization._id=${user.refs.organization._id}`,
28 | "get",
29 | null,
30 | user.tokens.api
31 | );
32 | setAgents(agents);
33 | localStorage.setItem("agents", JSON.stringify(agents));
34 | setTabs([
35 | ...new Set([
36 | "All",
37 | ...agents
38 | .reduce((acc, { role }) => {
39 | return [...acc, role];
40 | }, [])
41 | .sort()
42 | ])
43 | ]);
44 | setLoading(false);
45 | } catch (error) {
46 | // TODO: Error Handling
47 | console.log(error);
48 | }
49 | }, [user.tokens.api, user.refs.organization._id]);
50 | useEffect(() => {
51 | getAgents();
52 | }, [getAgents]);
53 |
54 | return [agents, tabs, { loading, refetchAgents }];
55 | };
56 |
--------------------------------------------------------------------------------
/api/src/controllers/v1/auth/login.action.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import { StreamChat } from 'stream-chat';
3 | import bcrypt from 'bcrypt';
4 | import jwt from 'jsonwebtoken';
5 |
6 | import Agent from 'models/agent';
7 | import StreamClient from 'utils/stream';
8 |
9 | exports.login = async (req, res) => {
10 | try {
11 | // extract json from body
12 | const data = req.body;
13 |
14 | // if the agent does not exist, create a new agent
15 | let agent = await Agent.findOne({ email: data.email }).lean({
16 | autopopulate: true
17 | });
18 |
19 | // if the agent does not exist
20 | if (!agent) {
21 | // return the response
22 | return res.sendStatus(404);
23 | }
24 |
25 | // validate that the provided password matches the hashed password stored in the database
26 | const match = await bcrypt.compare(data.password, agent.password);
27 |
28 | // if the password does not match, throw a 403 forbidden error status code
29 | if (!match) {
30 | return res.sendStatus(403);
31 | }
32 |
33 | // sanitize / remove password
34 | delete agent.password;
35 |
36 | const { key, secret } = await StreamClient();
37 |
38 | const client = new StreamChat(key, secret);
39 | const streamToken = client.createToken(agent._id.toString());
40 |
41 | // jwt token generation (for api)
42 | const apiToken = jwt.sign(
43 | {
44 | sub: agent._id,
45 | role: agent.role
46 | },
47 | process.env.AUTH_SECRET
48 | );
49 |
50 | // return the response with user data, token, and api key
51 | return res.json({
52 | ...agent,
53 | tokens: {
54 | api: apiToken,
55 | stream: streamToken
56 | }
57 | });
58 | } catch (error) {
59 | console.error(error);
60 | res.status(500).json({ error: error.message });
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/dashboard/src/screens/Dashboard/views/Plugins/Plugins.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Route } from 'react-router-dom';
4 | import { Container } from "@comba.se/ui";
5 | import { PluginsIcon } from "@comba.se/ui/Icons";
6 |
7 | // Contexts //
8 | import PluginsContext from 'contexts/Plugins';
9 |
10 | // Hooks //
11 | import useActivePlugins from 'hooks/useActivePlugins';
12 |
13 | // Views //
14 | import PluginDetail from './views/PluginDetail';
15 |
16 | // Components ///
17 | import ScreenRoot from 'shared/ScreenRoot';
18 | import FullScreenHeader from 'components/FullScreenHeader';
19 | import PluginsList from 'components/PluginsList';
20 |
21 | const Root = styled(ScreenRoot)`
22 | flex: 1;
23 | padding-bottom: 40px;
24 | overflow-y: scroll;
25 | @media (min-width: ${({ theme }) => theme.breakpoints.sm}px) {
26 | overflow-y: scroll;
27 | }
28 | `;
29 |
30 | const renderPluginModal = props => ;
31 |
32 | export default ({ match }) => {
33 | const activePlugins = useActivePlugins();
34 | return (
35 |
36 |
37 |
43 |
44 |
45 |
46 |
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/dashboard/src/hooks/usePageSheet.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import getNestedProperty from 'utils/getNestedProperty';
3 |
4 | export default (data, searchKey, tabKey) => {
5 | const [query, setQuery] = useState('');
6 | const [activeTab, setActiveTab] = useState('All');
7 | const [results, setResults] = useState(data);
8 |
9 | useEffect(() => {
10 | setResults(
11 | data.filter(item => {
12 | let searchTerm = '';
13 | if (typeof searchKey !== 'string') {
14 | searchTerm = searchKey
15 | .map(key => getNestedProperty(item, key))
16 | .join(' ');
17 | } else {
18 | searchTerm = getNestedProperty(item, searchKey);
19 | }
20 |
21 | if (activeTab !== 'All') {
22 | return (
23 | searchTerm
24 | .toLowerCase()
25 | .includes(query.trim().toLowerCase()) &&
26 | getNestedProperty(item, tabKey) === activeTab
27 | );
28 | } else {
29 | if (typeof searchKey === 'string') {
30 | return searchTerm
31 | .toLowerCase()
32 | .includes(query.trim().toLowerCase());
33 | } else {
34 | return searchTerm
35 | .toLowerCase()
36 | .includes(query.trim().toLowerCase());
37 | }
38 | }
39 | })
40 | );
41 | }, [data, query, activeTab, searchKey, tabKey]);
42 |
43 | return [results, setQuery, activeTab, setActiveTab];
44 | };
45 |
--------------------------------------------------------------------------------