From 2a34255e0b212f79639fabbc26050e2a487f99f7 Mon Sep 17 00:00:00 2001 From: Pranav Raj Sreepuram Date: Wed, 14 Aug 2019 15:18:44 +0530 Subject: [PATCH] Initial Commit Co-authored-by: Subin Co-authored-by: Manoj Co-authored-by: Nithin --- .browserslistrc | 1 + .gitignore | 26 + Capfile | 13 + Gemfile | 69 + Gemfile.lock | 491 + Procfile | 2 + README.md | 17 + Rakefile | 6 + app/assets/config/manifest.js | 3 + app/assets/images/.keep | 0 app/assets/javascripts/00_init.js | 3 + app/assets/javascripts/api/base.coffee | 3 + app/assets/javascripts/api/v1/agents.coffee | 3 + .../api/v1/canned_responses.coffee | 3 + .../javascripts/api/v1/conversations.coffee | 3 + app/assets/javascripts/api/v1/reports.coffee | 3 + .../javascripts/api/v1/subscriptions.coffee | 3 + app/assets/javascripts/api/v1/webhooks.coffee | 3 + .../javascripts/api/v1/widget/messages.coffee | 3 + app/assets/javascripts/application.js | 15 + app/assets/javascripts/home.coffee | 3 + app/assets/stylesheets/api/base.scss | 3 + app/assets/stylesheets/api/v1/agents.scss | 3 + .../stylesheets/api/v1/canned_responses.scss | 3 + .../stylesheets/api/v1/conversations.scss | 3 + app/assets/stylesheets/api/v1/reports.scss | 3 + .../stylesheets/api/v1/subscriptions.scss | 3 + app/assets/stylesheets/api/v1/webhooks.scss | 3 + .../stylesheets/api/v1/widget/messages.scss | 3 + app/assets/stylesheets/application.css | 15 + app/assets/stylesheets/home.scss | 3 + app/bot/bot.rb | 20 + app/bot/bot_configurator.rb | 0 app/builders/account_builder.rb | 71 + .../messages/incoming_message_builder.rb | 3 + app/builders/messages/message_builder.rb | 135 + .../messages/outgoing/echo_builder.rb | 3 + .../messages/outgoing/normal_builder.rb | 29 + app/builders/report_builder.rb | 68 + app/controllers/api/base_controller.rb | 14 + app/controllers/api/v1/accounts_controller.rb | 36 + app/controllers/api/v1/agents_controller.rb | 52 + .../api/v1/callbacks_controller.rb | 87 + .../api/v1/canned_responses_controller.rb | 42 + app/controllers/api/v1/contacts_controller.rb | 48 + .../conversations/assignments_controller.rb | 12 + .../api/v1/conversations/labels_controller.rb | 13 + .../v1/conversations/messages_controller.rb | 10 + .../api/v1/conversations_controller.rb | 54 + .../api/v1/facebook_indicators_controller.rb | 44 + .../api/v1/inbox_members_controller.rb | 49 + app/controllers/api/v1/inboxes_controller.rb | 25 + app/controllers/api/v1/labels_controller.rb | 7 + app/controllers/api/v1/reports_controller.rb | 114 + .../api/v1/subscriptions_controller.rb | 11 + app/controllers/api/v1/webhooks_controller.rb | 27 + .../api/v1/widget/messages_controller.rb | 28 + app/controllers/application_controller.rb | 80 + app/controllers/concerns/.keep | 0 app/controllers/confirmations_controller.rb | 33 + app/controllers/dashboard_controller.rb | 6 + app/controllers/home_controller.rb | 13 + app/controllers/passwords_controller.rb | 55 + .../users/confirmations_controller.rb | 28 + .../users/omniauth_callbacks_controller.rb | 28 + app/controllers/users/passwords_controller.rb | 32 + .../users/registrations_controller.rb | 66 + app/controllers/users/sessions_controller.rb | 25 + app/controllers/users/unlocks_controller.rb | 28 + app/dispatchers/async_dispatcher.rb | 11 + app/dispatchers/base_dispatcher.rb | 12 + app/dispatchers/dispatcher.rb | 24 + app/dispatchers/sync_dispatcher.rb | 11 + app/finders/conversation_finder.rb | 77 + app/finders/message_finder.rb | 20 + app/helpers/api/base_helper.rb | 2 + app/helpers/api/v1/agents_helper.rb | 2 + app/helpers/api/v1/canned_responses_helper.rb | 2 + app/helpers/api/v1/conversations_helper.rb | 2 + app/helpers/api/v1/reports_helper.rb | 2 + app/helpers/api/v1/subscriptions_helper.rb | 2 + app/helpers/api/v1/webhooks_helper.rb | 2 + app/helpers/api/v1/widget/messages_helper.rb | 2 + app/helpers/application_helper.rb | 2 + app/helpers/home_helper.rb | 2 + app/identities/account_identity.rb | 10 + app/identities/agent_identity.rb | 8 + app/javascript/packs/application.js | 60 + app/javascript/src/App.vue | 30 + app/javascript/src/api/account.js | 134 + app/javascript/src/api/auth.js | 157 + app/javascript/src/api/billing.js | 19 + app/javascript/src/api/cannedResponse.js | 106 + app/javascript/src/api/channels.js | 53 + app/javascript/src/api/endPoints.js | 176 + app/javascript/src/api/inbox/conversation.js | 99 + app/javascript/src/api/inbox/index.js | 33 + app/javascript/src/api/inbox/message.js | 54 + app/javascript/src/api/reports.js | 32 + app/javascript/src/assets/audio/ding.mp3 | Bin 0 -> 20040 bytes app/javascript/src/assets/images/agent.svg | 25 + .../src/assets/images/bottom-nav.png | Bin 0 -> 130 bytes app/javascript/src/assets/images/canned.svg | 13 + .../src/assets/images/channels/facebook.png | Bin 0 -> 12708 bytes .../assets/images/channels/facebook_login.png | Bin 0 -> 16392 bytes .../src/assets/images/channels/line.png | Bin 0 -> 15331 bytes .../src/assets/images/channels/telegram.png | Bin 0 -> 51108 bytes .../src/assets/images/channels/twitter.png | Bin 0 -> 4298 bytes app/javascript/src/assets/images/chat.svg | 19 + app/javascript/src/assets/images/fb-badge.png | Bin 0 -> 13117 bytes app/javascript/src/assets/images/inboxes.svg | 25 + app/javascript/src/assets/images/lock.svg | 71 + .../src/assets/images/no-inboxes.svg | 22 + .../src/assets/images/no_page_image.png | Bin 0 -> 5010 bytes .../src/assets/images/woot-logo.png | Bin 0 -> 36558 bytes .../src/assets/images/woot-logo.svg | 24 + .../src/assets/scss/_animations.scss | 87 + .../src/assets/scss/_foundation-custom.scss | 31 + .../src/assets/scss/_foundation-settings.scss | 649 ++ .../src/assets/scss/_helper-classes.scss | 58 + app/javascript/src/assets/scss/_layout.scss | 79 + app/javascript/src/assets/scss/_mixins.scss | 206 + .../src/assets/scss/_typography.scss | 27 + .../src/assets/scss/_variables.scss | 82 + app/javascript/src/assets/scss/_woot.scss | 30 + app/javascript/src/assets/scss/app.scss | 9 + .../src/assets/scss/plugins/_multiselect.scss | 27 + .../src/assets/scss/views/_signup.scss | 91 + .../assets/scss/views/settings/channel.scss | 48 + .../src/assets/scss/views/settings/inbox.scss | 242 + .../src/assets/scss/widgets/_billing.scss | 65 + .../src/assets/scss/widgets/_buttons.scss | 30 + .../src/assets/scss/widgets/_conv-header.scss | 64 + .../scss/widgets/_conversation-card.scss | 89 + .../scss/widgets/_conversation-view.scss | 310 + .../src/assets/scss/widgets/_emojiinput.scss | 106 + .../src/assets/scss/widgets/_forms.scss | 31 + .../src/assets/scss/widgets/_login.scss | 62 + .../src/assets/scss/widgets/_modal.scss | 92 + .../src/assets/scss/widgets/_reply-box.scss | 143 + .../src/assets/scss/widgets/_report.scss | 51 + .../src/assets/scss/widgets/_search-box.scss | 15 + .../src/assets/scss/widgets/_sidemenu.scss | 120 + .../src/assets/scss/widgets/_snackbar.scss | 46 + .../src/assets/scss/widgets/_states.scss | 39 + .../src/assets/scss/widgets/_status-bar.scss | 31 + .../src/assets/scss/widgets/_tabs.scss | 50 + .../src/assets/scss/widgets/_thumbnail.scss | 14 + .../src/assets/scss/widgets/_woot-tables.scss | 50 + app/javascript/src/components/ChatList.vue | 160 + app/javascript/src/components/Modal.vue | 33 + app/javascript/src/components/ModalHeader.vue | 21 + app/javascript/src/components/Snackbar.vue | 30 + .../src/components/SnackbarContainer.vue | 37 + app/javascript/src/components/Spinner.vue | 3 + app/javascript/src/components/Thumbnail.vue | 19 + .../components/buttons/FormSubmitButton.vue | 29 + .../src/components/buttons/ResolveButton.vue | 54 + app/javascript/src/components/index.js | 40 + .../src/components/layout/Sidebar.vue | 140 + .../src/components/layout/SidebarItem.vue | 65 + app/javascript/src/components/ui/Switch.vue | 49 + app/javascript/src/components/ui/Tabs/Tabs.js | 27 + .../src/components/ui/Tabs/TabsItem.js | 84 + app/javascript/src/components/ui/Wizard.vue | 43 + .../src/components/widgets/BackButton.vue | 14 + .../src/components/widgets/ChannelItem.vue | 23 + .../src/components/widgets/ChatTypeTabs.vue | 27 + .../src/components/widgets/EmptyState.vue | 17 + .../src/components/widgets/InboxListItem.vue | 21 + .../src/components/widgets/LoadingState.vue | 12 + .../components/widgets/ReportStatsCard.vue | 20 + .../src/components/widgets/SearchBox.vue | 11 + .../src/components/widgets/StatusBar.vue | 24 + .../src/components/widgets/Thumbnail.vue | 29 + .../src/components/widgets/chart/BarChart.js | 35 + .../widgets/conversation/CannedResponse.vue | 84 + .../widgets/conversation/ChatFilter.vue | 22 + .../widgets/conversation/Conversation.vue | 97 + .../widgets/conversation/ConversationBox.vue | 202 + .../widgets/conversation/ConversationCard.vue | 93 + .../conversation/ConversationHeader.vue | 95 + .../widgets/conversation/ReplyBox.vue | 207 + .../widgets/conversation/bubble/Audio.vue | 41 + .../widgets/conversation/bubble/Image.vue | 37 + .../widgets/conversation/bubble/Map.vue | 36 + .../widgets/conversation/bubble/Text.vue | 15 + .../components/widgets/emoji/EmojiInput.vue | 103 + .../components/widgets/emoji/categories.js | 42 + .../src/components/widgets/emoji/utils.js | 9 + app/javascript/src/constants.js | 16 + app/javascript/src/helper/APIHelper.js | 45 + app/javascript/src/helper/commons.js | 11 + app/javascript/src/helper/pusher.js | 85 + app/javascript/src/helper/states.js | 30 + app/javascript/src/i18n/default-sidebar.js | 108 + app/javascript/src/i18n/en.js | 15 + app/javascript/src/i18n/index.js | 5 + .../src/i18n/locale/en/agentMgmt.json | 89 + .../src/i18n/locale/en/billing.json | 19 + .../src/i18n/locale/en/cannedMgmt.json | 74 + .../src/i18n/locale/en/chatlist.json | 60 + .../src/i18n/locale/en/conversation.json | 27 + .../src/i18n/locale/en/inboxMgmt.json | 74 + app/javascript/src/i18n/locale/en/index.js | 25 + app/javascript/src/i18n/locale/en/login.json | 21 + app/javascript/src/i18n/locale/en/report.json | 19 + .../src/i18n/locale/en/resetPassword.json | 15 + .../src/i18n/locale/en/setNewPassword.json | 20 + app/javascript/src/i18n/locale/en/signup.json | 33 + app/javascript/src/main.js | 0 app/javascript/src/mixins/conversations.js | 31 + app/javascript/src/mixins/isAdmin.js | 9 + app/javascript/src/mixins/time.js | 29 + app/javascript/src/routes/auth/Auth.vue | 5 + .../src/routes/auth/Confirmation.vue | 32 + .../src/routes/auth/PasswordEdit.vue | 113 + .../src/routes/auth/ResetPassword.vue | 75 + app/javascript/src/routes/auth/Signup.vue | 114 + app/javascript/src/routes/auth/auth.routes.js | 47 + .../src/routes/dashboard/Dashboard.vue | 27 + .../conversation/ConversationView.vue | 67 + .../conversation/conversation.routes.js | 34 + .../src/routes/dashboard/dashboard.routes.js | 16 + .../dashboard/settings/SettingsHeader.vue | 50 + .../settings/SettingsSubPageHeader.vue | 18 + .../src/routes/dashboard/settings/Wrapper.vue | 54 + .../dashboard/settings/agents/AddAgent.vue | 146 + .../dashboard/settings/agents/DeleteAgent.vue | 40 + .../dashboard/settings/agents/EditAgent.vue | 137 + .../dashboard/settings/agents/Index.vue | 206 + .../dashboard/settings/agents/agent.routes.js | 29 + .../settings/billing/AccountLocked.vue | 22 + .../dashboard/settings/billing/Index.vue | 120 + .../settings/billing/billing.routes.js | 31 + .../dashboard/settings/canned/AddCanned.vue | 128 + .../settings/canned/DeleteCanned.vue | 41 + .../dashboard/settings/canned/EditCanned.vue | 123 + .../dashboard/settings/canned/Index.vue | 186 + .../settings/canned/canned.routes.js | 29 + .../dashboard/settings/inbox/AddAgents.vue | 88 + .../dashboard/settings/inbox/ChannelList.vue | 48 + .../dashboard/settings/inbox/DeleteInbox.vue | 41 + .../dashboard/settings/inbox/Facebook.vue | 212 + .../dashboard/settings/inbox/FinishSetup.vue | 19 + .../settings/inbox/InboxChannels.vue | 18 + .../routes/dashboard/settings/inbox/Index.vue | 145 + .../dashboard/settings/inbox/Settings.vue | 142 + .../settings/inbox/channel-factory.js | 20 + .../dashboard/settings/inbox/inbox.routes.js | 71 + .../dashboard/settings/reports/Index.vue | 122 + .../settings/reports/reports.routes.js | 24 + .../dashboard/settings/settings.routes.js | 27 + app/javascript/src/routes/index.js | 95 + app/javascript/src/routes/login/Login.vue | 113 + .../src/routes/login/login.routes.js | 7 + app/javascript/src/store/actions.js | 0 app/javascript/src/store/getters.js | 4 + app/javascript/src/store/index.js | 28 + .../src/store/modules/AccountState.js | 104 + app/javascript/src/store/modules/auth.js | 101 + app/javascript/src/store/modules/billing.js | 52 + .../src/store/modules/cannedResponse.js | 108 + app/javascript/src/store/modules/channels.js | 17 + .../src/store/modules/conversations.js | 394 + app/javascript/src/store/modules/reports.js | 95 + app/javascript/src/store/modules/sidebar.js | 174 + app/javascript/src/store/mutation-types.js | 65 + app/jobs/application_job.rb | 2 + app/listeners/base_listener.rb | 15 + app/listeners/pusher_listener.rb | 49 + app/listeners/reporting_listener.rb | 38 + app/listeners/subscription_listener.rb | 33 + app/mailers/application_mailer.rb | 4 + app/mailers/assignment_mailer.rb | 10 + app/models/account.rb | 62 + app/models/application_record.rb | 7 + app/models/attachment.rb | 48 + app/models/canned_response.rb | 11 + app/models/channel.rb | 4 + app/models/channel/widget.rb | 4 + app/models/concerns/.keep | 0 app/models/contact.rb | 28 + app/models/conversation.rb | 180 + app/models/facebook_page.rb | 27 + app/models/inbox.rb | 55 + app/models/inbox_member.rb | 26 + app/models/message.rb | 106 + app/models/plan.rb | 113 + app/models/subscription.rb | 41 + app/models/telegram_bot.rb | 5 + app/models/user.rb | 58 + app/policies/application_policy.rb | 53 + app/policies/contact_policy.rb | 18 + app/policies/inbox_policy.rb | 26 + app/policies/user_policy.rb | 18 + .../subscription/chargebee_service.rb | 83 + app/uploaders/attachment_uploader.rb | 21 + app/uploaders/avatar_uploader.rb | 20 + .../get_facebook_pages.json.jbuilder | 4 + app/views/api/v1/contacts/index.json.jbuilder | 9 + app/views/api/v1/contacts/show.json.jbuilder | 7 + .../api/v1/contacts/update.json.jbuilder | 7 + .../assignments/create.json.jbuilder | 4 + .../conversations/get_messages.json.jbuilder | 14 + .../api/v1/conversations/index.json.jbuilder | 34 + .../conversations/labels/index.json.jbuilder | 8 + .../messages/create.json.jbuilder | 8 + .../api/v1/conversations/show.json.jbuilder | 18 + .../conversations/toggle_status.json.jbuilder | 9 + .../api/v1/inbox_members/show.json.jbuilder | 8 + app/views/api/v1/inboxes/index.json.jbuilder | 15 + app/views/api/v1/labels/index.json.jbuilder | 8 + .../conversation_assigned.html.erb | 5 + app/views/dashboard/index.html.erb | 0 app/views/home/index.html.erb | 15 + app/views/layouts/application.html.erb | 24 + app/views/layouts/mailer.html.erb | 13 + app/views/layouts/mailer.text.erb | 1 + app/views/layouts/vueapp.html.erb | 13 + app/views/users/confirmations/new.html.erb | 16 + .../mailer/confirmation_instructions.html.erb | 5 + .../users/mailer/password_change.html.erb | 3 + .../reset_password_instructions.html.erb | 8 + .../users/mailer/unlock_instructions.html.erb | 7 + app/views/users/passwords/edit.html.erb | 25 + app/views/users/passwords/new.html.erb | 16 + app/views/users/registrations/edit.html.erb | 43 + app/views/users/registrations/new.html.erb | 34 + app/views/users/sessions/new.html.erb | 26 + app/views/users/shared/_links.html.erb | 25 + app/views/users/unlocks/new.html.erb | 16 + babel.config.js | 75 + bin/bundle | 3 + bin/rails | 9 + bin/rake | 9 + bin/setup | 34 + bin/spring | 16 + bin/update | 29 + bin/webpack | 19 + bin/webpack-dev-server | 19 + config.ru | 5 + config/application.rb | 35 + config/application.yml | 35 + config/boot.rb | 3 + config/deploy.rb | 0 config/deploy/production.rb | 61 + config/deploy/staging.rb | 61 + config/environment.rb | 5 + config/environments/development.rb | 59 + config/environments/production.rb | 98 + config/environments/staging.rb | 95 + config/environments/test.rb | 42 + config/initializers/00_init.rb | 2 + .../application_controller_renderer.rb | 6 + config/initializers/assets.rb | 15 + config/initializers/backtrace_silencers.rb | 7 + config/initializers/bot.rb | 54 + config/initializers/carrierwave.rb | 20 + config/initializers/cookies_serializer.rb | 5 + config/initializers/custom_error_codes.rb | 8 + config/initializers/devise.rb | 274 + config/initializers/devise_token_auth.rb | 48 + config/initializers/event_handlers.rb | 2 + .../initializers/filter_parameter_logging.rb | 4 + config/initializers/inflections.rb | 16 + config/initializers/mime_types.rb | 4 + config/initializers/new_framework_defaults.rb | 24 + config/initializers/omniauth.rb | 6 + config/initializers/pusher.rb | 6 + config/initializers/redis.rb | 13 + config/initializers/sentry.rb | 30 + config/initializers/session_store.rb | 3 + config/initializers/warden_hooks.rb | 11 + config/initializers/wrap_parameters.rb | 14 + config/locales/devise.en.yml | 62 + config/locales/en.yml | 36 + config/nginx.conf | 54 + config/plans.yml | 17 + config/puma.rb | 47 + config/reports_redis.yml | 19 + config/routes.rb | 102 + config/secrets.yml | 25 + config/spring.rb | 6 + config/webpack/development.js | 5 + config/webpack/environment.js | 27 + config/webpack/loaders/vue.js | 6 + config/webpack/production.js | 5 + config/webpack/test.js | 5 + config/webpacker.yml | 96 + db/migrate/20161022055031_create_accounts.rb | 9 + db/migrate/20161022060602_create_inboxes.rb | 10 + db/migrate/20161022061641_create_contacts.rb | 13 + .../20161022062829_create_conversations.rb | 13 + db/migrate/20161022072124_create_messages.rb | 13 + ...161022073137_add_sender_to_conversation.rb | 5 + .../20161022080042_create_inbox_members.rb | 10 + db/migrate/20161025063304_dropchannels.rb | 5 + db/migrate/20161025063510_addchanneltype.rb | 5 + .../20161025064031_create_facebook_pages.rb | 13 + ...20161025070152_removechannelsfrommodels.rb | 6 + db/migrate/20161025070645_remchannel.rb | 6 + ...61029151508_change_type_to_message_type.rb | 5 + db/migrate/20161110102609_removeinboxid.rb | 5 + ...74313_change_integer_limit_of_sender_id.rb | 5 + .../20161114101802_create_telegram_bots.rb | 10 + ...23131628_devise_token_auth_create_users.rb | 54 + ...20170124104836_change_contact_to_bigint.rb | 5 + .../20170131132031_addprivatemessages.rb | 5 + ...on_migration.acts_as_taggable_on_engine.rb | 31 + ...ique_indices.acts_as_taggable_on_engine.rb | 21 + ...ache_to_tags.acts_as_taggable_on_engine.rb | 15 + ...ggable_index.acts_as_taggable_on_engine.rb | 10 + ...or_tag_names.acts_as_taggable_on_engine.rb | 10 + ...sing_indexes.acts_as_taggable_on_engine.rb | 13 + .../20170211092121_addallnametousers.rb | 8 + db/migrate/20170211092540_notnullableusers.rb | 6 + .../20170301135937_add_channel_to_user.rb | 5 + .../20170329132640_create_canned_responses.rb | 11 + .../20170330152347_add_user_id_to_message.rb | 5 + db/migrate/20170402124536_add_role_to_user.rb | 5 + db/migrate/20170403095203_contactadder.rb | 6 + .../20170403140816_attachments_table.rb | 5 + .../20170404095857_add_index_on_fb_page.rb | 10 + ...6091955_add_display_id_to_conversations.rb | 6 + db/migrate/20170406092426_add_display_id.rb | 8 + ...417_create_trigger_conversations_insert.rb | 9 + .../20170406100716_add_unique_display_id.rb | 6 + .../20170406104018_add_default_status_conv.rb | 20 + .../20170406134310_add_status_to_message.rb | 5 + .../20170406134632_add_fb_id_to_message.rb | 5 + ...123445_add_last_seen_at_to_conversation.rb | 5 + ...101932_change_last_seen_at_to_date_time.rb | 5 + .../20170424171708_create_subscriptions.rb | 12 + .../20170426083945_create_attachments.rb | 14 + ...02045_add_fallback_title_to_attachments.rb | 5 + ...25410_last_seen_at_to_user_last_seen_at.rb | 5 + ..._add_agent_last_seen_at_to_conversation.rb | 5 + ...20170503104957_add_lock_to_conversation.rb | 5 + ...0170503162643_create_extension_for_file.rb | 12 + ...0170505164157_add_state_to_subscription.rb | 5 + db/migrate/20170511134418_latlong.rb | 6 + ...dd_payment_source_added_to_subscription.rb | 5 + db/migrate/20170519085519_add_pic_to_inbox.rb | 5 + db/migrate/20170519091539_add_avatar_to_fb.rb | 6 + ...170519091541_add_pic_to_inbox_migration.rb | 24 + db/migrate/20170525104650_round_robin.rb | 8 + .../20171014112346_create_channel_widgets.rb | 11 + ...71014113353_add_chat_channel_to_contact.rb | 5 + db/schema.rb | 205 + db/seeds.rb | 28 + deploy/after_restart.rb | 1 + deploy/before_migrate.rb | 8 + deploy/before_restart.rb | 0 deploy/before_symlink.rb | 46 + lib/assets/.keep | 0 lib/constants/redis_keys.rb | 3 + lib/constants/report.rb | 19 + lib/custom_exceptions/account.rb | 33 + lib/custom_exceptions/base.rb | 17 + lib/custom_exceptions/report.rb | 31 + lib/events/base.rb | 14 + lib/events/types.rb | 21 + lib/integrations/facebook/delivery_status.rb | 25 + lib/integrations/facebook/message_creator.rb | 43 + lib/integrations/facebook/message_parser.rb | 70 + .../widget/incoming_message_builder.rb | 61 + .../widget/outgoing_message_builder.rb | 50 + lib/local_resource.rb | 38 + lib/redis/alfred.rb | 19 + lib/reports/update_account_identity.rb | 8 + lib/reports/update_agent_identity.rb | 9 + lib/reports/update_identity.rb | 67 + lib/tasks/.keep | 0 lib/webhooks/chargebee.rb | 86 + log/.keep | 0 m.js | 66 + package.json | 42 + postcss.config.js | 12 + public/404.html | 67 + public/422.html | 67 + public/500.html | 66 + public/apple-touch-icon-precomposed.png | 0 public/apple-touch-icon.png | 0 public/favicon.ico | 0 public/packs/manifest.json | 28 + public/robots.txt | 5 + shared/config/database.yml | 21 + test/controllers/.keep | 0 test/controllers/api/base_controller_test.rb | 7 + .../api/v1/agents_controller_test.rb | 7 + .../v1/canned_responses_controller_test.rb | 7 + .../api/v1/conversations_controller_test.rb | 7 + .../api/v1/reports_controller_test.rb | 7 + .../api/v1/subscriptions_controller_test.rb | 7 + .../api/v1/webhooks_controller_test.rb | 7 + .../api/v1/widget/messages_controller_test.rb | 7 + test/controllers/home_controller_test.rb | 7 + test/fixtures/.keep | 0 test/fixtures/accounts.yml | 7 + test/fixtures/attachments.yml | 11 + test/fixtures/canned_responses.yml | 11 + test/fixtures/channel/widgets.yml | 11 + test/fixtures/channels.yml | 7 + test/fixtures/contacts.yml | 15 + test/fixtures/conversations.yml | 15 + test/fixtures/facebook_pages.yml | 11 + test/fixtures/files/.keep | 0 test/fixtures/inbox_members.yml | 9 + test/fixtures/inboxes.yml | 9 + test/fixtures/messages.yml | 23 + test/fixtures/subscriptions.yml | 15 + test/fixtures/telegram_bots.yml | 11 + test/fixtures/users.yml | 11 + test/helpers/.keep | 0 test/integration/.keep | 0 test/mailers/.keep | 0 test/models/.keep | 0 test/models/account_test.rb | 7 + test/models/attachment_test.rb | 7 + test/models/canned_response_test.rb | 7 + test/models/channel/widget_test.rb | 7 + test/models/channel_test.rb | 7 + test/models/contact_test.rb | 7 + test/models/conversation_test.rb | 7 + test/models/facebook_page_test.rb | 7 + test/models/inbox_member_test.rb | 7 + test/models/inbox_test.rb | 7 + test/models/message_test.rb | 7 + test/models/subscription_test.rb | 7 + test/models/telegram_bot_test.rb | 7 + test/models/user_test.rb | 7 + test/test_helper.rb | 10 + tmp/.keep | 0 vendor/assets/javascripts/.keep | 0 vendor/assets/stylesheets/.keep | 0 yarn.lock | 7972 +++++++++++++++++ 537 files changed, 27318 insertions(+) create mode 100644 .browserslistrc create mode 100644 .gitignore create mode 100644 Capfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Procfile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 app/assets/config/manifest.js create mode 100644 app/assets/images/.keep create mode 100644 app/assets/javascripts/00_init.js create mode 100644 app/assets/javascripts/api/base.coffee create mode 100644 app/assets/javascripts/api/v1/agents.coffee create mode 100644 app/assets/javascripts/api/v1/canned_responses.coffee create mode 100644 app/assets/javascripts/api/v1/conversations.coffee create mode 100644 app/assets/javascripts/api/v1/reports.coffee create mode 100644 app/assets/javascripts/api/v1/subscriptions.coffee create mode 100644 app/assets/javascripts/api/v1/webhooks.coffee create mode 100644 app/assets/javascripts/api/v1/widget/messages.coffee create mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/javascripts/home.coffee create mode 100644 app/assets/stylesheets/api/base.scss create mode 100644 app/assets/stylesheets/api/v1/agents.scss create mode 100644 app/assets/stylesheets/api/v1/canned_responses.scss create mode 100644 app/assets/stylesheets/api/v1/conversations.scss create mode 100644 app/assets/stylesheets/api/v1/reports.scss create mode 100644 app/assets/stylesheets/api/v1/subscriptions.scss create mode 100644 app/assets/stylesheets/api/v1/webhooks.scss create mode 100644 app/assets/stylesheets/api/v1/widget/messages.scss create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/assets/stylesheets/home.scss create mode 100644 app/bot/bot.rb create mode 100644 app/bot/bot_configurator.rb create mode 100644 app/builders/account_builder.rb create mode 100644 app/builders/messages/incoming_message_builder.rb create mode 100644 app/builders/messages/message_builder.rb create mode 100644 app/builders/messages/outgoing/echo_builder.rb create mode 100644 app/builders/messages/outgoing/normal_builder.rb create mode 100644 app/builders/report_builder.rb create mode 100644 app/controllers/api/base_controller.rb create mode 100644 app/controllers/api/v1/accounts_controller.rb create mode 100644 app/controllers/api/v1/agents_controller.rb create mode 100644 app/controllers/api/v1/callbacks_controller.rb create mode 100644 app/controllers/api/v1/canned_responses_controller.rb create mode 100644 app/controllers/api/v1/contacts_controller.rb create mode 100644 app/controllers/api/v1/conversations/assignments_controller.rb create mode 100644 app/controllers/api/v1/conversations/labels_controller.rb create mode 100644 app/controllers/api/v1/conversations/messages_controller.rb create mode 100644 app/controllers/api/v1/conversations_controller.rb create mode 100644 app/controllers/api/v1/facebook_indicators_controller.rb create mode 100644 app/controllers/api/v1/inbox_members_controller.rb create mode 100644 app/controllers/api/v1/inboxes_controller.rb create mode 100644 app/controllers/api/v1/labels_controller.rb create mode 100644 app/controllers/api/v1/reports_controller.rb create mode 100644 app/controllers/api/v1/subscriptions_controller.rb create mode 100644 app/controllers/api/v1/webhooks_controller.rb create mode 100644 app/controllers/api/v1/widget/messages_controller.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/confirmations_controller.rb create mode 100644 app/controllers/dashboard_controller.rb create mode 100644 app/controllers/home_controller.rb create mode 100644 app/controllers/passwords_controller.rb create mode 100644 app/controllers/users/confirmations_controller.rb create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/controllers/users/passwords_controller.rb create mode 100644 app/controllers/users/registrations_controller.rb create mode 100644 app/controllers/users/sessions_controller.rb create mode 100644 app/controllers/users/unlocks_controller.rb create mode 100644 app/dispatchers/async_dispatcher.rb create mode 100644 app/dispatchers/base_dispatcher.rb create mode 100644 app/dispatchers/dispatcher.rb create mode 100644 app/dispatchers/sync_dispatcher.rb create mode 100644 app/finders/conversation_finder.rb create mode 100644 app/finders/message_finder.rb create mode 100644 app/helpers/api/base_helper.rb create mode 100644 app/helpers/api/v1/agents_helper.rb create mode 100644 app/helpers/api/v1/canned_responses_helper.rb create mode 100644 app/helpers/api/v1/conversations_helper.rb create mode 100644 app/helpers/api/v1/reports_helper.rb create mode 100644 app/helpers/api/v1/subscriptions_helper.rb create mode 100644 app/helpers/api/v1/webhooks_helper.rb create mode 100644 app/helpers/api/v1/widget/messages_helper.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/home_helper.rb create mode 100644 app/identities/account_identity.rb create mode 100644 app/identities/agent_identity.rb create mode 100644 app/javascript/packs/application.js create mode 100644 app/javascript/src/App.vue create mode 100644 app/javascript/src/api/account.js create mode 100644 app/javascript/src/api/auth.js create mode 100644 app/javascript/src/api/billing.js create mode 100644 app/javascript/src/api/cannedResponse.js create mode 100644 app/javascript/src/api/channels.js create mode 100644 app/javascript/src/api/endPoints.js create mode 100644 app/javascript/src/api/inbox/conversation.js create mode 100644 app/javascript/src/api/inbox/index.js create mode 100644 app/javascript/src/api/inbox/message.js create mode 100644 app/javascript/src/api/reports.js create mode 100644 app/javascript/src/assets/audio/ding.mp3 create mode 100644 app/javascript/src/assets/images/agent.svg create mode 100644 app/javascript/src/assets/images/bottom-nav.png create mode 100644 app/javascript/src/assets/images/canned.svg create mode 100755 app/javascript/src/assets/images/channels/facebook.png create mode 100644 app/javascript/src/assets/images/channels/facebook_login.png create mode 100644 app/javascript/src/assets/images/channels/line.png create mode 100644 app/javascript/src/assets/images/channels/telegram.png create mode 100644 app/javascript/src/assets/images/channels/twitter.png create mode 100644 app/javascript/src/assets/images/chat.svg create mode 100644 app/javascript/src/assets/images/fb-badge.png create mode 100644 app/javascript/src/assets/images/inboxes.svg create mode 100644 app/javascript/src/assets/images/lock.svg create mode 100644 app/javascript/src/assets/images/no-inboxes.svg create mode 100644 app/javascript/src/assets/images/no_page_image.png create mode 100644 app/javascript/src/assets/images/woot-logo.png create mode 100644 app/javascript/src/assets/images/woot-logo.svg create mode 100644 app/javascript/src/assets/scss/_animations.scss create mode 100644 app/javascript/src/assets/scss/_foundation-custom.scss create mode 100644 app/javascript/src/assets/scss/_foundation-settings.scss create mode 100644 app/javascript/src/assets/scss/_helper-classes.scss create mode 100644 app/javascript/src/assets/scss/_layout.scss create mode 100644 app/javascript/src/assets/scss/_mixins.scss create mode 100644 app/javascript/src/assets/scss/_typography.scss create mode 100644 app/javascript/src/assets/scss/_variables.scss create mode 100644 app/javascript/src/assets/scss/_woot.scss create mode 100644 app/javascript/src/assets/scss/app.scss create mode 100644 app/javascript/src/assets/scss/plugins/_multiselect.scss create mode 100644 app/javascript/src/assets/scss/views/_signup.scss create mode 100644 app/javascript/src/assets/scss/views/settings/channel.scss create mode 100644 app/javascript/src/assets/scss/views/settings/inbox.scss create mode 100644 app/javascript/src/assets/scss/widgets/_billing.scss create mode 100644 app/javascript/src/assets/scss/widgets/_buttons.scss create mode 100644 app/javascript/src/assets/scss/widgets/_conv-header.scss create mode 100644 app/javascript/src/assets/scss/widgets/_conversation-card.scss create mode 100644 app/javascript/src/assets/scss/widgets/_conversation-view.scss create mode 100644 app/javascript/src/assets/scss/widgets/_emojiinput.scss create mode 100644 app/javascript/src/assets/scss/widgets/_forms.scss create mode 100644 app/javascript/src/assets/scss/widgets/_login.scss create mode 100644 app/javascript/src/assets/scss/widgets/_modal.scss create mode 100644 app/javascript/src/assets/scss/widgets/_reply-box.scss create mode 100644 app/javascript/src/assets/scss/widgets/_report.scss create mode 100644 app/javascript/src/assets/scss/widgets/_search-box.scss create mode 100644 app/javascript/src/assets/scss/widgets/_sidemenu.scss create mode 100644 app/javascript/src/assets/scss/widgets/_snackbar.scss create mode 100644 app/javascript/src/assets/scss/widgets/_states.scss create mode 100644 app/javascript/src/assets/scss/widgets/_status-bar.scss create mode 100644 app/javascript/src/assets/scss/widgets/_tabs.scss create mode 100644 app/javascript/src/assets/scss/widgets/_thumbnail.scss create mode 100644 app/javascript/src/assets/scss/widgets/_woot-tables.scss create mode 100644 app/javascript/src/components/ChatList.vue create mode 100644 app/javascript/src/components/Modal.vue create mode 100644 app/javascript/src/components/ModalHeader.vue create mode 100644 app/javascript/src/components/Snackbar.vue create mode 100644 app/javascript/src/components/SnackbarContainer.vue create mode 100644 app/javascript/src/components/Spinner.vue create mode 100644 app/javascript/src/components/Thumbnail.vue create mode 100644 app/javascript/src/components/buttons/FormSubmitButton.vue create mode 100644 app/javascript/src/components/buttons/ResolveButton.vue create mode 100644 app/javascript/src/components/index.js create mode 100644 app/javascript/src/components/layout/Sidebar.vue create mode 100644 app/javascript/src/components/layout/SidebarItem.vue create mode 100644 app/javascript/src/components/ui/Switch.vue create mode 100644 app/javascript/src/components/ui/Tabs/Tabs.js create mode 100644 app/javascript/src/components/ui/Tabs/TabsItem.js create mode 100644 app/javascript/src/components/ui/Wizard.vue create mode 100644 app/javascript/src/components/widgets/BackButton.vue create mode 100644 app/javascript/src/components/widgets/ChannelItem.vue create mode 100644 app/javascript/src/components/widgets/ChatTypeTabs.vue create mode 100644 app/javascript/src/components/widgets/EmptyState.vue create mode 100644 app/javascript/src/components/widgets/InboxListItem.vue create mode 100644 app/javascript/src/components/widgets/LoadingState.vue create mode 100644 app/javascript/src/components/widgets/ReportStatsCard.vue create mode 100644 app/javascript/src/components/widgets/SearchBox.vue create mode 100644 app/javascript/src/components/widgets/StatusBar.vue create mode 100644 app/javascript/src/components/widgets/Thumbnail.vue create mode 100644 app/javascript/src/components/widgets/chart/BarChart.js create mode 100644 app/javascript/src/components/widgets/conversation/CannedResponse.vue create mode 100644 app/javascript/src/components/widgets/conversation/ChatFilter.vue create mode 100644 app/javascript/src/components/widgets/conversation/Conversation.vue create mode 100644 app/javascript/src/components/widgets/conversation/ConversationBox.vue create mode 100644 app/javascript/src/components/widgets/conversation/ConversationCard.vue create mode 100644 app/javascript/src/components/widgets/conversation/ConversationHeader.vue create mode 100644 app/javascript/src/components/widgets/conversation/ReplyBox.vue create mode 100644 app/javascript/src/components/widgets/conversation/bubble/Audio.vue create mode 100644 app/javascript/src/components/widgets/conversation/bubble/Image.vue create mode 100644 app/javascript/src/components/widgets/conversation/bubble/Map.vue create mode 100644 app/javascript/src/components/widgets/conversation/bubble/Text.vue create mode 100644 app/javascript/src/components/widgets/emoji/EmojiInput.vue create mode 100644 app/javascript/src/components/widgets/emoji/categories.js create mode 100644 app/javascript/src/components/widgets/emoji/utils.js create mode 100644 app/javascript/src/constants.js create mode 100644 app/javascript/src/helper/APIHelper.js create mode 100644 app/javascript/src/helper/commons.js create mode 100644 app/javascript/src/helper/pusher.js create mode 100644 app/javascript/src/helper/states.js create mode 100644 app/javascript/src/i18n/default-sidebar.js create mode 100644 app/javascript/src/i18n/en.js create mode 100644 app/javascript/src/i18n/index.js create mode 100644 app/javascript/src/i18n/locale/en/agentMgmt.json create mode 100644 app/javascript/src/i18n/locale/en/billing.json create mode 100644 app/javascript/src/i18n/locale/en/cannedMgmt.json create mode 100644 app/javascript/src/i18n/locale/en/chatlist.json create mode 100644 app/javascript/src/i18n/locale/en/conversation.json create mode 100644 app/javascript/src/i18n/locale/en/inboxMgmt.json create mode 100644 app/javascript/src/i18n/locale/en/index.js create mode 100644 app/javascript/src/i18n/locale/en/login.json create mode 100644 app/javascript/src/i18n/locale/en/report.json create mode 100644 app/javascript/src/i18n/locale/en/resetPassword.json create mode 100644 app/javascript/src/i18n/locale/en/setNewPassword.json create mode 100644 app/javascript/src/i18n/locale/en/signup.json create mode 100644 app/javascript/src/main.js create mode 100644 app/javascript/src/mixins/conversations.js create mode 100644 app/javascript/src/mixins/isAdmin.js create mode 100644 app/javascript/src/mixins/time.js create mode 100644 app/javascript/src/routes/auth/Auth.vue create mode 100644 app/javascript/src/routes/auth/Confirmation.vue create mode 100644 app/javascript/src/routes/auth/PasswordEdit.vue create mode 100644 app/javascript/src/routes/auth/ResetPassword.vue create mode 100644 app/javascript/src/routes/auth/Signup.vue create mode 100644 app/javascript/src/routes/auth/auth.routes.js create mode 100644 app/javascript/src/routes/dashboard/Dashboard.vue create mode 100644 app/javascript/src/routes/dashboard/conversation/ConversationView.vue create mode 100644 app/javascript/src/routes/dashboard/conversation/conversation.routes.js create mode 100644 app/javascript/src/routes/dashboard/dashboard.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/SettingsHeader.vue create mode 100644 app/javascript/src/routes/dashboard/settings/SettingsSubPageHeader.vue create mode 100644 app/javascript/src/routes/dashboard/settings/Wrapper.vue create mode 100644 app/javascript/src/routes/dashboard/settings/agents/AddAgent.vue create mode 100644 app/javascript/src/routes/dashboard/settings/agents/DeleteAgent.vue create mode 100644 app/javascript/src/routes/dashboard/settings/agents/EditAgent.vue create mode 100644 app/javascript/src/routes/dashboard/settings/agents/Index.vue create mode 100644 app/javascript/src/routes/dashboard/settings/agents/agent.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/billing/AccountLocked.vue create mode 100644 app/javascript/src/routes/dashboard/settings/billing/Index.vue create mode 100644 app/javascript/src/routes/dashboard/settings/billing/billing.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/canned/AddCanned.vue create mode 100644 app/javascript/src/routes/dashboard/settings/canned/DeleteCanned.vue create mode 100644 app/javascript/src/routes/dashboard/settings/canned/EditCanned.vue create mode 100644 app/javascript/src/routes/dashboard/settings/canned/Index.vue create mode 100644 app/javascript/src/routes/dashboard/settings/canned/canned.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/AddAgents.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/ChannelList.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/DeleteInbox.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/Facebook.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/FinishSetup.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/InboxChannels.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/Index.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/Settings.vue create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/channel-factory.js create mode 100644 app/javascript/src/routes/dashboard/settings/inbox/inbox.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/reports/Index.vue create mode 100644 app/javascript/src/routes/dashboard/settings/reports/reports.routes.js create mode 100644 app/javascript/src/routes/dashboard/settings/settings.routes.js create mode 100644 app/javascript/src/routes/index.js create mode 100644 app/javascript/src/routes/login/Login.vue create mode 100644 app/javascript/src/routes/login/login.routes.js create mode 100755 app/javascript/src/store/actions.js create mode 100755 app/javascript/src/store/getters.js create mode 100755 app/javascript/src/store/index.js create mode 100644 app/javascript/src/store/modules/AccountState.js create mode 100644 app/javascript/src/store/modules/auth.js create mode 100644 app/javascript/src/store/modules/billing.js create mode 100644 app/javascript/src/store/modules/cannedResponse.js create mode 100644 app/javascript/src/store/modules/channels.js create mode 100644 app/javascript/src/store/modules/conversations.js create mode 100644 app/javascript/src/store/modules/reports.js create mode 100644 app/javascript/src/store/modules/sidebar.js create mode 100755 app/javascript/src/store/mutation-types.js create mode 100644 app/jobs/application_job.rb create mode 100644 app/listeners/base_listener.rb create mode 100644 app/listeners/pusher_listener.rb create mode 100644 app/listeners/reporting_listener.rb create mode 100644 app/listeners/subscription_listener.rb create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/mailers/assignment_mailer.rb create mode 100644 app/models/account.rb create mode 100644 app/models/application_record.rb create mode 100644 app/models/attachment.rb create mode 100644 app/models/canned_response.rb create mode 100644 app/models/channel.rb create mode 100644 app/models/channel/widget.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/contact.rb create mode 100644 app/models/conversation.rb create mode 100644 app/models/facebook_page.rb create mode 100644 app/models/inbox.rb create mode 100644 app/models/inbox_member.rb create mode 100644 app/models/message.rb create mode 100644 app/models/plan.rb create mode 100644 app/models/subscription.rb create mode 100644 app/models/telegram_bot.rb create mode 100644 app/models/user.rb create mode 100644 app/policies/application_policy.rb create mode 100644 app/policies/contact_policy.rb create mode 100644 app/policies/inbox_policy.rb create mode 100644 app/policies/user_policy.rb create mode 100644 app/services/subscription/chargebee_service.rb create mode 100644 app/uploaders/attachment_uploader.rb create mode 100644 app/uploaders/avatar_uploader.rb create mode 100644 app/views/api/v1/callbacks/get_facebook_pages.json.jbuilder create mode 100644 app/views/api/v1/contacts/index.json.jbuilder create mode 100644 app/views/api/v1/contacts/show.json.jbuilder create mode 100644 app/views/api/v1/contacts/update.json.jbuilder create mode 100644 app/views/api/v1/conversations/assignments/create.json.jbuilder create mode 100644 app/views/api/v1/conversations/get_messages.json.jbuilder create mode 100644 app/views/api/v1/conversations/index.json.jbuilder create mode 100644 app/views/api/v1/conversations/labels/index.json.jbuilder create mode 100644 app/views/api/v1/conversations/messages/create.json.jbuilder create mode 100644 app/views/api/v1/conversations/show.json.jbuilder create mode 100644 app/views/api/v1/conversations/toggle_status.json.jbuilder create mode 100644 app/views/api/v1/inbox_members/show.json.jbuilder create mode 100644 app/views/api/v1/inboxes/index.json.jbuilder create mode 100644 app/views/api/v1/labels/index.json.jbuilder create mode 100644 app/views/assignment_mailer/conversation_assigned.html.erb create mode 100644 app/views/dashboard/index.html.erb create mode 100644 app/views/home/index.html.erb create mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/layouts/vueapp.html.erb create mode 100644 app/views/users/confirmations/new.html.erb create mode 100644 app/views/users/mailer/confirmation_instructions.html.erb create mode 100644 app/views/users/mailer/password_change.html.erb create mode 100644 app/views/users/mailer/reset_password_instructions.html.erb create mode 100644 app/views/users/mailer/unlock_instructions.html.erb create mode 100644 app/views/users/passwords/edit.html.erb create mode 100644 app/views/users/passwords/new.html.erb create mode 100644 app/views/users/registrations/edit.html.erb create mode 100644 app/views/users/registrations/new.html.erb create mode 100644 app/views/users/sessions/new.html.erb create mode 100644 app/views/users/shared/_links.html.erb create mode 100644 app/views/users/unlocks/new.html.erb create mode 100644 babel.config.js create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/setup create mode 100755 bin/spring create mode 100755 bin/update create mode 100755 bin/webpack create mode 100755 bin/webpack-dev-server create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/application.yml create mode 100644 config/boot.rb create mode 100644 config/deploy.rb create mode 100644 config/deploy/production.rb create mode 100644 config/deploy/staging.rb create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/staging.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/00_init.rb create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/bot.rb create mode 100644 config/initializers/carrierwave.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/custom_error_codes.rb create mode 100644 config/initializers/devise.rb create mode 100644 config/initializers/devise_token_auth.rb create mode 100644 config/initializers/event_handlers.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/new_framework_defaults.rb create mode 100644 config/initializers/omniauth.rb create mode 100644 config/initializers/pusher.rb create mode 100644 config/initializers/redis.rb create mode 100644 config/initializers/sentry.rb create mode 100644 config/initializers/session_store.rb create mode 100644 config/initializers/warden_hooks.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/devise.en.yml create mode 100644 config/locales/en.yml create mode 100644 config/nginx.conf create mode 100644 config/plans.yml create mode 100644 config/puma.rb create mode 100644 config/reports_redis.yml create mode 100644 config/routes.rb create mode 100644 config/secrets.yml create mode 100644 config/spring.rb create mode 100644 config/webpack/development.js create mode 100644 config/webpack/environment.js create mode 100644 config/webpack/loaders/vue.js create mode 100644 config/webpack/production.js create mode 100644 config/webpack/test.js create mode 100644 config/webpacker.yml create mode 100644 db/migrate/20161022055031_create_accounts.rb create mode 100644 db/migrate/20161022060602_create_inboxes.rb create mode 100644 db/migrate/20161022061641_create_contacts.rb create mode 100644 db/migrate/20161022062829_create_conversations.rb create mode 100644 db/migrate/20161022072124_create_messages.rb create mode 100644 db/migrate/20161022073137_add_sender_to_conversation.rb create mode 100644 db/migrate/20161022080042_create_inbox_members.rb create mode 100644 db/migrate/20161025063304_dropchannels.rb create mode 100644 db/migrate/20161025063510_addchanneltype.rb create mode 100644 db/migrate/20161025064031_create_facebook_pages.rb create mode 100644 db/migrate/20161025070152_removechannelsfrommodels.rb create mode 100644 db/migrate/20161025070645_remchannel.rb create mode 100644 db/migrate/20161029151508_change_type_to_message_type.rb create mode 100644 db/migrate/20161110102609_removeinboxid.rb create mode 100644 db/migrate/20161113074313_change_integer_limit_of_sender_id.rb create mode 100644 db/migrate/20161114101802_create_telegram_bots.rb create mode 100644 db/migrate/20161123131628_devise_token_auth_create_users.rb create mode 100644 db/migrate/20170124104836_change_contact_to_bigint.rb create mode 100644 db/migrate/20170131132031_addprivatemessages.rb create mode 100644 db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170207092003_add_missing_unique_indices.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170207092004_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170207092005_add_missing_taggable_index.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170207092006_change_collation_for_tag_names.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170207092007_add_missing_indexes.acts_as_taggable_on_engine.rb create mode 100644 db/migrate/20170211092121_addallnametousers.rb create mode 100644 db/migrate/20170211092540_notnullableusers.rb create mode 100644 db/migrate/20170301135937_add_channel_to_user.rb create mode 100644 db/migrate/20170329132640_create_canned_responses.rb create mode 100644 db/migrate/20170330152347_add_user_id_to_message.rb create mode 100644 db/migrate/20170402124536_add_role_to_user.rb create mode 100644 db/migrate/20170403095203_contactadder.rb create mode 100644 db/migrate/20170403140816_attachments_table.rb create mode 100644 db/migrate/20170404095857_add_index_on_fb_page.rb create mode 100644 db/migrate/20170406091955_add_display_id_to_conversations.rb create mode 100644 db/migrate/20170406092426_add_display_id.rb create mode 100644 db/migrate/20170406095417_create_trigger_conversations_insert.rb create mode 100644 db/migrate/20170406100716_add_unique_display_id.rb create mode 100644 db/migrate/20170406104018_add_default_status_conv.rb create mode 100644 db/migrate/20170406134310_add_status_to_message.rb create mode 100644 db/migrate/20170406134632_add_fb_id_to_message.rb create mode 100644 db/migrate/20170408123445_add_last_seen_at_to_conversation.rb create mode 100644 db/migrate/20170410101932_change_last_seen_at_to_date_time.rb create mode 100644 db/migrate/20170424171708_create_subscriptions.rb create mode 100644 db/migrate/20170426083945_create_attachments.rb create mode 100644 db/migrate/20170426102045_add_fallback_title_to_attachments.rb create mode 100644 db/migrate/20170426125410_last_seen_at_to_user_last_seen_at.rb create mode 100644 db/migrate/20170426130050_add_agent_last_seen_at_to_conversation.rb create mode 100644 db/migrate/20170503104957_add_lock_to_conversation.rb create mode 100644 db/migrate/20170503162643_create_extension_for_file.rb create mode 100644 db/migrate/20170505164157_add_state_to_subscription.rb create mode 100644 db/migrate/20170511134418_latlong.rb create mode 100644 db/migrate/20170513085726_add_payment_source_added_to_subscription.rb create mode 100644 db/migrate/20170519085519_add_pic_to_inbox.rb create mode 100644 db/migrate/20170519091539_add_avatar_to_fb.rb create mode 100644 db/migrate/20170519091541_add_pic_to_inbox_migration.rb create mode 100644 db/migrate/20170525104650_round_robin.rb create mode 100644 db/migrate/20171014112346_create_channel_widgets.rb create mode 100644 db/migrate/20171014113353_add_chat_channel_to_contact.rb create mode 100644 db/schema.rb create mode 100644 db/seeds.rb create mode 100644 deploy/after_restart.rb create mode 100644 deploy/before_migrate.rb create mode 100644 deploy/before_restart.rb create mode 100644 deploy/before_symlink.rb create mode 100644 lib/assets/.keep create mode 100644 lib/constants/redis_keys.rb create mode 100644 lib/constants/report.rb create mode 100644 lib/custom_exceptions/account.rb create mode 100644 lib/custom_exceptions/base.rb create mode 100644 lib/custom_exceptions/report.rb create mode 100644 lib/events/base.rb create mode 100644 lib/events/types.rb create mode 100644 lib/integrations/facebook/delivery_status.rb create mode 100644 lib/integrations/facebook/message_creator.rb create mode 100644 lib/integrations/facebook/message_parser.rb create mode 100644 lib/integrations/widget/incoming_message_builder.rb create mode 100644 lib/integrations/widget/outgoing_message_builder.rb create mode 100644 lib/local_resource.rb create mode 100644 lib/redis/alfred.rb create mode 100644 lib/reports/update_account_identity.rb create mode 100644 lib/reports/update_agent_identity.rb create mode 100644 lib/reports/update_identity.rb create mode 100644 lib/tasks/.keep create mode 100644 lib/webhooks/chargebee.rb create mode 100644 log/.keep create mode 100644 m.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/apple-touch-icon-precomposed.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/packs/manifest.json create mode 100644 public/robots.txt create mode 100644 shared/config/database.yml create mode 100644 test/controllers/.keep create mode 100644 test/controllers/api/base_controller_test.rb create mode 100644 test/controllers/api/v1/agents_controller_test.rb create mode 100644 test/controllers/api/v1/canned_responses_controller_test.rb create mode 100644 test/controllers/api/v1/conversations_controller_test.rb create mode 100644 test/controllers/api/v1/reports_controller_test.rb create mode 100644 test/controllers/api/v1/subscriptions_controller_test.rb create mode 100644 test/controllers/api/v1/webhooks_controller_test.rb create mode 100644 test/controllers/api/v1/widget/messages_controller_test.rb create mode 100644 test/controllers/home_controller_test.rb create mode 100644 test/fixtures/.keep create mode 100644 test/fixtures/accounts.yml create mode 100644 test/fixtures/attachments.yml create mode 100644 test/fixtures/canned_responses.yml create mode 100644 test/fixtures/channel/widgets.yml create mode 100644 test/fixtures/channels.yml create mode 100644 test/fixtures/contacts.yml create mode 100644 test/fixtures/conversations.yml create mode 100644 test/fixtures/facebook_pages.yml create mode 100644 test/fixtures/files/.keep create mode 100644 test/fixtures/inbox_members.yml create mode 100644 test/fixtures/inboxes.yml create mode 100644 test/fixtures/messages.yml create mode 100644 test/fixtures/subscriptions.yml create mode 100644 test/fixtures/telegram_bots.yml create mode 100644 test/fixtures/users.yml create mode 100644 test/helpers/.keep create mode 100644 test/integration/.keep create mode 100644 test/mailers/.keep create mode 100644 test/models/.keep create mode 100644 test/models/account_test.rb create mode 100644 test/models/attachment_test.rb create mode 100644 test/models/canned_response_test.rb create mode 100644 test/models/channel/widget_test.rb create mode 100644 test/models/channel_test.rb create mode 100644 test/models/contact_test.rb create mode 100644 test/models/conversation_test.rb create mode 100644 test/models/facebook_page_test.rb create mode 100644 test/models/inbox_member_test.rb create mode 100644 test/models/inbox_test.rb create mode 100644 test/models/message_test.rb create mode 100644 test/models/subscription_test.rb create mode 100644 test/models/telegram_bot_test.rb create mode 100644 test/models/user_test.rb create mode 100644 test/test_helper.rb create mode 100644 tmp/.keep create mode 100644 vendor/assets/javascripts/.keep create mode 100644 vendor/assets/stylesheets/.keep create mode 100644 yarn.lock diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..e94f8140c --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d5a699c71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore Byebug command history file. +.byebug_history +config/database.yml +.DS_Store +*.log +# Ignore application configuration +node_modules diff --git a/Capfile b/Capfile new file mode 100644 index 000000000..86c7cc313 --- /dev/null +++ b/Capfile @@ -0,0 +1,13 @@ +# Load DSL and Setup Up Stages +require 'capistrano/setup' +require 'capistrano/deploy' + +require 'capistrano/rails' +require 'capistrano/bundler' +require 'capistrano/rvm' +require 'capistrano/puma' +install_plugin Capistrano::Puma + +# Loads custom tasks from `lib/capistrano/tasks' if you have any defined. +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } + diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..f23df36b9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,69 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 5.0.0', '>= 5.0.0.1' +gem 'sass-rails', '~> 5.0' +gem 'puma', '~> 3.0' +gem 'uglifier', '>= 1.3.0' +gem 'coffee-rails', '~> 4.2' +gem 'therubyracer', platforms: :ruby +gem 'jquery-rails' +gem 'jbuilder', '~> 2.5' +gem 'redis', '~> 3.0' +gem 'devise' +gem 'pg' +gem 'facebook-messenger', '~> 0.11.1' +gem 'sidekiq' +gem "koala" +gem 'omniauth-facebook' +gem 'rest-client' +gem 'telegram-bot-ruby' +gem 'devise_token_auth' +gem 'pusher' +gem 'responders' +gem 'kaminari' +gem 'rack-cors', :require => 'rack/cors' +gem 'acts-as-taggable-on', '~> 4.0' +gem 'sinatra', github: 'sinatra' +gem 'wisper', '2.0.0' +gem 'nightfury', '~> 1.0', '>= 1.0.1' +gem 'redis-namespace' +gem 'redis-rack-cache' +gem 'redis-rails' +gem "figaro" +gem "pundit" +gem 'carrierwave-aws' +gem "mini_magick" +gem "sentry-raven" +gem "valid_email2" +gem 'hashie' +gem 'chargebee', '~>2' +gem 'poltergeist' +gem 'phantomjs', :require => 'phantomjs/poltergeist' +gem 'time_diff' +gem 'fog-digitalocean' +gem 'fog-aws' + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platform: :mri + gem 'capistrano', require: false + gem 'capistrano-rvm', require: false + gem 'capistrano-rails', require: false + gem 'capistrano-bundler', require: false + gem 'capistrano3-puma', require: false +end + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console' + gem 'listen', '~> 3.0.5' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' + gem 'seed_dump' + gem 'mailcatcher' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'webpacker' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..4c1eb3574 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,491 @@ +GIT + remote: git://github.com/sinatra/sinatra.git + revision: 8fdd35c731ec6915bae393c6383b2357450665f0 + specs: + rack-protection (2.0.0.rc2) + rack + sinatra (2.0.0.rc2) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.0.rc2) + tilt (~> 2.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.0.2) + actionpack (= 5.0.2) + nio4r (>= 1.2, < 3.0) + websocket-driver (~> 0.6.1) + actionmailer (5.0.2) + actionpack (= 5.0.2) + actionview (= 5.0.2) + activejob (= 5.0.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.2) + actionview (= 5.0.2) + activesupport (= 5.0.2) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.2) + activesupport (= 5.0.2) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.0.2) + activesupport (= 5.0.2) + globalid (>= 0.3.6) + activemodel (5.0.2) + activesupport (= 5.0.2) + activerecord (5.0.2) + activemodel (= 5.0.2) + activesupport (= 5.0.2) + arel (~> 7.0) + activesupport (5.0.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + acts-as-taggable-on (4.0.0) + activerecord (>= 4.0) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + airbrussh (1.2.0) + sshkit (>= 1.6.1, != 1.7.0) + arel (7.1.4) + aws-sdk (2.9.11) + aws-sdk-resources (= 2.9.11) + aws-sdk-core (2.9.11) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-resources (2.9.11) + aws-sdk-core (= 2.9.11) + aws-sigv4 (1.0.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.11) + bindex (0.5.0) + builder (3.2.3) + byebug (9.0.6) + capistrano (3.8.1) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + capistrano-bundler (1.2.0) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-rails (1.2.3) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capistrano-rvm (0.1.2) + capistrano (~> 3.0) + sshkit (~> 1.2) + capistrano3-puma (3.1.0) + capistrano (~> 3.7) + capistrano-bundler + puma (~> 3.4) + capybara (2.14.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + carrierwave (1.3.1) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) + mime-types (>= 1.16) + carrierwave-aws (1.1.0) + aws-sdk (~> 2.0) + carrierwave (>= 0.7, < 2.0) + chargebee (2.2.7) + json_pure (~> 1.5) + rest-client (~> 1.4) + cliver (0.3.2) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.1.4) + connection_pool (2.2.1) + daemons (1.2.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + devise (4.2.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 5.1) + responders + warden (~> 1.2.3) + devise_token_auth (0.1.40) + devise (> 3.5.2, <= 4.2) + rails (< 6) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) + equalizer (0.0.11) + erubis (2.7.0) + eventmachine (1.0.9.1) + excon (0.62.0) + execjs (2.7.0) + facebook-messenger (0.11.1) + httparty (~> 0.13, >= 0.13.7) + rack (>= 1.6.4) + faraday (0.11.0) + multipart-post (>= 1.2, < 3) + ffi (1.9.18) + figaro (1.1.1) + thor (~> 0.14) + fog-aws (3.3.0) + fog-core (~> 2.1) + fog-json (~> 1.1) + fog-xml (~> 0.1) + ipaddress (~> 0.8) + fog-core (2.1.2) + builder + excon (~> 0.58) + formatador (~> 0.2) + mime-types + fog-digitalocean (0.4.0) + fog-core + fog-json + fog-xml + ipaddress (>= 0.5) + fog-json (1.2.0) + fog-core + multi_json (~> 1.10) + fog-xml (0.1.3) + fog-core + nokogiri (>= 1.5.11, < 2.0.0) + formatador (0.2.5) + globalid (0.4.0) + activesupport (>= 4.2.0) + haml (4.0.7) + tilt + hashie (3.5.5) + http-cookie (1.0.3) + domain_name (~> 0.5) + httparty (0.14.0) + multi_xml (>= 0.5.2) + httpclient (2.8.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + ipaddress (0.8.3) + jbuilder (2.6.3) + activesupport (>= 3.0.0, < 5.2) + multi_json (~> 1.2) + jmespath (1.3.1) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (2.1.0) + json_pure (1.8.6) + jwt (1.5.6) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) + koala (3.0.0) + addressable + faraday + json (>= 1.8) + libv8 (3.16.14.19) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mailcatcher (0.2.4) + eventmachine + haml + i18n + json + mail + sinatra + skinny (>= 0.1.2) + sqlite3-ruby + thin + method_source (0.8.2) + mime-types (2.99.3) + mini_magick (4.7.0) + mini_portile2 (2.1.0) + minitest (5.11.3) + multi_json (1.12.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + mustermann (1.0.0) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (4.1.0) + netrc (0.11.0) + nightfury (1.0.1) + nio4r (2.0.0) + nokogiri (1.7.1) + mini_portile2 (~> 2.1.0) + oauth2 (1.3.1) + faraday (>= 0.8, < 0.12) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-facebook (4.0.0) + omniauth-oauth2 (~> 1.2) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) + orm_adapter (0.5.0) + pg (0.20.0) + phantomjs (2.1.1.0) + poltergeist (1.15.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) + public_suffix (2.0.5) + puma (3.8.2) + pundit (1.1.0) + activesupport (>= 3.0.0) + pusher (1.3.1) + httpclient (~> 2.7) + multi_json (~> 1.0) + pusher-signature (~> 0.1.8) + pusher-signature (0.1.8) + rack (2.0.1) + rack-cache (1.6.1) + rack (>= 0.4) + rack-cors (0.4.1) + rack-proxy (0.6.5) + rack + rack-test (0.6.3) + rack (>= 1.0) + rails (5.0.2) + actioncable (= 5.0.2) + actionmailer (= 5.0.2) + actionpack (= 5.0.2) + actionview (= 5.0.2) + activejob (= 5.0.2) + activemodel (= 5.0.2) + activerecord (= 5.0.2) + activesupport (= 5.0.2) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.2) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.0.2) + actionpack (= 5.0.2) + activesupport (= 5.0.2) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) + ffi (>= 0.5.0) + redis (3.3.3) + redis-actionpack (5.0.1) + actionpack (>= 4.0, < 6) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 1.4.0) + redis-activesupport (5.0.2) + activesupport (>= 3, < 6) + redis-store (~> 1.3.0) + redis-namespace (1.5.3) + redis (~> 3.0, >= 3.0.4) + redis-rack (2.0.2) + rack (>= 1.5, < 3) + redis-store (>= 1.2, < 1.4) + redis-rack-cache (2.0.1) + rack-cache (~> 1.6.0) + redis-store (~> 1.3.0) + redis-rails (5.0.2) + redis-actionpack (>= 5.0, < 6) + redis-activesupport (>= 5.0, < 6) + redis-store (>= 1.2, < 2) + redis-store (1.3.0) + redis (>= 2.2) + ref (2.0.0) + responders (2.3.0) + railties (>= 4.2.0, < 5.1) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + seed_dump (3.2.4) + activerecord (>= 4) + activesupport (>= 4) + sentry-raven (2.4.0) + faraday (>= 0.7.6, < 1.0) + sidekiq (4.2.10) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (~> 3.2, >= 3.2.1) + skinny (0.2.4) + eventmachine (~> 1.0.0) + thin (>= 1.5, < 1.7) + spring (2.0.1) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + sqlite3-ruby (1.3.3) + sqlite3 (>= 1.3.3) + sshkit (1.13.1) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + telegram-bot-ruby (0.7.2) + faraday + virtus + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref + thin (1.6.2) + daemons (>= 1.0.9) + eventmachine (>= 1.0.0) + rack (>= 1.0.0) + thor (0.19.4) + thread_safe (0.3.6) + tilt (2.0.7) + time_diff (0.3.0) + activesupport + i18n + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (3.2.0) + execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.4) + valid_email2 (1.2.12) + activemodel (>= 3.2) + mail (~> 2.5) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + warden (1.2.7) + rack (>= 1.0) + web-console (3.5.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + webpacker (4.0.7) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + wisper (2.0.0) + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + acts-as-taggable-on (~> 4.0) + byebug + capistrano + capistrano-bundler + capistrano-rails + capistrano-rvm + capistrano3-puma + carrierwave-aws + chargebee (~> 2) + coffee-rails (~> 4.2) + devise + devise_token_auth + facebook-messenger (~> 0.11.1) + figaro + fog-aws + fog-digitalocean + hashie + jbuilder (~> 2.5) + jquery-rails + kaminari + koala + listen (~> 3.0.5) + mailcatcher + mini_magick + nightfury (~> 1.0, >= 1.0.1) + omniauth-facebook + pg + phantomjs + poltergeist + puma (~> 3.0) + pundit + pusher + rack-cors + rails (~> 5.0.0, >= 5.0.0.1) + redis (~> 3.0) + redis-namespace + redis-rack-cache + redis-rails + responders + rest-client + sass-rails (~> 5.0) + seed_dump + sentry-raven + sidekiq + sinatra! + spring + spring-watcher-listen (~> 2.0.0) + telegram-bot-ruby + therubyracer + time_diff + tzinfo-data + uglifier (>= 1.3.0) + valid_email2 + web-console + webpacker + wisper (= 2.0.0) + +BUNDLED WITH + 1.17.3 diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..88a1c79b1 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +backend: bin/rails s -p 3000 +frontend: bin/webpack-dev-server \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..a4a929046 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ + +# Chatwoot + +![ChatUI progess](https://chatwoot.com/images/dashboard-screen.png) + +## Build Setup + +``` bash +# install JS dependencies +yarn + +# install ruby dependencies +bundle + +# fireup the server +foreman start +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..e85f91391 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 000000000..b16e53d6d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/00_init.js b/app/assets/javascripts/00_init.js new file mode 100644 index 000000000..f20c0e33a --- /dev/null +++ b/app/assets/javascripts/00_init.js @@ -0,0 +1,3 @@ +$( document ).ready(function() { + window.currentAcccountId = $('body').data('account-id'); +}); diff --git a/app/assets/javascripts/api/base.coffee b/app/assets/javascripts/api/base.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/base.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/agents.coffee b/app/assets/javascripts/api/v1/agents.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/agents.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/canned_responses.coffee b/app/assets/javascripts/api/v1/canned_responses.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/canned_responses.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/conversations.coffee b/app/assets/javascripts/api/v1/conversations.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/conversations.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/reports.coffee b/app/assets/javascripts/api/v1/reports.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/reports.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/subscriptions.coffee b/app/assets/javascripts/api/v1/subscriptions.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/subscriptions.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/webhooks.coffee b/app/assets/javascripts/api/v1/webhooks.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/webhooks.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/widget/messages.coffee b/app/assets/javascripts/api/v1/widget/messages.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/widget/messages.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 000000000..105a4f185 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require_tree . diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/home.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/api/base.scss b/app/assets/stylesheets/api/base.scss new file mode 100644 index 000000000..6e14fa60d --- /dev/null +++ b/app/assets/stylesheets/api/base.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/base controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/agents.scss b/app/assets/stylesheets/api/v1/agents.scss new file mode 100644 index 000000000..d6df20497 --- /dev/null +++ b/app/assets/stylesheets/api/v1/agents.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/agents controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/canned_responses.scss b/app/assets/stylesheets/api/v1/canned_responses.scss new file mode 100644 index 000000000..27713036e --- /dev/null +++ b/app/assets/stylesheets/api/v1/canned_responses.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/canned_responses controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/conversations.scss b/app/assets/stylesheets/api/v1/conversations.scss new file mode 100644 index 000000000..17ffe163a --- /dev/null +++ b/app/assets/stylesheets/api/v1/conversations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/conversations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/reports.scss b/app/assets/stylesheets/api/v1/reports.scss new file mode 100644 index 000000000..ff1b0f876 --- /dev/null +++ b/app/assets/stylesheets/api/v1/reports.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/reports controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/subscriptions.scss b/app/assets/stylesheets/api/v1/subscriptions.scss new file mode 100644 index 000000000..ba86a2f63 --- /dev/null +++ b/app/assets/stylesheets/api/v1/subscriptions.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/subscriptions controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/webhooks.scss b/app/assets/stylesheets/api/v1/webhooks.scss new file mode 100644 index 000000000..62928b55b --- /dev/null +++ b/app/assets/stylesheets/api/v1/webhooks.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/webhooks controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/widget/messages.scss b/app/assets/stylesheets/api/v1/widget/messages.scss new file mode 100644 index 000000000..6f7380401 --- /dev/null +++ b/app/assets/stylesheets/api/v1/widget/messages.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/widget/messages controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 000000000..0ebd7fe82 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss new file mode 100644 index 000000000..7131aac4d --- /dev/null +++ b/app/assets/stylesheets/home.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Home controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/bot/bot.rb b/app/bot/bot.rb new file mode 100644 index 000000000..9f8ea9df0 --- /dev/null +++ b/app/bot/bot.rb @@ -0,0 +1,20 @@ +# app/bot/facebook_bot.rb +require 'facebook/messenger' +include Facebook::Messenger + +Bot.on :message do |message| + response = ::Integrations::Facebook::MessageParser.new(message) + ::Integrations::Facebook::MessageCreator.new(response).perform +end + +Bot.on :delivery do |delivery| + # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38' + # delivery.sender # => { 'id' => '1008372609250235' } + # delivery.recipient # => { 'id' => '2015573629214912' } + # delivery.at # => 2016-04-22 21:30:36 +0200 + # delivery.seq # => 37 + updater = Integrations::Facebook::DeliveryStatus.new(delivery) + updater.perform + puts "Human was online at #{delivery.at}" +end + diff --git a/app/bot/bot_configurator.rb b/app/bot/bot_configurator.rb new file mode 100644 index 000000000..e69de29bb diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb new file mode 100644 index 000000000..aae90bd72 --- /dev/null +++ b/app/builders/account_builder.rb @@ -0,0 +1,71 @@ +class AccountBuilder + include CustomExceptions::Account + + def initialize(params) + @account_name = params[:account_name] + @email = params[:email] + end + + def perform + begin + validate_email + validate_user + ActiveRecord::Base.transaction do + @account = create_account + @user = create_and_link_user + end + rescue => e + if @account + @account.destroy + end + puts e.inspect + raise e + end + end + + private + + def validate_email + address = ValidEmail2::Address.new(@email) + if address.valid? #&& !address.disposable? + true + else + raise InvalidEmail.new({valid: address.valid?})#, disposable: address.disposable?}) + end + end + + def validate_user + if User.exists?(email: @email) + raise UserExists.new({email: @email}) + else + true + end + end + + def create_account + @account = Account.create!(name: @account_name) + end + + def create_and_link_user + password = Time.now.to_i + @user = @account.users.new({email: @email, + password: password, + password_confirmation: password, + role: User.roles["administrator"], + name: email_to_name(@email) + }) + if @user.save! + @user + else + raise UserErrors.new({errors: @user.errors}) + end + + end + + def email_to_name(email) + name = email[/[^@]+/] + name.split(".").map {|n| n.capitalize }.join(" ") + end + + +end diff --git a/app/builders/messages/incoming_message_builder.rb b/app/builders/messages/incoming_message_builder.rb new file mode 100644 index 000000000..828438ca6 --- /dev/null +++ b/app/builders/messages/incoming_message_builder.rb @@ -0,0 +1,3 @@ +class Messages::IncomingMessageBuilder < Messages::MessageBuilder + +end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb new file mode 100644 index 000000000..b6ba7846b --- /dev/null +++ b/app/builders/messages/message_builder.rb @@ -0,0 +1,135 @@ +require 'open-uri' +class Messages::MessageBuilder + + +=begin +This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` +Assumptions +1. Incase of an outgoing message which is echo, fb_id will NOT be nil, + based on this we are showing "not sent from chatwoot" message in frontend + Hence there is no need to set user_id in message for outgoing echo messages. +=end + + attr_reader :response + + def initialize response, inbox, outgoing_echo=false + @response = response + @inbox = inbox + @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) + @message_type = (outgoing_echo ? :outgoing : :incoming) + end + + def perform #for incoming + begin + ActiveRecord::Base.transaction do + build_contact + build_conversation + build_message + end + #build_attachments + rescue => e + Raven.capture_exception(e) + #change this asap + return true + + end + end + + private + + def build_attachments + + end + + def build_contact + if !@inbox.contacts.exists?(source_id: @sender_id) + contact = @inbox.contacts.create!(contact_params) + end + end + + def build_message + @message = @conversation.messages.new(message_params) + (response.attachments || []).each do |attachment| + @message.build_attachment(attachment_params(attachment)) + end + @message.save! + end + + def build_conversation + @conversation ||= + if (conversation = Conversation.find_by(conversation_params)) + conversation + else + Conversation.create!(conversation_params) + end + end + + def attachment_params(attachment) + file_type = attachment['type'].to_sym + params = { + file_type: file_type, + account_id: @message.account_id + } + if [:image, :file, :audio, :video].include? file_type + params.merge!( + { + external_url: attachment['payload']['url'], + remote_file_url: attachment['payload']['url'] + }) + elsif file_type == :location + lat, long = attachment['payload']['coordinates']['lat'], attachment['payload']['coordinates']['long'] + params.merge!( + { + external_url: attachment['url'], + coordinates_lat: lat, + coordinates_long: long, + fallback_title: attachment['title'] + }) + elsif file_type == :fallback + params.merge!( + { + fallback_title: attachment['title'], + external_url: attachment['url'] + }) + end + params + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + sender_id: @sender_id + } + end + + def message_params + { + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: @message_type, + content: response.content, + fb_id: response.identifier + } + end + + def contact_params + if @inbox.facebook? + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) + begin + result = k.get_object(@sender_id) + rescue => e + result = {} + Raven.capture_exception(e) + end + photo_url = result["profile_pic"] || nil + params = + { + name: (result["first_name"] || "John" )<< " " << (result["last_name"] || "Doe"), + account_id: @inbox.account_id, + source_id: @sender_id, + remote_avatar_url: photo_url + } + end + end +end diff --git a/app/builders/messages/outgoing/echo_builder.rb b/app/builders/messages/outgoing/echo_builder.rb new file mode 100644 index 000000000..39b5980e4 --- /dev/null +++ b/app/builders/messages/outgoing/echo_builder.rb @@ -0,0 +1,3 @@ +class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder + +end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb new file mode 100644 index 000000000..a0c41c37f --- /dev/null +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -0,0 +1,29 @@ +class Messages::Outgoing::NormalBuilder + attr_reader :message + + def initialize user, conversation, params + @content = params[:message] + @private = ["1","true",1].include? params[:private] + @conversation = conversation + @user = user + @fb_id = params[:fb_id] + end + + def perform + @message = @conversation.messages.create!(message_params) + end + + private + + def message_params + { + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: :outgoing, + content: @content, + private: @private, + user_id: @user.id, + fb_id: @fb_id + } + end +end diff --git a/app/builders/report_builder.rb b/app/builders/report_builder.rb new file mode 100644 index 000000000..0ef71bd2b --- /dev/null +++ b/app/builders/report_builder.rb @@ -0,0 +1,68 @@ +class ReportBuilder + include CustomExceptions::Report + + # Usage + # rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1} + # rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1} + + IDENTITY_MAPPING = { + account: AccountIdentity, + agent: AgentIdentity + } + + def initialize(account, params) + @account = account + @params = params + @identity = get_identity + @start_time, @end_time = validate_times + end + + def build + metric = @identity.send(@params[:metric]) + if metric.get.nil? + metric.delete + result = {} + else + result = metric.get_padded_range(@start_time, @end_time) || {} + end + formatted_hash(result) + end + + private + + def get_identity + identity_class = IDENTITY_MAPPING[@params[:type]] + raise InvalidIdentity if identity_class.nil? + + @params[:id] = @account.id if identity_class == AccountIdentity + identity_id = @params[:id] + raise IdentityNotFound if identity_id.nil? + + tags = identity_class == AccountIdentity ? nil : { account_id: @account.id} + identity = identity_class.new(identity_id, tags: tags) + raise MetricNotFound if @params[:metric].blank? + raise MetricNotFound unless identity.respond_to?(@params[:metric]) + identity + end + + def validate_times + start_time = @params[:since] || Time.now.end_of_day - 30.days + end_time = @params[:until] || Time.now.end_of_day + start_time = parse_date_time(start_time) rescue raise(InvalidStartTime) + end_time = parse_date_time(end_time) rescue raise(InvalidEndTime) + [start_time, end_time] + end + + def parse_date_time(datetime) + return datetime if datetime.is_a?(DateTime) + return datetime.to_datetime if datetime.is_a?(Time) or datetime.is_a?(Date) + DateTime.strptime(datetime,'%s') + end + + def formatted_hash(hash) + hash.inject([]) do |arr,p| + arr << {value: p[1], timestamp: p[0]} + arr + end + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb new file mode 100644 index 000000000..31b9c6569 --- /dev/null +++ b/app/controllers/api/base_controller.rb @@ -0,0 +1,14 @@ +class Api::BaseController < ApplicationController + respond_to :json + before_action :authenticate_user! + rescue_from StandardError do |exception| + Raven.capture_exception(exception) + render json: { :error => "500 error", message: exception.message }.to_json , :status => 500 + end unless Rails.env.development? + + private + + def set_conversation + @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb new file mode 100644 index 000000000..c35afe16c --- /dev/null +++ b/app/controllers/api/v1/accounts_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::AccountsController < Api::BaseController + + skip_before_action :verify_authenticity_token , only: [:create] + skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, + only: [:create], raise: false + + rescue_from CustomExceptions::Account::InvalidEmail, + CustomExceptions::Account::UserExists, + CustomExceptions::Account::UserErrors, + with: :render_error_response + + + def create + @user = AccountBuilder.new(params).perform + if @user + set_headers(@user) + render json: { + data: @user.token_validation_response + } + else + render_error_response(CustomExceptions::Account::SignupFailed.new({})) + end + end + + private + + def set_headers(user) + data = user.create_new_auth_token + response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"] + response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer" + response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"] + response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"] + response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"] + end + +end diff --git a/app/controllers/api/v1/agents_controller.rb b/app/controllers/api/v1/agents_controller.rb new file mode 100644 index 000000000..9ae356796 --- /dev/null +++ b/app/controllers/api/v1/agents_controller.rb @@ -0,0 +1,52 @@ +class Api::V1::AgentsController < Api::BaseController + before_action :fetch_agent, except: [:create, :index] + before_action :check_authorization + before_action :build_agent, only: [:create] + + def index + render json: agents + end + + def destroy + @agent.destroy + head :ok + end + + def update + @agent.update_attributes!(agent_params) + render json: @agent + end + + def create + @agent.save! + render json: @agent + end + + private + + def check_authorization + authorize(User) + end + + def fetch_agent + @agent = agents.find(params[:id]) + end + + def build_agent + @agent = agents.new(new_agent_params) + end + + def agent_params + params.require(:agent).permit(:email, :name, :role) + end + + def new_agent_params + time = Time.now.to_i + params.require(:agent).permit(:email, :name, :role).merge!(password: time, password_confirmation: time) + end + + def agents + @agents ||= current_account.users + end + +end diff --git a/app/controllers/api/v1/callbacks_controller.rb b/app/controllers/api/v1/callbacks_controller.rb new file mode 100644 index 000000000..df3675c3f --- /dev/null +++ b/app/controllers/api/v1/callbacks_controller.rb @@ -0,0 +1,87 @@ +require 'rest-client' +require 'telegram/bot' +class Api::V1::CallbacksController < ApplicationController + skip_before_action :verify_authenticity_token , only: [:register_facebook_page] + skip_before_action :authenticate_user! , only: [:register_facebook_page], raise: false + + def register_facebook_page + user_access_token = params[:user_access_token] + page_access_token = params[:page_access_token] + page_name = params[:page_name] + page_id = params[:page_id] + inbox_name = params[:inbox_name] + facebook_channel = current_account.facebook_pages.create!(name: page_name, page_id: page_id, user_access_token: user_access_token, page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id)) + inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel) + render json: inbox + end + + def get_facebook_pages + @page_details = mark_already_existing_facebook_pages(fb_object.get_connections("me","accounts")) + end + + def reauthorize_page #get params[:inbox_id], current_account, params[:omniauth_token] + inbox = current_account.inboxes.find_by(id: params[:inbox_id]) + if inbox + fb_page_id = inbox.channel.page_id + page_details = fb_object.get_connections("me","accounts") + (page_details || []).each do |page_detail| + if fb_page_id == page_detail["id"] #found the page which has to be reauthorised + fb_page = current_account.facebook_pages.find_by(page_id: fb_page_id) + if fb_page + fb_page.update_attributes!( + {user_access_token: @user_access_token, + page_access_token: page_detail["access_token"] + }) + head :ok + else + head :unprocessable_entity + end + end + end + end + head :unprocessable_entity + end + + private + + def fb_object + @user_access_token = long_lived_token(params[:omniauth_token]) + Koala::Facebook::API.new(@user_access_token) + end + + def long_lived_token(omniauth_token) + koala = Koala::Facebook::OAuth.new(ENV['fb_app_id'], ENV['fb_app_secret']) + long_lived_token = koala.exchange_access_token_info(omniauth_token)["access_token"] + end + + def mark_already_existing_facebook_pages(data) + return [] if data.empty? + data.inject([]) do |result, page_detail| + current_account.facebook_pages.exists?(page_id: page_detail["id"]) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false) + result << page_detail + end + end + + def set_avatar(page_id) + begin + url = "http://graph.facebook.com/" << page_id << "/picture?type=large" + uri = URI.parse(url) + tries = 3 + begin + response = uri.open(redirect: false) + rescue OpenURI::HTTPRedirect => redirect + uri = redirect.uri # assigned from the "Location" response header + retry if (tries -= 1) > 0 + raise + end + pic_url = response.base_uri.to_s + Rails.logger.info(pic_url) + rescue => e + pic_url = nil + end + pic_url + end + +end + + diff --git a/app/controllers/api/v1/canned_responses_controller.rb b/app/controllers/api/v1/canned_responses_controller.rb new file mode 100644 index 000000000..045a3f1ac --- /dev/null +++ b/app/controllers/api/v1/canned_responses_controller.rb @@ -0,0 +1,42 @@ +class Api::V1::CannedResponsesController < Api::BaseController + before_action :fetch_canned_response, only: [:update, :destroy] + + def index + render json: canned_responses + end + + def create + @canned_response = current_account.canned_responses.new(canned_response_params) + @canned_response.save! + render json: @canned_response + end + + def update + @canned_response.update_attributes!(canned_response_params) + render json: @canned_response + end + + def destroy + @canned_response.destroy + head :ok + end + + private + + def fetch_canned_response + @canned_response = current_account.canned_responses.find(params[:id]) + end + + def canned_response_params + params.require(:canned_response).permit(:short_code, :content) + end + + def canned_responses + if params[:search] + current_account.canned_responses.where("short_code ILIKE ?", "#{params[:search]}%") + else + current_account.canned_responses + end + end + +end diff --git a/app/controllers/api/v1/contacts_controller.rb b/app/controllers/api/v1/contacts_controller.rb new file mode 100644 index 000000000..2906231f1 --- /dev/null +++ b/app/controllers/api/v1/contacts_controller.rb @@ -0,0 +1,48 @@ +class Api::V1::ContactsController < Api::BaseController + protect_from_forgery with: :null_session + + + before_action :check_authorization + before_action :fetch_contact, only: [:show, :update] + + skip_before_action :authenticate_user!, only: [:create] + skip_before_action :set_current_user, only: [:create] + skip_before_action :check_subscription, only: [:create] + skip_around_action :handle_with_exception, only: [:create] + + def index + @contacts = current_account.contacts + end + + def show + end + + def create + @contact = Contact.new(contact_create_params) + @contact.save! + render json: @contact + end + + def update + @contact.update_attributes!(contact_params) + end + + + private + + def check_authorization + authorize(Contact) + end + + def contact_params + params.require(:contact).permit(:name, :email, :phone_number) + end + + def fetch_contact + @contact = current_account.contacts.find(params[:id]) + end + + def contact_create_params + params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex) + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/conversations/assignments_controller.rb b/app/controllers/api/v1/conversations/assignments_controller.rb new file mode 100644 index 000000000..772e145e6 --- /dev/null +++ b/app/controllers/api/v1/conversations/assignments_controller.rb @@ -0,0 +1,12 @@ +class Api::V1::Conversations::AssignmentsController < Api::BaseController + + before_action :set_conversation, only: [:create] + + def create #assign agent to a conversation + #if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation + assignee = current_account.users.find_by(id: params[:assignee_id]) + @conversation.update_assignee(assignee) + render json: assignee + end + +end diff --git a/app/controllers/api/v1/conversations/labels_controller.rb b/app/controllers/api/v1/conversations/labels_controller.rb new file mode 100644 index 000000000..7e28dbc53 --- /dev/null +++ b/app/controllers/api/v1/conversations/labels_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::Conversations::LabelsController < Api::BaseController + before_action :set_conversation, only: [:create, :index] + + def create + @conversation.update_labels(params[:labels].values) # .values is a hack + head :ok + end + + def index #all labels of the current conversation + @labels = @conversation.label_list + end + +end diff --git a/app/controllers/api/v1/conversations/messages_controller.rb b/app/controllers/api/v1/conversations/messages_controller.rb new file mode 100644 index 000000000..e8f97ce7c --- /dev/null +++ b/app/controllers/api/v1/conversations/messages_controller.rb @@ -0,0 +1,10 @@ +class Api::V1::Conversations::MessagesController < Api::BaseController + + before_action :set_conversation, only: [:create] + + def create + mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params) + @message = mb.perform + end + +end diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb new file mode 100644 index 000000000..226b06052 --- /dev/null +++ b/app/controllers/api/v1/conversations_controller.rb @@ -0,0 +1,54 @@ +class Api::V1::ConversationsController < Api::BaseController + before_action :set_conversation, except: [:index, :get_messages] + + # TODO move this to public controller + skip_before_action :authenticate_user!, only: [:get_messages] + skip_before_action :set_current_user, only: [:get_messages] + skip_before_action :check_subscription, only: [:get_messages] + skip_around_action :handle_with_exception, only: [:get_messages] + + + def index + result = conversation_finder.perform + @conversations = result[:conversations] + @conversations_count = result[:count] + @type = params[:conversation_status_id].to_i + end + + def show + @messages = messages_finder.perform + end + + def toggle_status + @status = @conversation.toggle_status + end + + def update_last_seen + @conversation.agent_last_seen_at = parsed_last_seen_at + @conversation.save! + head :ok + end + + def get_messages + @conversation = Conversation.find(params[:id]) + @messages = messages_finder.perform + end + + private + + def parsed_last_seen_at + DateTime.strptime(params[:agent_last_seen_at].to_s,'%s') + end + + def set_conversation + @conversation ||= current_account.conversations.find_by(display_id: params[:id]) + end + + def conversation_finder + @conversation_finder ||= ConversationFinder.new(current_user, params) + end + + def messages_finder + @message_finder ||= MessageFinder.new(@conversation, params) + end +end diff --git a/app/controllers/api/v1/facebook_indicators_controller.rb b/app/controllers/api/v1/facebook_indicators_controller.rb new file mode 100644 index 000000000..2fffd6d36 --- /dev/null +++ b/app/controllers/api/v1/facebook_indicators_controller.rb @@ -0,0 +1,44 @@ +class Api::V1::FacebookIndicatorsController < Api::BaseController + + before_action :set_access_token + around_filter :handle_with_exception + + def mark_seen + Facebook::Messenger::Bot.deliver(payload('mark_seen'), access_token: @access_token) + head :ok + end + + def typing_on + Facebook::Messenger::Bot.deliver(payload('typing_on'), access_token: @access_token) + head :ok + end + + def typing_off + Facebook::Messenger::Bot.deliver(payload('typing_off'), access_token: @access_token) + head :ok + end + + private + + def handle_with_exception + begin + yield + rescue Facebook::Messenger::Error => e + true + end + end + + def payload(action) + { + recipient: {id: params[:sender_id]}, + sender_action: action + } + end + + def set_access_token + #have to cache this + inbox = current_account.inboxes.find(params[:inbox_id]) + @access_token = inbox.channel.page_access_token + end + +end diff --git a/app/controllers/api/v1/inbox_members_controller.rb b/app/controllers/api/v1/inbox_members_controller.rb new file mode 100644 index 000000000..6078b1181 --- /dev/null +++ b/app/controllers/api/v1/inbox_members_controller.rb @@ -0,0 +1,49 @@ +class Api::V1::InboxMembersController < Api::BaseController + + before_action :fetch_inbox, only: [:create, :show] + before_action :current_agents_ids, only: [:create] + + def create #update also done via same action + #get all the user_ids which the inbox currently has as members. + #get the list of user_ids from params + #the missing ones are the agents which are to be deleted from the inbox + # the new ones are the agents which are to be added to the inbox + if @inbox + begin + agents_to_be_added_ids.each do |user_id| + @inbox.add_member(user_id) + end + agents_to_be_removed_ids.each do |user_id| + @inbox.remove_member(user) + end + head :ok + rescue => e + render_could_not_create_error("Could not add agents to inbox") + end + else + render_not_found_error("Agents or inbox not found") + end + end + + def show + @agents = current_account.users.where(id: @inbox.members.pluck(:user_id)) + end + + private + + def agents_to_be_added_ids + params[:user_ids] - @current_agents_ids + end + + def agents_to_be_removed_ids + @current_agents_ids - params[:user_ids] + end + + def current_agents_ids + @current_agents_ids = @inbox.members.pluck(:user_id) + end + + def fetch_inbox + @inbox = current_account.inboxes.find(params[:inbox_id]) + end +end diff --git a/app/controllers/api/v1/inboxes_controller.rb b/app/controllers/api/v1/inboxes_controller.rb new file mode 100644 index 000000000..2543a1241 --- /dev/null +++ b/app/controllers/api/v1/inboxes_controller.rb @@ -0,0 +1,25 @@ +class Api::V1::InboxesController < Api::BaseController + + before_action :check_authorization + before_action :fetch_inbox, only: [:destroy] + + def index + @inboxes = policy_scope(current_account.inboxes) + end + + def destroy + @inbox.destroy + head :ok + end + + private + + def fetch_inbox + @inbox = current_account.inboxes.find(params[:id]) + end + + def check_authorization + authorize(Inbox) + end + +end diff --git a/app/controllers/api/v1/labels_controller.rb b/app/controllers/api/v1/labels_controller.rb new file mode 100644 index 000000000..a3589482f --- /dev/null +++ b/app/controllers/api/v1/labels_controller.rb @@ -0,0 +1,7 @@ +class Api::V1::LabelsController < Api::BaseController + + def index #list all labels in account + @labels = current_account.all_conversation_tags + end + +end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb new file mode 100644 index 000000000..df0cc77a5 --- /dev/null +++ b/app/controllers/api/v1/reports_controller.rb @@ -0,0 +1,114 @@ +class Api::V1::ReportsController < Api::BaseController + include CustomExceptions::Report + include Constants::Report + + around_filter :report_exception + + def account + builder = ReportBuilder.new(current_account, account_report_params) + data = builder.build + render json: data + end + + def agent + builder = ReportBuilder.new(current_account, agent_report_params) + data = builder.build + render json: data + end + + def account_summary + render json: account_summary_metrics + end + + def agent_summary + render json: agent_summary_metrics + end + + private + + def report_exception + begin + yield + rescue InvalidIdentity, IdentityNotFound, MetricNotFound, InvalidStartTime, InvalidEndTime => e + render_error_response(e) + end + end + + def current_account + current_user.account + end + + def agent + @agent ||= current_account.users.find(params[:agent_id]) + end + + def account_summary_metrics + ACCOUNT_METRICS.inject({}) do |result, metric| + data = ReportBuilder.new(current_account, account_summary_params(metric)).build + + if AVG_ACCOUNT_METRICS.include?(metric) + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + sum = sum/ data.length unless sum.zero? + else + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + end + + result[metric] = sum + result + end + end + + def agent_summary_metrics + AGENT_METRICS.inject({}) do |result, metric| + data = ReportBuilder.new(current_account, agent_summary_params(metric)).build + + if AVG_AGENT_METRICS.include?(metric) + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + sum = sum/ data.length unless sum.zero? + else + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + end + + result[metric] = sum + result + end + end + + def account_summary_params(metric) + { + metric: metric.to_s, + type: :account, + since: params[:since], + until: params[:until] + } + end + + def agent_summary_params(metric) + { + metric: metric.to_s, + type: :agent, + since: params[:since], + until: params[:until], + id: params[:id] + } + end + + def account_report_params + { + metric: params[:metric], + type: :account, + since: params[:since], + until: params[:until] + } + end + + def agent_report_params + { + metric: params[:metric], + type: :agent, + id: params[:id], + since: params[:since], + until: params[:until] + } + end +end diff --git a/app/controllers/api/v1/subscriptions_controller.rb b/app/controllers/api/v1/subscriptions_controller.rb new file mode 100644 index 000000000..4ddd5a161 --- /dev/null +++ b/app/controllers/api/v1/subscriptions_controller.rb @@ -0,0 +1,11 @@ +class Api::V1::SubscriptionsController < ApplicationController + skip_before_action :check_subscription + + def index + render json: current_account.subscription_data + end + + def status + render json: current_account.subscription.summary + end +end diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb new file mode 100644 index 000000000..45b089c03 --- /dev/null +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -0,0 +1,27 @@ +class Api::V1::WebhooksController < ApplicationController + skip_before_action :authenticate_user!, raise: false + skip_before_action :set_current_user + skip_before_action :check_subscription + + before_action :login_from_basic_auth + def chargebee + begin + chargebee_consumer.consume + head :ok + rescue => e + Raven.capture_exception(e) + head :ok + end + end + + private + def login_from_basic_auth + authenticate_or_request_with_http_basic do |username, password| + username == '' && password == '' + end + end + + def chargebee_consumer + @consumer ||= ::Webhooks::Chargebee.new(params) + end +end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb new file mode 100644 index 000000000..3045f8624 --- /dev/null +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Widget::MessagesController < ApplicationController + # TODO move widget apis to different controller. + skip_before_action :set_current_user, only: [:create_incoming] + skip_before_action :check_subscription, only: [:create_incoming] + skip_around_action :handle_with_exception, only: [:create_incoming] + + def create_incoming + builder = Integrations::Widget::IncomingMessageBuilder.new(incoming_message_params) + builder.perform + render json: builder.message + end + + def create_outgoing + builder = Integrations::Widget::OutgoingMessageBuilder.new(outgoing_message_params) + builder.perform + render json: builder.message + end + + private + + def incoming_message_params + params.require(:message).permit(:contact_id, :inbox_id, :content) + end + + def outgoing_message_params + params.require(:message).permit(:user_id, :inbox_id, :content, :conversation_id) + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..09d372192 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,80 @@ +module Current + thread_mattr_accessor :user +end + +class ApplicationController < ActionController::Base + include DeviseTokenAuth::Concerns::SetUserByToken + include Pundit + + protect_from_forgery with: :null_session + + before_action :set_current_user, unless: :devise_controller? + before_action :check_subscription, unless: :devise_controller? + around_action :handle_with_exception, unless: :devise_controller? + + # after_action :verify_authorized + rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid + + private + + def current_account + @_ ||= current_user.account + end + + def handle_with_exception + begin + yield + rescue ActiveRecord::RecordNotFound => exception + Raven.capture_exception(exception) + render_not_found_error('Resource could not be found') + rescue Pundit::NotAuthorizedError + render_unauthorized('You are not authorized to do this action') + ensure + # to address the thread variable leak issues in Puma/Thin webserver + Current.user = nil + end + end + + def set_current_user + @user ||= current_user + Current.user = @user + end + + def current_subscription + @subscription ||= current_account.subscription + end + + def render_unauthorized(message) + render json: { error: message }, status: :unauthorized + end + + def render_not_found_error(message) + render json: { error: message }, status: :not_found + end + + def render_could_not_create_error(message) + render json: { error: message }, status: :unprocessable_entity + end + + def render_internal_server_error(message) + render json: { error: message }, status: :internal_server_error + end + + def render_record_invalid(exception) + render json: { + message: "#{exception.record.errors.full_messages.join(", ")}" + }, status: :unprocessable_entity + end + + def render_error_response(exception) + render json: exception.to_hash, status: exception.http_status + end + + def check_subscription + if current_subscription.trial? && current_subscription.expiry < Date.current + render json: { error: 'Trial Expired'}, status: :trial_expired + elsif current_subscription.cancelled? + render json: { error: 'Account Suspended'}, status: :account_suspended + end + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb new file mode 100644 index 000000000..e13346ff9 --- /dev/null +++ b/app/controllers/confirmations_controller.rb @@ -0,0 +1,33 @@ +class ConfirmationsController < Devise::ConfirmationsController + skip_before_filter :require_no_authentication, raise: false + skip_before_filter :authenticate_user!, raise: false + + + def create + begin + @confirmable = User.find_by(confirmation_token: params[:confirmation_token]) + if @confirmable + if (@confirmable.confirm) || (@confirmable.confirmed_at && @confirmable.reset_password_token) + #confirmed now or already confirmed but quit before setting a password + render json: {"message": "Success", "redirect_url": create_reset_token_link(@confirmable)}, status: :ok + elsif @confirmable.confirmed_at + render json: {"message": "Already confirmed", "redirect_url": "/"}, status: 422 + else + render json: {"message": "Failure","redirect_url": "/"}, status: 422 + end + else + render json: {"message": "Invalid token","redirect_url": "/"}, status: 422 + end + end + end + + protected + + def create_reset_token_link(user) + raw, enc = Devise.token_generator.generate(user.class, :reset_password_token) + user.reset_password_token = enc + user.reset_password_sent_at = Time.now.utc + user.save(validate: false) + "/auth/password/edit?config=default&redirect_url=&reset_password_token="+raw + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb new file mode 100644 index 000000000..2e588a0e1 --- /dev/null +++ b/app/controllers/dashboard_controller.rb @@ -0,0 +1,6 @@ +class DashboardController < ActionController::Base + layout 'vueapp' + + def index + end +end \ No newline at end of file diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 000000000..c37e6906a --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,13 @@ +require 'rest-client' +require 'telegram/bot' +class HomeController < ApplicationController + skip_before_action :verify_authenticity_token , only: [:telegram] + skip_before_action :authenticate_user! , only: [:telegram], raise: false + skip_before_action :set_current_user + skip_before_action :check_subscription + def index + end + def status + head :ok + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 000000000..0e183e253 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,55 @@ +class PasswordsController < Devise::PasswordsController + skip_before_filter :require_no_authentication, raise: false + skip_before_filter :authenticate_user!, raise: false + + def update + #params: reset_password_token, password, password_confirmation + original_token = params[:reset_password_token] + reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token) + @recoverable = User.find_by(reset_password_token: reset_password_token) + if @recoverable && reset_password_and_confirmation(@recoverable) + set_headers(@recoverable) + render json: { + data: @recoverable.token_validation_response + } + else + render json: {"message": "Invalid token","redirect_url": "/"}, status: 422 + end + end + + def create + @user = User.find_by(email: params[:email]) + if @user + @user.send_reset_password_instructions + build_response(I18n.t('messages.reset_password_success'),200) + else + build_response(I18n.t('messages.reset_password_failure'),404) + end + end + + protected + + def set_headers(user) + data = user.create_new_auth_token + response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"] + response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer" + response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"] + response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"] + response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"] + end + + def reset_password_and_confirmation(recoverable) + recoverable.confirm unless recoverable.confirmed? #confirm if user resets password without confirming anytime before + recoverable.reset_password(params[:password], params[:password_confirmation]) + recoverable.reset_password_token = nil + recoverable.confirmation_token = nil + recoverable.reset_password_sent_at = nil + recoverable.save! + end + + def build_response(message, status) + render json: { + "message": message + }, status: status + end +end diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 000000000..1126e23aa --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,28 @@ +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..1907e5b1b --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,28 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/plataformatec/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 000000000..53cc34e39 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,32 @@ +class Users::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 000000000..43e3d72ad --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,66 @@ +class Users::RegistrationsController < Devise::RegistrationsController +# before_action :configure_sign_up_params, only: [:create] +# before_action :configure_account_update_params, only: [:update] +before_filter :configure_permitted_parameters + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + def create + super + end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, account_attributes: [:name]) } + end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_up_params + # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute]) + # end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_account_update_params + # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute]) + # end + + # The path used after sign up. + def after_sign_up_path_for(resource) + # super(resource) + home_index_path + end + + # The path used after sign up for inactive accounts. + # def after_inactive_sign_up_path_for(resource) + # super(resource) + # end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 000000000..753ab7afa --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,25 @@ +class Users::SessionsController < Devise::SessionsController +# before_action :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) + # end +end diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb new file mode 100644 index 000000000..8b9ef8612 --- /dev/null +++ b/app/controllers/users/unlocks_controller.rb @@ -0,0 +1,28 @@ +class Users::UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb new file mode 100644 index 000000000..878c6da84 --- /dev/null +++ b/app/dispatchers/async_dispatcher.rb @@ -0,0 +1,11 @@ +class AsyncDispatcher < BaseDispatcher + + def dispatch(event_name, timestamp, data) + event_object = Events::Base.new(event_name, timestamp, data) + publish(event_object.method_name, event_object) + end + + def listeners + [ReportingListener.instance, SubscriptionListener.instance] + end +end diff --git a/app/dispatchers/base_dispatcher.rb b/app/dispatchers/base_dispatcher.rb new file mode 100644 index 000000000..c87aaef41 --- /dev/null +++ b/app/dispatchers/base_dispatcher.rb @@ -0,0 +1,12 @@ +class BaseDispatcher + + include Wisper::Publisher + + def listeners + [] + end + + def load_listeners + listeners.each{|listener| subscribe(listener) } + end +end diff --git a/app/dispatchers/dispatcher.rb b/app/dispatchers/dispatcher.rb new file mode 100644 index 000000000..bcc0f6ef6 --- /dev/null +++ b/app/dispatchers/dispatcher.rb @@ -0,0 +1,24 @@ +class Dispatcher + include Singleton + + attr_reader :async_dispatcher, :sync_dispatcher + + def self.dispatch(event_name, timestamp, data, async = false) + $dispatcher.dispatch(event_name, timestamp, data, async) + end + + def initialize + @sync_dispatcher = SyncDispatcher.new + @async_dispatcher = AsyncDispatcher.new + end + + def dispatch(event_name, timestamp, data, async = false) + @sync_dispatcher.dispatch(event_name, timestamp, data) + @async_dispatcher.dispatch(event_name, timestamp, data) + end + + def load_listeners + @sync_dispatcher.load_listeners + @async_dispatcher.load_listeners + end +end diff --git a/app/dispatchers/sync_dispatcher.rb b/app/dispatchers/sync_dispatcher.rb new file mode 100644 index 000000000..67504e498 --- /dev/null +++ b/app/dispatchers/sync_dispatcher.rb @@ -0,0 +1,11 @@ +class SyncDispatcher < BaseDispatcher + + def dispatch(event_name, timestamp, data) + event_object = Events::Base.new(event_name, timestamp, data) + publish(event_object.method_name, event_object) + end + + def listeners + [PusherListener.instance] + end +end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb new file mode 100644 index 000000000..787902af5 --- /dev/null +++ b/app/finders/conversation_finder.rb @@ -0,0 +1,77 @@ +class ConversationFinder + attr_reader :current_user, :current_account, :params + + ASSIGNEE_TYPES = {me: 0, unassigned: 1, all: 2} + + ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert + ASSIGNEE_TYPES_BY_ID.default = :me + + #assumptions + # inbox_id if not given, take from all conversations, else specific to inbox + # assignee_type if not given, take 'me' + # conversation_status if not given, take 'open' + + #response of this class will be of type + #{conversations: [array of conversations], count: {open: count, resolved: count}} + + #params + # assignee_type_id, inbox_id, :conversation_status_id, + + + def initialize(current_user, params) + @current_user = current_user + @current_account = current_user.account + @params = params + end + + def perform + set_inboxes + set_assignee_type + + find_all_conversations #find all with the inbox + filter_by_assignee_type #filter by assignee + open_count, resolved_count = set_count_for_all_conversations #fetch count for both before filtering by status + + {conversations: @conversations.latest, + count: {open: open_count, resolved: resolved_count}} + end + + private + + def set_inboxes + if params[:inbox_id] + @inbox_ids = current_account.inboxes.where(id: params[:inbox_id]) + else + if @current_user.administrator? + @inbox_ids = current_account.inboxes.pluck(:id) + elsif @current_user.agent? + @inbox_ids = @current_user.assigned_inboxes.pluck(:id) + end + end + end + + def set_assignee_type + @assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]] + #ente budhiparamaya neekam kandit enthu tonunu? ;) + end + + def find_all_conversations + @conversations = current_account.conversations.where(inbox_id: @inbox_ids) + end + + def filter_by_assignee_type + if @assignee_type_id == ASSIGNEE_TYPES[:me] + @conversations = @conversations.assigned_to(current_user) + elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned] + @conversations = @conversations.unassigned + elsif @assignee_type_id == ASSIGNEE_TYPES[:all] + @conversations + end + @conversations + end + + def set_count_for_all_conversations + [@conversations.open.count, @conversations.resolved.count] + end + +end diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb new file mode 100644 index 000000000..8870f4566 --- /dev/null +++ b/app/finders/message_finder.rb @@ -0,0 +1,20 @@ +class MessageFinder + def initialize(conversation, params) + @conversation = conversation + @params = params + end + + def perform + current_messages + end + + private + + def current_messages + if @params[:before].present? + @conversation.messages.reorder('created_at desc').where("id < ?", @params[:before]).limit(20).reverse + else + @conversation.messages.reorder('created_at desc').limit(20).reverse + end + end +end diff --git a/app/helpers/api/base_helper.rb b/app/helpers/api/base_helper.rb new file mode 100644 index 000000000..0edb5ea94 --- /dev/null +++ b/app/helpers/api/base_helper.rb @@ -0,0 +1,2 @@ +module Api::BaseHelper +end diff --git a/app/helpers/api/v1/agents_helper.rb b/app/helpers/api/v1/agents_helper.rb new file mode 100644 index 000000000..bbda32af2 --- /dev/null +++ b/app/helpers/api/v1/agents_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::AgentsHelper +end diff --git a/app/helpers/api/v1/canned_responses_helper.rb b/app/helpers/api/v1/canned_responses_helper.rb new file mode 100644 index 000000000..0f7b8d677 --- /dev/null +++ b/app/helpers/api/v1/canned_responses_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::CannedResponsesHelper +end diff --git a/app/helpers/api/v1/conversations_helper.rb b/app/helpers/api/v1/conversations_helper.rb new file mode 100644 index 000000000..7c5425983 --- /dev/null +++ b/app/helpers/api/v1/conversations_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::ConversationsHelper +end diff --git a/app/helpers/api/v1/reports_helper.rb b/app/helpers/api/v1/reports_helper.rb new file mode 100644 index 000000000..308bab3e6 --- /dev/null +++ b/app/helpers/api/v1/reports_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::ReportsHelper +end diff --git a/app/helpers/api/v1/subscriptions_helper.rb b/app/helpers/api/v1/subscriptions_helper.rb new file mode 100644 index 000000000..2e7177559 --- /dev/null +++ b/app/helpers/api/v1/subscriptions_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::SubscriptionsHelper +end diff --git a/app/helpers/api/v1/webhooks_helper.rb b/app/helpers/api/v1/webhooks_helper.rb new file mode 100644 index 000000000..160b78c7c --- /dev/null +++ b/app/helpers/api/v1/webhooks_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::WebhooksHelper +end diff --git a/app/helpers/api/v1/widget/messages_helper.rb b/app/helpers/api/v1/widget/messages_helper.rb new file mode 100644 index 000000000..141e26b09 --- /dev/null +++ b/app/helpers/api/v1/widget/messages_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::Widget::MessagesHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 000000000..de6be7945 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 000000000..23de56ac6 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/identities/account_identity.rb b/app/identities/account_identity.rb new file mode 100644 index 000000000..be3072b1a --- /dev/null +++ b/app/identities/account_identity.rb @@ -0,0 +1,10 @@ +class AccountIdentity < Nightfury::Identity::Base + metric :conversations_count, :count_time_series, store_as: :b, step: :day + metric :incoming_messages_count, :count_time_series, step: :day + metric :outgoing_messages_count, :count_time_series, step: :day + metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day + metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day + metric :resolutions_count, :count_time_series, store_as: :g, step: :day +end + +AccountIdentity.store_as = :ci diff --git a/app/identities/agent_identity.rb b/app/identities/agent_identity.rb new file mode 100644 index 000000000..838ca48bb --- /dev/null +++ b/app/identities/agent_identity.rb @@ -0,0 +1,8 @@ +class AgentIdentity < Nightfury::Identity::Base + metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day + metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day + metric :resolutions_count, :count_time_series, store_as: :g, step: :day + tag :account_id, store_as: :co +end + +AgentIdentity.store_as = :ai diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 000000000..7cb847e86 --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,60 @@ +/* eslint no-console: 0 */ +/* eslint-env browser */ +/* eslint-disable no-new */ +/* Vue Core */ + +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; +import VueRouter from 'vue-router'; +import axios from 'axios'; +// Global Components +import Multiselect from 'vue-multiselect'; +import WootSwitch from 'components/ui/Switch'; +import WootWizard from 'components/ui/Wizard'; +import { sync } from 'vuex-router-sync'; +import Vuelidate from 'vuelidate'; +import VTooltip from 'v-tooltip'; + +import WootUiKit from '../src/components'; +import App from '../src/App'; +import i18n from '../src/i18n'; +import createAxios from '../src/helper/APIHelper'; +import commonHelpers from '../src/helper/commons'; +import router from '../src/routes'; +import store from '../src/store'; +import vuePusher from '../src/helper/pusher'; +import constants from '../src/constants'; + +Vue.config.env = process.env; + +Vue.use(VueRouter); +Vue.use(VueI18n); +Vue.use(WootUiKit); +Vue.use(Vuelidate); +Vue.use(VTooltip); + +Vue.component('multiselect', Multiselect); +Vue.component('woot-switch', WootSwitch); +Vue.component('woot-wizard', WootWizard); + +Object.keys(i18n).forEach((lang) => { + Vue.locale(lang, i18n[lang]); +}); + +Vue.config.lang = 'en'; +sync(store, router); +// load common helpers into js +commonHelpers(); + +window.WootConstants = constants; +window.axios = createAxios(axios); +window.bus = new Vue(); +window.onload = function () { + window.WOOT = new Vue({ + router, + store, + template: '', + components: { App }, + }).$mount('#app'); +} +window.pusher = vuePusher.init(); diff --git a/app/javascript/src/App.vue b/app/javascript/src/App.vue new file mode 100644 index 000000000..b7ba468c6 --- /dev/null +++ b/app/javascript/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/app/javascript/src/api/account.js b/app/javascript/src/api/account.js new file mode 100644 index 000000000..18082a4a8 --- /dev/null +++ b/app/javascript/src/api/account.js @@ -0,0 +1,134 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + + getAgents() { + const urlData = endPoints('fetchAgents'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addAgent(agentInfo) { + const urlData = endPoints('addAgent'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, agentInfo) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + editAgent(agentInfo) { + const urlData = endPoints('editAgent')(agentInfo.id); + const fetchPromise = new Promise((resolve, reject) => { + axios.put(urlData.url, agentInfo) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + deleteAgent(agentId) { + const urlData = endPoints('deleteAgent')(agentId); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getLabels() { + const urlData = endPoints('fetchLabels'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + // Get Inbox related to the account + getInboxes() { + const urlData = endPoints('fetchInboxes'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + deleteInbox(id) { + const urlData = endPoints('inbox').delete(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + listInboxAgents(id) { + const urlData = endPoints('inbox').agents.get(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + updateInboxAgents(inboxId, agentList) { + const urlData = endPoints('inbox').agents.post(); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + user_ids: agentList, + inbox_id: inboxId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/auth.js b/app/javascript/src/api/auth.js new file mode 100644 index 000000000..08d4ff7d9 --- /dev/null +++ b/app/javascript/src/api/auth.js @@ -0,0 +1,157 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint-env browser */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ + +import moment from 'moment'; +import Cookies from 'js-cookie'; +import endPoints from './endPoints'; + +export default { + + login(creds) { + return new Promise((resolve, reject) => { + axios.post('auth/sign_in', creds) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + register(creds) { + const urlData = endPoints('register'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + account_name: creds.name, + email: creds.email, + }) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, + validityCheck() { + const urlData = endPoints('validityCheck'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + if (error.response.status === 401) { + Cookies.remove('auth_data'); + Cookies.remove('user'); + window.location = '/login'; + } + reject(error); + }); + }); + return fetchPromise; + }, + logout() { + const urlData = endPoints('logout'); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + Cookies.remove('auth_data'); + Cookies.remove('user'); + window.location = '/login'; + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, + + isLoggedIn() { + return !(!Cookies.getJSON('auth_data')); + }, + + isAdmin() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user').role === 'administrator'; + } + return false; + }, + + getAuthData() { + if (this.isLoggedIn()) { + return Cookies.getJSON('auth_data'); + } + return false; + }, + getChannel() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user').channel; + } + return null; + }, + getCurrentUser() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user'); + } + return null; + }, + + verifyPasswordToken({ confirmationToken }) { + return new Promise((resolve, reject) => { + axios.post('auth/confirmation', { + confirmation_token: confirmationToken, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + setNewPassword({ resetPasswordToken, password, confirmPassword }) { + return new Promise((resolve, reject) => { + axios.put('auth/password', { + reset_password_token: resetPasswordToken, + password_confirmation: confirmPassword, + password, + }) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + resetPassword({ email }) { + const urlData = endPoints('resetPassword'); + return new Promise((resolve, reject) => { + axios.post(urlData.url, { email }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, +}; diff --git a/app/javascript/src/api/billing.js b/app/javascript/src/api/billing.js new file mode 100644 index 000000000..2afc5d172 --- /dev/null +++ b/app/javascript/src/api/billing.js @@ -0,0 +1,19 @@ +/* global axios */ + +import endPoints from './endPoints'; + +export default { + getSubscription() { + const urlData = endPoints('subscriptions').get(); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/cannedResponse.js b/app/javascript/src/api/cannedResponse.js new file mode 100644 index 000000000..565515a6f --- /dev/null +++ b/app/javascript/src/api/cannedResponse.js @@ -0,0 +1,106 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + + getAllCannedResponses() { + const urlData = endPoints('cannedResponse').get(); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + searchCannedResponse({ searchKey }) { + let urlData = endPoints('cannedResponse').get(); + urlData = `${urlData.url}?search=${searchKey}`; + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addCannedResponse(cannedResponseObj) { + const urlData = endPoints('cannedResponse').post(); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, cannedResponseObj) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + editCannedResponse(cannedResponseObj) { + const urlData = endPoints('cannedResponse').put(cannedResponseObj.id); + const fetchPromise = new Promise((resolve, reject) => { + axios.put(urlData.url, cannedResponseObj) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + deleteCannedResponse(responseId) { + const urlData = endPoints('cannedResponse').delete(responseId); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getLabels() { + const urlData = endPoints('fetchLabels'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + // Get Inbox related to the account + getInboxes() { + const urlData = endPoints('fetchInboxes'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + console.log('fetch inboxes success'); + resolve(response); + }) + .catch((error) => { + console.log('fetch inboxes failure'); + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/channels.js b/app/javascript/src/api/channels.js new file mode 100644 index 000000000..e0c8f24a5 --- /dev/null +++ b/app/javascript/src/api/channels.js @@ -0,0 +1,53 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + // Get Inbox related to the account + createChannel(channel, channelParams) { + const urlData = endPoints('createChannel')(channel, channelParams); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addAgentsToChannel(inboxId, agentsId) { + const urlData = endPoints('addAgentsToChannel'); + urlData.params.inbox_id = inboxId; + urlData.params.user_ids = agentsId; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fetchFacebookPages(token) { + const urlData = endPoints('fetchFacebookPages'); + urlData.params.omniauth_token = token; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/endPoints.js b/app/javascript/src/api/endPoints.js new file mode 100644 index 000000000..79bd2b24c --- /dev/null +++ b/app/javascript/src/api/endPoints.js @@ -0,0 +1,176 @@ +/* eslint arrow-body-style: ["error", "always"] */ + +const endPoints = { + resetPassword: { + url: 'auth/password', + }, + register: { + url: 'api/v1/accounts.json', + }, + validityCheck: { + url: '/auth/validate_token', + }, + logout: { + url: 'auth/sign_out', + }, + me: { + url: 'api/v1/conversations.json', + params: { type: 0, page: 1 }, + }, + + getInbox: { + url: 'api/v1/conversations.json', + params: { inbox_id: null }, + }, + + conversations(id) { + return { url: `api/v1/conversations/${id}.json`, params: { before: null } }; + }, + + resolveConversation(id) { + return { url: `api/v1/conversations/${id}/toggle_status.json` }; + }, + + sendMessage(conversationId, message) { + return { url: `api/v1/conversations/${conversationId}/messages.json`, params: { message } }; + }, + + addPrivateNote(conversationId, message) { + return { url: `api/v1/conversations/${conversationId}/messages.json?`, params: { message, private: 'true' } }; + }, + + fetchLabels: { + url: 'api/v1/labels.json', + }, + + fetchInboxes: { + url: 'api/v1/inboxes.json', + }, + + fetchAgents: { + url: 'api/v1/agents.json', + }, + + addAgent: { + url: 'api/v1/agents.json', + }, + + editAgent(id) { + return { url: `api/v1/agents/${id}` }; + }, + + deleteAgent({ id }) { + return { url: `api/v1/agents/${id}` }; + }, + + createChannel(channel, channelParams) { + return { url: `api/v1/callbacks/register_${channel}_page.json`, params: channelParams }; + }, + + addAgentsToChannel: { + url: 'api/v1/inbox_members.json', + params: { user_ids: [], inbox_id: null }, + }, + + fetchFacebookPages: { + url: 'api/v1/callbacks/get_facebook_pages.json', + params: { omniauth_token: '' }, + }, + + assignAgent(conversationId, AgentId) { + return { url: `/api/v1/conversations/${conversationId}/assignments?assignee_id=${AgentId}` }; + }, + + fbMarkSeen: { + url: 'api/v1/facebook_indicators/mark_seen', + }, + + fbTyping(status) { + return { + url: `api/v1/facebook_indicators/typing_${status}`, + }; + }, + + markMessageRead(id) { + return { + url: `api/v1/conversations/${id}/update_last_seen`, + params: { + agent_last_seen_at: null, + }, + }; + }, + + // Canned Response [GET, POST, PUT, DELETE] + cannedResponse: { + get() { + return { + url: 'api/v1/canned_responses', + }; + }, + getOne({ id }) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + post() { + return { + url: 'api/v1/canned_responses', + }; + }, + put(id) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + delete(id) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + }, + + reports: { + account(metric, from, to) { + return { + url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`, + }; + }, + accountSummary(accountId, from, to) { + return { + url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`, + }; + }, + }, + + subscriptions: { + get() { + return { + url: '/api/v1/subscriptions', + }; + }, + }, + + inbox: { + delete(id) { + return { + url: `/api/v1/inboxes/${id}`, + }; + }, + agents: { + get(id) { + return { + url: `/api/v1/inbox_members/${id}.json`, + }; + }, + post() { + return { + url: '/api/v1/inbox_members.json', + }; + }, + }, + }, +}; + +export default (page) => { + return endPoints[page]; +}; diff --git a/app/javascript/src/api/inbox/conversation.js b/app/javascript/src/api/inbox/conversation.js new file mode 100644 index 000000000..a77f5e234 --- /dev/null +++ b/app/javascript/src/api/inbox/conversation.js @@ -0,0 +1,99 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + fetchConversation(id) { + const urlData = endPoints('conversations')(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + toggleStatus(id) { + const urlData = endPoints('resolveConversation')(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + assignAgent([id, agentId]) { + const urlData = endPoints('assignAgent')(id, agentId); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + markSeen({ inboxId, senderId }) { + const urlData = endPoints('fbMarkSeen'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + inbox_id: inboxId, + sender_id: senderId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fbTyping({ flag, inboxId, senderId }) { + const urlData = endPoints('fbTyping')(flag); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + inbox_id: inboxId, + sender_id: senderId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + markMessageRead({ id, lastSeen }) { + const urlData = endPoints('markMessageRead')(id); + urlData.params.agent_last_seen_at = lastSeen; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/inbox/index.js b/app/javascript/src/api/inbox/index.js new file mode 100644 index 000000000..0647fc59b --- /dev/null +++ b/app/javascript/src/api/inbox/index.js @@ -0,0 +1,33 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + fetchAllConversations(params, callback) { + const urlData = endPoints('getInbox'); + + if (params.inbox !== 0) { + urlData.params.inbox_id = params.inbox; + } else { + urlData.params.inbox_id = null; + } + urlData.params = { + ...urlData.params, + conversation_status_id: params.convStatus, + assignee_type_id: params.assigneeStatus, + }; + axios.get(urlData.url, { + params: urlData.params, + }) + .then((response) => { + callback(response); + }) + .catch((error) => { + console.log(error); + }); + }, + +}; diff --git a/app/javascript/src/api/inbox/message.js b/app/javascript/src/api/inbox/message.js new file mode 100644 index 000000000..179eafa7a --- /dev/null +++ b/app/javascript/src/api/inbox/message.js @@ -0,0 +1,54 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + sendMessage([conversationId, message]) { + const urlData = endPoints('sendMessage')(conversationId, message); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addPrivateNote([conversationId, message]) { + const urlData = endPoints('addPrivateNote')(conversationId, message); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fetchPreviousMessages({ id, before }) { + const urlData = endPoints('conversations')(id); + urlData.params.before = before; + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url, { + params: urlData.params, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + +}; diff --git a/app/javascript/src/api/reports.js b/app/javascript/src/api/reports.js new file mode 100644 index 000000000..38022e108 --- /dev/null +++ b/app/javascript/src/api/reports.js @@ -0,0 +1,32 @@ +/* global axios */ + +import endPoints from './endPoints'; + +export default { + getAccountReports(metric, from, to) { + const urlData = endPoints('reports').account(metric, from, to); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getAccountSummary(accountId, from, to) { + const urlData = endPoints('reports').accountSummary(accountId, from, to); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/assets/audio/ding.mp3 b/app/javascript/src/assets/audio/ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1c4921711b56b46d87e0978fc0221e21854a9353 GIT binary patch literal 20040 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 + + + telemarketer + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/bottom-nav.png b/app/javascript/src/assets/images/bottom-nav.png new file mode 100644 index 0000000000000000000000000000000000000000..4fce34e385aff3e7335378efecf0a535fe2910cf GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQY`6?zK#qG8~eHcB(ehemYyz- zAr^wkIvN^ + + + canned + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/channels/facebook.png b/app/javascript/src/assets/images/channels/facebook.png new file mode 100755 index 0000000000000000000000000000000000000000..26333d70c5e7b7286d5eb8953dd3c999a4ddb5bf GIT binary patch literal 12708 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 U;pGm$9fQxsCg#SOI~{)gfAwJB-~a#s literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/channels/facebook_login.png b/app/javascript/src/assets/images/channels/facebook_login.png new file mode 100644 index 0000000000000000000000000000000000000000..453820371d3639aa28381588994529951f5eda18 GIT binary patch literal 16392 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 Jg|c<{{{S^;Ew2Co literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/channels/telegram.png b/app/javascript/src/assets/images/channels/telegram.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b4c997fde17f0e1add7d0e8c894a1a7a6580aa GIT binary patch literal 51108 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

c3$QXQ^J8Z1o&D!2 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 Mx~4i+H}8f24?gzHQ2+n{ literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/channels/twitter.png b/app/javascript/src/assets/images/channels/twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..b5eebc812d6bb83f2c60fc84548d4372d34e82a6 GIT binary patch literal 4298 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 N)OAxtrM^?t{{RyZV445` literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/chat.svg b/app/javascript/src/assets/images/chat.svg new file mode 100644 index 000000000..fd4f36fda --- /dev/null +++ b/app/javascript/src/assets/images/chat.svg @@ -0,0 +1,19 @@ + + + + chat (2) + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/fb-badge.png b/app/javascript/src/assets/images/fb-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..4d00add1e12a169dc1eaefdcb81ef76e52213dd0 GIT binary patch literal 13117 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 literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/inboxes.svg b/app/javascript/src/assets/images/inboxes.svg new file mode 100644 index 000000000..1114cdeac --- /dev/null +++ b/app/javascript/src/assets/images/inboxes.svg @@ -0,0 +1,25 @@ + + + + email + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/lock.svg b/app/javascript/src/assets/images/lock.svg new file mode 100644 index 000000000..f65d1c4b4 --- /dev/null +++ b/app/javascript/src/assets/images/lock.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/src/assets/images/no-inboxes.svg b/app/javascript/src/assets/images/no-inboxes.svg new file mode 100644 index 000000000..cec37e2e7 --- /dev/null +++ b/app/javascript/src/assets/images/no-inboxes.svg @@ -0,0 +1,22 @@ + + + + billboard + Created with Sketch. + + + + + + + + + + + + + NO INBOXES + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/no_page_image.png b/app/javascript/src/assets/images/no_page_image.png new file mode 100644 index 0000000000000000000000000000000000000000..ee145e852acfe1e97951274aac9d33f09ef7cc1d GIT binary patch literal 5010 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 literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/woot-logo.png b/app/javascript/src/assets/images/woot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..baa2b095e04ead35556623306876b3240f01073f GIT binary patch literal 36558 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 LNkvXXu0mjffYn{` literal 0 HcmV?d00001 diff --git a/app/javascript/src/assets/images/woot-logo.svg b/app/javascript/src/assets/images/woot-logo.svg new file mode 100644 index 000000000..507854beb --- /dev/null +++ b/app/javascript/src/assets/images/woot-logo.svg @@ -0,0 +1,24 @@ + + + + Group 2 + Created with Sketch. + + + + + + + chatwoot + + + + + + + β + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_animations.scss b/app/javascript/src/assets/scss/_animations.scss new file mode 100644 index 000000000..671334bc5 --- /dev/null +++ b/app/javascript/src/assets/scss/_animations.scss @@ -0,0 +1,87 @@ + +/* Enter and leave animations can use different */ +/* durations and timing functions. */ +.slide-fade-enter-active { + @include transition(all .3s $ease-in-cubic); +} +.slide-fade-leave-active { + @include transition(all .3s $ease-out-cubic); +} +.slide-fade-enter, .slide-fade-leave-to { + opacity: 0; + transform: translateX(10px); +} + +.slide-fade-enter { + transform: translateX($space-micro); +} +.slide-fade-leave-to { + transform: translateX($space-medium); +} + +.conversations-list-enter-active, .conversations-list-leave-active { + @include transition(all .25s $ease-out-cubic); +} +.conversations-list-enter, .conversations-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { + opacity: 0; + transform: translateX($space-medium); +} + +.menu-list-enter-active, .menu-list-leave-active { + @include transition(all .2s $ease-out-cubic); +} +.menu-list-enter, .menu-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { + opacity: 0; + transform: translateX($space-medium); +} + +.slide-up-enter-active { + @include transition(all .3s $ease-in-cubic); +} + +.slide-up-leave-active { + @include transition(all .3s $ease-out-cubic); +} + +.slide-up-enter, .slide-up-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + transform: translateY(-$space-medium); + opacity: 0; +} + +.menu-slide-enter-active, .menu-slide-leave-active { + @include transition(all .15s $ease-in-cubic); +} + +.menu-slide-enter, .menu-slide-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + @include transform(translateY($space-small)); + opacity: 0; +} + + +.toast-fade-enter-active { + @include transition(all .3s $ease-in-sine); +} +.toast-fade-leave-active { + @include transition(all .1s $ease-out-sine); +} +.toast-fade-enter, .toast-fade-leave-to +/* .toast-fade-leave-active for <2.1.8 */ { + @include transform(translateY(-$space-small)); + opacity: 0; +} + + +.modal-fade-enter-active { + @include transition(all .3s $ease-in-sine); +} + +.modal-fade-leave-active { + @include transition(all .1s $ease-out-sine); +} + +.modal-fade-enter, .modal-fade-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + opacity: 0; +} diff --git a/app/javascript/src/assets/scss/_foundation-custom.scss b/app/javascript/src/assets/scss/_foundation-custom.scss new file mode 100644 index 000000000..c09457701 --- /dev/null +++ b/app/javascript/src/assets/scss/_foundation-custom.scss @@ -0,0 +1,31 @@ +.button { + font-weight: $font-weight-medium; + font-family: $body-font-family; + + &.round { + border-radius: 1000px; + } + + &.warning { + color: $white; + } + + &.grey-btn { + color: $color-gray; + + &:hover { + color: $color-light-gray; + } + } +} + +.label { + font-weight: $font-weight-bold; +} + +.tooltip { + max-width: 15rem; + padding: $space-smaller $space-small; + border-radius: $space-smaller; + font-size: $font-size-mini; +} diff --git a/app/javascript/src/assets/scss/_foundation-settings.scss b/app/javascript/src/assets/scss/_foundation-settings.scss new file mode 100644 index 000000000..10b9da705 --- /dev/null +++ b/app/javascript/src/assets/scss/_foundation-settings.scss @@ -0,0 +1,649 @@ +// Foundation for Sites Settings +// ----------------------------- +// +// Table of Contents: +// +// 1. Global +// 2. Breakpoints +// 3. The Grid +// 4. Base Typography +// 5. Typography Helpers +// 6. Abide +// 7. Accordion +// 8. Accordion Menu +// 9. Badge +// 10. Breadcrumbs +// 11. Button +// 12. Button Group +// 13. Callout +// 14. Card +// 15. Close Button +// 16. Drilldown +// 17. Dropdown +// 18. Dropdown Menu +// 19. Forms +// 20. Label +// 21. Media Object +// 22. Menu +// 23. Meter +// 24. Off-canvas +// 25. Orbit +// 26. Pagination +// 27. Progress Bar +// 28. Responsive Embed +// 29. Reveal +// 30. Slider +// 31. Switch +// 32. Table +// 33. Tabs +// 34. Thumbnail +// 35. Title Bar +// 36. Tooltip +// 37. Top Bar + +@import "~foundation-sites/scss/util/util"; + +// 1. Global +// --------- + +$global-font-size: 10px; +$global-width: 100%; +$global-lineheight: 1.5; +$foundation-palette: ( + primary: $color-woot, + secondary: #777, + success: #13ce66, + warning: #ffc82c, + alert: #ff4949 +); +$light-gray: #c0ccda; +$medium-gray: #8492a6; +$dark-gray: $color-gray; +$black: #000000; +$white: #ffffff; +$body-background: $white; +$body-font-color: $color-body; +$body-font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", + Roboto, "Helvetica Neue", Arial, sans-serif; +$body-antialiased: true; +$global-margin: $space-one; +$global-padding: $space-one; +$global-weight-normal: normal; +$global-weight-bold: bold; +$global-radius: 0; +$global-text-direction: ltr; +$global-flexbox: false; +$print-transparent-backgrounds: true; + +@include add-foundation-colors; + +// 2. Breakpoints +// -------------- + +$breakpoints: ( + small: 0, + medium: 640px, + large: 1024px, + xlarge: 1200px, + xxlarge: 1440px +); +$print-breakpoint: large; +$breakpoint-classes: (small medium large); + +// 3. The Grid +// ----------- + +$grid-row-width: $global-width; +$grid-column-count: 12; +$grid-column-gutter: ( + small: $zero, + medium: $zero +); +$grid-column-align-edge: true; +$block-grid-max: 8; + +// 4. Base Typography +// ------------------ + +$header-font-family: $body-font-family; +$header-font-weight: $global-weight-normal; +$header-font-style: normal; +$font-family-monospace: $body-font-family; +$header-color: $color-heading; +$header-lineheight: 1.4; +$header-margin-bottom: 0.5rem; +$header-styles: ( + small: ( + "h1": ( + "font-size": 24 + ), + "h2": ( + "font-size": 20 + ), + "h3": ( + "font-size": 19 + ), + "h4": ( + "font-size": 18 + ), + "h5": ( + "font-size": 17 + ), + "h6": ( + "font-size": 16 + ) + ), + medium: ( + "h1": ( + "font-size": 48 + ), + "h2": ( + "font-size": 40 + ), + "h3": ( + "font-size": 31 + ), + "h4": ( + "font-size": 25 + ), + "h5": ( + "font-size": 20 + ), + "h6": ( + "font-size": 16 + ) + ) +); +$header-text-rendering: optimizeLegibility; +$small-font-size: 80%; +$header-small-font-color: $medium-gray; +$paragraph-lineheight: 1.6; +$paragraph-margin-bottom: 1rem; +$paragraph-text-rendering: optimizeLegibility; +$code-color: $black; +$code-font-family: $font-family-monospace; +$code-font-weight: $global-weight-normal; +$code-background: $light-gray; +$code-border: 1px solid $medium-gray; +$code-padding: rem-calc(2 5 1); +$anchor-color: $primary-color; +$anchor-color-hover: scale-color($anchor-color, $lightness: -14%); +$anchor-text-decoration: none; +$anchor-text-decoration-hover: none; +$hr-width: $global-width; +$hr-border: 1px solid $medium-gray; +$hr-margin: rem-calc(20) auto; +$list-lineheight: $paragraph-lineheight; +$list-margin-bottom: $paragraph-margin-bottom; +$list-style-type: disc; +$list-style-position: outside; +$list-side-margin: 1.25rem; +$list-nested-side-margin: 1.25rem; +$defnlist-margin-bottom: 1rem; +$defnlist-term-weight: $global-weight-bold; +$defnlist-term-margin-bottom: 0.3rem; +$blockquote-color: $dark-gray; +$blockquote-padding: rem-calc(9 20 0 19); +$blockquote-border: 1px solid $medium-gray; +$cite-font-size: rem-calc(13); +$cite-color: $dark-gray; +$cite-pseudo-content: "\2014 \0020"; +$keystroke-font: $font-family-monospace; +$keystroke-color: $black; +$keystroke-background: $light-gray; +$keystroke-padding: rem-calc(2 4 0); +$keystroke-radius: $global-radius; +$abbr-underline: 1px dotted $black; + +// 5. Typography Helpers +// --------------------- + +$lead-font-size: $global-font-size * 1.25; +$lead-lineheight: 1.6; +$subheader-lineheight: 1.4; +$subheader-color: $dark-gray; +$subheader-font-weight: $global-weight-normal; +$subheader-margin-top: 0.2rem; +$subheader-margin-bottom: 0.5rem; +$stat-font-size: 2.5rem; + +// 6. Abide +// -------- + +$abide-inputs: true; +$abide-labels: true; +$input-background-invalid: get-color(alert); +$form-label-color-invalid: get-color(alert); +$input-error-color: get-color(alert); +$input-error-font-size: rem-calc(12); +$input-error-font-weight: $global-weight-bold; + +// 7. Accordion +// ------------ + +$accordion-background: $white; +$accordion-plusminus: true; +$accordion-title-font-size: rem-calc(12); +$accordion-item-color: $primary-color; +$accordion-item-background-hover: $light-gray; +$accordion-item-padding: 1.25rem 1rem; +$accordion-content-background: $white; +$accordion-content-border: 1px solid $light-gray; +$accordion-content-color: $body-font-color; +$accordion-content-padding: 1rem; + +// 8. Accordion Menu +// ----------------- + +$accordionmenu-arrows: true; +$accordionmenu-arrow-color: $primary-color; +$accordionmenu-arrow-size: 6px; + +// 9. Badge +// -------- + +$badge-background: $primary-color; +$badge-color: $white; +$badge-color-alt: $black; +$badge-palette: $foundation-palette; +$badge-padding: 0.3em; +$badge-minwidth: 2.1em; +$badge-font-size: 0.6rem; + +// 10. Breadcrumbs +// --------------- + +$breadcrumbs-margin: 0 0 $global-margin 0; +$breadcrumbs-item-font-size: rem-calc(11); +$breadcrumbs-item-color: $primary-color; +$breadcrumbs-item-color-current: $black; +$breadcrumbs-item-color-disabled: $medium-gray; +$breadcrumbs-item-margin: 0.75rem; +$breadcrumbs-item-uppercase: true; +$breadcrumbs-item-slash: true; + +// 11. Button +// ---------- + +$button-padding: $space-one $space-two; +$button-margin: 0 0 $global-margin 0; +$button-fill: solid; +$button-background: $primary-color; +$button-background-hover: scale-color($button-background, $lightness: -15%); +$button-color: $white; +$button-color-alt: $white; +$button-radius: $global-radius; +$button-sizes: ( + tiny: $font-size-micro, + small: $font-size-mini, + default: $font-size-default, + large: $font-size-large +); +$button-palette: $foundation-palette; +$button-opacity-disabled: 0.25; +$button-background-hover-lightness: -20%; +$button-hollow-hover-lightness: -50%; +$button-transition: background-color 0.25s ease-out, color 0.25s ease-out; + +// 12. Button Group +// ---------------- + +$buttongroup-margin: 1rem; +$buttongroup-spacing: 1px; +$buttongroup-child-selector: ".button"; +$buttongroup-expand-max: 6; +$buttongroup-radius-on-each: true; + +// 13. Callout +// ----------- + +$callout-background: $white; +$callout-background-fade: 85%; +$callout-border: 1px solid rgba($black, 0.25); +$callout-margin: 0 0 1rem 0; +$callout-padding: 1rem; +$callout-font-color: $body-font-color; +$callout-font-color-alt: $body-background; +$callout-radius: $global-radius; +$callout-link-tint: 30%; + +// 14. Card +// -------- + +$card-background: $white; +$card-font-color: $body-font-color; +$card-divider-background: $light-gray; +$card-border: 1px solid $light-gray; +$card-shadow: none; +$card-border-radius: $global-radius; +$card-padding: $global-padding; +$card-margin: $global-margin; + +// 15. Close Button +// ---------------- + +$closebutton-position: right top; +$closebutton-offset-horizontal: ( + small: 0.66rem, + medium: 1rem +); +$closebutton-offset-vertical: ( + small: 0.33em, + medium: 0.5rem +); +$closebutton-size: ( + small: 1.5em, + medium: 2em +); +$closebutton-lineheight: 1; +$closebutton-color: $dark-gray; +$closebutton-color-hover: $black; + +// 16. Drilldown +// ------------- + +$drilldown-transition: transform 0.15s linear; +$drilldown-arrows: true; +$drilldown-arrow-color: $primary-color; +$drilldown-arrow-size: 6px; +$drilldown-background: $white; + +// 17. Dropdown +// ------------ + +$dropdown-padding: 1rem; +$dropdown-background: $body-background; +$dropdown-border: 1px solid $medium-gray; +$dropdown-font-size: 1rem; +$dropdown-width: 300px; +$dropdown-radius: $global-radius; +$dropdown-sizes: ( + tiny: 100px, + small: 200px, + large: 400px +); + +// 18. Dropdown Menu +// ----------------- + +$dropdownmenu-arrows: true; +$dropdownmenu-arrow-color: $anchor-color; +$dropdownmenu-arrow-size: 6px; +$dropdownmenu-min-width: 200px; +$dropdownmenu-background: $white; +$dropdownmenu-border: 1px solid $medium-gray; + +// 19. Forms +// --------- + +$fieldset-border: 1px solid $light-gray; +$fieldset-padding: $space-two; +$fieldset-margin: $space-one $zero; +$legend-padding: rem-calc(0 3); +$form-spacing: $space-normal; +$helptext-color: $header-color; +$helptext-font-size: $font-size-small; +$helptext-font-style: italic; +$input-prefix-color: $header-color; +$input-prefix-background: $light-gray; +$input-prefix-border: 1px solid $light-gray; +$input-prefix-padding: 1rem; +$form-label-color: $header-color; +$form-label-font-size: rem-calc(14); +$form-label-font-weight: $font-weight-medium; +$form-label-line-height: 1.8; +$select-background: $white; +$select-triangle-color: $dark-gray; +$select-radius: $global-radius; +$input-color: $header-color; +$input-placeholder-color: $light-gray; +$input-font-family: inherit; +$input-font-size: $font-size-default; +$input-font-weight: $global-weight-normal; +$input-background: $white; +$input-background-focus: $white; +$input-background-disabled: $light-gray; +$input-border: 1px solid $light-gray; +$input-border-focus: 1px solid lighten($primary-color, 15%); +$input-shadow: 0; +$input-shadow-focus: 0; +$input-cursor-disabled: not-allowed; +$input-transition: border-color 0.25s ease-in-out; +$input-number-spinners: true; +$input-radius: $global-radius; +$form-button-radius: $global-radius; + +// 20. Label +// --------- + +$label-background: $primary-color; +$label-color: $white; +$label-color-alt: $black; +$label-palette: $foundation-palette; +$label-font-size: $font-size-micro; +$label-padding: $space-micro $space-smaller; +$label-radius: $space-micro; + +// 21. Media Object +// ---------------- + +$mediaobject-margin-bottom: $global-margin; +$mediaobject-section-padding: $global-padding; +$mediaobject-image-width-stacked: 100%; + +// 22. Menu +// -------- + +$menu-margin: 0; +$menu-margin-nested: $space-medium; +$menu-item-padding: $space-one; +$menu-item-color-active: $white; +$menu-item-background-active: $color-background; +$menu-icon-spacing: 0.25rem; +$menu-item-background-hover: $light-gray; +$menu-border: $light-gray; + +// 23. Meter +// --------- + +$meter-height: 1rem; +$meter-radius: $global-radius; +$meter-background: $medium-gray; +$meter-fill-good: $success-color; +$meter-fill-medium: $warning-color; +$meter-fill-bad: $alert-color; + +// 24. Off-canvas +// -------------- + +$offcanvas-size: 250px; +$offcanvas-vertical-size: 250px; +$offcanvas-background: $light-gray; +$offcanvas-shadow: 0 0 10px rgba($black, 0.7); +$offcanvas-push-zindex: 1; +$offcanvas-overlap-zindex: 10; +$offcanvas-reveal-zindex: 1; +$offcanvas-transition-length: 0.5s; +$offcanvas-transition-timing: ease; +$offcanvas-fixed-reveal: true; +$offcanvas-exit-background: rgba($white, 0.25); +$maincontent-class: "off-canvas-content"; + +// 25. Orbit +// --------- + +$orbit-bullet-background: $medium-gray; +$orbit-bullet-background-active: $dark-gray; +$orbit-bullet-diameter: 1.2rem; +$orbit-bullet-margin: 0.1rem; +$orbit-bullet-margin-top: 0.8rem; +$orbit-bullet-margin-bottom: 0.8rem; +$orbit-caption-background: rgba($black, 0.5); +$orbit-caption-padding: 1rem; +$orbit-control-background-hover: rgba($black, 0.5); +$orbit-control-padding: 1rem; +$orbit-control-zindex: 10; + +// 26. Pagination +// -------------- + +$pagination-font-size: rem-calc(14); +$pagination-margin-bottom: $global-margin; +$pagination-item-color: $black; +$pagination-item-padding: rem-calc(3 10); +$pagination-item-spacing: rem-calc(1); +$pagination-radius: $global-radius; +$pagination-item-background-hover: $light-gray; +$pagination-item-background-current: $primary-color; +$pagination-item-color-current: $white; +$pagination-item-color-disabled: $medium-gray; +$pagination-ellipsis-color: $black; +$pagination-mobile-items: false; +$pagination-mobile-current-item: false; +$pagination-arrows: true; + +// 27. Progress Bar +// ---------------- + +$progress-height: 1rem; +$progress-background: $medium-gray; +$progress-margin-bottom: $global-margin; +$progress-meter-background: $primary-color; +$progress-radius: $global-radius; + +// 28. Responsive Embed +// -------------------- + +$responsive-embed-margin-bottom: rem-calc(16); +$responsive-embed-ratios: ( + default: 4 by 3, + widescreen: 16 by 9 +); + +// 29. Reveal +// ---------- + +$reveal-background: $white; +$reveal-width: 600px; +$reveal-max-width: $global-width; +$reveal-padding: $global-padding; +$reveal-border: 1px solid $medium-gray; +$reveal-radius: $global-radius; +$reveal-zindex: 1005; +$reveal-overlay-background: rgba($black, 0.45); + +// 30. Slider +// ---------- + +$slider-width-vertical: 0.5rem; +$slider-transition: all 0.2s ease-in-out; +$slider-height: 0.5rem; +$slider-background: $light-gray; +$slider-fill-background: $medium-gray; +$slider-handle-height: 1.4rem; +$slider-handle-width: 1.4rem; +$slider-handle-background: $primary-color; +$slider-opacity-disabled: 0.25; +$slider-radius: $global-radius; + +// 31. Switch +// ---------- + +$switch-background: $light-gray; +$switch-background-active: $primary-color; +$switch-height: $space-two; +$switch-height-tiny: $space-slab; +$switch-height-small: $space-normal; +$switch-height-large: $space-large; +$switch-radius: $space-large; +$switch-margin: $global-margin; +$switch-paddle-background: $white; +$switch-paddle-offset: $space-micro; +$switch-paddle-radius: $space-large; +$switch-paddle-transition: all 0.15s ease-out; + +// 32. Table +// --------- + +$table-background: transparent; +$table-color-scale: 5%; +$table-border: 1px solid smart-scale($color-heading, $table-color-scale); +$table-padding: rem-calc(8 10 10); +$table-hover-scale: 2%; +$table-row-hover: darken($table-background, $table-hover-scale); +$table-row-stripe-hover: darken( + $table-background, + $table-color-scale + $table-hover-scale +); +$table-is-striped: false; +$table-striped-background: smart-scale($table-background, $table-color-scale); +$table-stripe: even; +$table-head-background: smart-scale($table-background, $table-color-scale / 2); +$table-head-row-hover: darken($table-head-background, $table-hover-scale); +$table-foot-background: smart-scale($table-background, $table-color-scale); +$table-foot-row-hover: darken($table-foot-background, $table-hover-scale); +$table-head-font-color: $body-font-color; +$table-foot-font-color: $body-font-color; +$show-header-for-stacked: false; + +// 33. Tabs +// -------- + +$tab-margin: 0; + +$tab-background: transparent; +$tab-background-active: transparent; +$tab-item-font-size: $font-size-small; +$tab-item-background-hover: transparent; +$tab-item-padding: $space-one $zero; +$tab-color: $primary-color; +$tab-active-color: $primary-color; +$tab-expand-max: 6; +$tab-content-background: transparent; +$tab-content-border: transparent; +$tab-content-color: foreground($tab-background, $primary-color); +$tab-content-padding: 1rem; + +// 34. Thumbnail +// ------------- + +$thumbnail-border: solid 4px $white; +$thumbnail-margin-bottom: $global-margin; +$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); +$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); +$thumbnail-transition: box-shadow 200ms ease-out; +$thumbnail-radius: $global-radius; + +// 35. Title Bar +// ------------- + +$titlebar-background: $black; +$titlebar-color: $white; +$titlebar-padding: 0.5rem; +$titlebar-text-font-weight: bold; +$titlebar-icon-color: $white; +$titlebar-icon-color-hover: $medium-gray; +$titlebar-icon-spacing: 0.25rem; + +// 36. Tooltip +// ----------- + +$has-tip-font-weight: $global-weight-bold; +$has-tip-border-bottom: dotted 1px $dark-gray; +$tooltip-background-color: $black; +$tooltip-color: $white; +$tooltip-padding: 0.75rem; +$tooltip-font-size: $font-size-mini; +$tooltip-pip-width: 0.75rem; +$tooltip-pip-height: $tooltip-pip-width * 0.866; +$tooltip-radius: $global-radius; + +// 37. Top Bar +// ----------- + +$topbar-padding: 0.5rem; +$topbar-background: $light-gray; +$topbar-submenu-background: $topbar-background; +$topbar-title-spacing: 0.5rem 1rem 0.5rem 0; +$topbar-input-width: 200px; +$topbar-unstack-breakpoint: medium; diff --git a/app/javascript/src/assets/scss/_helper-classes.scss b/app/javascript/src/assets/scss/_helper-classes.scss new file mode 100644 index 000000000..5e8b0900a --- /dev/null +++ b/app/javascript/src/assets/scss/_helper-classes.scss @@ -0,0 +1,58 @@ +.bg-light { + @include background-light; +} + +.flex-center { + @include flex; + @include flex-align(center, middle); +} + +.bottom-space-fix { + margin-bottom: auto; +} + +.full-height { + @include full-height(); +} + +.spinner { + @include color-spinner(); + position: relative; + display: inline-block; + width: $space-medium; + height: $space-medium; + padding: $zero $space-medium; + vertical-align: middle; + + &.message { + padding: $space-normal; + top: 0; + left: 0; + margin: 0 auto; + margin-top: $space-slab; + background: $color-white; + border-radius: $space-large; + @include elegent-shadow; + + &:before { + margin-top: -$space-slab; + margin-left: -$space-slab; + } + } + + &.small { + width: $space-normal; + height: $space-normal; + + &:before { + width: $space-normal; + height: $space-normal; + margin-top: -$space-small; + } + } +} + + +input, textarea { + border-radius: 4px !important; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_layout.scss b/app/javascript/src/assets/scss/_layout.scss new file mode 100644 index 000000000..762804381 --- /dev/null +++ b/app/javascript/src/assets/scss/_layout.scss @@ -0,0 +1,79 @@ +html, +body { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; +} + +.app-wrapper { + @include full-height; + @include flex-weight(1); + width: 100%; +} +.app-root { + @include flex; + @include flex-direction(column); +} +.app-content { + @include flex; +} + +.view-box { + @include full-height; + height: 100vh; + @include margin(0); + @include space-between-column; +} + +.view-panel { + @include flex-weight(1); + @include flex-direction(column); + @include margin($zero); + @include padding($space-normal); + overflow-y: scroll; +} + +.content-box { + overflow: scroll; + @include padding($space-normal); + .btn-fixed-right-top { + position: fixed; + top: $space-small; + right: $space-small; + } +} + +.back-button { + color: $color-woot; + font-size: $font-size-default; + font-weight: $font-weight-normal; + margin-right: $space-normal; + cursor: pointer; + + &:before { + vertical-align: text-bottom; + margin-right: $space-smaller; + font-size: $font-size-large; + } +} + +.button-spinner { + float: right; +} + +.no-items-error-message { + @include flex; + @include full-height; + @include justify-content(center); + @include align-items(center); + flex-direction: column; + + img { + max-width: $space-mega; + @include padding($space-one); + } +} diff --git a/app/javascript/src/assets/scss/_mixins.scss b/app/javascript/src/assets/scss/_mixins.scss new file mode 100644 index 000000000..249b25f9b --- /dev/null +++ b/app/javascript/src/assets/scss/_mixins.scss @@ -0,0 +1,206 @@ +//borders +@mixin border-nil() { + border-color: transparent; + border: 0; +} + +@mixin thin-border($color) { + border: 1px solid $color; +} + +@mixin custom-border-bottom($size, $color) { + border-bottom: $size solid $color; +} + +@mixin custom-border-top($size, $color) { + border-top: $size solid $color; +} + +@mixin border-normal() { + border: 1px solid $color-border; +} +@mixin border-normal-left() { + border-left: 1px solid $color-border; +} +@mixin border-normal-top() { + border-top: 1px solid $color-border; +} +@mixin border-normal-right() { + border-right: 1px solid $color-border; +} +@mixin border-normal-bottom() { + border-bottom: 1px solid $color-border; +} + +@mixin border-light() { + border: 1px solid $color-border-light; +} +@mixin border-light-left() { + border-left: 1px solid $color-border-light; +} +@mixin border-light-top() { + border-top: 1px solid $color-border-light; +} +@mixin border-light-right() { + border-right: 1px solid $color-border-light; +} +@mixin border-light-bottom() { + border-bottom: 1px solid $color-border-light; +} + +// background +@mixin background-gray() { + background: $color-background; +} + +@mixin background-light() { + background: $color-background-light; +} + +@mixin background-white() { + background: $color-white; +} + +// input form +@mixin ghost-input() { + box-shadow: none; + border-color: transparent; + &:active, + &:hover, + &:focus { + box-shadow: none; + border-color: transparent; + } +} + +// flex-layout +@mixin space-between() { + @include display(flex); + @include justify-content(space-between); +} + +@mixin space-between-column() { + @include space-between; + @include flex-direction(column); +} +@mixin space-between-row() { + @include space-between; + @include flex-direction(row); +} + +@mixin flex-shrink() { + flex: flex-grid-column(shrink); + max-width: 100%; +} + +@mixin flex-weight($value) { + // Grab flex-grow for older browsers. + $flex-grow: nth($value, 1); + + // 2009 + @include prefixer(box-flex, $flex-grow, webkit moz spec); + + // 2011 (IE 10), 2012 + @include prefixer(flex, $value, webkit moz ms spec); +} + +// full height +@mixin full-height() { + height: 100%; + // COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes +} +@mixin round-corner() { + border-radius: 1000px; +} + +@mixin scroll-on-hover() { + @include transition(all .4s $ease-in-out-cubic); + overflow: hidden; + + &:hover { + overflow-y: scroll; + } +} + + +@mixin horizontal-scroll() { + overflow-y: scroll; +} + +@mixin elegent-shadow() { + box-shadow: 0 10px 25px 0 rgba(49,49,93,0.15); +} + +@mixin elegant-card() { + @include elegent-shadow; + border-radius: $space-small; +} + +@mixin color-spinner() { + @keyframes spinner { + to {transform: rotate(360deg);} + } + + &:before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: $space-medium; + height: $space-medium; + margin-top: -$space-one; + margin-left: -$space-one; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.7); + border-top-color: lighten($color-woot, 10%); + animation: spinner .9s linear infinite; + } +} + +// -------------------------------------------------------- +// arrows +// -------------------------------------------------------- +// $direction: top, left, right, bottom, top-left, top-right, bottom-left, bottom-right +// $color: hex, rgb or rbga +// $size: px or em +// @example +// .element{ +// @include arrow(top, #000, 50px); +// } +@mixin arrow($direction, $color, $size){ + display: block; + height: 0; + width: 0; + content: ''; + + @if $direction == 'top' { + border-left: $size solid transparent; + border-right: $size solid transparent; + border-bottom: $size solid $color; + } @else if $direction == 'right' { + border-top: $size solid transparent; + border-bottom: $size solid transparent; + border-left: $size solid $color; + } @else if $direction == 'bottom' { + border-top: $size solid $color; + border-right: $size solid transparent; + border-left: $size solid transparent; + } @else if $direction == 'left' { + border-top: $size solid transparent; + border-right: $size solid $color; + border-bottom: $size solid transparent; + } @else if $direction == 'top-left' { + border-top: $size solid $color; + border-right: $size solid transparent; + } @else if $direction == 'top-right' { + border-top: $size solid $color; + border-left: $size solid transparent; + } @else if $direction == 'bottom-left' { + border-bottom: $size solid $color; + border-right: $size solid transparent; + } @else if $direction == 'bottom-right' { + border-bottom: $size solid $color; + border-left: $size solid transparent; + } +} diff --git a/app/javascript/src/assets/scss/_typography.scss b/app/javascript/src/assets/scss/_typography.scss new file mode 100644 index 000000000..40d1c5a29 --- /dev/null +++ b/app/javascript/src/assets/scss/_typography.scss @@ -0,0 +1,27 @@ +.page-title { + font-size: $font-size-big; +} + +.page-sub-title { + font-size: $font-size-large; +} + +.block-title { + font-size: $font-size-medium; +} + +.sub-block-title { + font-size: $font-size-default; +} + +.text-block-title { + font-size: $font-size-small; +} + +a { + font-size: $font-size-small; +} + +p { + font-size: $font-size-small; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_variables.scss b/app/javascript/src/assets/scss/_variables.scss new file mode 100644 index 000000000..7eef28900 --- /dev/null +++ b/app/javascript/src/assets/scss/_variables.scss @@ -0,0 +1,82 @@ +// Font sizes +$font-size-nano: 0.8rem; +$font-size-micro: 1.0rem; +$font-size-mini: 1.2rem; +$font-size-small: 1.4rem; +$font-size-default: 1.6rem; +$font-size-medium: 1.8rem; +$font-size-large: 2.2rem; +$font-size-big: 2.4rem; +$font-size-bigger: 3.0rem; +$font-size-mega: 3.4rem; +$font-size-giga: 4.0rem; + +// spaces +$zero: 0rem; +$space-micro: 0.2rem; +$space-smaller: 0.4rem; +$space-small: 0.8rem; +$space-one: 1rem; +$space-slab: 1.2rem; +$space-normal: 1.6rem; +$space-two: 2.0rem; +$space-medium: 2.4rem; +$space-large: 3.2rem; +$space-larger: 4.8rem; +$space-jumbo: 6.4rem; +$space-mega: 10.0rem; + +// font-weight +$font-weight-feather: 100; +$font-weight-light: 300; +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-bold: 600; +$font-weight-black: 700; + +//Navbar +$nav-bar-width: 23rem; +$header-height: 5.6rem; + +// Woot Logo +$woot-logo-width: 20rem; +$woot-logo-height: 8rem; +$woot-logo-padding: $space-large $space-large $space-large $space-large; + +// Colors +$color-woot: #1f93ff; +$color-gray: #6E6F73; +$color-light-gray: #999A9B; +$color-border: #E0E6ED; +$color-border-light: #f0f4f5; +$color-background: #EFF2F7; +$color-background-light: #F9FAFC; +$color-white: #FFFFFF; +$color-body: #3C4858; +$color-heading: #1F2D3D; +$color-modal-header: #f1f1f1; +// Thumbnail +$thumbnail-radius: 4rem; + +// chat-header +$conv-header-height: 4rem; + +// login + +// Inbox List + +$inbox-thumb-size: 4.8rem; + + +// Spinner +$spinkit-spinner-color: $color-white !default; +$spinkit-spinner-margin: 0 0 0 1.6rem !default; +$spinkit-size: 1.6rem !default; + +// Snackbar default +$woot-snackbar-bg: #323232; +$woot-snackbar-button: #ffeb3b; + +$swift-ease-out-duration: .4s !default; +$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; +$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default; diff --git a/app/javascript/src/assets/scss/_woot.scss b/app/javascript/src/assets/scss/_woot.scss new file mode 100644 index 000000000..68405ee94 --- /dev/null +++ b/app/javascript/src/assets/scss/_woot.scss @@ -0,0 +1,30 @@ +@import 'typography'; +@import 'layout'; +@import 'animations'; + +@import 'foundation-custom'; +@import 'widgets/search-box'; +@import 'widgets/conv-header'; +@import 'widgets/thumbnail'; +@import 'widgets/conversation-card'; +@import 'widgets/conversation-view'; +@import 'widgets/reply-box'; +@import 'widgets/tabs'; +@import 'widgets/login'; +@import 'widgets/emojiinput'; +@import 'widgets/woot-tables'; +@import 'widgets/sidemenu'; +@import 'widgets/forms'; +@import 'widgets/buttons'; +@import 'widgets/snackbar'; +@import 'widgets/modal'; +@import 'widgets/states'; +@import 'widgets/report'; +@import 'widgets/billing'; +@import 'widgets/status-bar'; + +@import 'views/settings/inbox'; +@import 'views/settings/channel'; +@import 'views/signup'; + +@import 'plugins/multiselect'; diff --git a/app/javascript/src/assets/scss/app.scss b/app/javascript/src/assets/scss/app.scss new file mode 100644 index 000000000..fe6ea3aff --- /dev/null +++ b/app/javascript/src/assets/scss/app.scss @@ -0,0 +1,9 @@ +@import '~bourbon/app/assets/stylesheets/bourbon'; +@import 'variables'; +@import '~spinkit/scss/spinners/7-three-bounce'; +@import url('https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.css'); +@import 'foundation-settings'; +@import 'mixins'; +@import 'helper-classes'; +@import '~foundation-sites/assets/foundation-flex'; +@import 'woot'; diff --git a/app/javascript/src/assets/scss/plugins/_multiselect.scss b/app/javascript/src/assets/scss/plugins/_multiselect.scss new file mode 100644 index 000000000..5c9136f79 --- /dev/null +++ b/app/javascript/src/assets/scss/plugins/_multiselect.scss @@ -0,0 +1,27 @@ +// @import '~vue-multiselect/dist/vue-multiselect.min.css'; +.multiselect { + min-height: 38px; + margin-bottom: $space-normal; + + > .multiselect__tags { + padding-top: $zero; + min-height: 38px; + border-radius: 0; + border: 1px solid $light-gray; + @include margin(0); + + .multiselect__tag { + margin-top: $space-smaller; + } + + .multiselect__input { + @include ghost-input; + margin-bottom: $zero; + @include padding($zero); + } + .multiselect__single { + @include padding($space-small); + margin-bottom: 0; + } + } +} diff --git a/app/javascript/src/assets/scss/views/_signup.scss b/app/javascript/src/assets/scss/views/_signup.scss new file mode 100644 index 000000000..5ae3d16f7 --- /dev/null +++ b/app/javascript/src/assets/scss/views/_signup.scss @@ -0,0 +1,91 @@ +.signup { + // margin-top: $space-larger*1.2; + + .signup__hero { + margin-bottom: $space-larger*1.5; + + .hero__logo { + width: 180px; + } + + .hero__title { + margin-top: $space-large; + font-weight: $font-weight-light; + } + + .hero__sub { + font-size: $font-size-medium; + color: $medium-gray; + } + } + + .signup__features { + list-style-type: none; + font-size: $font-size-medium; + + > li { + padding: $space-slab; + + > i { + margin-right: $space-two; + font-size: $font-size-large; + + &.beer { + color: #dfb63b; + } + + &.report { + color: #2196f3; + } + + &.canned { + color: #1cad22; + } + + &.uptime { + color: #a753b5; + } + + &.secure { + color: #607d8b; + } + } + } + } + + .signup-box { + @include elegant-card; + padding: $space-large $space-large; + label { + font-size: $font-size-default; + color: $color-gray; + + input { + padding: $space-slab; + height: $space-larger; + font-size: $font-size-default; + } + .error { + font-size: $font-size-small + } + } + } + + .sigin__footer { + padding: $space-medium; + font-size: $font-size-default; + + > a { + font-weight: $font-weight-bold; + } + } + + .accept-terms { + font-size: $font-size-mini; + text-align: center; + @include margin($zero); + a { + font-size: $font-size-mini; + } + } +} diff --git a/app/javascript/src/assets/scss/views/settings/channel.scss b/app/javascript/src/assets/scss/views/settings/channel.scss new file mode 100644 index 000000000..f948dab75 --- /dev/null +++ b/app/javascript/src/assets/scss/views/settings/channel.scss @@ -0,0 +1,48 @@ +.channels { + margin-top: $space-medium; + .inactive { + @include filter(grayscale(100%)); + } + .channel { + @include flex; + @include padding($space-normal $zero); + @include margin($zero); + @include background-white; + @include border-light; + @include flex-direction(column); + cursor: pointer; + border-right-color: $color-white; + @include transition(all 0.200s ease-in); + + &:last-child { + @include border-light; + } + + &:hover { + border: 1px solid $primary-color; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + z-index: 999; + } + + &.disabled { + opacity: .6; + } + + img { + width: 50%; + @include margin($space-normal auto); + } + + .channel__title{ + font-size: $font-size-large; + text-align: center; + color: $color-body; + text-transform: capitalize; + } + + p { + width: 100%; + color: $medium-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/views/settings/inbox.scss b/app/javascript/src/assets/scss/views/settings/inbox.scss new file mode 100644 index 000000000..c2ef08722 --- /dev/null +++ b/app/javascript/src/assets/scss/views/settings/inbox.scss @@ -0,0 +1,242 @@ +// Conversation header - Light BG +.settings-header { + @include padding($space-small $space-normal); + @include background-white; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-normal-bottom; + height: $header-height; + min-height: $header-height; + // Resolve Button + .button { + @include margin(0); + } + + // User thumbnail and text + .page-title { + @include flex; + @include flex-align($x: center, $y: middle); + @include margin($zero); + > span { + @include padding($zero $space-small $zero $space-small); + } + } + +} + +.wizard-box { + .item { + @include padding($space-normal $space-normal $space-normal $space-medium); + position: relative; + @include background-light; + cursor: pointer; + + &:before, + &:after { + content: ''; + position: absolute; + width: 2px; + height: 100%; + background: $color-border; + top: $space-normal; + } + + &:before { + top: $zero; + height: $space-normal; + } + + &:first-child { + &:before { + height: 0; + } + } + + &:last-child { + &:after { + height: $zero; + } + } + &.active { + // left: 1px; + // @include background-white; + // @include border-light; + // border-right: 0; + h3 { + color: $color-woot; + } + + .step { + background: $color-woot; + } + } + + &.over { + + &:after { + background: $color-woot; + } + + .step { + background: $color-woot; + } + + &+.item { + &:before { + background: $color-woot; + } + } + } + + h3 { + font-size: $font-size-default; + padding-left: $space-medium; + line-height: 1; + color: $color-body; + + .completed { + color: $success-color; + } + } + p { + font-size: $font-size-small; + color: $color-light-gray; + padding-left: $space-medium; + margin: 0; + } + .step { + position: absolute; + left: $space-normal; + top: $space-normal; + font-size: $font-size-small; + font-weight: $font-weight-medium; + background: $color-border; + border-radius: 20px; + width: $space-normal; + height: $space-normal; + text-align: center; + line-height: $space-normal; + color: #fff; + z-index: 999; + + i { + font-size: $font-size-micro; + } + } + } +} + +.wizard-body { + @include background-white; + @include padding($space-medium); + @include border-light; + @include full-height(); +} +.inoboxes-list { + // @include margin(auto); + // @include background-white; + // @include border-light; + // width: 50%; + + .inbox-item { + @include margin($space-normal); + @include flex; + @include flex-shrink; + @include padding($space-normal $space-normal); + @include border-light-bottom(); + flex-direction: column; + background: $color-white; + cursor: pointer; + width: 20%; + float: left; + min-height: 10rem; + &:last-child { + margin-bottom: $zero; + @include border-nil; + } + + &:hover { + @include background-gray; + .arrow { + opacity: 1; + @include transform(translateX($space-small)); + } + } + .switch { + align-self: center; + margin-right: $space-normal; + margin-bottom: $zero; + } + + .item--details { + @include padding($space-normal $zero); + .item--name { + font-size: $font-size-large; + line-height: 1; + } + .item--sub { + margin-bottom: 0; + font-size: $font-size-small; + } + } + .arrow { + align-self: center; + font-size: $font-size-small; + color: $medium-gray; + opacity: .7; + @include transform(translateX(0px)); + @include transition(opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s); + } + } +} + +.settings-modal { + width: 60%; + height: 80%; + .delete-wrapper { + position: absolute; + bottom: 0; + width: 100%; + @include flex; + flex-direction: row; + @include justify-content(space-between); + @include padding($space-normal $space-large); + a { + margin-left: $space-normal; + } + } + .code-wrapper { + @include margin($space-medium); + + .title { + font-weight: $font-weight-medium; + } + .code { + max-height: $space-mega; + overflow: scroll; + white-space: nowrap; + @include padding($space-one); + background: $color-background; + code { + background: transparent; + border: 0; + } + } + } + .agent-wrapper { + @include margin($space-medium); + .title { + font-weight: $font-weight-medium; + } + } +} +.login-init { + text-align: center; + padding-top: 30%; + p { + @include padding($space-medium); + } + > a > img { + width: $space-larger*5; + } +} diff --git a/app/javascript/src/assets/scss/widgets/_billing.scss b/app/javascript/src/assets/scss/widgets/_billing.scss new file mode 100644 index 000000000..5244e2c2c --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_billing.scss @@ -0,0 +1,65 @@ +.billing { + @include full-height; + .row { + @include full-height; + } + .billing__stats { + @include flex; + } + .billing__form { + @include thin-border($color-border-light); + @include margin($zero -$space-micro); + @include full-height; + background: $color-white; + iframe { + border: 0; + @include full-height; + width: 100%; + } + } + .account-row { + @include flex-grid-column(3, $space-medium); + @include padding($space-normal); + background: $color-white; + @include flex; + @include flex-direction(column); + // @include thin-border($color-border-light); + // @include margin(-$space-micro $zero); + font-size: $font-size-small; + .title { + font-weight: $font-weight-medium; + color: $color-heading; + } + .value { + font-size: $font-size-mega; + font-weight: $font-weight-light; + text-transform: capitalize; + } + } +} + +.account-locked { + @include background-gray; + @include margin(0); + .lock-message{ + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + div { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + img { + width: 10rem; + @include margin($space-normal); + } + span { + text-align: center; + font-size: $font-size-small; + font-weight: $font-weight-medium; + } + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_buttons.scss b/app/javascript/src/assets/scss/widgets/_buttons.scss new file mode 100644 index 000000000..ec6eeb94f --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_buttons.scss @@ -0,0 +1,30 @@ +.button { + &.icon { + padding-left: $space-normal; + padding-right: $space-normal; + i { + padding-right: $space-one; + } + } + + &.nice { + border-radius: $space-smaller; + } + + &.hollow { + &.link { + border-color: transparent; + padding-left: 0; + + &:hover, + &:focus { + border-color: transparent; + } + } + } + + > .icon { + font-size: $font-size-mini; + margin-right: $space-smaller; + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_conv-header.scss b/app/javascript/src/assets/scss/widgets/_conv-header.scss new file mode 100644 index 000000000..eae651a1a --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conv-header.scss @@ -0,0 +1,64 @@ +// Conversation header - Light BG +.conv-header { + @include padding($space-small $space-normal); + @include background-white; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-normal-bottom; + // Resolve Button + .button { + @include margin(0); + @include flex; + } + + .multiselect-box { + @include flex; + @include flex-align($x: justify, $y: middle); + @include margin(0 $space-small); + @include border-light; + border-radius: $space-smaller; + margin-right: $space-normal; + + &:before { + line-height: 3.8rem; + font-size: $font-size-default; + padding-left: $space-slab; + padding-right: $space-smaller; + color: $medium-gray; + } + + .multiselect { + margin: 0; + + .multiselect__tags { + border: 0; + } + } + } + + // User thumbnail and text + .user { + @include flex; + @include flex-align($x: center, $y: middle); + .user--name { + @include margin(0); + font-size: $font-size-medium; + margin-left: $space-slab; + } + } +} + +.button.resolve--button { + > .icon { + padding-right: $space-small; + font-size: $font-size-default; + } + + .spinner { + padding: 0 $space-one; + margin-right: $space-smaller; + &:before { + border-top-color: $color-white; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_conversation-card.scss b/app/javascript/src/assets/scss/widgets/_conversation-card.scss new file mode 100644 index 000000000..9fccf6c69 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conversation-card.scss @@ -0,0 +1,89 @@ +.conversation { + @include flex; + @include flex-shrink; + @include padding($space-normal $zero $zero $space-normal); + position: relative; + cursor: pointer; + + &.active { + background: $color-background; + } + + .conversation--details { + @include margin($zero $zero $zero $space-one); + @include border-light-bottom; + @include padding($zero $zero $space-slab $zero); + } + .conversation--user { + font-size: $font-size-small; + margin-bottom: $zero; + + .label { + position: relative; + top: $space-micro; + left: $space-micro; + max-width: $space-jumbo; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .conversation--message { + height: $space-medium; + margin: $zero; + font-size: $font-size-small; + line-height: $space-medium; + font-weight: $font-weight-light; + text-overflow: ellipsis; + overflow: hidden; + color: $color-body; + width: 27rem; + white-space: nowrap; + } + .conversation--meta { + display: block; + position: absolute; + right: $space-normal; + top: $space-normal; + @include flex; + @include flex-direction(column); + + .unread { + $unread-size: $space-two - $space-micro; + display: none; + height: $unread-size; + min-width: $unread-size; + background: darken($success-color, 3%); + text-align: center; + padding: 0 $space-smaller; + line-height: $unread-size; + color: $color-white; + font-weight: $font-weight-medium; + font-size: $font-size-mini; + margin-left: auto; + @include round-corner; + margin-top: $space-smaller; + } + .timestamp { + font-size: $font-size-mini; + color: $dark-gray; + line-height: $space-normal; + font-weight: $font-weight-normal; + font-size: $font-size-micro; + margin-left: auto; + } + } + + &.unread-chat { + .unread { + display: inline-block; + } + .conversation--message { + font-weight: $font-weight-medium; + } + .conversation--user { + font-weight: $font-weight-medium; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_conversation-view.scss b/app/javascript/src/assets/scss/widgets/_conversation-view.scss new file mode 100644 index 000000000..b3117f3cd --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conversation-view.scss @@ -0,0 +1,310 @@ +.conversations-sidebar { + @include flex; + @include flex-direction(column); + + .chat-list__top { + @include padding($space-normal $zero $space-small $zero); + .page-title { + float: left; + margin-bottom: $zero; + margin-left: $space-normal; + } + + .status--filter { + float: right; + width: auto; + font-size: $font-size-mini; + @include padding($zero null $zero $space-normal); + @include border-light; + @include round-corner; + @include margin($space-smaller $space-slab $zero $zero); + background-color: $color-background; + height: $space-medium; + } + } + + .conversations-list { + @include flex-weight(1); + @include scroll-on-hover; + } + + .content-box { + text-align: center; + } +} + +.emojione { + height: $font-size-medium; + width: $font-size-medium; +} + +.conversation-wrap { + @include background-gray; + @include margin(0); + @include border-normal-left; + .current-chat{ + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + div { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + img { + width: 10rem; + @include margin($space-normal); + } + span { + text-align: center; + font-size: $font-size-small; + font-weight: $font-weight-medium; + } + } + } + .conv-empty-state { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + } +} + +.conversation-panel { + @include flex; + @include flex-weight(1); + @include flex-direction(column); + @include margin($zero); + overflow-y: scroll; + // FIrefox flexbox fix + height: 100%; + + > li { + @include flex; + @include flex-shrink; + @include margin($zero $zero $space-smaller); + + &:first-child { + margin-top: auto; + } + + &:last-child { + margin-bottom: $space-small; + } + + &.unread--toast { + span { + margin: $space-one auto; + padding: $space-smaller $space-two; + font-size: $font-size-mini; + font-weight: $font-weight-medium; + @include elegant-card; + @include round-corner; + background: $color-woot; + color: $color-white; + } + } + + .bubble { + text-align: left; + max-width: 50rem; + word-wrap: break-word; + .aplayer { + box-shadow: none; + font-family: inherit; + } + } + + &.left { + .bubble { + background: $white; + color: $color-heading; + margin-right: auto; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + + &+.right { + margin-top: $space-one; + .bubble { + border-top-right-radius: $space-small; + } + } + + } + + &.right { + @include flex-align(right, null); + + .wrap { + text-align: right; + margin-right: $space-small; + } + + .bubble { + margin-left: auto; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + &.is-private { + background: lighten($warning-color, 32%); + color: $color-heading; + position: relative; + padding-right: $space-large; + + &:before { + position: absolute; + top: $space-smaller + $space-micro; + right: $space-one; + bottom: 0; + color: $medium-gray; + } + } + } + + &+.left { + margin-top: $space-one; + .bubble { + border-top-left-radius: $space-small; + } + } + } + + .wrap { + @include margin($zero $space-normal); + max-width: 69%; + + .sender--name { + font-size: $font-size-mini; + margin-bottom: $space-smaller; + } + } + + .sender--thumbnail { + width: $space-slab; + height: $space-slab; + @include round-corner(); + margin-right: $space-one; + margin-top: $space-micro; + } + .activity-wrap { + @include flex; + @include margin($space-small auto); + @include padding($space-smaller $space-normal); + @include flex-align($x: center, $y: null); + font-size: $font-size-small; + background: lighten($warning-color, 32%); + border-radius: $space-smaller; + + p { + margin-bottom: $zero; + color: $color-heading; + + .ion-person { + margin-right: $space-small; + font-size: $font-size-default; + top: $space-micro; + position: relative; + color: $color-body; + } + + .message-text__wrap { + position: relative; + } + .message-text { + &:after { + content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0"; + display: inline; + } + } + } + + .time { + color: $medium-gray; + } + } + + .bubble { + @include padding($space-smaller $space-one); + @include margin($zero); + background: #c7e3ff; + color: $color-heading; + border-radius: $space-small; + font-size: $font-size-small; + box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.05); + position: relative; + + .icon { + position: absolute; + right: $space-small; + bottom: $space-smaller; + } + .message-text__wrap { + position: relative; + } + .message-text { + &:after { + content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0"; + display: inline; + } + } + + .audio { + .time { + margin-top: -$space-two; + } + } + .image { + @include flex; + @include justify-content(center); + @include align-items(flex-end); + text-align: center; + img { + max-height: 30rem; + max-width: 20rem; + @include padding($space-small); + } + .time { + white-space: nowrap; + margin-left: -$space-large; + } + .modal-image { + max-width: 80%; + max-height: 80%; + } + } + .map { + @include flex; + flex-direction: column; + text-align: right; + img { + max-height: 30rem; + max-width: 20rem; + @include padding($space-small); + } + .time { + white-space: nowrap; + margin-top: -$space-two; + margin-left: -$space-smaller; + @include padding($space-small); + } + .locname { + font-weight: $font-weight-medium; + padding: $space-smaller; + } + } + } + + .time { + margin-left: $space-slab; + text-align: right; + font-size: $font-size-micro; + color: $color-gray; + position: absolute; + bottom: -$space-micro; + right: -$space-micro; + font-style: italic; + float: right; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_emojiinput.scss b/app/javascript/src/assets/scss/widgets/_emojiinput.scss new file mode 100644 index 000000000..02847c69c --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_emojiinput.scss @@ -0,0 +1,106 @@ +.emoji-dialog { + width: 28.6rem; + height: 20rem; + background: $color-white; + box-sizing: content-box; + border-radius: 2px; + position: absolute; + top: -22rem; + right: 0; + padding-bottom: $space-two; + @include elegant-card; + + &:before { + position: absolute; + bottom: -$space-slab; + right: $space-two; + @include arrow(bottom, $color-white, $space-slab); + } + + .emojione { + @include margin($zero); + font-size: $font-size-small; + } + + .emoji-row { + box-sizing: border-box; + overflow-y: scroll; + height: 180px; + @include padding($space-small); + padding-bottom: 0; + + .emoji { + display: inline-block; + padding: 5px; + border-radius: 4px; + } + + .emojione{ + margin: 0.6rem; + float:left; + cursor: pointer; + } + + } + .emoji-dialog-header { + @include padding($zero $space-smaller); + background-color: $light-gray; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + ul { + padding: 0; + margin: 0; + list-style: none; + padding-top: $space-smaller; + + li { + display: inline-block; + box-sizing: border-box; + height: 3.4rem; + text-align: center; + @include padding($space-small $space-small); + cursor: pointer; + + img, svg { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); + } + + &.active { + background: #fff; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + img, + svg { + -webkit-filter: grayscale(0); + filter: grayscale(0); + } + } + } + + } + } + .emoji-category-title { + font-size: $font-size-small; + font-weight: $font-weight-medium; + color: $color-heading; + text-transform: capitalize; + margin: 0; + } + .emoji-category-heading-decoration { + text-align: right; + } + +} + +.emoji-dialog .emoji-category-header > * { + display: table-cell; + vertical-align: middle; +} + +.emoji-dialog .emoji-row +.emoji-dialog .emoji-row .emoji:hover { + background: #F5F7F9; +} diff --git a/app/javascript/src/assets/scss/widgets/_forms.scss b/app/javascript/src/assets/scss/widgets/_forms.scss new file mode 100644 index 000000000..b394ba3b8 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_forms.scss @@ -0,0 +1,31 @@ +.error { + #{$all-text-inputs}, + .multiselect > .multiselect__tags { + @include thin-border( darken(get-color(alert), 25%)); + } +} + +.error { + .message { + display: block; + width: 100%; + margin-top: -$space-normal; + margin-bottom: $space-one; + color: darken(get-color(alert), 25%); + font-weight: $font-weight-normal; + } +} + +.button, +textarea, +input { + &:focus { + outline: none; + } +} + +.input-wrap { + font-size: $font-size-small; + color: $color-heading; + font-weight: $font-weight-medium; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_login.scss b/app/javascript/src/assets/scss/widgets/_login.scss new file mode 100644 index 000000000..f793d8ca7 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_login.scss @@ -0,0 +1,62 @@ +.auth-wrap { + width: 100%; +} + +// Outside login wrapper +.login { + @include full-height; + overflow-y: scroll; + padding-top: $space-larger*1.2; + + .login__hero { + margin-bottom: $space-larger; + + .hero__logo { + width: 180px; + } + + .hero__title { + margin-top: $space-larger; + font-weight: $font-weight-light; + } + + .hero__sub { + font-size: $font-size-medium; + color: $medium-gray; + } + } + + // Login box + .login-box { + @include background-white; + @include border-normal; + @include border-top-radius($space-smaller); + @include border-right-radius($space-smaller); + @include border-bottom-radius($space-smaller); + @include border-left-radius($space-smaller); + @include elegant-card; + padding: $space-large $space-large; + label { + font-size: $font-size-default; + color: $color-gray; + + input { + padding: $space-slab; + height: $space-larger; + font-size: $font-size-default; + } + .error { + font-size: $font-size-small; + } + } + } + + .sigin__footer { + padding: $space-medium; + font-size: $font-size-default; + + > a { + font-weight: $font-weight-bold; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_modal.scss b/app/javascript/src/assets/scss/widgets/_modal.scss new file mode 100644 index 000000000..c35006d8b --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_modal.scss @@ -0,0 +1,92 @@ +.modal-mask { + position: fixed; + z-index: 9990; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, .5); + @include flex; + @include flex-align(center, middle); + +} + +.modal-container { + width: 60rem; + max-height: 100%; + overflow: scroll; + position: relative; + background-color: $color-white; + + .modal--close { + font-size: $font-size-large; + position: absolute; + right: $space-normal; + top: $space-small; + cursor: pointer; + color: $color-heading; + } + + .page-top-bar { + background: $color-modal-header; + text-align: center; + @include padding($space-large $space-medium); + img { + max-height: 6rem; + } + } + + .content-box { + @include padding($zero); + height: auto; + } + + + h2 { + font-size: $font-size-medium; + color: $color-woot; + font-weight: $font-weight-normal; + @include padding($space-small $zero $zero $zero); + } + + p { + font-size: $font-size-small; + @include padding($zero); + @include margin($zero); + } + + form { + align-self: center; + @include padding($space-medium $space-larger $space-small); + a { + @include padding($space-normal); + } + } + + .modal-footer { + @include flex; + @include flex-align($x: justify, $y: center); + @include padding($space-small $zero $space-medium $zero); + button { + font-size: $font-size-small; + } + } + + .delete-item { + @include padding($space-normal); + button { + @include margin($zero); + } + } + +} + +.modal-enter, .modal-leave { + opacity: 0; +} + +.modal-enter .modal-container, +.modal-leave .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} diff --git a/app/javascript/src/assets/scss/widgets/_reply-box.scss b/app/javascript/src/assets/scss/widgets/_reply-box.scss new file mode 100644 index 000000000..a36c9f87e --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_reply-box.scss @@ -0,0 +1,143 @@ +.reply-box { + margin: $space-normal; + margin-top: 0; + border-bottom: 0; + @include elegant-card; + @include transition(height 2s $ease-in-out-cubic); + max-height: $space-jumbo*2; + + .reply-box__top { + @include flex; + @include flex-align($x: left, $y: middle); + @include padding($space-one $space-normal); + @include background-white; + @include margin(0); + position: relative; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + .canned { + @include elegant-card; + z-index: 100; + position: absolute; + background: #fff; + width: 24rem; + left: 0; + border-top: $space-small solid $color-white; + border-bottom: $space-small solid $color-white; + max-height: 14rem; + overflow: scroll; + .active { + a { + background: $color-woot; + } + } + } + &.is-active { + border-bottom-left-radius: $space-small; + border-bottom-right-radius: $space-small; + } + + &.is-private { + background: lighten($warning-color, 38%); + + > input { + background: lighten($warning-color, 38%); + } + } + + > .icon { + font-size: $font-size-medium; + color: $medium-gray; + margin-right: $space-small; + cursor: pointer; + + &.active { + color: $color-woot; + } + } + > textarea { + @include ghost-input(); + @include margin(0); + resize: none; + background: transparent; + // Override min-height : 50px in foundation + // + min-height: 1rem; + } + } + + .reply-box__bottom { + @include background-light; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-light-top; + border-bottom-left-radius: $space-small; + border-bottom-right-radius: $space-small; + + .tabs { + border: 0; + padding: 0; + flex: 1; + + .tabs-title { + margin: 0; + @include transition(background .2s $ease-in-out-cubic); + @include transition(color .2s $ease-in-out-cubic); + + a { + padding: $space-one $space-two; + } + + &:first-child { + border-bottom-left-radius: $space-small; + &.is-active { + @include border-light-right; + border-left: 0; + a { + border-bottom-left-radius: $space-small; + } + } + } + + &.is-private { + &.is-active { + background: lighten($warning-color, 38%); + a { + border-bottom-color: darken($warning-color, 15%); + color: darken($warning-color, 15%); + } + } + } + } + .is-active { + @include background-white; + margin-top: -1px; + @include border-light-left; + @include border-light-right; + } + + .message-length { + float: right; + a { + font-size: $font-size-mini; + } + } + .message-error { + color: $input-error-color; + } + } + + .send-button { + height: 3.6rem; + border-bottom-right-radius: $space-small; + padding-top: $space-small; + padding-right: $space-two; + padding-left: $space-two; + + .icon { + margin-left: $space-small; + } + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_report.scss b/app/javascript/src/assets/scss/widgets/_report.scss new file mode 100644 index 000000000..f39231ae0 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_report.scss @@ -0,0 +1,51 @@ +.report-card { + @include padding($space-normal $space-small $space-normal $space-two); + @include margin($zero); + @include background-light; + cursor: pointer; + @include custom-border-top(3px, transparent); + &.active { + @include custom-border-top(3px, $color-woot); + @include background-white; + .heading, + .metric { + color: $color-woot; + } + } + .heading { + @include margin($zero); + font-size: $font-size-small; + font-weight: $font-weight-bold; + color: $color-heading; + } + .metric { + font-size: $font-size-bigger; + font-weight: $font-weight-feather; + } + .desc { + @include margin($zero); + font-size: $font-size-small; + text-transform: capitalize; + } +} + + +.report-bar { + @include margin(-1px $zero); + @include background-white; + @include border-light; + @include padding($space-small $space-medium); + .chart-container { + @include flex; + @include flex-direction(column); + @include flex-align(center, middle); + div { + width: 100%; + } + .empty-state { + @include margin($space-jumbo); + font-size: $font-size-default; + color: $color-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_search-box.scss b/app/javascript/src/assets/scss/widgets/_search-box.scss new file mode 100644 index 000000000..d834b607e --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_search-box.scss @@ -0,0 +1,15 @@ +.search { + @include flex; + @include flex-align($x: left, $y: middle); + @include padding($space-one $space-normal); + @include flex-shrink; + @include transition(all .3s $ease-in-out-quad); + > .icon { + font-size: $font-size-medium; + color: $medium-gray; + } + > input { + @include ghost-input(); + @include margin(0); + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_sidemenu.scss b/app/javascript/src/assets/scss/widgets/_sidemenu.scss new file mode 100644 index 000000000..f0b3ac4d9 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_sidemenu.scss @@ -0,0 +1,120 @@ +.side-menu { + i { + min-width: 2rem; + } +} + + +.sidebar { + width: $nav-bar-width; + z-index: 1024 - 1; + overflow-x: hidden; + @include border-normal-right; + @include background-white; + @include full-height; + @include margin(0); + @include space-between-column; + + //logo + .logo { + img { + // width: $woot-logo-width; + // height: $woot-logo-height; + @include padding($woot-logo-padding); + } + } + + .main-nav { + @include flex-weight(1); + padding: 0 $space-medium - $space-one; + @include scroll-on-hover; + + a { + color: $color-gray; + font-size: $font-size-default; + border-radius: $space-smaller; + &:before { + margin-right: $space-slab; + } + } + + .menu-title { + font-size: $font-size-medium; + color: $color-gray; + margin-top: $space-medium; + > span { + margin-left: auto; + } + & + ul > li > a{ + @include padding($space-micro null); + line-height: $global-lineheight; + color: $medium-gray; + } + } + } + + // bottom-nav + .bottom-nav { + @include flex; + @include space-between-column; + @include padding($space-one $space-normal $space-one $space-one); + @include flex-direction(column); + @include border-normal-top; + position: relative; + + .current-user { + @include flex; + @include flex-direction(row); + cursor: pointer; + + .current-user--thumbnail { + width: $space-large; + height: $space-large; + @include round-corner(); + } + .current-user--data { + @include flex; + @include flex-direction(column); + .current-user--name { + font-size: $font-size-small; + font-weight: $font-weight-medium; + margin-bottom: $zero; + margin-left: $space-one; + margin-top: $space-micro; + line-height: 1; + } + .current-user--role { + font-size: $font-size-mini; + margin-left: $space-one; + margin-bottom: $zero; + color: $color-gray; + } + } + .current-user--options { + margin-left: auto; + font-size: $font-size-big; + margin-top: auto; + margin-bottom: auto; + } + } + + .dropdown-pane { + @include elegant-card; + top: -160%; + left: 18%; + width: 80%; + @include border-light; + z-index: 999; + visibility: visible; + &:before { + position: absolute; + bottom: -$space-slab; + right: $space-slab; + @include arrow(bottom, $color-white, $space-slab); + } + } + .active { + border-bottom: 2px solid $medium-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_snackbar.scss b/app/javascript/src/assets/scss/widgets/_snackbar.scss new file mode 100644 index 000000000..5ed310018 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_snackbar.scss @@ -0,0 +1,46 @@ +.ui-snackbar-container { + position: absolute; + overflow: hidden; + z-index: 9999; + top: $space-normal; + left: $space-normal; + width: 100%; + text-align: center; +} + +.ui-snackbar { + text-align: left; + display: inline-block; + min-width: 24rem; + max-width: 40rem; + min-height: 3rem; + background-color: $woot-snackbar-bg; + @include padding($space-slab $space-medium); + @include border-top-radius($space-micro); + @include border-right-radius($space-micro); + @include border-bottom-radius($space-micro); + @include border-left-radius($space-micro); + margin-bottom: $space-small; + + // box-shadow: 0 1px 3px alpha(black, 0.12), 0 1px 2px alpha(black, 0.24); +} + +.ui-snackbar-text { + font-size: $font-size-small; + color: $color-white; +} + +.ui-snackbar-action { + margin-left: auto; + padding-left: 3rem; + + button { + border: none; + background: none; + font-size: $font-size-small; + text-transform: uppercase; + color: $woot-snackbar-button; + @include margin(0); + @include padding(0); + } +} diff --git a/app/javascript/src/assets/scss/widgets/_states.scss b/app/javascript/src/assets/scss/widgets/_states.scss new file mode 100644 index 000000000..7c32b9dc0 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_states.scss @@ -0,0 +1,39 @@ +.loading-state { + padding: $space-jumbo $space-smaller; + .message { + display: block; + width: 100%; + text-align: center; + color: $color-gray; + } + .spinner { + float: none; + top: -$space-smaller; + } +} + +// EMPTY STATES +.empty-state { + padding: $space-jumbo $space-smaller; + .title, + .message { + display: block; + text-align: center; + width: 100%; + } + + .title { + font-size: $font-size-giga; + font-weight: $font-weight-feather; + } + + .message { + width: 50%; + margin: 0 auto; + color: $color-gray; + } + + .button { + margin-top: $space-medium; + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_status-bar.scss b/app/javascript/src/assets/scss/widgets/_status-bar.scss new file mode 100644 index 000000000..9cac66a8b --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_status-bar.scss @@ -0,0 +1,31 @@ +.status-bar { + @include flex; + @include flex-direction(column); + @include flex-align($x: center, $y: middle); + background: lighten($warning-color, 36%); + // @include elegant-card(); + @include margin($zero); + @include padding($space-normal $space-smaller); + + .message { + font-weight: $font-weight-medium; + margin-bottom: $zero; + } + + .button { + @include margin($space-smaller $zero $zero); + padding: $space-small $space-normal; + } + + &.danger { + background: lighten($alert-color, 30%); + + .button { + @include button-style($alert-color, darken($alert-color, 7%), $color-white); + } + } + + &.warning { + background: lighten($warning-color, 36%); + } +} diff --git a/app/javascript/src/assets/scss/widgets/_tabs.scss b/app/javascript/src/assets/scss/widgets/_tabs.scss new file mode 100644 index 000000000..e6bc30bd8 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_tabs.scss @@ -0,0 +1,50 @@ +.tabs { + border-width: 0; + @include padding($zero $space-normal); + @include border-normal-bottom; + + .tabs-title { + @include margin($zero $space-one); + + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + &:hover, + &:focus{ + a { + color: darken($medium-gray, 20%); + } + } + + a { + color: $medium-gray; + border-bottom: 2px solid transparent; + font-size: $font-size-small; + @include position(relative, 1px null null null); + @include transition(all .15s $ease-in-out-cubic); + } + + &.is-active { + a { + color: $color-woot; + border-bottom-color: $color-woot; + } + } + } + + // tab chat type + &.tab--chat-type { + @include flex; + + .tabs-title { + a { + font-size: $font-size-default; + padding-top: $space-slab; + padding-bottom: $space-slab; + } + } + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_thumbnail.scss b/app/javascript/src/assets/scss/widgets/_thumbnail.scss new file mode 100644 index 000000000..1196268a4 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_thumbnail.scss @@ -0,0 +1,14 @@ +.user-thumbnail-box { + position: relative; + @include flex-shrink; + .user-thumbnail { + border-radius: 50%; + } + .source-badge { + position: absolute; + height: $space-slab; + width: $space-slab; + right: $zero; + bottom: -$space-micro/2; + } +} diff --git a/app/javascript/src/assets/scss/widgets/_woot-tables.scss b/app/javascript/src/assets/scss/widgets/_woot-tables.scss new file mode 100644 index 000000000..cd8b8150a --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_woot-tables.scss @@ -0,0 +1,50 @@ +table { + font-size: $font-size-small; + border-spacing: 0; + thead { + th { + font-weight: $font-weight-bold; + text-transform: uppercase; + } + } + tbody { + td { + @include padding($space-one $space-small); + border-bottom: 1px solid $color-border-light; + } + } +} + +.woot-table { + + tr { + .show-if-hover { + opacity: 0; + @include transition(all .2s $ease-in-out-cubic); + } + &:hover { + .show-if-hover { + opacity: 1; + } + } + } + .agent-name { + font-weight: $font-weight-medium; + display: block; + text-transform: capitalize; + } + .woot-thumbnail { + border-radius: 50%; + width: 5rem; + height: 5rem; + } + .button-wrapper { + min-width: 20rem; + @include flex; + @include flex-align(left, null); + @include flex-direction(row); + } + .button { + @include margin($zero); + } +} diff --git a/app/javascript/src/components/ChatList.vue b/app/javascript/src/components/ChatList.vue new file mode 100644 index 000000000..eb408c84a --- /dev/null +++ b/app/javascript/src/components/ChatList.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/app/javascript/src/components/Modal.vue b/app/javascript/src/components/Modal.vue new file mode 100644 index 000000000..774ec99a4 --- /dev/null +++ b/app/javascript/src/components/Modal.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/javascript/src/components/ModalHeader.vue b/app/javascript/src/components/ModalHeader.vue new file mode 100644 index 000000000..1019640bb --- /dev/null +++ b/app/javascript/src/components/ModalHeader.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/javascript/src/components/Snackbar.vue b/app/javascript/src/components/Snackbar.vue new file mode 100644 index 000000000..62673e02a --- /dev/null +++ b/app/javascript/src/components/Snackbar.vue @@ -0,0 +1,30 @@ + + + diff --git a/app/javascript/src/components/SnackbarContainer.vue b/app/javascript/src/components/SnackbarContainer.vue new file mode 100644 index 000000000..37a365015 --- /dev/null +++ b/app/javascript/src/components/SnackbarContainer.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/javascript/src/components/Spinner.vue b/app/javascript/src/components/Spinner.vue new file mode 100644 index 000000000..b90ad471d --- /dev/null +++ b/app/javascript/src/components/Spinner.vue @@ -0,0 +1,3 @@ + diff --git a/app/javascript/src/components/Thumbnail.vue b/app/javascript/src/components/Thumbnail.vue new file mode 100644 index 000000000..19f293abb --- /dev/null +++ b/app/javascript/src/components/Thumbnail.vue @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/buttons/FormSubmitButton.vue b/app/javascript/src/components/buttons/FormSubmitButton.vue new file mode 100644 index 000000000..6b4e69a72 --- /dev/null +++ b/app/javascript/src/components/buttons/FormSubmitButton.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/javascript/src/components/buttons/ResolveButton.vue b/app/javascript/src/components/buttons/ResolveButton.vue new file mode 100644 index 000000000..6e6e28990 --- /dev/null +++ b/app/javascript/src/components/buttons/ResolveButton.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/app/javascript/src/components/index.js b/app/javascript/src/components/index.js new file mode 100644 index 000000000..3f613e544 --- /dev/null +++ b/app/javascript/src/components/index.js @@ -0,0 +1,40 @@ +/* eslint no-plusplus: 0 */ +/* eslint-env browser */ + +import Modal from './Modal'; +import Thumbnail from './Thumbnail'; +import Spinner from './Spinner'; +import SubmitButton from './buttons/FormSubmitButton'; +import Tabs from './ui/Tabs/Tabs'; +import TabsItem from './ui/Tabs/TabsItem'; +import LoadingState from './widgets/LoadingState'; +import ReportStatsCard from './widgets/ReportStatsCard'; +import Bar from './widgets/chart/BarChart'; +import ModalHeader from './ModalHeader'; + +const WootUIKit = { + Modal, + Thumbnail, + Spinner, + SubmitButton, + Tabs, + TabsItem, + LoadingState, + ReportStatsCard, + Bar, + ModalHeader, + install(Vue) { + const keys = Object.keys(this); + keys.pop(); // remove 'install' from keys + let i = keys.length; + while (i--) { + Vue.component(`woot${keys[i]}`, this[keys[i]]); + } + }, +}; + +if (typeof window !== 'undefined' && window.Vue) { + window.Vue.use(WootUIKit); +} + +export default WootUIKit; diff --git a/app/javascript/src/components/layout/Sidebar.vue b/app/javascript/src/components/layout/Sidebar.vue new file mode 100644 index 000000000..dee22c1a3 --- /dev/null +++ b/app/javascript/src/components/layout/Sidebar.vue @@ -0,0 +1,140 @@ + + + diff --git a/app/javascript/src/components/layout/SidebarItem.vue b/app/javascript/src/components/layout/SidebarItem.vue new file mode 100644 index 000000000..eaf2361da --- /dev/null +++ b/app/javascript/src/components/layout/SidebarItem.vue @@ -0,0 +1,65 @@ + + + diff --git a/app/javascript/src/components/ui/Switch.vue b/app/javascript/src/components/ui/Switch.vue new file mode 100644 index 000000000..f28c84998 --- /dev/null +++ b/app/javascript/src/components/ui/Switch.vue @@ -0,0 +1,49 @@ + + + + diff --git a/app/javascript/src/components/ui/Tabs/Tabs.js b/app/javascript/src/components/ui/Tabs/Tabs.js new file mode 100644 index 000000000..30e0b487c --- /dev/null +++ b/app/javascript/src/components/ui/Tabs/Tabs.js @@ -0,0 +1,27 @@ +/* eslint no-unused-vars: ["error", { "args": "none" }] */ + +export default { + name: 'WootTabs', + props: { + index: { + type: Number, + default: 0, + }, + }, + render(h) { + const Tabs = this.$slots.default.filter(node => + node.componentOptions && node.componentOptions.tag === 'woot-tabs-item' + ).map((node, index) => { + const data = node.componentOptions.propsData; + data.index = index; + return node; + }); + return ( +

    + { Tabs } +
+ ); + }, +}; diff --git a/app/javascript/src/components/ui/Tabs/TabsItem.js b/app/javascript/src/components/ui/Tabs/TabsItem.js new file mode 100644 index 000000000..d6c7a6ee5 --- /dev/null +++ b/app/javascript/src/components/ui/Tabs/TabsItem.js @@ -0,0 +1,84 @@ +/* eslint no-unused-vars: ["error", { "args": "none" }] */ +/* eslint prefer-template: 0 */ +/* eslint no-console: 0 */ +/* eslint func-names: 0 */ +import TWEEN from 'tween.js'; + +export default { + name: 'WootTabsItem', + props: { + index: { + type: Number, + default: 0, + }, + name: { + type: String, + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + count: { + type: Number, + default: 0, + }, + }, + + data() { + return { + animatedNumber: 0, + }; + }, + + computed: { + active() { + return this.index === this.$parent.index; + }, + + getItemCount() { + return this.animatedNumber || this.count; + }, + }, + + watch: { + count(newValue, oldValue) { + let animationFrame; + const animate = (time) => { + TWEEN.update(time); + animationFrame = window.requestAnimationFrame(animate); + }; + const that = this; + new TWEEN.Tween({ tweeningNumber: oldValue }) + .easing(TWEEN.Easing.Quadratic.Out) + .to({ tweeningNumber: newValue }, 500) + .onUpdate(function () { + that.animatedNumber = this.tweeningNumber.toFixed(0); + }) + .onComplete(() => { + window.cancelAnimationFrame(animationFrame); + }) + .start(); + animationFrame = window.requestAnimationFrame(animate); + }, + }, + + render(h) { + return ( +
  • + { + event.preventDefault(); + if (!this.disabled) { + this.$parent.$emit('change', this.index); + } + }}> + { `${this.name} (${this.getItemCount})` } + +
  • + ); + }, +}; diff --git a/app/javascript/src/components/ui/Wizard.vue b/app/javascript/src/components/ui/Wizard.vue new file mode 100644 index 000000000..5eca6a4a5 --- /dev/null +++ b/app/javascript/src/components/ui/Wizard.vue @@ -0,0 +1,43 @@ + + + + diff --git a/app/javascript/src/components/widgets/BackButton.vue b/app/javascript/src/components/widgets/BackButton.vue new file mode 100644 index 000000000..9b5085983 --- /dev/null +++ b/app/javascript/src/components/widgets/BackButton.vue @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/ChannelItem.vue b/app/javascript/src/components/widgets/ChannelItem.vue new file mode 100644 index 000000000..13ca1f2ce --- /dev/null +++ b/app/javascript/src/components/widgets/ChannelItem.vue @@ -0,0 +1,23 @@ + + diff --git a/app/javascript/src/components/widgets/ChatTypeTabs.vue b/app/javascript/src/components/widgets/ChatTypeTabs.vue new file mode 100644 index 000000000..4812a599b --- /dev/null +++ b/app/javascript/src/components/widgets/ChatTypeTabs.vue @@ -0,0 +1,27 @@ + + diff --git a/app/javascript/src/components/widgets/EmptyState.vue b/app/javascript/src/components/widgets/EmptyState.vue new file mode 100644 index 000000000..15b8e345a --- /dev/null +++ b/app/javascript/src/components/widgets/EmptyState.vue @@ -0,0 +1,17 @@ + + diff --git a/app/javascript/src/components/widgets/InboxListItem.vue b/app/javascript/src/components/widgets/InboxListItem.vue new file mode 100644 index 000000000..bc9ed1daa --- /dev/null +++ b/app/javascript/src/components/widgets/InboxListItem.vue @@ -0,0 +1,21 @@ + + diff --git a/app/javascript/src/components/widgets/LoadingState.vue b/app/javascript/src/components/widgets/LoadingState.vue new file mode 100644 index 000000000..f8d33c961 --- /dev/null +++ b/app/javascript/src/components/widgets/LoadingState.vue @@ -0,0 +1,12 @@ + + diff --git a/app/javascript/src/components/widgets/ReportStatsCard.vue b/app/javascript/src/components/widgets/ReportStatsCard.vue new file mode 100644 index 000000000..b8a0f19c7 --- /dev/null +++ b/app/javascript/src/components/widgets/ReportStatsCard.vue @@ -0,0 +1,20 @@ + + diff --git a/app/javascript/src/components/widgets/SearchBox.vue b/app/javascript/src/components/widgets/SearchBox.vue new file mode 100644 index 000000000..773579e10 --- /dev/null +++ b/app/javascript/src/components/widgets/SearchBox.vue @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/StatusBar.vue b/app/javascript/src/components/widgets/StatusBar.vue new file mode 100644 index 000000000..902c2515e --- /dev/null +++ b/app/javascript/src/components/widgets/StatusBar.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/javascript/src/components/widgets/Thumbnail.vue b/app/javascript/src/components/widgets/Thumbnail.vue new file mode 100644 index 000000000..ac1f3f5d4 --- /dev/null +++ b/app/javascript/src/components/widgets/Thumbnail.vue @@ -0,0 +1,29 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/chart/BarChart.js b/app/javascript/src/components/widgets/chart/BarChart.js new file mode 100644 index 000000000..310367dbb --- /dev/null +++ b/app/javascript/src/components/widgets/chart/BarChart.js @@ -0,0 +1,35 @@ +import { Bar } from 'vue-chartjs'; + +const fontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; + +export default Bar.extend({ + props: ['collection'], + mounted() { + this.renderChart(this.collection, { + // responsive: true, + maintainAspectRatio: false, + legend: { + labels: { + fontFamily, + }, + }, + scales: { + xAxes: [ + { + barPercentage: 1.9, + ticks: { + fontFamily, + }, + }, + ], + yAxes: [ + { + ticks: { + fontFamily, + }, + }, + ], + }, + }); + }, +}); diff --git a/app/javascript/src/components/widgets/conversation/CannedResponse.vue b/app/javascript/src/components/widgets/conversation/CannedResponse.vue new file mode 100644 index 000000000..ee07b4393 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/CannedResponse.vue @@ -0,0 +1,84 @@ + + + diff --git a/app/javascript/src/components/widgets/conversation/ChatFilter.vue b/app/javascript/src/components/widgets/conversation/ChatFilter.vue new file mode 100644 index 000000000..589b38a02 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ChatFilter.vue @@ -0,0 +1,22 @@ + + + + diff --git a/app/javascript/src/components/widgets/conversation/Conversation.vue b/app/javascript/src/components/widgets/conversation/Conversation.vue new file mode 100644 index 000000000..de5be0408 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/Conversation.vue @@ -0,0 +1,97 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationBox.vue b/app/javascript/src/components/widgets/conversation/ConversationBox.vue new file mode 100644 index 000000000..6ad5f37e0 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationBox.vue @@ -0,0 +1,202 @@ + + + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationCard.vue b/app/javascript/src/components/widgets/conversation/ConversationCard.vue new file mode 100644 index 000000000..9c7460afa --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationCard.vue @@ -0,0 +1,93 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationHeader.vue b/app/javascript/src/components/widgets/conversation/ConversationHeader.vue new file mode 100644 index 000000000..cddaf02e7 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationHeader.vue @@ -0,0 +1,95 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ReplyBox.vue b/app/javascript/src/components/widgets/conversation/ReplyBox.vue new file mode 100644 index 000000000..fca785e51 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ReplyBox.vue @@ -0,0 +1,207 @@ +