├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── login │ │ │ ├── login1.woff │ │ │ ├── login2.woff │ │ │ ├── login3.woff │ │ │ └── login4.woff │ │ ├── main │ │ │ ├── add-file.svg │ │ │ ├── blue.png │ │ │ ├── check.svg │ │ │ ├── clear.png │ │ │ ├── create-server.png │ │ │ ├── empty-left.png │ │ │ ├── empty-right.png │ │ │ ├── exclamation.svg │ │ │ ├── font │ │ │ │ ├── fat.woff │ │ │ │ ├── fatter.woff │ │ │ │ ├── middle.woff │ │ │ │ ├── thin.woff │ │ │ │ └── thinner.woff │ │ │ ├── friend-x.svg │ │ │ ├── friendless.svg │ │ │ ├── hangup.svg │ │ │ ├── headphones.svg │ │ │ ├── join-server.png │ │ │ ├── long logo.svg │ │ │ ├── online-empty.svg │ │ │ ├── or.svg │ │ │ ├── pending-empty.svg │ │ │ ├── personal.svg │ │ │ ├── search.svg │ │ │ ├── server-modal-left.png │ │ │ ├── server-modal-right.png │ │ │ ├── welcome-comp.svg │ │ │ ├── welcome-phone.svg │ │ │ ├── white_logo.png │ │ │ ├── x-button.svg │ │ │ └── x-user.svg │ │ └── splash │ │ │ ├── discor-font.woff │ │ │ ├── discors-font-bold.woff │ │ │ └── p-font.woff │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── channels │ │ │ ├── .keep │ │ │ ├── chat.coffee │ │ │ └── voice.coffee │ └── stylesheets │ │ ├── application.css │ │ ├── channel_modal.scss │ │ ├── chrome.scss │ │ ├── css_reset.css │ │ ├── edge.scss │ │ ├── edit_user.scss │ │ ├── firefox.scss │ │ ├── friends.scss │ │ ├── loading.scss │ │ ├── login.scss │ │ ├── main.scss │ │ ├── messages.scss │ │ ├── server_modal.scss │ │ ├── spash.scss │ │ └── user_popup.scss ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── chat_channel.rb │ ├── notifications_channel.rb │ └── voice_channel.rb ├── controllers │ ├── api │ │ ├── audio_channels_controller.rb │ │ ├── channels_controller.rb │ │ ├── dm_channel_memberships_controller.rb │ │ ├── friend_requests_controller.rb │ │ ├── friends_controller.rb │ │ ├── messages_controller.rb │ │ ├── servers_controller.rb │ │ ├── sessions_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── static_pages_controller.rb ├── jobs │ ├── application_job.rb │ ├── friend_request_broadcast_job.rb │ ├── friendship_broadcast_job.rb │ └── notification_broadcast_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── audio_channel.rb │ ├── channel.rb │ ├── concerns │ │ └── .keep │ ├── dm_channel_membership.rb │ ├── friend_request.rb │ ├── friendship.rb │ ├── message.rb │ ├── server.rb │ ├── server_membership.rb │ ├── session.rb │ └── user.rb └── views │ ├── api │ ├── audio_channels │ │ ├── _audio_channel.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── channels │ │ ├── _channel.json.jbuilder │ │ ├── dm_index.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── friend_requests │ │ ├── _friend_request.json.jbuilder │ │ └── show.json.jbuilder │ ├── messages │ │ └── _message.json.jbuilder │ ├── servers │ │ ├── _server.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ └── users │ │ ├── _user.json.jbuilder │ │ ├── index.json.jbuilder │ │ ├── show.json.jbuilder │ │ └── user_data.json.jbuilder │ ├── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ └── static_pages │ └── root.html.erb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring ├── update └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20190121174348_create_users.rb │ ├── 20190122185229_create_servers.rb │ ├── 20190122190214_create_server_memberships.rb │ ├── 20190123145743_create_channels.rb │ ├── 20190124162319_create_messages.rb │ ├── 20190125020020_create_dm_channel_memberships.rb │ ├── 20190126030608_create_friend_requests.rb │ ├── 20190126032316_create_friendships.rb │ ├── 20190127215251_create_sessions.rb │ ├── 20190129004713_create_voice_channels.rb │ ├── 20190129010218_change_voice_channels_to_audio_channels.rb │ ├── 20190129201658_create_active_storage_tables.active_storage.rb │ ├── 20190203031645_add_column_to_sessions.rb │ └── 20190218200301_remove_session_token_from_users.rb ├── schema.rb └── seeds.rb ├── frontend ├── actions │ ├── channel_actions.js │ ├── friends_actions.js │ ├── notification_actions.js │ ├── server_actions.js │ ├── session_actions.js │ ├── ui_actions.js │ └── voice_channel_actions.js ├── components │ ├── app.jsx │ ├── app │ │ ├── app_root.jsx │ │ ├── app_routes │ │ │ ├── friends_main.jsx │ │ │ ├── me_main.jsx │ │ │ ├── me_route.jsx │ │ │ ├── server_main.jsx │ │ │ └── server_route.jsx │ │ ├── channels │ │ │ ├── channels │ │ │ │ ├── channel.jsx │ │ │ │ ├── channels.jsx │ │ │ │ └── channels_container.jsx │ │ │ ├── create_channel_form.jsx │ │ │ ├── dm_channels │ │ │ │ ├── dm_channel.jsx │ │ │ │ ├── dm_channels.jsx │ │ │ │ └── dm_channels_container.jsx │ │ │ ├── edit_user_form.jsx │ │ │ ├── user_bar │ │ │ │ ├── user_bar.jsx │ │ │ │ └── user_bar_container.jsx │ │ │ └── voice_channels │ │ │ │ ├── voice_channel.jsx │ │ │ │ ├── voice_channels.jsx │ │ │ │ └── voice_channels_container.jsx │ │ ├── chat │ │ │ ├── chat │ │ │ │ ├── chat.jsx │ │ │ │ └── chat_container.jsx │ │ │ ├── message.jsx │ │ │ └── message_form.jsx │ │ ├── friends │ │ │ ├── friend.jsx │ │ │ ├── friends_header.jsx │ │ │ ├── friends_list │ │ │ │ ├── friends.jsx │ │ │ │ ├── friends_container.jsx │ │ │ │ └── online_friends_container.jsx │ │ │ ├── mutual_server.jsx │ │ │ └── pending_friends │ │ │ │ ├── pending_friends.jsx │ │ │ │ └── pending_friends_container.jsx │ │ ├── header │ │ │ ├── header │ │ │ │ ├── header.jsx │ │ │ │ └── header_container.jsx │ │ │ └── me_header │ │ │ │ ├── me_header.jsx │ │ │ │ └── me_header_container.jsx │ │ ├── loading_screen.jsx │ │ ├── modal │ │ │ ├── tooltip.jsx │ │ │ ├── user_popup.jsx │ │ │ └── user_popup_container.jsx │ │ ├── server_members │ │ │ ├── server_member.jsx │ │ │ └── server_members │ │ │ │ ├── server_members.jsx │ │ │ │ └── server_members_container.jsx │ │ └── servers │ │ │ ├── create_server_form.jsx │ │ │ ├── dm_notification.jsx │ │ │ ├── join_server_form.jsx │ │ │ ├── search_server_item.jsx │ │ │ ├── search_servers_form.jsx │ │ │ ├── server.jsx │ │ │ └── servers │ │ │ ├── servers.jsx │ │ │ └── servers_container.jsx │ ├── root.jsx │ ├── session_form │ │ ├── login │ │ │ ├── login_form.jsx │ │ │ └── login_form_container.jsx │ │ └── signup │ │ │ ├── signup_form.jsx │ │ │ └── signup_form_container.jsx │ └── splash │ │ ├── splash.jsx │ │ └── splash_container.jsx ├── discors.jsx ├── reducers │ ├── entities │ │ ├── channels_reducer.js │ │ ├── entities_reducer.js │ │ ├── friend_requests_reducer.js │ │ ├── friends_reducer.js │ │ ├── servers_reducer.js │ │ ├── users_reducer.js │ │ └── voice_channels_reducer.js │ ├── errors │ │ ├── channel_errors_reducer.js │ │ ├── errors_reducer.js │ │ ├── server_errors_reducer.js │ │ └── session_errors_reducer.js │ ├── loading_reducer.js │ ├── notifications │ │ ├── dm_notifications_reducer.js │ │ └── notifications_reducer.js │ ├── root_reducer.js │ ├── session_reducer.js │ └── ui_reducer.js ├── store │ └── store.js └── util │ ├── channel_api_util.js │ ├── friends_api_util.js │ ├── route_util.jsx │ ├── server_api_utl.js │ ├── session_api_util.js │ └── voice_channel_api_util.js ├── helpers └── application_helper.rb ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── sessions.rake ├── log └── .keep ├── package-lock.json ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── controllers │ └── .keep ├── fixtures │ ├── .keep │ └── files │ │ └── .keep ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── system │ └── .keep └── test_helper.rb ├── tmp └── .keep ├── vendor └── .keep └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore uploaded files in development 17 | /storage/* 18 | !/storage/.keep 19 | 20 | /node_modules 21 | /yarn-error.log 22 | 23 | /public/assets 24 | .byebug_history 25 | 26 | # Ignore master key for decrypting credentials and more. 27 | /config/master.key 28 | 29 | node_modules/ 30 | bundle.js 31 | bundle.js* 32 | bundle.js.map 33 | .byebug_history 34 | .DS_Store 35 | npm-debug.log -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.1 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '2.5.1' 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem 'rails', '~> 5.2.2' 8 | # Use postgresql as the database for Active Record 9 | gem 'pg', '>= 0.18', '< 2.0' 10 | # Use Puma as the app server 11 | gem 'puma', '~> 3.11' 12 | # Use SCSS for stylesheets 13 | gem 'sass-rails', '~> 5.0' 14 | # Use Uglifier as compressor for JavaScript assets 15 | gem 'uglifier', '>= 1.3.0' 16 | # See https://github.com/rails/execjs#readme for more supported runtimes 17 | # gem 'mini_racer', platforms: :ruby 18 | 19 | # Use CoffeeScript for .coffee assets and views 20 | gem 'coffee-rails', '~> 4.2' 21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 22 | gem 'jbuilder', '~> 2.5' 23 | # Use Redis adapter to run Action Cable in production 24 | gem 'redis', '~> 4.0' 25 | # Use ActiveModel has_secure_password 26 | gem 'bcrypt', '~> 3.1.7' 27 | gem 'ez_download' 28 | # Use ActiveStorage variant 29 | # gem 'mini_magick', '~> 4.8' 30 | 31 | # Use Capistrano for deployment 32 | # gem 'capistrano-rails', group: :development 33 | 34 | # Reduces boot times through caching; required in config/boot.rb 35 | gem 'bootsnap', '>= 1.1.0', require: false 36 | gem "aws-sdk-s3" 37 | gem 'jquery-rails' 38 | 39 | group :development, :test do 40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 41 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 42 | end 43 | 44 | group :development do 45 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 46 | gem 'web-console', '>= 3.3.0' 47 | gem 'listen', '>= 3.0.5', '< 3.2' 48 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 49 | gem 'spring' 50 | gem 'spring-watcher-listen', '~> 2.0.0' 51 | gem 'better_errors' 52 | gem 'binding_of_caller' 53 | gem 'pry-rails' 54 | gem 'annotate' 55 | end 56 | 57 | group :test do 58 | # Adds support for Capybara system testing and selenium driver 59 | gem 'capybara', '>= 2.15' 60 | gem 'selenium-webdriver' 61 | # Easy installation and use of chromedriver to run system tests with Chrome 62 | gem 'chromedriver-helper' 63 | end 64 | 65 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 66 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/login/login1.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/login/login1.woff -------------------------------------------------------------------------------- /app/assets/images/login/login2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/login/login2.woff -------------------------------------------------------------------------------- /app/assets/images/login/login3.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/login/login3.woff -------------------------------------------------------------------------------- /app/assets/images/login/login4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/login/login4.woff -------------------------------------------------------------------------------- /app/assets/images/main/add-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/main/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/blue.png -------------------------------------------------------------------------------- /app/assets/images/main/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/main/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/clear.png -------------------------------------------------------------------------------- /app/assets/images/main/create-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/create-server.png -------------------------------------------------------------------------------- /app/assets/images/main/empty-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/empty-left.png -------------------------------------------------------------------------------- /app/assets/images/main/empty-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/empty-right.png -------------------------------------------------------------------------------- /app/assets/images/main/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/assets/images/main/font/fat.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/font/fat.woff -------------------------------------------------------------------------------- /app/assets/images/main/font/fatter.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/font/fatter.woff -------------------------------------------------------------------------------- /app/assets/images/main/font/middle.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/font/middle.woff -------------------------------------------------------------------------------- /app/assets/images/main/font/thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/font/thin.woff -------------------------------------------------------------------------------- /app/assets/images/main/font/thinner.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/font/thinner.woff -------------------------------------------------------------------------------- /app/assets/images/main/friend-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/main/hangup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/main/headphones.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/images/main/join-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/join-server.png -------------------------------------------------------------------------------- /app/assets/images/main/long logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /app/assets/images/main/or.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/assets/images/main/personal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/assets/images/main/server-modal-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/server-modal-left.png -------------------------------------------------------------------------------- /app/assets/images/main/server-modal-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/server-modal-right.png -------------------------------------------------------------------------------- /app/assets/images/main/welcome-comp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/images/main/welcome-phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/assets/images/main/white_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/main/white_logo.png -------------------------------------------------------------------------------- /app/assets/images/main/x-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/main/x-user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/images/splash/discor-font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/splash/discor-font.woff -------------------------------------------------------------------------------- /app/assets/images/splash/discors-font-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/splash/discors-font-bold.woff -------------------------------------------------------------------------------- /app/assets/images/splash/p-font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/images/splash/p-font.woff -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's 5 | // vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require jquery 16 | //= require jquery_ujs 17 | //= require_tree . 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/channels/chat.coffee: -------------------------------------------------------------------------------- 1 | # App.chat = App.cable.subscriptions.create "ChatChannel", 2 | # connected: -> 3 | # # Called when the subscription is ready for use on the server 4 | 5 | # disconnected: -> 6 | # # Called when the subscription has been terminated by the server 7 | 8 | # received: (data) -> 9 | # # Called when there's incoming data on the websocket for this channel 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/voice.coffee: -------------------------------------------------------------------------------- 1 | # App.voice = App.cable.subscriptions.create "VoiceChannel", 2 | # connected: -> 3 | # # Called when the subscription is ready for use on the server 4 | 5 | # disconnected: -> 6 | # # Called when the subscription has been terminated by the server 7 | 8 | # received: (data) -> 9 | # # Called when there's incoming data on the websocket for this channel 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/channel_modal.scss: -------------------------------------------------------------------------------- 1 | .create-channel-form { 2 | user-select: none; 3 | box-sizing: border-box; 4 | } 5 | 6 | .create-channel-form header { 7 | padding: 20px; 8 | } 9 | .create-channel-form main { 10 | margin-top: 18px; 11 | margin-bottom: 66px; 12 | padding: 20px; 13 | } 14 | 15 | .create-channel-form h4 { 16 | color: #f6f6f7; 17 | font-weight: 600; 18 | line-height: 20px; 19 | letter-spacing: .3px; 20 | font-family: main4; 21 | } 22 | 23 | .create-channel-form p { 24 | color: #b9bbbe; 25 | line-height: 16px; 26 | font-size: 12px; 27 | font-family: main2; 28 | margin-bottom: 20px; 29 | } 30 | 31 | .create-channel-form-bottom { 32 | background-color: #2f3136; 33 | box-shadow: inset 0 1px 0 rgba(47,49,54,.6); 34 | padding: 20px; 35 | height: 78px; 36 | display: flex; 37 | justify-content: flex-end; 38 | align-items: center; 39 | box-sizing: border-box; 40 | } 41 | 42 | .create-channel-form #session-submit { 43 | width: 123px; 44 | margin-bottom: 0; 45 | } 46 | 47 | .create-channel-cancel { 48 | font-size: 14px; 49 | font-weight: 500; 50 | justify-content: center; 51 | line-height: 16px; 52 | padding: 2px 16px; 53 | height: 38px; 54 | color: #fff; 55 | font-family: main2; 56 | } 57 | 58 | .create-channel-cancel:hover { 59 | text-decoration: underline; 60 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/chrome.scss: -------------------------------------------------------------------------------- 1 | @media screen and (-webkit-min-device-pixel-ratio:0) and (min-resolution:.001dpcm) { 2 | .chat-main { 3 | 4 | } 5 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/css_reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | input, textarea, select, button { 45 | text-rendering: auto; 46 | color: initial; 47 | letter-spacing: normal; 48 | word-spacing: normal; 49 | text-transform: none; 50 | text-indent: 0px; 51 | text-shadow: none; 52 | display: inline-block; 53 | text-align: start; 54 | margin: 0em; 55 | font: 400 11px system-ui; 56 | } 57 | a { text-decoration: none; } 58 | textarea, select, input, button { outline: none; } 59 | 60 | button, input[type="submit"], input[type="reset"] { 61 | background: none; 62 | color: inherit; 63 | border: none; 64 | padding: 0; 65 | font: inherit; 66 | cursor: pointer; 67 | outline: inherit; 68 | } 69 | 70 | body { 71 | background-color: #26262b; 72 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/edge.scss: -------------------------------------------------------------------------------- 1 | @supports (-ms-ime-align: auto) { 2 | .side-bar { 3 | overflow-x: hidden; 4 | } 5 | 6 | .side-scroll-container { 7 | width: 86px; 8 | } 9 | 10 | .channel-scroll-wrapper { 11 | width: 256px; 12 | } 13 | 14 | .channels { 15 | overflow: hidden 16 | } 17 | 18 | .server-members-scroll { 19 | width: 256px; 20 | } 21 | 22 | .message-scroll-wrapper { 23 | height: 100%; 24 | width: 100%; 25 | overflow-x: hidden; 26 | padding-right: 16px; 27 | box-sizing: content-box; 28 | } 29 | 30 | .chat-main { 31 | overflow: hidden; 32 | height: calc(100vh - 38px); 33 | } 34 | 35 | .message-wrapper { 36 | display: block !important; 37 | overflow: hidden; 38 | } 39 | 40 | .right-main { 41 | width: 240px; 42 | } 43 | 44 | .friends-scroll-wrapper { 45 | width: calc(100vw - 294px); 46 | } 47 | 48 | .ReactModal__Content { 49 | overflow: hidden !important; 50 | } 51 | 52 | .create-server-form-bottom button { 53 | align-items: center; 54 | } 55 | 56 | .message-list { 57 | overflow-x: visible !important; 58 | } 59 | } 60 | 61 | @supports (-ms-accelerator:true) { 62 | .side-bar { 63 | overflow-x: hidden; 64 | } 65 | 66 | .side-scroll-container { 67 | width: 86px; 68 | } 69 | 70 | .channel-scroll-wrapper { 71 | width: 256px; 72 | } 73 | 74 | .channels { 75 | overflow: hidden 76 | } 77 | 78 | .server-members-scroll { 79 | width: 256px; 80 | } 81 | 82 | .message-scroll-wrapper { 83 | height: 100%; 84 | width: 100%; 85 | overflow-x: hidden; 86 | padding-right: 16px; 87 | box-sizing: content-box; 88 | } 89 | 90 | .chat-main { 91 | overflow: hidden; 92 | height: calc(100vh - 38px); 93 | } 94 | 95 | .message-wrapper { 96 | display: block !important; 97 | overflow: hidden; 98 | } 99 | 100 | .right-main { 101 | width: 240px; 102 | } 103 | 104 | .friends-scroll-wrapper { 105 | width: calc(100vw - 294px); 106 | } 107 | 108 | .ReactModal__Content { 109 | overflow: hidden !important; 110 | background-color: black; 111 | opacity: 0.85; 112 | } 113 | 114 | .create-server-form-bottom button { 115 | align-items: center; 116 | } 117 | 118 | .search-results ul { 119 | width: 435px; 120 | padding-right: 20px; 121 | box-sizing: border-box; 122 | } 123 | 124 | .message-list { 125 | overflow-x: visible !important; 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /app/assets/stylesheets/edit_user.scss: -------------------------------------------------------------------------------- 1 | .edit-user-form { 2 | padding: 20px; 3 | user-select: none; 4 | } 5 | 6 | .edit-user-form-top { 7 | flex-wrap: nowrap; 8 | justify-content: flex-start; 9 | align-items: stretch; 10 | flex: 1 1 auto; 11 | box-sizing: border-box; 12 | display: flex; 13 | } 14 | 15 | .avatar-container { 16 | margin-right: 10px; 17 | flex: 0 1 auto; 18 | width: 100px; 19 | } 20 | 21 | .edit-user-username-email { 22 | flex: 1 1 auto; 23 | margin-left: 10px; 24 | } 25 | 26 | .edit-user-form-bottom { 27 | margin-top: 10px; 28 | padding-top: 20px; 29 | box-sizing: border-box; 30 | border-top: 1px solid rgba(114,118,125,.3); 31 | } 32 | 33 | .edit-user-button-container { 34 | display: flex; 35 | justify-content: flex-end; 36 | align-items: center; 37 | } 38 | 39 | .edit-user-button { 40 | background-color: #43b581; 41 | color: #fff; 42 | cursor: pointer; 43 | height: 32px; 44 | width: auto; 45 | transition: background-color .17s ease,color .17s ease; 46 | align-items: center; 47 | border-radius: 3px; 48 | box-sizing: border-box; 49 | display: flex; 50 | font-size: 14px; 51 | font-weight: 500; 52 | justify-content: center; 53 | line-height: 16px; 54 | padding: 2px 16px; 55 | font-family: main3; 56 | } 57 | 58 | .edit-user-button:hover { 59 | background-color: #3ca374; 60 | } 61 | 62 | .avatar-container input { 63 | position: absolute; 64 | top: 0px; 65 | left: 0px; 66 | width: 100%; 67 | height: 100%; 68 | opacity: 0; 69 | cursor: pointer; 70 | } 71 | 72 | .avatar-wrapper:hover { 73 | box-shadow: inset 0 0 120px rgba(0,0,0,.75); 74 | } 75 | 76 | .avatar-wrapper { 77 | background-position: 50%; 78 | background-repeat: no-repeat; 79 | background-size: cover; 80 | border-radius: 50%; 81 | box-sizing: border-box; 82 | display: inline-block; 83 | flex-shrink: 0; 84 | height: 100px; 85 | width: 100px; 86 | position: relative; 87 | margin-bottom: 10px; 88 | } 89 | 90 | .add-file-icon { 91 | align-items: center; 92 | background-position: 50%; 93 | background-repeat: no-repeat; 94 | border-radius: 50%; 95 | box-shadow: 0 2px 4px 0 rgba(0,0,0,.2); 96 | display: flex; 97 | height: 28px; 98 | justify-content: center; 99 | left: auto; 100 | position: absolute; 101 | right: 0; 102 | top: 0; 103 | width: 28px; 104 | background-color: #dcddde; 105 | background-image: image_url('main/add-file.svg'); 106 | } 107 | 108 | .avatar-wrapper p { 109 | align-items: center; 110 | bottom: 0; 111 | color: #fff; 112 | display: flex; 113 | justify-content: center; 114 | left: 0; 115 | pointer-events: none; 116 | position: absolute; 117 | right: 0; 118 | top: 0; 119 | text-align: center; 120 | font-family: main3; 121 | font-size: 10px; 122 | font-weight: 700; 123 | line-height: 12px; 124 | white-space: pre; 125 | visibility: hidden; 126 | } 127 | 128 | .avatar-wrapper:hover p { 129 | visibility: visible; 130 | } 131 | 132 | .remove-avatar-button { 133 | color: #b9bbbe; 134 | cursor: pointer; 135 | display: block; 136 | font-size: 12px; 137 | text-decoration: none; 138 | font-family: main3; 139 | transition: opacity .5s,color .5s; 140 | margin: auto; 141 | } 142 | 143 | .remove-avatar-button:hover { 144 | color: #72767d; 145 | text-decoration: underline; 146 | } 147 | -------------------------------------------------------------------------------- /app/assets/stylesheets/firefox.scss: -------------------------------------------------------------------------------- 1 | @-moz-document url-prefix() { 2 | 3 | .side-bar { 4 | overflow-x: hidden; 5 | } 6 | 7 | .side-scroll-container { 8 | width: 86px; 9 | } 10 | 11 | .channel-scroll-wrapper { 12 | width: 255px; 13 | } 14 | 15 | .channels { 16 | overflow: hidden 17 | } 18 | 19 | .server-members-scroll { 20 | width: 255px; 21 | } 22 | 23 | .message-scroll-wrapper { 24 | height: 100%; 25 | width: 100%; 26 | overflow-x: hidden; 27 | padding-right: 15px; 28 | box-sizing: content-box; 29 | } 30 | 31 | .chat-main { 32 | overflow: hidden; 33 | height: calc(100vh - 48px) !important; 34 | } 35 | 36 | .message-wrapper { 37 | display: block !important; 38 | overflow: hidden; 39 | } 40 | 41 | .right-main { 42 | width: 240px; 43 | } 44 | 45 | .friends-scroll-wrapper { 46 | width: calc(100vw - 294px); 47 | } 48 | 49 | .ReactModal__Content { 50 | overflow: hidden !important; 51 | } 52 | 53 | .create-server-form-bottom button { 54 | align-items: center; 55 | } 56 | 57 | .search-results ul { 58 | width: 435px; 59 | padding-right: 20px; 60 | box-sizing: border-box; 61 | } 62 | 63 | .message-list { 64 | overflow-x: visible !important; 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/assets/stylesheets/loading.scss: -------------------------------------------------------------------------------- 1 | .loading-screen { 2 | align-items: center; 3 | background-color: #2f3136; 4 | bottom: 0; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | left: 0; 9 | overflow: hidden; 10 | position: absolute; 11 | right: 0; 12 | text-rendering: optimizeLegibility; 13 | top: 0; 14 | z-index: 3000; 15 | transition: opacity 200ms ease-in-out; 16 | opacity: 1; 17 | } 18 | 19 | .loading-screen h5 { 20 | color: #72767d; 21 | font-size: 12px; 22 | line-height: 16px; 23 | max-width: 600px; 24 | text-transform: uppercase; 25 | font-family: main3; 26 | } 27 | 28 | .hidden { 29 | opacity: 0; 30 | // transition: opacity 200ms ease-in-out; 31 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/user_popup.scss: -------------------------------------------------------------------------------- 1 | .user-popup-top { 2 | padding: 20px 10px; 3 | background-color: #2f3136; 4 | display: flex; 5 | justify-content: center; 6 | align-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | } 10 | 11 | .user-popup{ 12 | color: #fff; 13 | line-height: 20px; 14 | font-size: 16px; 15 | user-select: none; 16 | } 17 | 18 | .user-popup-img { 19 | height: 90px; 20 | width: 90px; 21 | border-radius: 50%; 22 | background-clip: padding-box; 23 | background-position: 50%; 24 | background-size: 100%; 25 | margin-bottom: 10px; 26 | position: relative; 27 | } 28 | 29 | .user-popup-top h5 { 30 | font-weight: 600; 31 | color: #fff; 32 | font-family: main3; 33 | font-size: 16px; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | max-width: 80%; 37 | } 38 | 39 | .user-popup-bottom { 40 | background: #18191c; 41 | box-shadow: 0 0 1px rgba(0,0,0,.82), 0 1px 4px rgba(0,0,0,.1); 42 | border-radius: 0 0 5px 5px; 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | .user-popup-button:hover { 48 | background: #040405; 49 | color: #fff; 50 | } 51 | 52 | .user-popup-button { 53 | color: hsla(0,0%,100%,.6); 54 | border-radius: 5px; 55 | box-sizing: border-box; 56 | font-size: 13px; 57 | font-weight: 500; 58 | line-height: 16px; 59 | margin: 2px 0; 60 | overflow: hidden; 61 | padding: 6px 9px; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | width: 100%; 65 | } 66 | 67 | .popup-online-indicator { 68 | background-size: 22px; 69 | bottom: 3px; 70 | height: 18px; 71 | position: absolute; 72 | right: 3px; 73 | width: 18px; 74 | background-position: 50% 50%; 75 | background-repeat: no-repeat; 76 | border: 3px solid #2f3136; 77 | border-radius: 50%; 78 | } -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/notifications_channel.rb: -------------------------------------------------------------------------------- 1 | class NotificationsChannel < ApplicationCable::Channel 2 | def subscribed 3 | current_user = User.find(params['userId']) 4 | stream_for current_user 5 | end 6 | 7 | def unsubscribed 8 | end 9 | end -------------------------------------------------------------------------------- /app/channels/voice_channel.rb: -------------------------------------------------------------------------------- 1 | class VoiceChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from "voice_channel_#{params['channelId']}" 4 | end 5 | 6 | def unsubscribed 7 | end 8 | 9 | def broadcast(options) 10 | channel_id = options["channelId"] 11 | type = options["data"]["type"] 12 | from = options["data"]["from"] || nil 13 | to = options["data"]["to"] || nil 14 | sdp = options["data"]["sdp"] || nil 15 | candidate = options["data"]["candidate"] || nil 16 | 17 | ActionCable.server.broadcast("voice_channel_#{channel_id}", 18 | type: type, 19 | from: from, 20 | to: to, 21 | sdp: sdp, 22 | candidate: candidate 23 | ) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/api/audio_channels_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::AudioChannelsController < ApplicationController 2 | def create 3 | @audio_channel = AudioChannel.new(audio_channel_params) 4 | 5 | if @audio_channel.save 6 | render "api/audio_channels/show" 7 | else 8 | render json: @audio_channel.errors.full_messages, status: 422 9 | end 10 | end 11 | 12 | def index 13 | server = Server.find_by(id: audio_channel_params[:server_id]) 14 | 15 | if server 16 | @audio_channels = server.audio_channels 17 | render :index 18 | else 19 | render json: ["Server does not exist"], status: 422 20 | end 21 | end 22 | 23 | def destroy 24 | current_audio_channel.destroy 25 | render "api/audio_channels/show" 26 | end 27 | 28 | private 29 | 30 | def current_audio_channel 31 | @audio_channel ||= AudioChannel.find(params[:id]) 32 | end 33 | 34 | def audio_channel_params 35 | params.require(:audio_channel).permit(:name, :server_id) 36 | end 37 | end -------------------------------------------------------------------------------- /app/controllers/api/channels_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ChannelsController < ApplicationController 2 | def create 3 | @channel = Channel.new(channel_params) 4 | 5 | if @channel.save 6 | render "api/channels/show" 7 | else 8 | render json: @channel.errors.full_messages, status: 422 9 | end 10 | end 11 | 12 | def index 13 | server = Server.find_by(id: channel_params[:server_id]) 14 | 15 | if server 16 | @channels = server.channels 17 | render :index 18 | else 19 | render json: ["Server does not exist"], status: 422 20 | end 21 | end 22 | 23 | def destroy 24 | current_channel.destroy 25 | render "api/channels/show" 26 | end 27 | 28 | private 29 | 30 | def current_channel 31 | @channel ||= Channel.find(params[:id]) 32 | end 33 | 34 | def channel_params 35 | params.require(:channel).permit(:name, :server_id) 36 | end 37 | end -------------------------------------------------------------------------------- /app/controllers/api/dm_channel_memberships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::DmChannelMembershipsController < ApplicationController 2 | 3 | def destroy 4 | channel_id = params[:id] 5 | @dm_channel_membership = DmChannelMembership.find_by(user_id: current_user.id, channel_id: channel_id) 6 | @dm_channel_membership.destroy 7 | render json: @dm_channel_membership 8 | end 9 | 10 | def create 11 | user_id = dm_channel_params[:user_id].to_i 12 | current_user_id = current_user.id 13 | unless current_user_id == user_id 14 | name = current_user_id > user_id ? "#{user_id}-#{current_user_id}" : "#{current_user_id}-#{user_id}" 15 | @channel = Channel.find_or_create_by(name: name) 16 | @channel.dm_memberships.create(user_id: current_user_id) 17 | @channel.dm_memberships.create(user_id: user_id) 18 | 19 | render "api/channels/show" 20 | else 21 | render json: ['Cannot DM yourself'], status: 401 22 | end 23 | end 24 | 25 | private 26 | 27 | def dm_channel_params 28 | params.require(:dm_channel).permit(:user_id) 29 | end 30 | end -------------------------------------------------------------------------------- /app/controllers/api/friend_requests_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FriendRequestsController < ApplicationController 2 | 3 | def create 4 | friend = User.find(friend_request_params[:friend_id]) 5 | @friend_request = current_user.friend_requests.new(friend: friend) 6 | 7 | if @friend_request.save 8 | @user = current_user 9 | render 'api/friend_requests/show' 10 | else 11 | render json: @friend_request.errors.full_messages, status: :unprocessable_entity 12 | end 13 | end 14 | 15 | def destroy 16 | current_friend_request.destroy 17 | render :show 18 | end 19 | 20 | def update 21 | current_friend_request.accept 22 | render :show 23 | end 24 | 25 | private 26 | 27 | def friend_request_params 28 | params.require(:friend_request).permit(:friend_id) 29 | end 30 | 31 | def current_friend_request 32 | @friend_request ||= FriendRequest.find(params[:id]) 33 | end 34 | end -------------------------------------------------------------------------------- /app/controllers/api/friends_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FriendsController < ApplicationController 2 | 3 | def destroy 4 | current_user.remove_friend(current_friend) 5 | head :no_content 6 | end 7 | 8 | private 9 | 10 | def current_friend 11 | @friend ||= current_user.friends.find(params[:id]) 12 | end 13 | end -------------------------------------------------------------------------------- /app/controllers/api/messages_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::MessagesController < ApplicationController 2 | def create 3 | @message = Message.new(message_params) 4 | @message.author_id = current_user.id 5 | 6 | if @message.save 7 | render :show 8 | else 9 | render json: @message.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | def index 14 | server = Channel.find(message_params[:channel_id]) 15 | @messages = channel.messges 16 | render :index 17 | end 18 | 19 | private 20 | 21 | def current_message 22 | @message ||= Message.find(params[:id]) 23 | end 24 | 25 | def message_params 26 | params.require(:message).permit(:body, :channel_id, :author_id) 27 | end 28 | end -------------------------------------------------------------------------------- /app/controllers/api/servers_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ServersController < ApplicationController 2 | def create 3 | @server = current_user.admin_servers.new(server_params) 4 | 5 | if @server.save 6 | current_user.server_memberships.create!(server_id: @server.id) 7 | @server.channels.create!(name: 'general') 8 | @server.audio_channels.create!(name: 'General') 9 | 10 | render "api/servers/show" 11 | else 12 | render json: @server.errors.full_messages, status: 422 13 | end 14 | end 15 | 16 | def index 17 | @servers = Server.all.includes(:channels) 18 | end 19 | 20 | def join 21 | @server = Server.find_by(name: server_params[:name]) 22 | if @server 23 | current_user.server_memberships.create(server_id: @server.id) 24 | render :show 25 | else 26 | render json: ["Server does not exist"], status: 401 27 | end 28 | end 29 | 30 | def members 31 | if current_server 32 | @users = User.distinct.select('users.*').left_outer_joins(:message_channels).left_outer_joins(:server_memberships) 33 | .where("server_memberships.server_id = :current_server_id OR channels.server_id = :current_server_id", current_server_id: current_server.id) 34 | .includes(:sessions, :server_memberships) 35 | # @users = current_server.members.includes(:sessions, :server_memberships) 36 | render "api/users/index" 37 | else 38 | render json: ["Server does not exist"], status: 401 39 | end 40 | end 41 | 42 | def destroy 43 | if current_user.id == current_server.admin_id 44 | current_server.destroy 45 | else 46 | current_user.server_memberships.find_by(server_id: current_server.id).destroy 47 | end 48 | 49 | render :show 50 | end 51 | 52 | private 53 | 54 | def current_server 55 | @server ||= Server.find_by(id: params[:id]) 56 | end 57 | 58 | def server_params 59 | params.require(:server).permit(:name, :icon) 60 | end 61 | end -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | def create 3 | @user = User.find_by_credentials( 4 | user_params[:username], 5 | user_params[:password] 6 | ) 7 | 8 | if @user 9 | login(@user, request.user_agent) 10 | render "api/users/show" 11 | else 12 | render json: ["Invalid username/password combination"], status: 401 13 | end 14 | end 15 | 16 | def destroy 17 | @user = current_user 18 | if @user 19 | logout(request.user_agent) 20 | render "api/users/show" 21 | else 22 | render json: ["Nobody signed in"], status: 404 23 | end 24 | end 25 | 26 | private 27 | 28 | def user_params 29 | params.require(:user).permit(:username, :password) 30 | end 31 | end -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.new(user_params) 5 | 6 | if @user.save 7 | login(@user, request.user_agent) 8 | render :show 9 | else 10 | render json: @user.errors.full_messages, status: 422 11 | end 12 | end 13 | 14 | def update 15 | @user = current_user 16 | 17 | if @user.id == 71 18 | render json: ['Can not edit the demo user'], status: 422 19 | else 20 | if current_user.update_attributes(user_params) 21 | render :show 22 | else 23 | render json: current_user.errors.full_messages, status: 422 24 | end 25 | end 26 | end 27 | 28 | def data 29 | current_user_id = current_user.id 30 | 31 | @servers = current_user.servers.includes(:channels, :audio_channels) 32 | @channels = @servers.map(&:channels).flatten.uniq 33 | @audio_channels = @servers.map(&:audio_channels).flatten.uniq 34 | @requests = FriendRequest.where(friend_id: current_user_id).or(FriendRequest.where(user_id: current_user_id)) 35 | @friendships = current_user.friendships.pluck(:friend_id) 36 | @dm_channels = current_user.dm_channels 37 | 38 | # This is the code I would actually use; however, to decrease subsequent load times for non-tech people viewing my 39 | # site, I have temporarily replaced it with a load of all the users. 40 | # dm_user_ids = [current_user_id] 41 | 42 | # @dm_channels.each do |channel| 43 | # dm_arr = channel.name.split('-') 44 | # if dm_arr[0].to_i == current_user_id 45 | # dm_user_ids << dm_arr[1].to_i 46 | # else 47 | # dm_user_ids << dm_arr[0].to_i 48 | # end 49 | # end 50 | 51 | # @users = User.distinct.select('users.*').left_outer_joins(:friend_requests) 52 | # .left_outer_joins(:incoming_friend_requests).left_outer_joins(:friendships) 53 | # .where("incoming_friend_requests_users.user_id = :current_user_id OR friend_requests.friend_id = :current_user_id OR friendships.friend_id = :current_user_id OR users.id IN (:dm_user_ids)", current_user_id: current_user_id, dm_user_ids: dm_user_ids) 54 | # .includes(:sessions, :server_memberships) 55 | 56 | @users = User.all.includes(:sessions, :server_memberships) 57 | render "api/users/user_data" 58 | end 59 | 60 | private 61 | 62 | def user_params 63 | params.require(:user).permit(:username, :password, :email, :avatar) 64 | end 65 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | helper_method :current_user, :logged_in? 3 | 4 | private 5 | 6 | def current_user 7 | unless @current_user 8 | current_session = Session.find_by(session_token: session[:session_token]) 9 | @current_user = User.find(current_session.user_id) if current_session 10 | end 11 | 12 | @current_user 13 | end 14 | 15 | def logged_in? 16 | !!current_user 17 | end 18 | 19 | def login(user, user_agent) 20 | session[:session_token] = user.reset_session_token!(user_agent) 21 | @current_user = user 22 | end 23 | 24 | def logout(user_agent) 25 | current_user.destroy_session!(user_agent) 26 | session[:session_token] = nil 27 | @current_user = nil 28 | end 29 | 30 | def require_logged_in 31 | unless current_user 32 | render json: { base: ['invalid credentials'] }, status: 401 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/friend_request_broadcast_job.rb: -------------------------------------------------------------------------------- 1 | class FriendRequestBroadcastJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(friend_request, create) 5 | friend = User.find(friend_request.friend_id) 6 | 7 | if create 8 | NotificationsChannel.broadcast_to( 9 | friend, {type: 'friend_request', 10 | friend_request: render_friend_request(friend_request), 11 | user: render_user(friend_request.user), 12 | } 13 | ) 14 | else 15 | user = User.find(friend_request.user_id) 16 | 17 | NotificationsChannel.broadcast_to( 18 | user, {type: 'friend_request_destroy', 19 | friend_request: render_friend_request(friend_request) 20 | } 21 | ) 22 | 23 | NotificationsChannel.broadcast_to( 24 | friend, {type: 'friend_request_destroy', 25 | friend_request: render_friend_request(friend_request) 26 | } 27 | ) 28 | end 29 | end 30 | 31 | private 32 | 33 | def render_friend_request(friend_request) 34 | ApplicationController.renderer.render( 35 | partial: 'api/friend_requests/friend_request', 36 | locals: { friend_request: friend_request } 37 | ) 38 | end 39 | 40 | def render_user(user) 41 | ApplicationController.renderer.render( 42 | partial: 'api/users/user', 43 | locals: { user: user } 44 | ) 45 | end 46 | end -------------------------------------------------------------------------------- /app/jobs/friendship_broadcast_job.rb: -------------------------------------------------------------------------------- 1 | class FriendshipBroadcastJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(user, friend, create) 5 | if create 6 | NotificationsChannel.broadcast_to( 7 | user, {type: 'friend', 8 | user: render_user(friend) 9 | } 10 | ) 11 | else 12 | NotificationsChannel.broadcast_to( 13 | user, {type: 'friend_destroy', 14 | user_id: friend.id 15 | } 16 | ) 17 | end 18 | end 19 | 20 | private 21 | 22 | def render_user(user) 23 | ApplicationController.renderer.render( 24 | partial: 'api/users/user', 25 | locals: { user: user } 26 | ) 27 | end 28 | end -------------------------------------------------------------------------------- /app/jobs/notification_broadcast_job.rb: -------------------------------------------------------------------------------- 1 | class NotificationBroadcastJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(message, user, channel) 5 | NotificationsChannel.broadcast_to( 6 | user, {type: 'message', 7 | user: render_user(message.author), 8 | channel: render_channel(channel) 9 | } 10 | ) 11 | end 12 | 13 | private 14 | 15 | def render_user(user) 16 | ApplicationController.renderer.render( 17 | partial: 'api/users/user', 18 | locals: { user: user } 19 | ) 20 | end 21 | 22 | def render_channel(channel) 23 | ApplicationController.renderer.render( 24 | partial: 'api/channels/channel', 25 | locals: { channel: channel } 26 | ) 27 | end 28 | end -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/audio_channel.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: audio_channels 4 | # 5 | # id :bigint(8) not null, primary key 6 | # name :string not null 7 | # server_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class AudioChannel < ApplicationRecord 13 | validates :name, presence: true 14 | validates :name, uniqueness: {scope: :server_id} 15 | 16 | belongs_to :server, 17 | optional: true 18 | end 19 | -------------------------------------------------------------------------------- /app/models/channel.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: channels 4 | # 5 | # id :bigint(8) not null, primary key 6 | # name :string not null 7 | # server_id :integer 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Channel < ApplicationRecord 13 | validates :name, presence: true 14 | validates :name, uniqueness: {scope: :server_id} 15 | 16 | belongs_to :server, 17 | optional: true 18 | 19 | has_many :messages, 20 | dependent: :destroy 21 | 22 | has_many :dm_memberships, 23 | class_name: :DmChannelMembership, 24 | dependent: :destroy 25 | 26 | has_many :members, 27 | through: :dm_memberships, 28 | source: :user 29 | 30 | end 31 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/dm_channel_membership.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: dm_channel_memberships 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # channel_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class DmChannelMembership < ApplicationRecord 13 | validates :user_id, uniqueness: {scope: :channel_id} 14 | 15 | belongs_to :channel 16 | 17 | belongs_to :user 18 | end 19 | -------------------------------------------------------------------------------- /app/models/friend_request.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: friend_requests 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # friend_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class FriendRequest < ApplicationRecord 13 | validates :user, presence: true 14 | validates :friend, presence: true, uniqueness: { scope: :user } 15 | validate :not_self 16 | validate :not_friends 17 | validate :not_pending 18 | 19 | belongs_to :user 20 | 21 | belongs_to :friend, 22 | class_name: :User 23 | 24 | def accept 25 | user.friends << friend 26 | destroy 27 | end 28 | 29 | def send_request 30 | FriendRequestBroadcastJob.perform_later(self, true) 31 | end 32 | 33 | def send_request_destroy 34 | FriendRequestBroadcastJob.perform_later(self, false) 35 | end 36 | 37 | after_create_commit :send_request 38 | after_destroy :send_request_destroy 39 | 40 | private 41 | 42 | def not_self 43 | errors.add(:friend, "can't be equal to user") if user == friend 44 | end 45 | 46 | def not_friends 47 | errors.add(:friend, 'is already added') if user.friends.include?(friend) 48 | end 49 | 50 | def not_pending 51 | errors.add(:friend, 'already requested friendship') if friend.pending_friends.include?(user) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/models/friendship.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: friendships 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # friend_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Friendship < ApplicationRecord 13 | validates :user, presence: true 14 | validates :friend, presence: true, uniqueness: { scope: :user } 15 | validate :not_self 16 | 17 | after_create :create_inverse_relationship 18 | after_destroy :destroy_inverse_relationship 19 | 20 | belongs_to :user 21 | 22 | belongs_to :friend, 23 | class_name: :User 24 | 25 | def send_friend 26 | FriendshipBroadcastJob.perform_later(self.user, self.friend, true) 27 | end 28 | 29 | def send_friend_destroy 30 | FriendshipBroadcastJob.perform_later(self.user, self.friend, false) 31 | end 32 | 33 | after_create_commit :send_friend 34 | after_destroy :send_friend_destroy 35 | 36 | private 37 | 38 | def create_inverse_relationship 39 | friend.friendships.create(friend: user) 40 | end 41 | 42 | def destroy_inverse_relationship 43 | friendship = friend.friendships.find_by(friend: user) 44 | friendship.destroy if friendship 45 | end 46 | 47 | def not_self 48 | errors.add(:friend, "can't be equal to user") if user == friend 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: messages 4 | # 5 | # id :bigint(8) not null, primary key 6 | # body :string not null 7 | # author_id :integer not null 8 | # channel_id :integer not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Message < ApplicationRecord 14 | validates :body, presence: true 15 | 16 | belongs_to :channel 17 | 18 | belongs_to :author, 19 | class_name: :User 20 | 21 | def send_notification 22 | message_channel = self.channel 23 | unless message_channel.server_id 24 | user_id = message_channel.name.split('-').find { |el| el != self.author_id.to_s }.to_i 25 | DmChannelMembership.create(user_id: user_id, channel_id: message_channel.id) 26 | user = User.find(user_id) 27 | NotificationBroadcastJob.perform_later(self, user, message_channel) 28 | end 29 | end 30 | 31 | after_create_commit :send_notification 32 | end 33 | -------------------------------------------------------------------------------- /app/models/server.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: servers 4 | # 5 | # id :bigint(8) not null, primary key 6 | # name :string not null 7 | # admin_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Server < ApplicationRecord 13 | validates :name, presence: true, uniqueness: true 14 | 15 | belongs_to :admin, 16 | class_name: :User 17 | 18 | has_many :memberships, 19 | class_name: :ServerMembership, 20 | dependent: :destroy 21 | 22 | has_many :members, 23 | through: :memberships, 24 | source: :user 25 | 26 | has_many :channels, 27 | dependent: :destroy 28 | 29 | has_many :audio_channels, 30 | dependent: :destroy 31 | 32 | has_many :server_messages, 33 | through: :channels, 34 | source: :messages 35 | 36 | has_many :message_users, 37 | through: :server_messages, 38 | source: :author 39 | 40 | has_one_attached :icon 41 | end 42 | -------------------------------------------------------------------------------- /app/models/server_membership.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: server_memberships 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # server_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class ServerMembership < ApplicationRecord 13 | validates :user_id, uniqueness: {scope: :server_id} 14 | 15 | belongs_to :server 16 | 17 | belongs_to :user 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/models/session.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: sessions 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # session_token :string not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # user_agent :string not null 11 | # 12 | 13 | class Session < ApplicationRecord 14 | validates :session_token, :user_agent, presence: true 15 | validates :session_token, uniqueness: true 16 | 17 | belongs_to :user 18 | end 19 | -------------------------------------------------------------------------------- /app/views/api/audio_channels/_audio_channel.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! audio_channel, :id, :name, :server_id -------------------------------------------------------------------------------- /app/views/api/audio_channels/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @audio_channels.each do |audio_channel| 2 | json.set! audio_channel.id do 3 | json.partial! 'api/audio_channels/audio_channel', audio_channel: audio_channel 4 | end 5 | end -------------------------------------------------------------------------------- /app/views/api/audio_channels/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/audio_channels/audio_channel", audio_channel: @audio_channel -------------------------------------------------------------------------------- /app/views/api/channels/_channel.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! channel, :id, :name, :server_id -------------------------------------------------------------------------------- /app/views/api/channels/dm_index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.channels do 2 | @channels.each do |channel| 3 | json.set! channel.id do 4 | json.partial! 'api/channels/channel', channel: channel 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/channels/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @channels.each do |channel| 2 | json.set! channel.id do 3 | json.partial! 'api/channels/channel', channel: channel 4 | end 5 | end -------------------------------------------------------------------------------- /app/views/api/channels/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/channels/channel", channel: @channel -------------------------------------------------------------------------------- /app/views/api/friend_requests/_friend_request.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! friend_request, :id, :user_id, :friend_id -------------------------------------------------------------------------------- /app/views/api/friend_requests/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'api/friend_requests/friend_request', friend_request: @friend_request -------------------------------------------------------------------------------- /app/views/api/messages/_message.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! message, :id, :author_id, :channel_id, :body -------------------------------------------------------------------------------- /app/views/api/servers/_server.json.jbuilder: -------------------------------------------------------------------------------- 1 | icon_url = (server.icon.attached? ? url_for(server.icon) : '') 2 | channels = server.channels 3 | first_channel = channels.select { |channel| channel.name == "general" }.first 4 | root_channel = (first_channel ? first_channel.id : '') 5 | 6 | json.extract! server, :id, :name, :admin_id 7 | json.icon_url icon_url 8 | json.root_channel root_channel -------------------------------------------------------------------------------- /app/views/api/servers/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @servers.with_attached_icon.each do |server| 2 | json.set! server.id do 3 | json.partial! 'api/servers/server', server: server 4 | end 5 | end -------------------------------------------------------------------------------- /app/views/api/servers/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/servers/server", server: @server -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | server_array = user.server_memberships.sort.map { |membership| membership.server_id } 2 | online = !user.sessions.empty? 3 | image_url = user.avatar.attached? ? url_for(user.avatar) : user.image_url 4 | 5 | json.extract! user, :id, :username, :email 6 | json.image_url image_url 7 | json.servers server_array 8 | json.online online -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.users do 2 | @users.with_attached_avatar.each do |user| 3 | json.set! user.id do 4 | json.partial! 'api/users/user', user: user 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user -------------------------------------------------------------------------------- /app/views/api/users/user_data.json.jbuilder: -------------------------------------------------------------------------------- 1 | outgoing = [] 2 | incoming = [] 3 | 4 | @requests.each do |request| 5 | if request.user_id == current_user.id 6 | outgoing << request 7 | else 8 | incoming << request 9 | end 10 | end 11 | 12 | json.channels do 13 | 14 | @channels.each do |channel| 15 | json.set! channel.id do 16 | json.partial! 'api/channels/channel', channel: channel 17 | end 18 | end 19 | 20 | @dm_channels.each do |channel| 21 | json.set! channel.id do 22 | json.partial! 'api/channels/channel', channel: channel 23 | end 24 | end 25 | end 26 | 27 | json.voice_channels do 28 | @audio_channels.each do |audio_channel| 29 | json.set! audio_channel.id do 30 | json.partial! 'api/audio_channels/audio_channel', audio_channel: audio_channel 31 | end 32 | end 33 | 34 | end 35 | 36 | json.users do 37 | @users.with_attached_avatar.each do |user| 38 | json.set! user.id do 39 | json.partial! 'api/users/user', user: user 40 | end 41 | end 42 | end 43 | 44 | json.servers do 45 | @servers.with_attached_icon.each do |server| 46 | json.set! server.id do 47 | json.partial! 'api/servers/server', server: server 48 | end 49 | end 50 | end 51 | 52 | json.friend_requests do 53 | 54 | outgoing.each do |request| 55 | json.set! request.id do 56 | json.partial! 'api/friend_requests/friend_request', friend_request: request 57 | end 58 | end 59 | 60 | incoming.each do |request| 61 | json.set! request.id do 62 | json.partial! 'api/friend_requests/friend_request', friend_request: request 63 | end 64 | end 65 | 66 | end 67 | 68 | json.friends do 69 | json.array! @friendships 70 | end 71 | 72 | json.currentUserId current_user.id -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | Discors 7 | <%= csrf_meta_tags %> 8 | <%= csp_meta_tag %> 9 | 10 | <%= stylesheet_link_tag 'application', media: 'all' %> 11 | <%= javascript_include_tag 'application' %> 12 | 13 | 14 | 15 | <%= yield %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | <% if logged_in? %> 2 | 6 | <% end %> 7 | 8 |
-------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Discors 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.2 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV['REDIS_URL'] %> 10 | channel_prefix: discors_production -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | Jt9ijb9pcSCrOUEGJ+os/Re03t/IXT34HE7jMI/0FlaniZaBCtdjbXmCxluscTumRaLRAocJ3mcisS/Feu8biltms7zq7X5LxRl484YgVRBp97RKeLvsKRiOoj2X3P4JSAjMLab4l+fekOEN/Y5t7J6zWWQZbA8wQfpCq8NbOYKVP46/G024k5AArUmY5mKblmF7jZ6PjrHy4ZJSaXgCjXBe4MlTJ7AzOtwNAN3aEe0OrHuTBY5xtvFoE8j9Qp5P2IsYIatbMrIb4oeNGgoyIn3i34gMxfHzgrBkvVkXlVQlYdafOqKUeqKoE0Q0YV1MO6gC3l7Ie+yDTg/p3/wUV7gXzW/nF9WmVC//9uZk3+Jky70CTh6AYcy5jJw2tEdxaQsXDG08B9b6TEKaMXPVACbkpo1GBzP6IYPU1sMKQyNOZnmQznLl/8NQEfHwfSLgT8T3E9vse/kaSxcoceipxSnIzGfvwYCsSpacVwj2Tu9a9pENxelBlEkOJ8KWvVH04oOXuY3w31eaNlEl2rLHRy/OiiRwoZ2/eIrF/6qGguHL4yHGecKU7sX+rBxDGYVWJxk7fy2GDCk4O71MjOXAAdr4X/i9--4HuINgUYfZJpO7Em--eG5T4nEF1bughzTmprpy4w== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see Rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: discors_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: discors 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: discors_test 61 | 62 | # As with config/secrets.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: discors_production 84 | username: discors 85 | password: <%= ENV['DISCORS_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | 20 | config.cache_store = :memory_store 21 | config.public_file_server.headers = { 22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 23 | } 24 | else 25 | config.action_controller.perform_caching = false 26 | 27 | config.cache_store = :null_store 28 | end 29 | 30 | # Store uploaded files on the local file system (see config/storage.yml for options) 31 | config.active_storage.service = :local 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Highlight code that triggered database queries in logs. 45 | config.active_record.verbose_query_logs = true 46 | 47 | # Debug mode disables concatenation and preprocessing of assets. 48 | # This option may cause significant delays in view rendering with a large 49 | # number of complex assets. 50 | config.assets.debug = true 51 | 52 | # Suppress logger output for asset requests. 53 | config.assets.quiet = true 54 | 55 | # Raises error for missing translations 56 | # config.action_view.raise_on_missing_translations = true 57 | 58 | # Use an evented file watcher to asynchronously detect changes in source code, 59 | # routes, locales, etc. This feature depends on the listen gem. 60 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 61 | 62 | config.active_storage.service = :amazon_dev 63 | end 64 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. 30 | # 31 | # preload_app! 32 | 33 | # Allow puma to be restarted by `rails restart` command. 34 | plugin :tmp_restart 35 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | namespace :api, defaults: {format: :json} do 4 | resources :users, only: [:create, :update] do 5 | collection do 6 | get 'data' 7 | end 8 | end 9 | resource :session, only: [:create, :destroy] 10 | resources :channels, only: [:create, :index, :destroy] 11 | resources :audio_channels, only: [:create, :index, :destroy] 12 | resources :friend_requests, only: [:create, :destroy, :update] 13 | resources :friends, only: [:destroy] 14 | resources :dm_channel_memberships, only: [:destroy, :create] 15 | resources :servers, only: [:create, :index, :destroy] do 16 | collection do 17 | post 'join' 18 | end 19 | member do 20 | get 'members' 21 | end 22 | end 23 | end 24 | 25 | mount ActionCable.server, at: '/cable' 26 | root "static_pages#root" 27 | 28 | end 29 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | amazon_dev: 11 | service: S3 12 | access_key_id: <%= Rails.application.credentials.aws[:access_key_id] %> 13 | secret_access_key: <%= Rails.application.credentials.aws[:secret_access_key] %> 14 | region: <%= Rails.application.credentials.aws[:region] %> 15 | bucket: <%= Rails.application.credentials.aws[:dev][:bucket] %> 16 | 17 | amazon_prod: 18 | service: S3 19 | access_key_id: <%= Rails.application.credentials.aws[:access_key_id] %> 20 | secret_access_key: <%= Rails.application.credentials.aws[:secret_access_key] %> 21 | region: <%= Rails.application.credentials.aws[:region] %> 22 | bucket: <%= Rails.application.credentials.aws[:prod][:bucket] %> 23 | # Remember not to checkin your GCS keyfile to a repository 24 | # google: 25 | # service: GCS 26 | # project: your_project 27 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 28 | # bucket: your_own_bucket 29 | 30 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 31 | # microsoft: 32 | # service: AzureStorage 33 | # storage_account_name: your_account_name 34 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 35 | # container: your_container_name 36 | 37 | # mirror: 38 | # service: Mirror 39 | # primary: local 40 | # mirrors: [ amazon, google, microsoft ] 41 | -------------------------------------------------------------------------------- /db/migrate/20190121174348_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :email, null: false 6 | t.string :image_url, null: false 7 | t.string :password_digest, null: false 8 | t.string :session_token, null: false 9 | 10 | t.timestamps 11 | end 12 | add_index :users, :username, unique: true 13 | add_index :users, :email, unique: true 14 | add_index :users, :session_token, unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20190122185229_create_servers.rb: -------------------------------------------------------------------------------- 1 | class CreateServers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :servers do |t| 4 | t.string :name, null: false 5 | t.integer :admin_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :servers, :name, unique: true 10 | add_index :servers, :admin_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190122190214_create_server_memberships.rb: -------------------------------------------------------------------------------- 1 | class CreateServerMemberships < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :server_memberships do |t| 4 | t.integer :user_id, null: false 5 | t.integer :server_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :server_memberships, :user_id 10 | add_index :server_memberships, :server_id 11 | add_index :server_memberships, [:user_id, :server_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190123145743_create_channels.rb: -------------------------------------------------------------------------------- 1 | class CreateChannels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :channels do |t| 4 | t.string :name, null: false 5 | t.integer :server_id 6 | 7 | t.timestamps 8 | end 9 | add_index :channels, :server_id 10 | add_index :channels, [:name, :server_id], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190124162319_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :messages do |t| 4 | t.string :body, null: false 5 | t.integer :author_id, null: false 6 | t.integer :channel_id, null: false 7 | 8 | t.timestamps 9 | end 10 | add_index :messages, :author_id 11 | add_index :messages, :channel_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190125020020_create_dm_channel_memberships.rb: -------------------------------------------------------------------------------- 1 | class CreateDmChannelMemberships < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :dm_channel_memberships do |t| 4 | t.integer :user_id, null: false 5 | t.integer :channel_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :dm_channel_memberships, :user_id 10 | add_index :dm_channel_memberships, :channel_id 11 | add_index :dm_channel_memberships, [:user_id, :channel_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190126030608_create_friend_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendRequests < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :friend_requests do |t| 4 | t.integer :user_id, null: false 5 | t.integer :friend_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :friend_requests, :user_id 10 | add_index :friend_requests, :friend_id 11 | add_index :friend_requests, [:user_id, :friend_id], unique: true 12 | add_index :channels, :name 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20190126032316_create_friendships.rb: -------------------------------------------------------------------------------- 1 | class CreateFriendships < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :friendships do |t| 4 | t.integer :user_id, null: false 5 | t.integer :friend_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :friendships, :user_id 10 | add_index :friendships, :friend_id 11 | add_index :friendships, [:user_id, :friend_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190127215251_create_sessions.rb: -------------------------------------------------------------------------------- 1 | class CreateSessions < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :sessions do |t| 4 | t.integer "user_id", null: false 5 | t.string "session_token", null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :sessions, :user_id 10 | add_index :sessions, :session_token 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190129004713_create_voice_channels.rb: -------------------------------------------------------------------------------- 1 | class CreateVoiceChannels < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :voice_channels do |t| 4 | t.string :name, null: false 5 | t.integer :server_id 6 | 7 | t.timestamps 8 | end 9 | add_index :voice_channels, :server_id 10 | add_index :voice_channels, [:name, :server_id], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190129010218_change_voice_channels_to_audio_channels.rb: -------------------------------------------------------------------------------- 1 | class ChangeVoiceChannelsToAudioChannels < ActiveRecord::Migration[5.2] 2 | def change 3 | rename_table :voice_channels, :audio_channels 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190129201658_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | create_table :active_storage_blobs do |t| 5 | t.string :key, null: false 6 | t.string :filename, null: false 7 | t.string :content_type 8 | t.text :metadata 9 | t.bigint :byte_size, null: false 10 | t.string :checksum, null: false 11 | t.datetime :created_at, null: false 12 | 13 | t.index [ :key ], unique: true 14 | end 15 | 16 | create_table :active_storage_attachments do |t| 17 | t.string :name, null: false 18 | t.references :record, null: false, polymorphic: true, index: false 19 | t.references :blob, null: false 20 | 21 | t.datetime :created_at, null: false 22 | 23 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true 24 | t.foreign_key :active_storage_blobs, column: :blob_id 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20190203031645_add_column_to_sessions.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToSessions < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :sessions, :user_agent, :string, null: false 4 | add_index :sessions, :user_agent 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190218200301_remove_session_token_from_users.rb: -------------------------------------------------------------------------------- 1 | class RemoveSessionTokenFromUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :users, :session_token 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /frontend/actions/channel_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/channel_api_util'; 2 | import { receiveUsers, removeServer } from './server_actions'; 3 | 4 | export const RECEIVE_CHANNELS = 'RECEIVE_CHANNELS'; 5 | export const RECEIVE_CHANNEL = 'RECEIVE_CHANNEL'; 6 | export const REMOVE_CHANNEL = 'REMOVE_CHANNEL'; 7 | export const RECEIVE_CHANNEL_ERRORS = 'RECEIVE_CHANNEL_ERRORS'; 8 | export const REMOVE_CHANNEL_ERRORS = 'REMOVE_CHANNEL_ERRORS'; 9 | 10 | 11 | export const receiveChannels = channels => ({ 12 | type: RECEIVE_CHANNELS, 13 | channels 14 | }); 15 | 16 | export const receiveChannel = channel => ({ 17 | type: RECEIVE_CHANNEL, 18 | channel 19 | }); 20 | 21 | export const receiveDmChannel = channel => ({ 22 | type: RECEIVE_CHANNEL, 23 | channel 24 | }); 25 | 26 | export const removeChannel = channelId => ({ 27 | type: REMOVE_CHANNEL, 28 | channelId 29 | }); 30 | 31 | export const receiveErrors = errors => ({ 32 | type: RECEIVE_CHANNEL_ERRORS, 33 | errors 34 | }); 35 | 36 | export const removeChannelErrors = () => ({ 37 | type: REMOVE_CHANNEL_ERRORS, 38 | }); 39 | 40 | 41 | export const fetchChannels = (serverId, userId) => dispatch => ( 42 | APIUtil.fetchChannels(serverId).then(channels => ( 43 | dispatch(receiveChannels(channels)) 44 | ), () => ( 45 | dispatch(removeServer(serverId, userId)) 46 | )) 47 | ); 48 | 49 | export const deleteChannel = (id) => dispatch => ( 50 | APIUtil.deleteChannel(id).then(() => ( 51 | dispatch(removeChannel(id)) 52 | )) 53 | ); 54 | 55 | export const deleteDmChannel = (id) => dispatch => ( 56 | APIUtil.deleteDmChannel(id).then((membership) => ( 57 | dispatch(removeChannel(membership.channel_id)) 58 | )) 59 | ); 60 | 61 | export const fetchDmChannels = () => dispatch => ( 62 | APIUtil.fetchDmChannels().then(({channels, users}) => { 63 | dispatch(receiveUsers(users)); 64 | return dispatch(receiveChannels(channels)); 65 | }) 66 | ); 67 | 68 | export const createChannel = (channel) => dispatch => ( 69 | APIUtil.createChannel(channel).then(channel => ( 70 | dispatch(receiveChannel(channel)) 71 | ), err => ( 72 | dispatch(receiveErrors(err.responseJSON)) 73 | )) 74 | ); 75 | 76 | export const createDmChannel = (userId) => dispatch => ( 77 | APIUtil.createDmChannel(userId).then(channel => ( 78 | dispatch(receiveChannel(channel)) 79 | )) 80 | ); 81 | 82 | -------------------------------------------------------------------------------- /frontend/actions/friends_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/friends_api_util'; 2 | import { receiveUsers } from './server_actions'; 3 | 4 | export const RECEIVE_REQUESTS = 'RECEIVE_REQUESTS'; 5 | export const RECEIVE_REQUEST = 'RECEIVE_REQUEST'; 6 | export const REMOVE_REQUEST = 'REMOVE_REQUEST'; 7 | export const REMOVE_FRIEND = 'REMOVE_FRIEND'; 8 | export const RECEIVE_FRIENDS = 'RECEIVE_FRIENDS'; 9 | export const RECEIVE_FRIEND = 'RECEIVE_FRIEND'; 10 | 11 | 12 | export const receiveFriendRequests = friendRequests => ({ 13 | type: RECEIVE_REQUESTS, 14 | friendRequests 15 | }); 16 | 17 | export const receiveFriends = friendData => ({ 18 | type: RECEIVE_FRIENDS, 19 | friendData 20 | }); 21 | 22 | export const receiveFriend = friendId => ({ 23 | type: RECEIVE_FRIEND, 24 | friend: [friendId] 25 | }); 26 | 27 | export const receiveFriendRequest = friendRequest => ({ 28 | type: RECEIVE_REQUEST, 29 | friendRequest 30 | }); 31 | 32 | export const removeFriendRequest = requestId => ({ 33 | type: REMOVE_REQUEST, 34 | requestId 35 | }); 36 | 37 | export const removeFriend = friendId => ({ 38 | type: REMOVE_FRIEND, 39 | friendId 40 | }); 41 | 42 | 43 | export const fetchFriendRequests = () => dispatch => ( 44 | APIUtil.fetchFriendRequests().then(({ requests, users }) => { 45 | dispatch(receiveUsers(users)); 46 | return dispatch(receiveFriendRequests(requests)); 47 | }) 48 | ); 49 | 50 | export const fetchFriends = () => dispatch => ( 51 | APIUtil.fetchFriends().then(friends => ( 52 | dispatch(receiveFriends(friends)) 53 | )) 54 | ); 55 | 56 | export const createFriendRequest = (friendRequest) => dispatch => ( 57 | APIUtil.createFriendRequest(friendRequest).then(request => ( 58 | dispatch(receiveFriendRequest(request)) 59 | )) 60 | ); 61 | 62 | export const acceptFriendRequest = (friendRequest) => dispatch => ( 63 | APIUtil.acceptFriendRequest(friendRequest).then(() => { 64 | dispatch(removeFriendRequest(friendRequest.id)); 65 | return dispatch(receiveFriend(friendRequest.user_id)); 66 | }) 67 | ); 68 | 69 | export const deleteFriendRequest = (friendRequest) => dispatch => ( 70 | APIUtil.deleteFriendRequest(friendRequest).then(() => ( 71 | dispatch(removeFriendRequest(friendRequest.id)) 72 | )) 73 | ); 74 | 75 | export const deleteFriend = (id) => dispatch => ( 76 | APIUtil.deleteFriend(id).then(() => ( 77 | dispatch(removeFriend(id)) 78 | )) 79 | ); -------------------------------------------------------------------------------- /frontend/actions/notification_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_DM_NOTIFICATION = 'RECEIVE_DM_NOTIFICATION'; 2 | export const REMOVE_DM_NOTIFICATION = 'REMOVE_DM_NOTIFICATION'; 3 | 4 | 5 | export const receiveDmNotification = (notification) => ({ 6 | type: RECEIVE_DM_NOTIFICATION, 7 | notification 8 | }); 9 | 10 | export const removeDmNotification = (channelId) => ({ 11 | type: REMOVE_DM_NOTIFICATION, 12 | channelId, 13 | }); -------------------------------------------------------------------------------- /frontend/actions/server_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/server_api_utl'; 2 | 3 | export const RECEIVE_SERVERS = 'RECEIVE_SERVERS'; 4 | export const RECEIVE_USERS = 'RECEIVE_USERS'; 5 | export const RECEIVE_SERVER = 'RECEIVE_SERVER'; 6 | export const RECEIVE_SERVER_ERRORS = 'RECEIVE_SERVER_ERRORS'; 7 | export const REMOVE_SERVER_ERRORS = 'REMOVE_SERVER_ERRORS'; 8 | export const REMOVE_SERVER = 'REMOVE_SERVER'; 9 | 10 | 11 | export const receiveServers = servers => ({ 12 | type: RECEIVE_SERVERS, 13 | servers 14 | }); 15 | 16 | export const receiveUsers = users => ({ 17 | type: RECEIVE_USERS, 18 | users 19 | }); 20 | 21 | export const receiveServer = (server, userId) => ({ 22 | type: RECEIVE_SERVER, 23 | server, 24 | userId 25 | }); 26 | 27 | export const removeServer = (serverId, userId) => ({ 28 | type: REMOVE_SERVER, 29 | serverId, 30 | userId 31 | }); 32 | 33 | export const receiveErrors = errors => ({ 34 | type: RECEIVE_SERVER_ERRORS, 35 | errors 36 | }); 37 | 38 | export const removeServerErrors = () => ({ 39 | type: REMOVE_SERVER_ERRORS, 40 | }); 41 | 42 | export const fetchServers = () => dispatch => ( 43 | APIUtil.fetchServers().then(servers => ( 44 | dispatch(receiveServers(servers)) 45 | )) 46 | ); 47 | 48 | export const fetchMembers = (id) => dispatch => ( 49 | APIUtil.fetchMembers(id).then(({users, requests}) => { 50 | return dispatch(receiveUsers(users)); 51 | }) 52 | ); 53 | 54 | export const createServer = (formData) => dispatch => ( 55 | APIUtil.createServer(formData).then(server => ( 56 | dispatch(receiveServer(server, server.admin_id)) 57 | ), err => ( 58 | dispatch(receiveErrors(err.responseJSON)) 59 | )) 60 | ); 61 | 62 | export const deleteServer = (serverId, userId) => dispatch => ( 63 | APIUtil.deleteServer(serverId).then(() => ( 64 | dispatch(removeServer(serverId, userId)) 65 | ) 66 | ) 67 | ); 68 | 69 | export const joinServer = (server, userId) => dispatch => ( 70 | APIUtil.joinServer(server).then(server => ( 71 | dispatch(receiveServer(server, userId)) 72 | ), err => ( 73 | dispatch(receiveErrors(err.responseJSON)) 74 | )) 75 | ); 76 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 4 | export const RECEIVE_USER = 'RECEIVE_USER'; 5 | export const RECEIVE_CURRENT_USER_DATA = 'RECEIVE_CURRENT_USER_DATA'; 6 | export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER'; 7 | export const RECEIVE_SESSION_ERRORS = 'RECEIVE_SESSION_ERRORS'; 8 | export const REMOVE_SESSION_ERRORS = 'REMOVE_SESSION_ERRORS'; 9 | 10 | 11 | export const receiveCurrentUser = currentUser => ({ 12 | type: RECEIVE_CURRENT_USER, 13 | currentUser 14 | }); 15 | 16 | export const receiveUser = user => ({ 17 | type: RECEIVE_USER, 18 | user 19 | }); 20 | 21 | export const receiveCurrentUserData = currentUserData => ({ 22 | type: RECEIVE_CURRENT_USER_DATA, 23 | currentUserData 24 | }); 25 | 26 | export const logoutCurrentUser = () => ({ 27 | type: LOGOUT_CURRENT_USER, 28 | }); 29 | 30 | export const receiveErrors = errors => ({ 31 | type: RECEIVE_SESSION_ERRORS, 32 | errors 33 | }); 34 | 35 | export const removeErrors = errors => ({ 36 | type: REMOVE_SESSION_ERRORS, 37 | }); 38 | 39 | 40 | export const signup = user => dispatch => ( 41 | APIUtil.signup(user).then(user => ( 42 | dispatch(receiveCurrentUser(user)) 43 | ), err => ( 44 | dispatch(receiveErrors(err.responseJSON)) 45 | )) 46 | ); 47 | 48 | export const editUser = formData => dispatch => ( 49 | APIUtil.editUser(formData).then(user => ( 50 | dispatch(receiveCurrentUser(user)) 51 | ), err => ( 52 | dispatch(receiveErrors(err.responseJSON)) 53 | )) 54 | ); 55 | 56 | export const login = user => dispatch => ( 57 | APIUtil.login(user).then(user => ( 58 | dispatch(receiveCurrentUser(user)) 59 | ), err => ( 60 | dispatch(receiveErrors(err.responseJSON)) 61 | )) 62 | ); 63 | 64 | export const fetchCurrentUserData = () => dispatch => ( 65 | APIUtil.fetchCurrentUserData().then(userData => ( 66 | dispatch(receiveCurrentUserData(userData)) 67 | )) 68 | ); 69 | 70 | export const logout = () => dispatch => ( 71 | APIUtil.logout().then(user => ( 72 | dispatch(logoutCurrentUser()) 73 | )) 74 | ); -------------------------------------------------------------------------------- /frontend/actions/ui_actions.js: -------------------------------------------------------------------------------- 1 | export const BEGIN_LOADING = 'BEGIN_LOADING'; 2 | export const FINISH_LOADING = 'FINISH_LOADING'; 3 | 4 | 5 | export const beginLoading = () => ({ 6 | type: BEGIN_LOADING, 7 | }); 8 | 9 | export const finishLoading = () => ({ 10 | type: FINISH_LOADING, 11 | }); -------------------------------------------------------------------------------- /frontend/actions/voice_channel_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/voice_channel_api_util'; 2 | import { receiveErrors } from './channel_actions'; 3 | 4 | export const RECEIVE_VOICE_CHANNELS = 'RECEIVE_VOICE_CHANNELS'; 5 | export const RECEIVE_VOICE_CHANNEL = 'RECEIVE_VOICE_CHANNEL'; 6 | export const REMOVE_VOICE_CHANNEL = 'REMOVE_VOICE_CHANNEL'; 7 | export const RECEIVE_VOICE_CHANNEL_ERRORS = 'RECEIVE_VOICE_CHANNEL_ERRORS'; 8 | export const REMOVE_VOICE_CHANNEL_ERRORS = 'REMOVE_VOICE_CHANNEL_ERRORS'; 9 | 10 | 11 | export const receiveVoiceChannels = voiceChannels => ({ 12 | type: RECEIVE_VOICE_CHANNELS, 13 | voiceChannels 14 | }); 15 | 16 | export const receiveVoiceChannel = voiceChannel => ({ 17 | type: RECEIVE_VOICE_CHANNEL, 18 | voiceChannel 19 | }); 20 | 21 | export const removeVoiceChannel = voiceChannelId => ({ 22 | type: REMOVE_VOICE_CHANNEL, 23 | voiceChannelId 24 | }); 25 | 26 | 27 | export const fetchVoiceChannels = (serverId) => dispatch => ( 28 | APIUtil.fetchVoiceChannels(serverId).then(voiceChannels => ( 29 | dispatch(receiveVoiceChannels(voiceChannels)) 30 | )) 31 | ); 32 | 33 | export const deleteVoiceChannel = (id) => dispatch => ( 34 | APIUtil.deleteVoiceChannel(id).then(() => ( 35 | dispatch(removeVoiceChannel(id)) 36 | )) 37 | ); 38 | 39 | export const createVoiceChannel = (voiceChannel) => dispatch => ( 40 | APIUtil.createVoiceChannel(voiceChannel).then(voiceChannel => ( 41 | dispatch(receiveVoiceChannel(voiceChannel)) 42 | ), err => ( 43 | dispatch(receiveErrors(err.responseJSON)) 44 | )) 45 | ); -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import LoginForm from './session_form/login/login_form_container'; 4 | import SignupForm from './session_form/signup/signup_form_container'; 5 | import Splash from './splash/splash_container'; 6 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 7 | import AppRoot from './app/app_root'; 8 | 9 | const App = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default App; -------------------------------------------------------------------------------- /frontend/components/app/app_routes/friends_main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FriendsHeader from '../friends/friends_header'; 3 | import Friends from '../friends/friends_list/friends_container'; 4 | import PendingFriends from '../friends/pending_friends/pending_friends_container'; 5 | import OnlineFriends from '../friends/friends_list/online_friends_container'; 6 | 7 | class FriendsMain extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { tab: 'all' }; 11 | this.changeTab = this.changeTab.bind(this); 12 | } 13 | 14 | changeTab(tabName) { 15 | this.setState({ tab: tabName }); 16 | } 17 | 18 | render() { 19 | let component; 20 | 21 | if (this.state.tab === 'all') { 22 | component = 23 | } else if (this.state.tab === 'online') { 24 | component = 25 | } else { 26 | component = 27 | } 28 | 29 | return ( 30 | <> 31 |
32 | 33 |
34 |
35 |
36 |
Name
37 |
38 |
Status
39 |
40 |
Mutual Servers
41 |
42 |
43 |
44 | {component} 45 |
46 |
47 |
48 | 49 | ) 50 | } 51 | } 52 | 53 | export default FriendsMain; 54 | -------------------------------------------------------------------------------- /frontend/components/app/app_routes/me_main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Chat from '../chat/chat/chat_container'; 3 | import MeHeader from '../header/me_header/me_header_container'; 4 | 5 | class MeMain extends React.Component { 6 | render() { 7 | return ( 8 | <> 9 |
10 | 11 |
12 | 13 |
14 |
15 | 16 | ) 17 | } 18 | } 19 | 20 | export default MeMain; -------------------------------------------------------------------------------- /frontend/components/app/app_routes/me_route.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DMChannels from '../channels/dm_channels/dm_channels_container'; 3 | import MeMain from './me_main'; 4 | import { Route, Switch } from 'react-router-dom'; 5 | import FriendsMain from './friends_main'; 6 | 7 | class MeRoute extends React.Component { 8 | render() { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | 21 | export default MeRoute; 22 | -------------------------------------------------------------------------------- /frontend/components/app/app_routes/server_main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../header/header/header_container'; 3 | import Chat from '../chat/chat/chat_container'; 4 | import ServerMembers from '../server_members/server_members/server_members_container'; 5 | 6 | class ServerMain extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { showMembers: true, classText: 'members-transition-left' }; 10 | this.toggleShowMembers = this.toggleShowMembers.bind(this); 11 | } 12 | 13 | toggleShowMembers() { 14 | const { showMembers } = this.state; 15 | 16 | if (showMembers) { 17 | this.setState({ classText: "members-transition-right" }); 18 | setTimeout(() => this.setState({ showMembers: !showMembers }), 200); 19 | } else { 20 | this.setState({ showMembers: !showMembers }); 21 | setTimeout(() => this.setState({ classText: 'members-transition-left' }), 1); 22 | } 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |
29 |
30 | 31 | {this.state.showMembers ? : null} 32 |
33 |
34 | ) 35 | } 36 | } 37 | 38 | export default ServerMain; -------------------------------------------------------------------------------- /frontend/components/app/app_routes/server_route.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Channels from '../channels/channels/channels_container'; 3 | import ServerMain from './server_main'; 4 | 5 | class ServerRoute extends React.Component { 6 | render() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ) 13 | } 14 | } 15 | 16 | export default ServerRoute; 17 | -------------------------------------------------------------------------------- /frontend/components/app/channels/channels/channel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, withRouter } from 'react-router-dom'; 3 | import Tooltip from '../../modal/tooltip'; 4 | 5 | class Channel extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { active: false }; 9 | 10 | this.handleMouseEnter = this.handleMouseEnter.bind(this); 11 | this.handleMouseLeave = this.handleMouseLeave.bind(this); 12 | this.handleDelete = this.handleDelete.bind(this); 13 | } 14 | 15 | handleMouseEnter() { 16 | if (this.props.channel.name !== 'general' && this.props.currentUser.id === this.props.server.admin_id) { 17 | this.setState({ active: true }); 18 | } 19 | } 20 | 21 | handleMouseLeave() { 22 | if (this.props.channel.name !== 'general' && this.props.currentUser.id === this.props.server.admin_id) { 23 | this.setState({ active: false }); 24 | } 25 | } 26 | 27 | handleDelete(e) { 28 | e.preventDefault(); 29 | const that = this; 30 | this.props.deleteChannel().then((action) => { 31 | if (action.channelId == that.props.match.params.channelId) { 32 | that.props.history.push(`/channels/${that.props.server.id}/${that.props.server.root_channel}`); 33 | } 34 | }); 35 | } 36 | 37 | render() { 38 | return ( 39 | 45 | 46 | 48 | 49 |
{this.props.channel.name}
50 | {this.state.active ? 51 | ( 53 | } 54 | position="top right" 55 | text="Delete Channel" 56 | />) : null} 57 |
58 | ) 59 | }; 60 | }; 61 | 62 | export default withRouter(Channel); -------------------------------------------------------------------------------- /frontend/components/app/channels/channels/channels_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Channels from './channels'; 3 | import { fetchChannels, createChannel, removeChannelErrors, deleteChannel } from '../../../../actions/channel_actions'; 4 | import { withRouter } from 'react-router-dom'; 5 | import { deleteServer } from '../../../../actions/server_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const serverId = ownProps.match.params.serverId; 9 | const channels = Object.values(state.entities.channels).filter(channel => channel.server_id == serverId); 10 | 11 | return { 12 | currentUser: state.entities.users[state.session.id], 13 | server: state.entities.servers[serverId] || {}, 14 | channels, 15 | errors: state.errors.channel, 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = dispatch => { 20 | return { 21 | fetchChannels: (serverId, userId) => dispatch(fetchChannels(serverId, userId)), 22 | createChannel: channel => dispatch(createChannel(channel)), 23 | removeChannelErrors: () => dispatch(removeChannelErrors()), 24 | deleteServer: (serverId, userId) => dispatch(deleteServer(serverId, userId)), 25 | deleteChannel: (id) => dispatch(deleteChannel(id)), 26 | }; 27 | }; 28 | 29 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Channels)); 30 | -------------------------------------------------------------------------------- /frontend/components/app/channels/create_channel_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import merge from 'lodash/merge'; 3 | 4 | class CreateChannelForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { name: '' }; 8 | this.handleSubmit = this.handleSubmit.bind(this); 9 | this.update = this.update.bind(this); 10 | } 11 | 12 | componentDidMount() { 13 | this.nameInput.focus(); 14 | } 15 | 16 | update(e) { 17 | this.setState({ 18 | name: e.currentTarget.value 19 | }); 20 | } 21 | 22 | handleSubmit(e) { 23 | e.preventDefault(); 24 | this.props.createChannel(merge(this.state, {server_id: this.props.server.id})).then(this.props.closeModal); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |
31 |

{`CREATE ${this.props.text} CHANNEL`}

32 |

in {this.props.server.name}

33 |
34 |
35 |
36 |
CHANNEL NAME
37 | {this.props.errors[0] ? `- ${this.props.errors[0]}` : ''} 38 |
39 | { this.nameInput = input; }} 44 | /> 45 |
46 |
47 | 48 | 49 |
50 |
51 | ) 52 | } 53 | } 54 | 55 | export default CreateChannelForm; 56 | -------------------------------------------------------------------------------- /frontend/components/app/channels/dm_channels/dm_channel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, withRouter } from 'react-router-dom'; 3 | 4 | class DmChannel extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { active: false }; 8 | 9 | this.handleMouseEnter = this.handleMouseEnter.bind(this); 10 | this.handleMouseLeave = this.handleMouseLeave.bind(this); 11 | this.handleDelete = this.handleDelete.bind(this); 12 | } 13 | 14 | handleMouseEnter() { 15 | this.setState({ active: true }); 16 | } 17 | 18 | handleMouseLeave() { 19 | this.setState({ active: false }); 20 | } 21 | 22 | handleDelete(e) { 23 | e.preventDefault(); 24 | const channelId = this.props.channel.id; 25 | const notification = document.getElementById(`dm-notification-${channelId}`); 26 | 27 | if (notification) { 28 | notification.className = 'dm-notification'; 29 | setTimeout(() => this.props.removeDmNotification(channelId), 200); 30 | } 31 | 32 | const that = this; 33 | this.props.deleteDmChannel().then((action) => { 34 | if (action.channelId == that.props.match.params.channelId) { 35 | that.props.history.push(`/channels/@me`); 36 | } 37 | }); 38 | } 39 | 40 | render() { 41 | return ( 42 | 49 |
50 |
54 |
55 |
{this.props.user.username}
56 | {this.state.active ? : null} 57 |
58 | ) 59 | }; 60 | }; 61 | 62 | export default withRouter(DmChannel); -------------------------------------------------------------------------------- /frontend/components/app/channels/dm_channels/dm_channels.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DmChannel from './dm_channel'; 3 | import UserBar from '../user_bar/user_bar_container'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | class DMChannels extends React.Component { 7 | render() { 8 | const that = this; 9 | const channels = this.props.channels.map((channel, idx) => { 10 | const nameArr = channel.name.split('-'); 11 | let userId; 12 | 13 | if (nameArr[0] == this.props.currentUser.id) { 14 | userId = nameArr[1]; 15 | } else { 16 | userId = nameArr[0]; 17 | } 18 | 19 | return this.props.deleteDmChannel(channel.id)} 24 | removeDmNotification={this.props.removeDmNotification} 25 | />; 26 | }); 27 | 28 | return ( 29 |
30 |
31 | 36 | 37 | Friends 38 | {this.props.requestCount ?
{this.props.requestCount}
: null} 39 |
40 |
41 |
42 |
43 |
44 |
45 |

DIRECT MESSAGES

46 |
47 |
48 | {channels} 49 |
50 |
51 |
52 | 53 |
54 |
55 | ) 56 | } 57 | } 58 | 59 | export default DMChannels; -------------------------------------------------------------------------------- /frontend/components/app/channels/dm_channels/dm_channels_container.jsx: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'react-router-dom'; 2 | import { logout } from '../../../../actions/session_actions'; 3 | import DMChannels from './dm_channels'; 4 | import { connect } from 'react-redux'; 5 | import { fetchDmChannels, deleteDmChannel } from '../../../../actions/channel_actions'; 6 | import { removeDmNotification } from '../../../../actions/notification_actions'; 7 | 8 | const mapStateToProps = (state) => { 9 | const incomingRequests = Object.values(state.entities.friendRequests).filter((request) => { 10 | return request.friend_id === state.session.id; 11 | }).length; 12 | 13 | return { 14 | currentUser: state.entities.users[state.session.id], 15 | channels: Object.values(state.entities.channels).filter(channel => !channel.server_id), 16 | users: state.entities.users, 17 | requestCount: incomingRequests, 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = dispatch => { 22 | return { 23 | fetchDmChannels: () => dispatch(fetchDmChannels()), 24 | logout: () => dispatch(logout()), 25 | deleteDmChannel: id => dispatch(deleteDmChannel(id)), 26 | removeDmNotification: channelId => dispatch(removeDmNotification(channelId)), 27 | }; 28 | }; 29 | 30 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(DMChannels)); 31 | -------------------------------------------------------------------------------- /frontend/components/app/channels/user_bar/user_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | import Tooltip from '../../modal/tooltip'; 4 | import EditUserForm from '../edit_user_form'; 5 | 6 | class UserBar extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { showUserModal: false }; 10 | this.handleOpenUserModal = this.handleOpenUserModal.bind(this); 11 | this.handleCloseUserModal = this.handleCloseUserModal.bind(this); 12 | } 13 | 14 | handleOpenUserModal() { 15 | this.setState({ showUserModal: true }); 16 | } 17 | 18 | handleCloseUserModal() { 19 | this.setState({ showUserModal: false }); 20 | this.props.removeErrors(); 21 | } 22 | 23 | render() { 24 | return ( 25 | <> 26 |
27 |
28 | 30 |
34 |
35 | } 36 | position="top center" 37 | text="Update Profile" 38 | /> 39 |
{this.props.currentUser.username}
40 |
41 | this.props.logout()}> 43 | 44 | 45 | } 46 | position="top center" 47 | text="Logout" 48 | /> 49 | 50 | 75 | 81 | 82 | 83 | ) 84 | } 85 | } 86 | 87 | export default UserBar; 88 | -------------------------------------------------------------------------------- /frontend/components/app/channels/user_bar/user_bar_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { logout, editUser, removeErrors } from '../../../../actions/session_actions'; 4 | import UserBar from './user_bar'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | currentUser: state.entities.users[state.session.id], 9 | errors: state.errors.session, 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = dispatch => { 14 | return { 15 | logout: () => dispatch(logout()), 16 | editUser: formData => dispatch(editUser(formData)), 17 | removeErrors: () => dispatch(removeErrors()), 18 | }; 19 | }; 20 | 21 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UserBar)); -------------------------------------------------------------------------------- /frontend/components/app/channels/voice_channels/voice_channels_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import VoiceChannels from './voice_channels'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { fetchVoiceChannels, createVoiceChannel, deleteVoiceChannel } from '../../../../actions/voice_channel_actions'; 5 | import { removeChannelErrors } from '../../../../actions/channel_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const serverId = ownProps.match.params.serverId; 9 | const voiceChannels = Object.values(state.entities.voiceChannels).filter(channel => channel.server_id == serverId); 10 | 11 | return { 12 | currentUser: state.entities.users[state.session.id], 13 | server: state.entities.servers[serverId] || {}, 14 | voiceChannels, 15 | errors: state.errors.channel, 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = dispatch => { 20 | return { 21 | fetchVoiceChannels: serverId => dispatch(fetchVoiceChannels(serverId)), 22 | createVoiceChannel: voiceChannel => dispatch(createVoiceChannel(voiceChannel)), 23 | removeChannelErrors: () => dispatch(removeChannelErrors()), 24 | deleteVoiceChannel: (id) => dispatch(deleteVoiceChannel(id)), 25 | }; 26 | }; 27 | 28 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(VoiceChannels)); -------------------------------------------------------------------------------- /frontend/components/app/chat/chat/chat_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import Chat from './chat'; 4 | import { removeDmNotification } from '../../../../actions/notification_actions'; 5 | import { removeChannel } from '../../../../actions/channel_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const channelId = ownProps.match.params.channelId; 9 | const channel = state.entities.channels[channelId] || {}; 10 | const server = state.entities.servers[channel.server_id] || {}; 11 | 12 | return { 13 | currentUser: state.entities.users[state.session.id], 14 | channelId, 15 | users: state.entities.users, 16 | channel, 17 | server, 18 | loading: state.ui.loading, 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = dispatch => { 23 | return { 24 | removeDmNotification: channelId => dispatch(removeDmNotification(channelId)), 25 | removeChannel: channelId => dispatch(removeChannel(channelId)), 26 | }; 27 | }; 28 | 29 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Chat)); 30 | -------------------------------------------------------------------------------- /frontend/components/app/chat/message_form.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { merge } from 'lodash'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | class MessageForm extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | body: "", 10 | author_id: props.user.id, 11 | }; 12 | 13 | this.messageInput = React.createRef(); 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | this.update = this.update.bind(this); 16 | } 17 | 18 | update(e) { 19 | this.setState({ body: e.currentTarget.value }); 20 | } 21 | 22 | componentDidMount() { 23 | this.messageInput.current.focus(); 24 | } 25 | 26 | componentDidUpdate(prevProps) { 27 | const channelId = this.props.match.params.channelId; 28 | 29 | if (prevProps.channel && prevProps.channel.id !== channelId) { 30 | this.messageInput.current.focus(); 31 | } 32 | } 33 | 34 | handleSubmit(e) { 35 | e.preventDefault(); 36 | this.props.subscription.speak({ message: merge({ channel_id: this.props.channelId }, this.state) }); 37 | this.setState({ body: "" }); 38 | } 39 | 40 | render() { 41 | let placeholder = ''; 42 | 43 | if (this.props.channel.name && this.props.match.params.serverId) { 44 | placeholder = `Message #${this.props.channel.name}`; 45 | } else if (this.props.channel.name) { 46 | const nameArr = this.props.channel.name.split('-'); 47 | let userId; 48 | 49 | if (nameArr[0] == this.props.user.id) { 50 | userId = nameArr[1]; 51 | } else { 52 | userId = nameArr[0]; 53 | } 54 | 55 | placeholder = `Message @${this.props.users[userId] ? this.props.users[userId].username : null}`; 56 | } 57 | 58 | return ( 59 |
60 |
61 | 69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | export default withRouter(MessageForm); -------------------------------------------------------------------------------- /frontend/components/app/friends/friend.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { intersection } from 'lodash'; 4 | import Tooltip from '../modal/tooltip'; 5 | import MutualServer from './mutual_server'; 6 | 7 | class Friend extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleClickParent = this.handleClickParent.bind(this); 11 | this.handleReject = this.handleReject.bind(this); 12 | this.handleAccept = this.handleAccept.bind(this); 13 | } 14 | 15 | handleClickParent() { 16 | this.props.createDmChannel(this.props.user.id).then((action) => this.props.history.push(`/channels/@me/${action.channel.id}`)); 17 | } 18 | 19 | handleReject(e) { 20 | e.stopPropagation(); 21 | this.props.reject(); 22 | } 23 | 24 | handleAccept(e) { 25 | e.stopPropagation(); 26 | this.props.accept(); 27 | } 28 | 29 | render() { 30 | const mutualServerIds = intersection(this.props.currentUser.servers, this.props.user.servers); 31 | 32 | let mutualServers; 33 | if (mutualServerIds.length <= 6) { 34 | mutualServers = mutualServerIds.map((serverId, idx) => { 35 | const server = this.props.servers[serverId] || {}; 36 | return ; 37 | }); 38 | } else { 39 | mutualServers = []; 40 | 41 | for (let i = 0; i < 5; i++) { 42 | const serverId = mutualServerIds[i]; 43 | const server = this.props.servers[serverId] || {}; 44 | mutualServers.push(); 45 | } 46 | 47 | mutualServers.push(
{`+${mutualServerIds.length - 5}`}
); 48 | } 49 | 50 | return ( 51 |
  • 55 |
    56 |
    57 |

    {this.props.user.username}

    58 |
    59 |
    60 |
    64 |

    {this.props.status}

    65 |
    66 |
    67 | {mutualServers} 68 |
    69 |
    70 | {this.props.status === 'Incoming friend request' ? 71 | 73 | } 74 | position="top center" 75 | text='Accept' 76 | /> 77 | : null} 78 | 83 | } 84 | position="top center" 85 | text={this.props.status.includes('request') ? (this.props.status.includes('Outgoing') ? "Cancel" : "Ignore") : "Remove Friend"} 86 | /> 87 |
    88 |
  • 89 | ) 90 | } 91 | } 92 | 93 | export default withRouter(Friend); -------------------------------------------------------------------------------- /frontend/components/app/friends/friends_header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class FriendsHeader extends React.Component { 5 | render() { 6 | return ( 7 |
    8 |
      9 |
    • this.props.changeTab('all')} 11 | >All
    • 12 |
    • this.props.changeTab('online')} 14 | >Online
    • 15 |
    • this.props.changeTab('pending')} 17 | >Pending 18 | {this.props.requestCount ?
      {this.props.requestCount}
      : null} 19 |
    • 20 |
    21 |
    22 | ) 23 | } 24 | } 25 | 26 | const mapStateToProps = (state, ownProps) => { 27 | const incomingRequests = Object.values(state.entities.friendRequests).filter((request) => { 28 | return request.friend_id === state.session.id; 29 | }).length; 30 | 31 | return { 32 | requestCount: incomingRequests, 33 | }; 34 | }; 35 | 36 | export default connect(mapStateToProps)(FriendsHeader); -------------------------------------------------------------------------------- /frontend/components/app/friends/friends_list/friends.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Friend from '../friend'; 3 | 4 | class Friends extends React.Component { 5 | render() { 6 | const friends = this.props.friends.map((friend, idx) => { 7 | return this.props.deleteFriend(friend.id)} 11 | createDmChannel={this.props.createDmChannel} 12 | status={friend.online ? 'Online' : 'Offline'} 13 | first={idx === 0} 14 | currentUser={this.props.currentUser} 15 | servers={this.props.servers} 16 | />; 17 | }); 18 | 19 | return ( 20 |
    21 |
      22 | {friends} 23 | {friends.length === 0 ? (this.props.allFriends ? ( 24 |
      25 |
      26 |

      Wumpus has no friends. You could though!

      27 |
      28 | ) : ( 29 |
      30 |
      31 |

      No one's around to play with Wumpus.

      32 |
      33 | )) : null} 34 |
    35 |
    36 | ) 37 | } 38 | } 39 | 40 | export default Friends; -------------------------------------------------------------------------------- /frontend/components/app/friends/friends_list/friends_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Friends from './friends'; 3 | import { deleteFriend } from '../../../../actions/friends_actions'; 4 | import { createDmChannel } from '../../../../actions/channel_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | const friends = state.entities.friends.map((id) => { 8 | return state.entities.users[id] || {}; 9 | }).sort((a, b) => { 10 | if (a.username < b.username) { return -1; } 11 | if (a.username > b.username) { return 1; } 12 | return 0; 13 | }); 14 | 15 | return { 16 | friends, 17 | currentUser: state.entities.users[state.session.id], 18 | servers: state.entities.servers, 19 | allFriends: true 20 | }; 21 | }; 22 | 23 | const mapDispatchToProps = dispatch => { 24 | return { 25 | deleteFriend: (id) => dispatch(deleteFriend(id)), 26 | createDmChannel: userId => dispatch(createDmChannel(userId)), 27 | }; 28 | }; 29 | 30 | export default connect(mapStateToProps, mapDispatchToProps)(Friends); -------------------------------------------------------------------------------- /frontend/components/app/friends/friends_list/online_friends_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Friends from './friends'; 3 | import { fetchFriends, deleteFriend } from '../../../../actions/friends_actions'; 4 | import { createDmChannel } from '../../../../actions/channel_actions'; 5 | 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const friends = state.entities.friends.map((id) => { 9 | return state.entities.users[id] || {}; 10 | }).filter((friend) => { 11 | return friend.online; 12 | }); 13 | 14 | return { 15 | friends, 16 | currentUser: state.entities.users[state.session.id], 17 | servers: state.entities.servers, 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = dispatch => { 22 | return { 23 | fetchFriends: () => dispatch(fetchFriends()), 24 | deleteFriend: (id) => dispatch(deleteFriend(id)), 25 | createDmChannel: userId => dispatch(createDmChannel(userId)), 26 | }; 27 | }; 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Friends); -------------------------------------------------------------------------------- /frontend/components/app/friends/mutual_server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from '../modal/tooltip'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | class MutualServer extends React.Component { 6 | render() { 7 | return ( 8 | e.stopPropagation()} 11 | className="mutual-server" 12 | style={this.props.server.icon_url ? 13 | { backgroundImage: `url(${this.props.server.icon_url})`, backgroundSize: '100%' } : 14 | { backgroundSize: '100%' }} 15 | onKeyDown={(e) => e.preventDefault()} 16 | > 17 | } 18 | position="top center" 19 | text={this.props.server.name} 20 | /> 21 | ) 22 | } 23 | } 24 | 25 | export default MutualServer; -------------------------------------------------------------------------------- /frontend/components/app/friends/pending_friends/pending_friends.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Friend from '../friend'; 3 | 4 | class PendingFriends extends React.Component { 5 | render() { 6 | const incomingFriends = this.props.incomingRequests.map((request, idx) => { 7 | const user = this.props.users[request.user_id]; 8 | return this.props.acceptFriendRequest(request)} 13 | reject={() => this.props.deleteFriendRequest(request)} 14 | createDmChannel={this.props.createDmChannel} 15 | first={idx === 0} 16 | currentUser={this.props.currentUser} 17 | servers={this.props.servers} 18 | />; 19 | }); 20 | 21 | const outgoingFriends = this.props.outgoingRequests.map((request, idx) => { 22 | const user = this.props.users[request.friend_id]; 23 | return this.props.acceptFriendRequest(request)} 28 | reject={() => this.props.deleteFriendRequest(request)} 29 | createDmChannel={this.props.createDmChannel} 30 | first={idx === 0 && incomingFriends.empty} 31 | currentUser={this.props.currentUser} 32 | servers={this.props.servers} 33 | />; 34 | }); 35 | 36 | return ( 37 |
    38 |
      39 | {incomingFriends} 40 | {outgoingFriends} 41 | {incomingFriends.length === 0 && outgoingFriends.length === 0 ? 42 |
      43 |
      44 |

      There are no pending friend requests. Here's a Wumpus for now.

      45 |
      46 | : null} 47 |
    48 |
    49 | ) 50 | } 51 | } 52 | 53 | export default PendingFriends; -------------------------------------------------------------------------------- /frontend/components/app/friends/pending_friends/pending_friends_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import PendingFriends from './pending_friends'; 3 | import { fetchFriendRequests, acceptFriendRequest, deleteFriendRequest } from '../../../../actions/friends_actions'; 4 | import { createDmChannel } from '../../../../actions/channel_actions'; 5 | 6 | const mapStateToProps = (state) => { 7 | const outgoingRequests = Object.values(state.entities.friendRequests).filter((request) => { 8 | return request.user_id === state.session.id; 9 | }); 10 | 11 | const incomingRequests = Object.values(state.entities.friendRequests).filter((request) => { 12 | return request.friend_id === state.session.id; 13 | }); 14 | 15 | return { 16 | users: state.entities.users, 17 | incomingRequests, 18 | outgoingRequests, 19 | currentUser: state.entities.users[state.session.id], 20 | servers: state.entities.servers 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = dispatch => { 25 | return { 26 | fetchFriendRequests: () => dispatch(fetchFriendRequests()), 27 | acceptFriendRequest: (request) => dispatch(acceptFriendRequest(request)), 28 | deleteFriendRequest: (request) => dispatch(deleteFriendRequest(request)), 29 | createDmChannel: userId => dispatch(createDmChannel(userId)), 30 | }; 31 | }; 32 | 33 | export default connect(mapStateToProps, mapDispatchToProps)(PendingFriends); -------------------------------------------------------------------------------- /frontend/components/app/header/header/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from '../../modal/tooltip'; 3 | 4 | class Header extends React.Component { 5 | render() { 6 | return ( 7 |
    8 |
    9 | 10 | 12 | 13 | {this.props.channel.name} 14 |
    15 | 18 | 19 | 20 | 21 | 22 | 23 | } 24 | position="bottom right" 25 | text="Member List" 26 | /> 27 |
    28 | ) 29 | } 30 | } 31 | 32 | export default Header; 33 | -------------------------------------------------------------------------------- /frontend/components/app/header/header/header_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Header from './header'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | channel: state.entities.channels[ownProps.match.params.channelId] || {}, 8 | }; 9 | }; 10 | 11 | export default withRouter(connect(mapStateToProps)(Header)); 12 | -------------------------------------------------------------------------------- /frontend/components/app/header/me_header/me_header_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import MeHeader from './me_header'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | currentUser: state.entities.users[state.session.id], 8 | users: state.entities.users, 9 | channel: state.entities.channels[ownProps.match.params.channelId] || {}, 10 | }; 11 | }; 12 | 13 | export default withRouter(connect(mapStateToProps)(MeHeader)); 14 | -------------------------------------------------------------------------------- /frontend/components/app/loading_screen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingScreen = (props) => { 4 | return ( 5 |
    6 | 11 |
    {props.text}
    12 |
    13 | ) 14 | } 15 | 16 | export default LoadingScreen; 17 | 18 | -------------------------------------------------------------------------------- /frontend/components/app/modal/tooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Popup from 'reactjs-popup'; 3 | 4 | class Tooltip extends React.Component { 5 | render() { 6 | return ( 7 | 33 |
    {this.props.text}
    34 |
    35 | ); 36 | } 37 | } 38 | 39 | export default Tooltip; -------------------------------------------------------------------------------- /frontend/components/app/modal/user_popup_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createDmChannel } from '../../../actions/channel_actions'; 3 | import { createFriendRequest } from '../../../actions/friends_actions'; 4 | import UserPopup from './user_popup'; 5 | import { withRouter } from 'react-router-dom'; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | currentUserId: state.session.id, 10 | friendRequests: Object.values(state.entities.friendRequests), 11 | friends: state.entities.friends, 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return { 17 | createDmChannel: userId => dispatch(createDmChannel(userId)), 18 | createFriendRequest: request => dispatch(createFriendRequest(request)) 19 | }; 20 | }; 21 | 22 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UserPopup)); 23 | -------------------------------------------------------------------------------- /frontend/components/app/server_members/server_member.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import UserPopup from '../modal/user_popup_container'; 4 | 5 | class ServerMember extends React.Component { 6 | 7 | transition() { 8 | const popUp = document.getElementsByClassName('popup-content'); 9 | popUp[0].style.transform = 'translate(10px, 0)'; 10 | } 11 | 12 | render() { 13 | return ( 14 | 16 |
    17 |
    21 |
    22 | {this.props.user.username} 23 | 24 | } 25 | onOpen={this.transition} 26 | user={this.props.user} 27 | position={"left center"} 28 | offsetX={-5} 29 | offsetY={this.props.admin ? 10 : -90} 30 | showMessageButton={true} 31 | /> 32 | ) 33 | } 34 | } 35 | 36 | export default withRouter(ServerMember); -------------------------------------------------------------------------------- /frontend/components/app/server_members/server_members/server_members.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ServerMember from '../server_member'; 3 | 4 | class ServerMembers extends React.Component { 5 | 6 | componentWillMount() { 7 | this.props.beginLoading(); 8 | } 9 | 10 | componentDidMount() { 11 | this.props.fetchMembers(this.props.match.params.serverId).then(this.props.finishLoading); 12 | } 13 | 14 | componentWillUpdate(prevProps) { 15 | if (prevProps.server.id && prevProps.server.id != this.props.match.params.serverId) { 16 | this.props.beginLoading(); 17 | } 18 | } 19 | 20 | componentDidUpdate(prevProps) { 21 | if (prevProps.server.id && prevProps.server.id != this.props.match.params.serverId) { 22 | this.props.fetchMembers(this.props.match.params.serverId).then(this.props.finishLoading); 23 | } 24 | } 25 | 26 | render() { 27 | const admin = this.props.members.find((member) => { 28 | return member.id === this.props.server.admin_id; 29 | }); 30 | 31 | let adminEl; 32 | let adminOnline; 33 | let adminOffline; 34 | 35 | if (admin) { 36 | adminEl = ; 37 | if (admin.online) { 38 | adminOnline = 1; 39 | adminOffline = 0; 40 | } else { 41 | adminOnline = 0; 42 | adminOffline = 1; 43 | } 44 | } else { 45 | adminEl = null; 46 | } 47 | 48 | const online = this.props.members.filter(member => member.online); 49 | const offline = this.props.members.filter(member => !member.online); 50 | 51 | const onlineMembers = online.map((member, idx) => { 52 | if (member.id === this.props.server.admin_id) { 53 | return null; 54 | } else { 55 | return ; 56 | } 57 | }); 58 | 59 | const offlineMembers = offline.map((member, idx) => { 60 | if (member.id === this.props.server.admin_id) { 61 | return null 62 | } else { 63 | return ; 64 | } 65 | }); 66 | 67 | return ( 68 |
    69 |
    70 |
    71 |

    ADMIN

    72 | {adminEl} 73 | {online.length - adminOnline > 0 ?

    {`ONLINE—${online.length - adminOnline}`}

    : null} 74 | {onlineMembers} 75 | {offline.length - adminOffline > 0 ?

    {`OFFLINE—${offline.length - adminOffline}`}

    : null} 76 | {offlineMembers} 77 |
    78 |
    79 |
    80 | ) 81 | } 82 | } 83 | 84 | export default ServerMembers; -------------------------------------------------------------------------------- /frontend/components/app/server_members/server_members/server_members_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import ServerMembers from './server_members'; 4 | import { fetchMembers } from '../../../../actions/server_actions'; 5 | import { beginLoading, finishLoading } from '../../../../actions/ui_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const server = state.entities.servers[ownProps.match.params.serverId] || {}; 9 | return { 10 | server, 11 | members: Object.values(state.entities.users).filter(user => user.servers.includes(server.id)).sort((a, b) => { 12 | if (a.username < b.username) { return -1; } 13 | if (a.username > b.username) { return 1; } 14 | return 0; 15 | }) 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = dispatch => { 20 | return { 21 | fetchMembers: id => dispatch(fetchMembers(id)), 22 | beginLoading: () => dispatch(beginLoading()), 23 | finishLoading: () => dispatch(finishLoading()), 24 | }; 25 | }; 26 | 27 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ServerMembers)); 28 | 29 | -------------------------------------------------------------------------------- /frontend/components/app/servers/dm_notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Tooltip from '../modal/tooltip'; 4 | 5 | class DmNotification extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { classText: 'dm-notification'}; 9 | } 10 | 11 | componentDidMount() { 12 | setTimeout(() => this.setState({classText: 'dm-notification right'}), 200); 13 | } 14 | 15 | render() { 16 | return ( 17 | e.preventDefault()} 23 | > 24 |
    {this.props.notification.count}
    25 | 26 | } 27 | position="right center" 28 | text={this.props.user.username} 29 | /> 30 | ) 31 | }; 32 | }; 33 | 34 | export default DmNotification; -------------------------------------------------------------------------------- /frontend/components/app/servers/join_server_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | class JoinServerForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { name: '' }; 8 | this.handleSubmit = this.handleSubmit.bind(this); 9 | this.handleRedirect = this.handleRedirect.bind(this); 10 | this.update = this.update.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.nameInput.focus(); 15 | } 16 | 17 | update(e) { 18 | this.setState({ 19 | name: e.currentTarget.value 20 | }); 21 | } 22 | 23 | handleSubmit(e) { 24 | e.preventDefault(); 25 | this.props.joinServer(this.state, this.props.currentUserId).then(this.handleRedirect); 26 | } 27 | 28 | handleRedirect(action) { 29 | this.props.closeModal(); 30 | this.props.history.push(`/channels/${action.server.id}/${action.server.root_channel}`); 31 | } 32 | 33 | render() { 34 | return ( 35 |
    36 |
    37 |
    JOIN A SERVER
    38 |

    Enter a server name below to join an existing server.

    39 |
    40 | 43 | { this.nameInput = input; }} /> 44 |
    45 |
    46 | 47 |
    48 | 49 |
    50 |
    51 | ) 52 | } 53 | } 54 | 55 | export default withRouter(JoinServerForm); 56 | -------------------------------------------------------------------------------- /frontend/components/app/servers/search_server_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | class SearchServerItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | this.handleRedirect = this.handleRedirect.bind(this); 9 | } 10 | 11 | handleSubmit(e) { 12 | e.preventDefault(); 13 | this.props.joinServer({name: this.props.server.name}, this.props.currentUserId).then(this.handleRedirect); 14 | } 15 | 16 | handleRedirect(action) { 17 | this.props.closeModal(); 18 | this.props.history.push(`/channels/${action.server.id}/${action.server.root_channel}`); 19 | } 20 | 21 | 22 | render() { 23 | return ( 24 | 30 | ) 31 | }; 32 | }; 33 | 34 | export default withRouter(SearchServerItem); -------------------------------------------------------------------------------- /frontend/components/app/servers/server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import Tooltip from '../modal/tooltip'; 4 | 5 | class Server extends React.Component { 6 | 7 | render() { 8 | return ( 9 | e.preventDefault()} 17 | >
    18 | } 19 | position="right center" 20 | text={this.props.server.name} 21 | /> 22 | ) 23 | }; 24 | }; 25 | 26 | export default Server; -------------------------------------------------------------------------------- /frontend/components/app/servers/servers/servers_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Servers from './servers'; 3 | import { fetchServers, createServer, joinServer } from '../../../../actions/server_actions'; 4 | import { removeServerErrors } from '../../../../actions/server_actions'; 5 | import { withRouter } from 'react-router-dom'; 6 | 7 | const mapStateToProps = state => { 8 | let onlineCount = 0; 9 | state.entities.friends.forEach(friendId => { 10 | if (state.entities.users[friendId] && state.entities.users[friendId].online) { 11 | onlineCount++; 12 | } 13 | }); 14 | 15 | const dmNotifications = Object.values(state.notifications.dm); 16 | const users = dmNotifications.map(notification => { 17 | return state.entities.users[notification.authorId]; 18 | }); 19 | const currentUser = state.entities.users[state.session.id]; 20 | 21 | const servers = currentUser.servers.map(serverId => { 22 | return state.entities.servers[serverId] || {}; 23 | }); 24 | 25 | return { 26 | servers, 27 | errors: state.errors.server, 28 | onlineCount, 29 | dmNotifications, 30 | users, 31 | currentUser 32 | }; 33 | }; 34 | 35 | const mapDispatchToProps = dispatch => { 36 | return { 37 | fetchServers: () => dispatch(fetchServers()), 38 | createServer: (formData) => dispatch(createServer(formData)), 39 | joinServer: (server, userId) => dispatch(joinServer(server, userId)), 40 | removeServerErrors: () => dispatch(removeServerErrors()), 41 | }; 42 | }; 43 | 44 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Servers)); 45 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import App from './app'; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default Root; -------------------------------------------------------------------------------- /frontend/components/session_form/login/login_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, removeErrors } from '../../../actions/session_actions'; 3 | import LoginForm from './login_form'; 4 | 5 | const mapStateToProps = ({ errors }) => { 6 | return { 7 | errors: errors.session, 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | removeErrors: () => dispatch(removeErrors()), 14 | login: (user) => dispatch(login(user)), 15 | }; 16 | }; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(LoginForm); -------------------------------------------------------------------------------- /frontend/components/session_form/signup/signup_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signup, removeErrors } from '../../../actions/session_actions'; 3 | import SignupForm from './signup_form'; 4 | 5 | const mapStateToProps = ({ errors }) => { 6 | return { 7 | errors: errors.session, 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | signup: (user) => dispatch(signup(user)), 14 | removeErrors: () => dispatch(removeErrors()), 15 | }; 16 | }; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(SignupForm); 19 | -------------------------------------------------------------------------------- /frontend/components/splash/splash_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Splash from './splash'; 3 | import { login } from '../../actions/session_actions'; 4 | 5 | const mapStateToProps = ({ session }) => { 6 | return { 7 | currentUserId: session.id, 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | login: () => dispatch(login({ username: 'hodor', password: 'hodorhodor' })), 14 | }; 15 | } 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Splash); 18 | -------------------------------------------------------------------------------- /frontend/discors.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './components/root'; 4 | import configureStore from './store/store'; 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | let store; 8 | if (window.currentUser) { 9 | const preloadedState = { 10 | session: { id: window.currentUser.id }, 11 | entities: { 12 | users: { [window.currentUser.id]: window.currentUser } 13 | } 14 | }; 15 | store = configureStore(preloadedState); 16 | delete window.currentUser; 17 | } else { 18 | store = configureStore(); 19 | } 20 | 21 | const root = document.getElementById('root'); 22 | ReactDOM.render(, root); 23 | }); -------------------------------------------------------------------------------- /frontend/reducers/entities/channels_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_CHANNELS, RECEIVE_CHANNEL, REMOVE_CHANNEL } from '../../actions/channel_actions'; 3 | import { RECEIVE_CURRENT_USER_DATA } from '../../actions/session_actions'; 4 | 5 | const channelsReducer = (state = {}, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER_DATA: 9 | return action.currentUserData.channels || {}; 10 | case RECEIVE_CHANNELS: 11 | return merge({}, state, action.channels); 12 | case RECEIVE_CHANNEL: 13 | return merge({}, state, { [action.channel.id]: action.channel }); 14 | case REMOVE_CHANNEL: 15 | const newState = merge({}, state); 16 | delete newState[action.channelId]; 17 | return newState; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default channelsReducer; 24 | -------------------------------------------------------------------------------- /frontend/reducers/entities/entities_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { combineReducers } from 'redux'; 3 | import users from './users_reducer'; 4 | import servers from './servers_reducer'; 5 | import channels from './channels_reducer'; 6 | import friendRequests from './friend_requests_reducer'; 7 | import friends from './friends_reducer'; 8 | import voiceChannels from './voice_channels_reducer'; 9 | 10 | export default combineReducers({ 11 | users, 12 | servers, 13 | channels, 14 | friendRequests, 15 | friends, 16 | voiceChannels, 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/reducers/entities/friend_requests_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_REQUESTS, REMOVE_REQUEST, RECEIVE_REQUEST } from '../../actions/friends_actions'; 3 | import { RECEIVE_CURRENT_USER_DATA } from '../../actions/session_actions'; 4 | 5 | const friendRequestsReducer = (state = {}, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER_DATA: 9 | return action.currentUserData.friend_requests || {}; 10 | case RECEIVE_REQUESTS: 11 | return action.friendRequests || {}; 12 | case RECEIVE_REQUEST: 13 | return merge({}, state, { [action.friendRequest.id]: action.friendRequest }); 14 | case REMOVE_REQUEST: 15 | const newState = merge({}, state); 16 | delete newState[action.requestId]; 17 | return newState; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default friendRequestsReducer; 24 | -------------------------------------------------------------------------------- /frontend/reducers/entities/friends_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_FRIENDS, RECEIVE_FRIEND, REMOVE_FRIEND } from "../../actions/friends_actions"; 2 | import { union } from 'lodash'; 3 | import { RECEIVE_CURRENT_USER_DATA } from "../../actions/session_actions"; 4 | 5 | const friendsReducer = (state = [], action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER_DATA: 9 | return action.currentUserData.friends; 10 | case RECEIVE_FRIENDS: 11 | return action.friendData.friends; 12 | case RECEIVE_FRIEND: 13 | return union([], state, action.friend); 14 | case REMOVE_FRIEND: 15 | const newState = union([], state); 16 | return newState.filter((friendId) => ( 17 | friendId !== action.friendId 18 | )); 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | export default friendsReducer; 25 | -------------------------------------------------------------------------------- /frontend/reducers/entities/servers_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_SERVERS, RECEIVE_SERVER, REMOVE_SERVER } from '../../actions/server_actions'; 3 | import { RECEIVE_CURRENT_USER_DATA } from '../../actions/session_actions'; 4 | 5 | const serversReducer = (state = {}, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER_DATA: 9 | return action.currentUserData.servers || {}; 10 | case RECEIVE_SERVERS: 11 | return merge({}, state, action.servers); 12 | case RECEIVE_SERVER: 13 | return merge({}, state, { [action.server.id]: action.server }); 14 | case REMOVE_SERVER: 15 | const newState = merge({}, state); 16 | delete newState[action.serverId]; 17 | return newState; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default serversReducer; 24 | -------------------------------------------------------------------------------- /frontend/reducers/entities/users_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_CURRENT_USER, RECEIVE_CURRENT_USER_DATA, RECEIVE_USER } from '../../actions/session_actions'; 3 | import { RECEIVE_USERS, REMOVE_SERVER, RECEIVE_SERVER } from '../../actions/server_actions'; 4 | import { RECEIVE_FRIENDS } from '../../actions/friends_actions'; 5 | 6 | const usersReducer = (state = {}, action) => { 7 | Object.freeze(state); 8 | let newState; 9 | switch (action.type) { 10 | case RECEIVE_CURRENT_USER_DATA: 11 | return merge({}, state, action.currentUserData.users); 12 | case RECEIVE_CURRENT_USER: 13 | return merge({}, state, { [action.currentUser.id]: action.currentUser }); 14 | case RECEIVE_USER: 15 | return merge({}, state, { [action.user.id]: action.user }); 16 | case RECEIVE_USERS: 17 | return merge({}, state, action.users); 18 | case RECEIVE_FRIENDS: 19 | return merge({}, state, action.friendData.users); 20 | case REMOVE_SERVER: 21 | newState = merge({}, state); 22 | newState[action.userId].servers = newState[action.userId].servers.filter(serverId => { 23 | return serverId !== action.serverId; 24 | }); 25 | 26 | return newState; 27 | case RECEIVE_SERVER: 28 | newState = merge({}, state); 29 | if (!newState[action.userId].servers.includes(action.server.id)) { 30 | newState[action.userId].servers.push(action.server.id); 31 | } 32 | 33 | return newState; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default usersReducer; -------------------------------------------------------------------------------- /frontend/reducers/entities/voice_channels_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_VOICE_CHANNEL, RECEIVE_VOICE_CHANNELS, REMOVE_VOICE_CHANNEL } from '../../actions/voice_channel_actions'; 3 | import { RECEIVE_CURRENT_USER_DATA } from '../../actions/session_actions'; 4 | 5 | const voiceChannelsReducer = (state = {}, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER_DATA: 9 | return action.currentUserData.voice_channels || {}; 10 | case RECEIVE_VOICE_CHANNELS: 11 | return merge({}, state, action.voiceChannels); 12 | case RECEIVE_VOICE_CHANNEL: 13 | return merge({}, state, { [action.voiceChannel.id]: action.voiceChannel }); 14 | case REMOVE_VOICE_CHANNEL: 15 | const newState = merge({}, state); 16 | delete newState[action.voiceChannelId]; 17 | return newState; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default voiceChannelsReducer; 24 | 25 | -------------------------------------------------------------------------------- /frontend/reducers/errors/channel_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CHANNEL_ERRORS, RECEIVE_CHANNEL, REMOVE_CHANNEL_ERRORS } from '../../actions/channel_actions'; 2 | import { RECEIVE_VOICE_CHANNEL } from '../../actions/voice_channel_actions'; 3 | 4 | export default (state = [], action) => { 5 | Object.freeze(state); 6 | switch (action.type) { 7 | case RECEIVE_CHANNEL_ERRORS: 8 | return action.errors; 9 | case RECEIVE_CHANNEL: 10 | return []; 11 | case RECEIVE_VOICE_CHANNEL: 12 | return []; 13 | case REMOVE_CHANNEL_ERRORS: 14 | return []; 15 | default: 16 | return state; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/reducers/errors/errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import session from './session_errors_reducer'; 3 | import server from './server_errors_reducer'; 4 | import channel from './channel_errors_reducer'; 5 | 6 | export default combineReducers({ 7 | session, 8 | server, 9 | channel, 10 | }); -------------------------------------------------------------------------------- /frontend/reducers/errors/server_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SERVER_ERRORS, RECEIVE_SERVER, REMOVE_SERVER_ERRORS } from '../../actions/server_actions'; 2 | 3 | export default (state = [], action) => { 4 | Object.freeze(state); 5 | switch (action.type) { 6 | case RECEIVE_SERVER_ERRORS: 7 | return action.errors; 8 | case RECEIVE_SERVER: 9 | return []; 10 | case REMOVE_SERVER_ERRORS: 11 | return []; 12 | default: 13 | return state; 14 | } 15 | }; -------------------------------------------------------------------------------- /frontend/reducers/errors/session_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_SESSION_ERRORS, 3 | RECEIVE_CURRENT_USER, 4 | REMOVE_SESSION_ERRORS, 5 | } from '../../actions/session_actions'; 6 | 7 | export default (state = [], action) => { 8 | Object.freeze(state); 9 | switch (action.type) { 10 | case RECEIVE_SESSION_ERRORS: 11 | return action.errors; 12 | case RECEIVE_CURRENT_USER: 13 | return []; 14 | case REMOVE_SESSION_ERRORS: 15 | return []; 16 | default: 17 | return state; 18 | } 19 | }; -------------------------------------------------------------------------------- /frontend/reducers/loading_reducer.js: -------------------------------------------------------------------------------- 1 | import { BEGIN_LOADING, FINISH_LOADING } from "../actions/ui_actions"; 2 | 3 | export default (state = false, action) => { 4 | Object.freeze(state); 5 | switch (action.type) { 6 | case BEGIN_LOADING: 7 | return true; 8 | case FINISH_LOADING: 9 | return false; 10 | default: 11 | return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/reducers/notifications/dm_notifications_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_DM_NOTIFICATION, REMOVE_DM_NOTIFICATION } from '../../actions/notification_actions'; 3 | 4 | const dmNotificationsReducer = (state = {}, action) => { 5 | let newState; 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_DM_NOTIFICATION: 9 | if (state[action.notification.channelId]) { 10 | newState = merge({}, state); 11 | newState[action.notification.channelId].count++; 12 | return newState; 13 | } else { 14 | return merge({}, state, { [action.notification.channelId]: merge(action.notification, { count: 1 }) }); 15 | } 16 | case REMOVE_DM_NOTIFICATION: 17 | newState = merge({}, state); 18 | delete newState[action.channelId]; 19 | return newState; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default dmNotificationsReducer; 26 | -------------------------------------------------------------------------------- /frontend/reducers/notifications/notifications_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import dm from './dm_notifications_reducer'; 3 | 4 | export default combineReducers({ 5 | dm, 6 | }); -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import entities from './entities/entities_reducer'; 3 | import ui from './ui_reducer'; 4 | import session from './session_reducer'; 5 | import errors from './errors/errors_reducer'; 6 | import notifications from './notifications/notifications_reducer'; 7 | 8 | const rootReducer = combineReducers({ 9 | entities, 10 | session, 11 | errors, 12 | ui, 13 | notifications, 14 | }); 15 | 16 | export default rootReducer; -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CURRENT_USER, 3 | LOGOUT_CURRENT_USER, 4 | RECEIVE_CURRENT_USER_DATA, 5 | } from '../actions/session_actions'; 6 | 7 | const _nullUser = Object.freeze({ 8 | id: null 9 | }); 10 | 11 | const sessionReducer = (state = _nullUser, action) => { 12 | Object.freeze(state); 13 | switch (action.type) { 14 | case RECEIVE_CURRENT_USER_DATA: 15 | return { id: action.currentUserData.currentUserId }; 16 | case RECEIVE_CURRENT_USER: 17 | return { id: action.currentUser.id }; 18 | case LOGOUT_CURRENT_USER: 19 | return _nullUser; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default sessionReducer; 26 | -------------------------------------------------------------------------------- /frontend/reducers/ui_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import loading from './loading_reducer'; 3 | 4 | export default combineReducers({ 5 | loading, 6 | }); -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | // import logger from 'redux-logger'; 3 | import thunk from 'redux-thunk'; 4 | import rootReducer from '../reducers/root_reducer'; 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | rootReducer, 9 | preloadedState, 10 | // applyMiddleware(thunk, logger) 11 | applyMiddleware(thunk) 12 | ) 13 | ); 14 | 15 | export default configureStore; -------------------------------------------------------------------------------- /frontend/util/channel_api_util.js: -------------------------------------------------------------------------------- 1 | export const createChannel = channel => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: '/api/channels', 5 | data: { channel } 6 | }) 7 | ); 8 | 9 | export const createDmChannel = user_id => ( 10 | $.ajax({ 11 | method: 'POST', 12 | url: '/api/dm_channel_memberships', 13 | data: {dm_channel: { user_id }} 14 | }) 15 | ); 16 | 17 | export const fetchChannel = id => ( 18 | $.ajax({ 19 | method: 'GET', 20 | url: `/api/channels/${id}`, 21 | }) 22 | ); 23 | 24 | export const deleteChannel = id => ( 25 | $.ajax({ 26 | method: 'DELETE', 27 | url: `/api/channels/${id}`, 28 | }) 29 | ); 30 | 31 | export const deleteDmChannel = id => ( 32 | $.ajax({ 33 | method: 'DELETE', 34 | url: `/api/dm_channel_memberships/${id}`, 35 | }) 36 | ); 37 | 38 | export const fetchChannels = (server_id) => ( 39 | $.ajax({ 40 | method: 'GET', 41 | url: '/api/channels', 42 | data: { channel: {server_id} } 43 | }) 44 | ); 45 | 46 | export const fetchDmChannels = () => ( 47 | $.ajax({ 48 | method: 'GET', 49 | url: '/api/dm_channel_memberships', 50 | }) 51 | ); 52 | 53 | -------------------------------------------------------------------------------- /frontend/util/friends_api_util.js: -------------------------------------------------------------------------------- 1 | export const createFriendRequest = friend_request => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: '/api/friend_requests', 5 | data: { friend_request } 6 | }) 7 | ); 8 | 9 | export const acceptFriendRequest = friend_request => ( 10 | $.ajax({ 11 | method: 'PATCH', 12 | url: `/api/friend_requests/${friend_request.id}`, 13 | }) 14 | ); 15 | 16 | export const deleteFriendRequest = friend_request => ( 17 | $.ajax({ 18 | method: 'DELETE', 19 | url: `/api/friend_requests/${friend_request.id}`, 20 | }) 21 | ); 22 | 23 | export const fetchFriendRequests = () => ( 24 | $.ajax({ 25 | method: 'GET', 26 | url: '/api/friend_requests', 27 | }) 28 | ); 29 | 30 | export const deleteFriend = id => ( 31 | $.ajax({ 32 | method: 'DELETE', 33 | url: `/api/friends/${id}`, 34 | }) 35 | ); 36 | 37 | export const fetchFriends = () => ( 38 | $.ajax({ 39 | method: 'GET', 40 | url: '/api/friends', 41 | }) 42 | ); -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect, withRouter } from 'react-router-dom'; 4 | 5 | const Auth = ({ component: Component, path, loggedIn, exact }) => ( 6 | ( 7 | !loggedIn ? ( 8 | 9 | ) : ( 10 | 11 | ) 12 | )} /> 13 | ); 14 | 15 | const Protected = ({ component: Component, path, loggedIn, exact }) => ( 16 | ( 17 | loggedIn ? ( 18 | 19 | ) : ( 20 | 21 | ) 22 | )} /> 23 | ); 24 | 25 | const mapStateToProps = state => ( 26 | { loggedIn: Boolean(state.session.id) } 27 | ); 28 | 29 | export const AuthRoute = withRouter(connect(mapStateToProps)(Auth)); 30 | export const ProtectedRoute = withRouter(connect(mapStateToProps)(Protected)); -------------------------------------------------------------------------------- /frontend/util/server_api_utl.js: -------------------------------------------------------------------------------- 1 | export const createServer = formData => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: '/api/servers', 5 | data: formData, 6 | contentType: false, 7 | processData: false 8 | }) 9 | ); 10 | 11 | export const fetchServer = id => ( 12 | $.ajax({ 13 | method: 'GET', 14 | url: `/api/servers/${id}`, 15 | }) 16 | ); 17 | 18 | export const fetchMembers = id => ( 19 | $.ajax({ 20 | method: 'GET', 21 | url: `/api/servers/${id}/members`, 22 | }) 23 | ); 24 | 25 | export const fetchServers = () => ( 26 | $.ajax({ 27 | method: 'GET', 28 | url: '/api/servers', 29 | }) 30 | ); 31 | 32 | export const joinServer = server => ( 33 | $.ajax({ 34 | method: 'POST', 35 | url: `/api/servers/join`, 36 | data: { server } 37 | }) 38 | ); 39 | 40 | export const deleteServer = id => ( 41 | $.ajax({ 42 | method: 'DELETE', 43 | url: `/api/servers/${id}`, 44 | }) 45 | ); -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const login = user => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: '/api/session', 5 | data: { user } 6 | }) 7 | ); 8 | 9 | export const fetchCurrentUserData = () => ( 10 | $.ajax({ 11 | method: 'GET', 12 | url: '/api/users/data', 13 | }) 14 | ); 15 | 16 | export const signup = user => ( 17 | $.ajax({ 18 | method: 'POST', 19 | url: '/api/users', 20 | data: { user } 21 | }) 22 | ); 23 | 24 | export const logout = () => ( 25 | $.ajax({ 26 | method: 'DELETE', 27 | url: '/api/session' 28 | }) 29 | ); 30 | 31 | export const editUser = (formData) => ( 32 | $.ajax({ 33 | url: `/api/users/${formData.get('user[id]')}`, 34 | method: 'PATCH', 35 | data: formData, 36 | contentType: false, 37 | processData: false 38 | }) 39 | ); -------------------------------------------------------------------------------- /frontend/util/voice_channel_api_util.js: -------------------------------------------------------------------------------- 1 | export const createVoiceChannel = audio_channel => ( 2 | $.ajax({ 3 | method: 'POST', 4 | url: '/api/audio_channels', 5 | data: { audio_channel } 6 | }) 7 | ); 8 | 9 | export const fetchVoiceChannel = id => ( 10 | $.ajax({ 11 | method: 'GET', 12 | url: `/api/audio_channels/${id}`, 13 | }) 14 | ); 15 | 16 | export const deleteVoiceChannel = id => ( 17 | $.ajax({ 18 | method: 'DELETE', 19 | url: `/api/audio_channels/${id}`, 20 | }) 21 | ); 22 | 23 | export const fetchVoiceChannels = (server_id) => ( 24 | $.ajax({ 25 | method: 'GET', 26 | url: '/api/audio_channels', 27 | data: { audio_channel: { server_id } } 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/sessions.rake: -------------------------------------------------------------------------------- 1 | namespace :sessions do 2 | desc "Clear expired sessions" 3 | task :clear_expired_sessions => :environment do 4 | sql = "DELETE FROM sessions WHERE updated_at < (CURRENT_TIMESTAMP - INTERVAL '1 days');" 5 | ActiveRecord::Base.connection.execute(sql) 6 | end 7 | end -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discors", 3 | "private": true, 4 | "dependencies": { 5 | "@babel/core": "^7.2.2", 6 | "@babel/preset-env": "^7.2.3", 7 | "@babel/preset-react": "^7.0.0", 8 | "@microlink/react": "^3.0.5", 9 | "babel-loader": "^8.0.5", 10 | "babel-polyfill": "^6.26.0", 11 | "lodash": "^4.17.11", 12 | "moment-timezone": "^0.5.23", 13 | "react": "^16.7.0", 14 | "react-dom": "^16.7.0", 15 | "react-modal": "^3.8.1", 16 | "react-redux": "^6.0.0", 17 | "react-router-dom": "^4.3.1", 18 | "reactjs-popup": "^1.3.2", 19 | "redux": "^4.0.1", 20 | "redux-logger": "^3.0.6", 21 | "redux-thunk": "^2.3.0", 22 | "styled-components": "^4.1.3", 23 | "webpack": "^4.29.0", 24 | "webpack-cli": "^3.2.1" 25 | }, 26 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 27 | "version": "1.0.0", 28 | "main": "index.js", 29 | "directories": { 30 | "lib": "lib", 31 | "test": "test" 32 | }, 33 | "scripts": { 34 | "test": "echo \"Error: no test specified\" && exit 1", 35 | "postinstall": "./node_modules/.bin/webpack --mode=production", 36 | "start": "./node_modules/.bin/webpack --mode=production", 37 | "webpack": "webpack --mode=development --watch" 38 | }, 39 | "engines": { 40 | "node": "10.13.0", 41 | "npm": "6.5.0" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/jeffdeliso/discors.git" 46 | }, 47 | "keywords": [], 48 | "author": "", 49 | "license": "ISC", 50 | "bugs": { 51 | "url": "https://github.com/jeffdeliso/discors/issues" 52 | }, 53 | "homepage": "https://github.com/jeffdeliso/discors#readme" 54 | } 55 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/storage/.keep -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/models/.keep -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/test/system/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require_relative '../config/environment' 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/tmp/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffdeliso/discors/f78504485ad26efcc087a51e2dcceff84e689ca5/vendor/.keep -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: [ 6 | 'babel-polyfill', 7 | './frontend/discors.jsx' 8 | ], 9 | output: { 10 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 11 | filename: 'bundle.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /(node_modules)/, 18 | use: { 19 | loader: 'babel-loader', 20 | query: { 21 | presets: ['@babel/env', '@babel/react'] 22 | } 23 | }, 24 | } 25 | ] 26 | }, 27 | devtool: 'source-map', 28 | resolve: { 29 | extensions: [".js", ".jsx", "*"] 30 | } 31 | }; 32 | --------------------------------------------------------------------------------