From 9a43c5108566567447b463da395f1e5b5602c989 Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Wed, 2 Dec 2020 23:22:51 -0500 Subject: [PATCH] Progress on dms, code cleanup Progress on dms, code cleanup --- .../admin/custom_emojis_controller.rb | 16 - app/controllers/api/base_controller.rb | 11 + .../api/v1/accounts/search_controller.rb | 24 ++ .../blocked_accounts_controller.rb | 60 +++ .../muted_accounts_controller.rb | 60 +++ ... chat_conversation_accounts_controller.rb} | 31 +- .../api/v1/chat_conversation_controller.rb | 82 ++++ .../approved_conversations_controller.rb | 71 ++++ .../chat_conversations/messages_controller.rb | 56 +++ .../requested_conversations_controller.rb | 69 ++++ .../api/v1/chat_messages_controller.rb | 46 +++ .../api/v1/messages/chats_controller.rb | 0 .../v1/messages/converations_controller.rb | 0 app/controllers/empty_controller.rb | 12 + app/controllers/react_controller.rb | 2 +- app/javascript/gabsocial/actions/accounts.js | 1 + .../gabsocial/actions/chat_compose.js | 28 -- .../actions/chat_conversation_messages.js | 111 ++++++ .../gabsocial/actions/chat_conversations.js | 371 +++++++++++++++--- .../gabsocial/actions/chat_messages.js | 81 ++-- app/javascript/gabsocial/actions/chats.js | 41 ++ .../gabsocial/actions/importer/index.js | 12 +- app/javascript/gabsocial/actions/lists.js | 2 +- app/javascript/gabsocial/actions/streaming.js | 17 +- .../gabsocial/components/account.js | 3 + .../gabsocial/components/avatar_group.js | 61 +++ .../components/display_name_group.js | 81 ++++ .../components/floating_action_button.js | 38 +- .../modal/chat_conversation_create_modal.js | 31 ++ .../modal/chat_conversation_delete_modal.js | 40 ++ .../gabsocial/components/modal/modal_root.js | 69 ++-- .../placeholder/chat_message_placeholder.js | 31 ++ .../placeholder/placeholder_layout.js | 3 + .../gabsocial/components/scrollable_list.js | 46 ++- .../components/sidebar/sidebar_layout.js | 44 +-- .../gabsocial/components/status_list.js | 2 +- app/javascript/gabsocial/components/text.js | 2 + app/javascript/gabsocial/components/toast.js | 48 +-- app/javascript/gabsocial/constants.js | 2 + .../gabsocial/containers/gabsocial.js | 2 + .../gabsocial/containers/toasts_container.js | 57 ++- .../chat_conversation_blocked_accounts.js | 93 +++++ .../features/chat_conversation_create.js | 86 ++++ .../chat_conversation_muted_accounts.js | 79 ++++ .../features/chat_conversation_requests.js | 21 + .../features/compose/components/poll_form.js | 2 +- .../chat_conversations_empty_block.js | 47 +++ .../components/chat_conversations_list.js | 98 +++++ .../chat_conversations_list_item.js | 116 ++++++ .../chat_conversations_requests_list_item.js | 57 +++ ...search.js => chat_conversations_search.js} | 17 +- .../components/chat_message_compose_form.js | 140 +++++++ .../components/chat_message_header.js | 84 ++++ .../messages/components/chat_message_item.js | 179 +++++++++ .../components/chat_message_scrolling_list.js | 212 ++++++++++ ...ages_header.js => chat_settings_header.js} | 34 +- .../messages/components/message_item.js | 116 ------ .../messages/components/messages_list.js | 36 -- .../messages/components/messages_list_item.js | 107 ----- .../gabsocial/features/messages/messages.js | 106 ++--- .../gabsocial/features/messages_settings.js | 87 ++++ .../features/{mutes.js => muted_accounts.js} | 6 +- app/javascript/gabsocial/features/ui/ui.js | 32 +- .../features/ui/util/async_components.js | 9 +- app/javascript/gabsocial/layouts/layout.js | 4 +- .../gabsocial/layouts/messages_layout.js | 104 ++++- .../gabsocial/layouts/profile_layout.js | 2 +- .../gabsocial/pages/messages_page.js | 27 +- .../gabsocial/reducers/chat_compose.js | 43 -- .../reducers/chat_conversation_lists.js | 85 ++++ .../reducers/chat_conversation_messages.js | 105 +++++ .../gabsocial/reducers/chat_conversations.js | 43 ++ .../gabsocial/reducers/chat_messages.js | 96 ++--- app/javascript/gabsocial/reducers/chats.js | 38 ++ app/javascript/gabsocial/reducers/index.js | 15 +- .../gabsocial/reducers/timelines.js | 1 - app/javascript/gabsocial/reducers/user.js | 1 - app/javascript/gabsocial/selectors/index.js | 43 ++ app/javascript/gabsocial/stream.js | 1 + app/javascript/gabsocial/utils/numbers.js | 6 +- app/javascript/styles/global.css | 15 +- app/lib/entity_cache.rb | 2 +- app/models/account.rb | 5 + app/models/chat_block.rb | 30 ++ app/models/chat_conversation.rb | 13 + app/models/chat_conversation_account.rb | 43 ++ app/models/chat_message.rb | 27 ++ app/models/chat_mute.rb | 29 ++ .../chat_conversation_account_serializer.rb | 21 + .../rest/chat_message_serializer.rb | 10 + app/services/account_search_service.rb | 1 + .../chat_message_length_validator.rb | 10 + app/validators/poll_validator.rb | 2 +- config/initializers/doorkeeper.rb | 2 + config/routes.rb | 35 +- ...0201127051070_create_chat_conversations.rb | 9 + .../20201127051071_create_chat_blocks.rb | 14 + .../20201127051072_create_chat_mutes.rb | 12 + ...51073_create_chat_conversation_accounts.rb | 16 + .../20201127051074_create_chat_messages.rb | 14 + ...1423_remove_disabled_from_custom_emojis.rb | 5 + db/schema.rb | 50 ++- streaming/index.js | 8 +- 103 files changed, 3656 insertions(+), 859 deletions(-) create mode 100644 app/controllers/api/v1/accounts/search_controller.rb create mode 100644 app/controllers/api/v1/chat_conversation_accounts/blocked_accounts_controller.rb create mode 100644 app/controllers/api/v1/chat_conversation_accounts/muted_accounts_controller.rb rename app/controllers/api/v1/{messages_controller.rb => chat_conversation_accounts_controller.rb} (50%) create mode 100644 app/controllers/api/v1/chat_conversation_controller.rb create mode 100644 app/controllers/api/v1/chat_conversations/approved_conversations_controller.rb create mode 100644 app/controllers/api/v1/chat_conversations/messages_controller.rb create mode 100644 app/controllers/api/v1/chat_conversations/requested_conversations_controller.rb create mode 100644 app/controllers/api/v1/chat_messages_controller.rb delete mode 100644 app/controllers/api/v1/messages/chats_controller.rb delete mode 100644 app/controllers/api/v1/messages/converations_controller.rb delete mode 100644 app/javascript/gabsocial/actions/chat_compose.js create mode 100644 app/javascript/gabsocial/actions/chat_conversation_messages.js create mode 100644 app/javascript/gabsocial/actions/chats.js create mode 100644 app/javascript/gabsocial/components/avatar_group.js create mode 100644 app/javascript/gabsocial/components/display_name_group.js create mode 100644 app/javascript/gabsocial/components/modal/chat_conversation_create_modal.js create mode 100644 app/javascript/gabsocial/components/modal/chat_conversation_delete_modal.js create mode 100644 app/javascript/gabsocial/components/placeholder/chat_message_placeholder.js create mode 100644 app/javascript/gabsocial/features/chat_conversation_blocked_accounts.js create mode 100644 app/javascript/gabsocial/features/chat_conversation_create.js create mode 100644 app/javascript/gabsocial/features/chat_conversation_muted_accounts.js create mode 100644 app/javascript/gabsocial/features/chat_conversation_requests.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_conversations_empty_block.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_conversations_list.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_conversations_list_item.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_conversations_requests_list_item.js rename app/javascript/gabsocial/features/messages/components/{messages_search.js => chat_conversations_search.js} (54%) create mode 100644 app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_message_header.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_message_item.js create mode 100644 app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js rename app/javascript/gabsocial/features/messages/components/{messages_header.js => chat_settings_header.js} (50%) delete mode 100644 app/javascript/gabsocial/features/messages/components/message_item.js delete mode 100644 app/javascript/gabsocial/features/messages/components/messages_list.js delete mode 100644 app/javascript/gabsocial/features/messages/components/messages_list_item.js create mode 100644 app/javascript/gabsocial/features/messages_settings.js rename app/javascript/gabsocial/features/{mutes.js => muted_accounts.js} (95%) delete mode 100644 app/javascript/gabsocial/reducers/chat_compose.js create mode 100644 app/javascript/gabsocial/reducers/chat_conversation_lists.js create mode 100644 app/javascript/gabsocial/reducers/chat_conversation_messages.js create mode 100644 app/javascript/gabsocial/reducers/chats.js create mode 100644 app/models/chat_block.rb create mode 100644 app/models/chat_conversation.rb create mode 100644 app/models/chat_conversation_account.rb create mode 100644 app/models/chat_message.rb create mode 100644 app/models/chat_mute.rb create mode 100644 app/serializers/rest/chat_conversation_account_serializer.rb create mode 100644 app/serializers/rest/chat_message_serializer.rb create mode 100644 app/validators/chat_message_length_validator.rb create mode 100644 db/migrate/20201127051070_create_chat_conversations.rb create mode 100644 db/migrate/20201127051071_create_chat_blocks.rb create mode 100644 db/migrate/20201127051072_create_chat_mutes.rb create mode 100644 db/migrate/20201127051073_create_chat_conversation_accounts.rb create mode 100644 db/migrate/20201127051074_create_chat_messages.rb create mode 100644 db/migrate/20201130171423_remove_disabled_from_custom_emojis.rb diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 051f4146..19339bcf 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -51,22 +51,6 @@ module Admin redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) end - def enable - authorize @custom_emoji, :enable? - @custom_emoji.update!(disabled: false) - log_action :enable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def disable - authorize @custom_emoji, :disable? - @custom_emoji.update!(disabled: true) - log_action :disable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - private def set_custom_emoji diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b65ba69f..d7629be4 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -3,12 +3,15 @@ class Api::BaseController < ApplicationController DEFAULT_STATUSES_LIMIT = 20 DEFAULT_ACCOUNTS_LIMIT = 20 + DEFAULT_CHAT_CONVERSATION_LIMIT = 12 + DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 10 include RateLimitHeaders skip_before_action :store_current_location skip_before_action :check_user_permissions + before_action :block_if_doorkeeper before_action :set_cache_headers protect_from_forgery with: :null_session @@ -90,6 +93,14 @@ class Api::BaseController < ApplicationController doorkeeper_authorize!(*scopes) if doorkeeper_token end + def superapp? + doorkeeper_token && doorkeeper_token.application.superapp? || false + end + + def block_if_doorkeeper + raise GabSocial::NotPermittedError unless superapp? + end + def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb new file mode 100644 index 00000000..5cc835c5 --- /dev/null +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::SearchController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action :require_user! + + def show + @accounts = account_search + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def account_search + AccountSearchService.new.call( + params[:q], + current_account, + limit: limit_param(DEFAULT_ACCOUNTS_LIMIT), + resolve: truthy_param?(:resolve), + following: truthy_param?(:following), + offset: params[:offset] + ) + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/chat_conversation_accounts/blocked_accounts_controller.rb b/app/controllers/api/v1/chat_conversation_accounts/blocked_accounts_controller.rb new file mode 100644 index 00000000..3e5bf768 --- /dev/null +++ b/app/controllers/api/v1/chat_conversation_accounts/blocked_accounts_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversationAccounts::BlockedAccountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :follow, :'read:blocks' } + before_action :require_user! + after_action :insert_pagination_headers + + def index + @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def load_accounts + paginated_blocks.map(&:target_account) + end + + def paginated_blocks + @paginated_blocks ||= ChatBlock.eager_load(target_account: :account_stat) + .where(account: current_account) + .paginate_by_max_id( + limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + if records_continue? + api_v1_chat_conversation_accounts_blocked_accounts_url pagination_params(max_id: pagination_max_id) + end + end + + def prev_path + unless paginated_blocks.empty? + api_v1_chat_conversation_accounts_blocked_accounts_url pagination_params(since_id: pagination_since_id) + end + end + + def pagination_max_id + paginated_blocks.last.id + end + + def pagination_since_id + paginated_blocks.first.id + end + + def records_continue? + paginated_blocks.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/chat_conversation_accounts/muted_accounts_controller.rb b/app/controllers/api/v1/chat_conversation_accounts/muted_accounts_controller.rb new file mode 100644 index 00000000..f0f6b6c1 --- /dev/null +++ b/app/controllers/api/v1/chat_conversation_accounts/muted_accounts_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversationAccounts::MutedAccountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :follow, :'read:mutes' } + before_action :require_user! + after_action :insert_pagination_headers + + def index + @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def load_accounts + paginated_mutes.map(&:target_account) + end + + def paginated_mutes + @paginated_mutes ||= ChatMute.eager_load(target_account: :account_stat) + .where(account: current_account) + .paginate_by_max_id( + limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + if records_continue? + api_v1_chat_conversation_accounts_muted_accounts_url pagination_params(max_id: pagination_max_id) + end + end + + def prev_path + unless paginated_mutes.empty? + api_v1_chat_conversation_accounts_muted_accounts_url pagination_params(since_id: pagination_since_id) + end + end + + def pagination_max_id + paginated_mutes.last.id + end + + def pagination_since_id + paginated_mutes.first.id + end + + def records_continue? + paginated_mutes.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/messages_controller.rb b/app/controllers/api/v1/chat_conversation_accounts_controller.rb similarity index 50% rename from app/controllers/api/v1/messages_controller.rb rename to app/controllers/api/v1/chat_conversation_accounts_controller.rb index 207a991d..11ef8459 100644 --- a/app/controllers/api/v1/messages_controller.rb +++ b/app/controllers/api/v1/chat_conversation_accounts_controller.rb @@ -1,24 +1,16 @@ # frozen_string_literal: true -class Api::V1::AccountsController < Api::BaseController - before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute] - before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow] - before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] - before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock] - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] +class Api::V1::ChatConversationAccountsController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :write, :'write:chats' }, only: [:create] - before_action :require_user!, except: [:show, :create] + before_action :require_user! before_action :set_account, except: [:create] - before_action :check_account_suspension, only: [:show] def show # end - def create - # - end - def block BlockMessengerService.new.call(current_user.account, @account) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships @@ -42,18 +34,19 @@ class Api::V1::AccountsController < Api::BaseController private def set_account - @account = Account.find(params[:id]) + # @account = Account.find(params[:id]) end - def relationships(**options) - AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) - end + # def relationships(**options) + # AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) + # end def check_account_suspension gone if @account.suspended? end - def account_params - params.permit(:username, :email, :password, :agreement, :locale) - end + # def account_params + # params.permit(:username, :email, :password, :agreement, :locale) + # end + end diff --git a/app/controllers/api/v1/chat_conversation_controller.rb b/app/controllers/api/v1/chat_conversation_controller.rb new file mode 100644 index 00000000..31f7feec --- /dev/null +++ b/app/controllers/api/v1/chat_conversation_controller.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversationController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' } + before_action -> { doorkeeper_authorize! :write, :'write:chats' } + + before_action :require_user! + before_action :set_account, only: :create + before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_unread] + + def show + puts "tilly ChatConversationsController-0" + render json: {}, each_serializer: REST::ChatConversationAccountSerializer + end + + def create + puts "tilly ChatConversationsController-1" + # : todo : + # check if already created + # check if blocked + # check if chat blocked + # check if allow anyone to message then create with approved:true + # unique account id, participants + chat_conversation_account = find_or_create_conversation + render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer + end + + def mark_chat_conversation_unread + @chat_conversation_account.update!(is_unread: true) + render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer + end + + def mark_chat_conversation_hidden + @chat_conversation_account.update!(is_hidden: true) + render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer + end + + def mark_chat_conversation_approved + @chat_conversation_account.update!(is_approved: true) + render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer + end + + private + + def find_or_create_conversation + chat = ChatConversationAccount.find_by(account: current_account, participant_account_ids: [@account.id.to_s]) + + return chat unless chat.nil? + + chat_conversation = ChatConversation.create + + my_chat = ChatConversationAccount.create!( + account: current_account, + participant_account_ids: [@account.id.to_s], + chat_conversation: chat_conversation, + is_approved: true + ) + + # : todo : if multiple ids + their_chat = ChatConversationAccount.create!( + account: @account, + participant_account_ids: [current_account.id.to_s], + chat_conversation: chat_conversation, + is_approved: false # default as request + ) + + return my_chat + end + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_chat_conversation + @chat_conversation = ChatConversation.find(params[:id]) + @chat_conversation_account = ChatConversationAccount.where( + account: current_account, + chat_conversation: @chat_conversation + ).first + end + +end diff --git a/app/controllers/api/v1/chat_conversations/approved_conversations_controller.rb b/app/controllers/api/v1/chat_conversations/approved_conversations_controller.rb new file mode 100644 index 00000000..c606c23b --- /dev/null +++ b/app/controllers/api/v1/chat_conversations/approved_conversations_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversations::ApprovedConversationsController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' } + + before_action :require_user! + after_action :insert_pagination_headers + + def index + puts "tilly ApprovedConversationsController-0" + @chat_conversations = load_chat_conversations + render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer + end + + def show + puts "tilly ApprovedConversationsController-1" + @chat_conversations = load_chat_conversations + render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer + end + + private + + def load_chat_conversations + paginated_chat_conversations + end + + def paginated_chat_conversations + ChatConversationAccount.where( + account: current_account, + is_hidden: false, + is_approved: true + ).paginate_by_max_id( + limit_param(DEFAULT_CHAT_CONVERSATION_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + if records_continue? + api_v1_chat_conversations_approved_conversations_url pagination_params(max_id: pagination_max_id) + end + end + + def prev_path + unless paginated_chat_conversations.empty? + api_v1_chat_conversations_approved_conversations_url pagination_params(since_id: pagination_since_id) + end + end + + def pagination_max_id + paginated_chat_conversations.last.id + end + + def pagination_since_id + paginated_chat_conversations.first.id + end + + def records_continue? + paginated_chat_conversations.size == limit_param(DEFAULT_CHAT_CONVERSATION_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + +end diff --git a/app/controllers/api/v1/chat_conversations/messages_controller.rb b/app/controllers/api/v1/chat_conversations/messages_controller.rb new file mode 100644 index 00000000..579e6af8 --- /dev/null +++ b/app/controllers/api/v1/chat_conversations/messages_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversations::MessagesController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' } + before_action -> { doorkeeper_authorize! :write, :'write:chats' } + + before_action :require_user! + before_action :set_chat_conversation + before_action :set_chat_messages + + after_action :insert_pagination_headers, unless: -> { @chats.empty? } + + def show + puts "tilly chat_message_conversations - 1: " + @chats.count.inspect + render json: @chats, each_serializer: REST::ChatMessageSerializer + end + + private + + def set_chat_conversation + @chat_conversation = ChatConversation.find(params[:id]) + end + + def set_chat_messages + @chats = cached_conversation_chats + end + + def cached_conversation_chats + cache_collection conversation_chats, ChatMessage + end + + def conversation_chats + chats = ChatMessage.where( + chat_conversation: @chat_conversation + ).paginate_by_id( + limit_param(DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, nil) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + + def next_path + api_v1_chat_conversations_message_url params[:id], pagination_params(since_id: pagination_since_id) + end + + def pagination_since_id + @chats.first.id + end +end diff --git a/app/controllers/api/v1/chat_conversations/requested_conversations_controller.rb b/app/controllers/api/v1/chat_conversations/requested_conversations_controller.rb new file mode 100644 index 00000000..80acc3a7 --- /dev/null +++ b/app/controllers/api/v1/chat_conversations/requested_conversations_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class Api::V1::ChatConversations::RequestedConversationsController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' } + + before_action :require_user! + after_action :insert_pagination_headers + + def index + @chat_conversations = load_chat_conversations + render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer + end + + def count + count = ChatConversationAccount.where(account: current_account, is_hidden: false, is_approved: false).count + render json: count + end + + private + + def load_chat_conversations + paginated_chat_conversations + end + + def paginated_chat_conversations + ChatConversationAccount.where( + account: current_account, + is_hidden: false, + is_approved: false + ).paginate_by_max_id( + limit_param(DEFAULT_CHAT_CONVERSATION_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + if records_continue? + api_v1_chat_conversations_requested_conversations_url pagination_params(max_id: pagination_max_id) + end + end + + def prev_path + unless paginated_chat_conversations.empty? + api_v1_chat_conversations_requested_conversations_url pagination_params(since_id: pagination_since_id) + end + end + + def pagination_max_id + paginated_chat_conversations.last.id + end + + def pagination_since_id + paginated_chat_conversations.first.id + end + + def records_continue? + paginated_chat_conversations.size == limit_param(DEFAULT_CHAT_CONVERSATION_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + +end diff --git a/app/controllers/api/v1/chat_messages_controller.rb b/app/controllers/api/v1/chat_messages_controller.rb new file mode 100644 index 00000000..8b37b642 --- /dev/null +++ b/app/controllers/api/v1/chat_messages_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Api::V1::ChatMessagesController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:chats' } + before_action -> { doorkeeper_authorize! :write, :'write:chats' } + + before_action :require_user! + before_action :set_chat_conversation + + def create + @chat = ChatMessage.create!( + from_account: current_account, + chat_conversation: @chat_conversation, + text: params[:text] + ) + + # : todo : + # Redis.current.publish("chat_messages:10", 'hi') + Redis.current.publish("chat_messages:10", Oj.dump(event: :chat_message, payload: InlineRenderer.render(@chat, current_user.account, :chat_message))) + + render json: @chat, serializer: REST::ChatMessageSerializer + end + + def destroy + @chat = ChatMessage.where(account: current_user.account).find(params[:id]) + authorize @chat, :destroy? + + # : todo : + # make sure last_chat_message_id in chat_account_conversation gets set to last + + @chat.destroy! + + render json: @chat, serializer: REST::ChatMessageSerializer + end + + private + + def set_chat_conversation + @chat_conversation = ChatConversation.find(params[:chat_conversation_id]) + end + + def chat_params + params.permit(:text, :chat_conversation_id) + end + +end diff --git a/app/controllers/api/v1/messages/chats_controller.rb b/app/controllers/api/v1/messages/chats_controller.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/app/controllers/api/v1/messages/converations_controller.rb b/app/controllers/api/v1/messages/converations_controller.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/app/controllers/empty_controller.rb b/app/controllers/empty_controller.rb index 8f215dfe..bde3493e 100644 --- a/app/controllers/empty_controller.rb +++ b/app/controllers/empty_controller.rb @@ -48,4 +48,16 @@ class EmptyController < ActionController::Base nil end + protected + + def limit_param(default_limit) + return default_limit unless params[:limit] + [params[:limit].to_i.abs, default_limit * 2].min + end + + def truthy_param?(key) + ActiveModel::Type::Boolean.new.cast(params[key]) + end + + end diff --git a/app/controllers/react_controller.rb b/app/controllers/react_controller.rb index cb7d4822..810abb8a 100644 --- a/app/controllers/react_controller.rb +++ b/app/controllers/react_controller.rb @@ -52,7 +52,7 @@ class ReactController < ApplicationController end def find_route_matches - request.path.match(/\A\/(home|news|suggestions|links|messages|shortcuts|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|mutes)/) + request.path.match(/\A\/(home|news|api|suggestions|links|chat_conversations|chat_conversation_accounts|messages|shortcuts|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|mutes)/) end def find_public_route_matches diff --git a/app/javascript/gabsocial/actions/accounts.js b/app/javascript/gabsocial/actions/accounts.js index dd752a25..e8b53e96 100644 --- a/app/javascript/gabsocial/actions/accounts.js +++ b/app/javascript/gabsocial/actions/accounts.js @@ -163,6 +163,7 @@ export const followAccount = (id, reblogs = true) => (dispatch, getState) => { dispatch(followAccountRequest(id, locked)) api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then((response) => { + console.log("response:", response) dispatch(followAccountSuccess(response.data, alreadyFollowing)) }).catch((error) => { dispatch(followAccountFail(error, locked)) diff --git a/app/javascript/gabsocial/actions/chat_compose.js b/app/javascript/gabsocial/actions/chat_compose.js deleted file mode 100644 index ac85f788..00000000 --- a/app/javascript/gabsocial/actions/chat_compose.js +++ /dev/null @@ -1,28 +0,0 @@ -import api from '../api' -import { me } from '../initial_state' - -export const MESSAGE_INPUT_CHANGE = 'MESSAGE_INPUT_CHANGE' -export const MESSAGE_INPUT_RESET = 'MESSAGE_INPUT_RESET' - -/** - * - */ -export const messageInputChange = (text) => (dispatch, getState) => { - if (!me) return - - //Ensure has conversation - const conversationId = getState().getIn(['chat_conversations', 'current', 'conversation_id'], null) - if (!conversationId) return - - dispatch({ - type: MESSAGE_INPUT_CHANGE, - text, - }) -} - -/** - * - */ -export const messageInputReset = (dispatch) => { - dispatch({ type: MESSAGE_INPUT_RESET }) -} diff --git a/app/javascript/gabsocial/actions/chat_conversation_messages.js b/app/javascript/gabsocial/actions/chat_conversation_messages.js new file mode 100644 index 00000000..2e8cb4ec --- /dev/null +++ b/app/javascript/gabsocial/actions/chat_conversation_messages.js @@ -0,0 +1,111 @@ +import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable' +import noop from 'lodash.noop' +import api, { getLinks } from '../api' +import { me } from '../initial_state' +import { importFetchedChatMessages } from './importer' + +export const CHAT_CONVERSATION_MESSAGES_EXPAND_REQUEST = 'CHAT_CONVERSATION_MESSAGES_EXPAND_REQUEST' +export const CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS = 'CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS' +export const CHAT_CONVERSATION_MESSAGES_EXPAND_FAIL = 'CHAT_CONVERSATION_MESSAGES_EXPAND_FAIL' + +// + +export const CHAT_CONVERSATION_MESSAGES_CONNECT = 'CHAT_CONVERSATION_MESSAGES_CONNECT' +export const CHAT_CONVERSATION_MESSAGES_DISCONNECT = 'CHAT_CONVERSATION_MESSAGES_DISCONNECT' +export const CHAT_CONVERSATION_MESSAGES_CLEAR = 'CHAT_CONVERSATION_MESSAGES_CLEAR' +export const CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM = 'CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM' + +/** + * + */ +export const connectChatMessageConversation = (chatConversationId) => ({ + type: CHAT_CONVERSATION_MESSAGES_CONNECT, + chatConversationId, +}) + +/** + * + */ +export const disconnectChatMessageConversation = (chatConversationId) => ({ + type: CHAT_CONVERSATION_MESSAGES_DISCONNECT, + chatConversationId, +}) + +/** + * + */ +export const clearChatMessageConversation = (chatConversationId) => (dispatch) => { + dispatch({ + type: CHAT_CONVERSATION_MESSAGES_CLEAR, + chatConversationId + }) +} + +/** + * + */ +export const scrollBottomChatMessageConversation = (chatConversationId, top) => ({ + type: CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM, + chatConversationId, + top, +}) + +/** + * + */ +export const expandChatMessages = (chatConversationId, params = {}, done = noop) => (dispatch, getState) => { + if (!me || !chatConversationId) return + + const chatConversation = getState().getIn(['chat_messages', chatConversationId], ImmutableMap()) + const isLoadingMore = !!params.maxId + + if (!!chatConversation && (chatConversation.get('isLoading') || chatConversation.get('isError'))) { + done() + return + } + + if (!params.maxId && chatConversation.get('items', ImmutableList()).size > 0) { + params.sinceId = chatConversation.getIn(['items', 0]) + } + + const isLoadingRecent = !!params.sinceId + + dispatch(expandChatMessagesRequest(chatConversationId, isLoadingMore)) + + api(getState).get(`/api/v1/chat_conversations/messages/${chatConversationId}`, { params }).then((response) => { + console.log("response:", response) + const next = getLinks(response).refs.find(link => link.rel === 'next') + console.log("next:", next, getLinks(response).refs) + dispatch(importFetchedChatMessages(response.data)) + dispatch(expandChatMessagesSuccess(chatConversationId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)) + done() + }).catch((error) => { + console.log("error:", error) + dispatch(expandChatMessagesFail(chatConversationId, error, isLoadingMore)) + done() + }) +} + +export const expandChatMessagesRequest = (chatConversationId, isLoadingMore) => ({ + type: CHAT_CONVERSATION_MESSAGES_EXPAND_REQUEST, + chatConversationId, + skipLoading: !isLoadingMore, +}) + +export const expandChatMessagesSuccess = (chatConversationId, chatMessages, next, partial, isLoadingRecent, isLoadingMore) => ({ + type: CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS, + chatConversationId, + chatMessages, + next, + partial, + isLoadingRecent, + skipLoading: !isLoadingMore, +}) + +export const expandChatMessagesFail = (chatConversationId, error, isLoadingMore) => ({ + type: CHAT_CONVERSATION_MESSAGES_EXPAND_FAIL, + chatConversationId, + error, + skipLoading: !isLoadingMore, +}) + diff --git a/app/javascript/gabsocial/actions/chat_conversations.js b/app/javascript/gabsocial/actions/chat_conversations.js index 6263209f..4a594ac3 100644 --- a/app/javascript/gabsocial/actions/chat_conversations.js +++ b/app/javascript/gabsocial/actions/chat_conversations.js @@ -3,13 +3,35 @@ import { fetchRelationships } from './accounts' import { importFetchedAccounts } from './importer' import { me } from '../initial_state' -export const CONVERSATION_BLOCKS_FETCH_REQUEST = 'CONVERSATION_BLOCKS_FETCH_REQUEST' -export const CONVERSATION_BLOCKS_FETCH_SUCCESS = 'CONVERSATION_BLOCKS_FETCH_SUCCESS' -export const CONVERSATION_BLOCKS_FETCH_FAIL = 'CONVERSATION_BLOCKS_FETCH_FAIL' +// -export const CONVERSATION_BLOCKS_EXPAND_REQUEST = 'CONVERSATION_BLOCKS_EXPAND_REQUEST' -export const CONVERSATION_BLOCKS_EXPAND_SUCCESS = 'CONVERSATION_BLOCKS_EXPAND_SUCCESS' -export const CONVERSATION_BLOCKS_EXPAND_FAIL = 'CONVERSATION_BLOCKS_EXPAND_FAIL' +export const CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST' +export const CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS' +export const CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL = 'CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL' + +export const CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST' +export const CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS' +export const CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL' + +// + +export const CHAT_CONVERSATIONS_CREATE_REQUEST = 'CHAT_CONVERSATIONS_CREATE_REQUEST' +export const CHAT_CONVERSATIONS_CREATE_SUCCESS = 'CHAT_CONVERSATIONS_CREATE_SUCCESS' +export const CHAT_CONVERSATIONS_CREATE_FAIL = 'CHAT_CONVERSATIONS_CREATE_FAIL' + +export const CHAT_CONVERSATIONS_DELETE_REQUEST = 'CHAT_CONVERSATIONS_DELETE_REQUEST' +export const CHAT_CONVERSATIONS_DELETE_SUCCESS = 'CHAT_CONVERSATIONS_DELETE_SUCCESS' +export const CHAT_CONVERSATIONS_DELETE_FAIL = 'CHAT_CONVERSATIONS_DELETE_FAIL' + +// + +export const CHAT_CONVERSATION_BLOCKS_FETCH_REQUEST = 'CHAT_CONVERSATION_BLOCKS_FETCH_REQUEST' +export const CHAT_CONVERSATION_BLOCKS_FETCH_SUCCESS = 'CHAT_CONVERSATION_BLOCKS_FETCH_SUCCESS' +export const CHAT_CONVERSATION_BLOCKS_FETCH_FAIL = 'CHAT_CONVERSATION_BLOCKS_FETCH_FAIL' + +export const CHAT_CONVERSATION_BLOCKS_EXPAND_REQUEST = 'CHAT_CONVERSATION_BLOCKS_EXPAND_REQUEST' +export const CHAT_CONVERSATION_BLOCKS_EXPAND_SUCCESS = 'CHAT_CONVERSATION_BLOCKS_EXPAND_SUCCESS' +export const CHAT_CONVERSATION_BLOCKS_EXPAND_FAIL = 'CHAT_CONVERSATION_BLOCKS_EXPAND_FAIL' export const BLOCK_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST' export const BLOCK_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS' @@ -21,13 +43,13 @@ export const UNBLOCK_MESSAGER_FAIL = 'UNBLOCK_MESSAGER_FAIL' // -export const CONVERSATION_MUTES_FETCH_REQUEST = 'CONVERSATION_MUTES_FETCH_REQUEST' -export const CONVERSATION_MUTES_FETCH_SUCCESS = 'CONVERSATION_MUTES_FETCH_SUCCESS' -export const CONVERSATION_MUTES_FETCH_FAIL = 'CONVERSATION_MUTES_FETCH_FAIL' +export const CHAT_CONVERSATION_MUTES_FETCH_REQUEST = 'CHAT_CONVERSATION_MUTES_FETCH_REQUEST' +export const CHAT_CONVERSATION_MUTES_FETCH_SUCCESS = 'CHAT_CONVERSATION_MUTES_FETCH_SUCCESS' +export const CHAT_CONVERSATION_MUTES_FETCH_FAIL = 'CHAT_CONVERSATION_MUTES_FETCH_FAIL' -export const CONVERSATION_MUTES_EXPAND_REQUEST = 'CONVERSATION_MUTES_EXPAND_REQUEST' -export const CONVERSATION_MUTES_EXPAND_SUCCESS = 'CONVERSATION_MUTES_EXPAND_SUCCESS' -export const CONVERSATION_MUTES_EXPAND_FAIL = 'CONVERSATION_MUTES_EXPAND_FAIL' +export const CHAT_CONVERSATION_MUTES_EXPAND_REQUEST = 'CHAT_CONVERSATION_MUTES_EXPAND_REQUEST' +export const CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS = 'CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS' +export const CHAT_CONVERSATION_MUTES_EXPAND_FAIL = 'CHAT_CONVERSATION_MUTES_EXPAND_FAIL' export const MUTE_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST' export const MUTE_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS' @@ -39,25 +61,238 @@ export const UNMUTE_MESSAGER_FAIL = 'UNMUTE_MESSAGER_FAIL' // -export const CONVERSATION_REQUEST_APPROVE_SUCCESS = 'CONVERSATION_REQUEST_APPROVE_SUCCESS' -export const CONVERSATION_REQUEST_APPROVE_FAIL = 'CONVERSATION_REQUEST_APPROVE_FAIL' +export const CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS = 'CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS' -export const CONVERSATION_REQUEST_REJECT_SUCCESS = 'CONVERSATION_REQUEST_REJECT_SUCCESS' -export const CONVERSATION_REQUEST_REJECT_FAIL = 'CONVERSATION_REQUEST_REJECT_FAIL' +export const CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST' +export const CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS' +export const CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL' -export const CONVERSATION_DELETE_REQUEST = 'CONVERSATION_DELETE_REQUEST' -export const CONVERSATION_DELETE_SUCCESS = 'CONVERSATION_DELETE_SUCCESS' -export const CONVERSATION_DELETE_FAIL = 'CONVERSATION_DELETE_FAIL' +export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST' +export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS' +export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL' // -export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST' -export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS' -export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL' +export const CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS = 'CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS' +export const CHAT_CONVERSATION_REQUEST_APPROVE_FAIL = 'CHAT_CONVERSATION_REQUEST_APPROVE_FAIL' + +export const CHAT_CONVERSATION_DELETE_REQUEST = 'CHAT_CONVERSATION_DELETE_REQUEST' +export const CHAT_CONVERSATION_DELETE_SUCCESS = 'CHAT_CONVERSATION_DELETE_SUCCESS' +export const CHAT_CONVERSATION_DELETE_FAIL = 'CHAT_CONVERSATION_DELETE_FAIL' + +/** + * + */ +export const fetchChatConversations = () => (dispatch, getState) => { + if (!me) return + + dispatch(fetchChatConversationsRequest()) + + api(getState).get('/api/v1/chat_conversations/approved_conversations').then((response) => { + console.log("chat_conversations response: ", response) + + const next = getLinks(response).refs.find(link => link.rel === 'next') + const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts)) + const conversationsChatMessages = response.data.map((c) => c.last_chat_message) + + dispatch(importFetchedAccounts(conversationsAccounts)) + // dispatch(importFetchedChatMessages(conversationsChatMessages)) + dispatch(fetchChatConversationsSuccess(response.data, next ? next.uri : null)) + }).catch((error) => { + console.log("fetchChatConversationsFail:", error) + dispatch(fetchChatConversationsFail(error)) + }) +} + +export const fetchChatConversationsRequest = () => ({ + type: CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST, +}) + +export const fetchChatConversationsSuccess = (chatConversations, next) => ({ + type: CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS, + chatConversations, + next, +}) + +export const fetchChatConversationsFail = (error) => ({ + type: CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL, + error, +}) + +/** + * + */ +export const expandChatConversations = () => (dispatch, getState) => { + if (!me) return + + const url = getState().getIn(['chat_conversations', 'approved', 'next']) + const isLoading = getState().getIn(['chat_conversations', 'approved', 'isLoading']) + + if (url === null || isLoading) return + + dispatch(expandChatConversationsRequest()) + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next') + const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts)) + const conversationsChatMessages = response.data.map((c) => c.last_chat_message) + + dispatch(importFetchedAccounts(conversationsAccounts)) + // dispatch(importFetchedChatMessages(conversationsChatMessages)) + dispatch(expandChatConversationsSuccess(response.data, next ? next.uri : null)) + }).catch(error => dispatch(expandChatConversationsFail(error))) +} + +export const expandChatConversationsRequest = () => ({ + type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS, +}) + +export const expandChatConversationsSuccess = (chatConversations, next) => ({ + type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS, + chatConversations, + next, +}) + +export const expandChatConversationsFail = (error) => ({ + type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS, + error, +}) + +/** + * + */ +export const fetchChatConversationRequested = () => (dispatch, getState) => { + if (!me) return + + dispatch(fetchChatConversationRequestedRequest()) + + api(getState).get('/api/v1/chat_conversations/requested_conversations').then((response) => { + const next = getLinks(response).refs.find(link => link.rel === 'next') + const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts)) + const conversationsChatMessages = response.data.map((c) => c.last_chat_message) + + dispatch(importFetchedAccounts(conversationsAccounts)) + // dispatch(importFetchedChatMessages(conversationsChatMessages)) + dispatch(fetchChatConversationRequestedSuccess(response.data, next ? next.uri : null)) + }).catch((error) => { + console.log("error:", error) + dispatch(fetchChatConversationRequestedFail(error)) + }) +} + +export const fetchChatConversationRequestedRequest = () => ({ + type: CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST, +}) + +export const fetchChatConversationRequestedSuccess = (chatConversations, next) => ({ + type: CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS, + chatConversations, + next, +}) + +export const fetchChatConversationRequestedFail = (error) => ({ + type: CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL, + error, +}) + +/** + * + */ +export const expandChatConversationRequested = () => (dispatch, getState) => { + if (!me) return + + const url = getState().getIn(['chat_conversations', 'requested', 'next']) + const isLoading = getState().getIn(['chat_conversations', 'requested', 'isLoading']) + + if (url === null || isLoading) return + + dispatch(expandChatConversationRequestedRequest()) + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next') + const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts)) + const conversationsChatMessages = response.data.map((c) => c.last_chat_message) + + dispatch(importFetchedAccounts(conversationsAccounts)) + // dispatch(importFetchedChatMessages(conversationsChatMessages)) + dispatch(expandChatConversationRequestedSuccess(response.data, next ? next.uri : null)) + }).catch(error => dispatch(expandChatConversationRequestedFail(error))) +} + +export const expandChatConversationRequestedRequest = () => ({ + type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST, +}) + +export const expandChatConversationRequestedSuccess = (chatConversations, next) => ({ + type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS, + chatConversations, + next, +}) + +export const expandChatConversationRequestedFail = (error) => ({ + type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL, + error, +}) + +/** + * + */ +export const createChatConversation = (accountId) => (dispatch, getState) => { + if (!me || !accountId) return + + dispatch(createChatConversationRequest()) + + api(getState).post('/api/v1/chat_conversation', { account_id: accountId }).then((response) => { + dispatch(createChatConversationSuccess(response.data)) + }).catch((error) => { + dispatch(createChatConversationFail(error)) + }) +} + +export const createChatConversationRequest = () => ({ + type: CHAT_CONVERSATIONS_CREATE_REQUEST, +}) + +export const createChatConversationSuccess = (chatConversation) => ({ + type: CHAT_CONVERSATIONS_CREATE_SUCCESS, + chatConversation, +}) + +export const createChatConversationFail = (error) => ({ + type: CHAT_CONVERSATIONS_CREATE_FAIL, + error, +}) + +/** + * + */ +export const deleteChatConversation = (chatConversationId) => (dispatch, getState) => { + if (!me || !chatConversationId) return + + dispatch(deleteChatConversationRequest(conversationId)) + + api(getState).delete(`/api/v1/chat_conversation/${chatConversationId}`).then((response) => { + console.log("chat_conversations delete response: ", response) + dispatch(deleteChatConversationSuccess()) + }).catch((error) => { + dispatch(deleteChatConversationFail(error)) + }) +} + +export const deleteChatConversationRequest = (conversationId) => ({ + type: CHAT_CONVERSATIONS_DELETE_REQUEST, + conversationId, +}) + +export const deleteChatConversationSuccess = () => ({ + type: CHAT_CONVERSATIONS_DELETE_SUCCESS, +}) + +export const deleteChatConversationFail = (error) => ({ + type: CHAT_CONVERSATIONS_DELETE_FAIL, + error, +}) -export const CONVERSATIONS_EXPAND_REQUEST = 'CONVERSATIONS_EXPAND_REQUEST' -export const CONVERSATIONS_EXPAND_SUCCESS = 'CONVERSATIONS_EXPAND_SUCCESS' -export const CONVERSATIONS_EXPAND_FAIL = 'CONVERSATIONS_EXPAND_FAIL' /** * @@ -110,12 +345,12 @@ const unblockMessengerRequest = (accountId) => ({ accountId, }) -const blockMessengerSuccess = (data) => ({ +const unblockMessengerSuccess = (data) => ({ type: UNBLOCK_MESSAGER_REQUEST, data, }) -const blockMessengerFail = (accountId, error) => ({ +const unblockMessengerFail = (accountId, error) => ({ type: UNBLOCK_MESSAGER_REQUEST, accountId, error, @@ -137,20 +372,20 @@ export const fetchBlocks = () => (dispatch, getState) => { }).catch(error => dispatch(fetchBlocksFail(error))) } -export const fetchBlocksRequest = () => ({ - type: BLOCKS_FETCH_REQUEST, -}) +// export const fetchBlocksRequest = () => ({ +// type: BLOCKS_FETCH_REQUEST, +// }) -export const fetchBlocksSuccess = (accounts, next) => ({ - type: BLOCKS_FETCH_SUCCESS, - accounts, - next, -}) +// export const fetchBlocksSuccess = (accounts, next) => ({ +// type: BLOCKS_FETCH_SUCCESS, +// accounts, +// next, +// }) -export const fetchBlocksFail = (error) => ({ - type: BLOCKS_FETCH_FAIL, - error, -}) +// export const fetchBlocksFail = (error) => ({ +// type: BLOCKS_FETCH_FAIL, +// error, +// }) /** * @@ -173,17 +408,51 @@ export const expandBlocks = () => (dispatch, getState) => { }).catch(error => dispatch(expandBlocksFail(error))) } -export const expandBlocksRequest = () => ({ - type: BLOCKS_EXPAND_REQUEST, +// export const expandBlocksRequest = () => ({ +// type: BLOCKS_EXPAND_REQUEST, +// }) + +// export const expandBlocksSuccess = (accounts, next) => ({ +// type: BLOCKS_EXPAND_SUCCESS, +// accounts, +// next, +// }) + +// export const expandBlocksFail = (error) => ({ +// type: BLOCKS_EXPAND_FAIL, +// error, +// }) + +/** + * + */ +export const fetchChatConversationRequestedCount = () => (dispatch, getState) => { + if (!me) return + + api(getState).get('/api/v1/chat_conversations/requested_conversations/count').then(response => { + dispatch({ + type: CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS, + count: response.data, + }) + }) +} + +/** + * + */ +export const approveChatConversationRequest = (chatConversationId) => (dispatch, getState) => { + if (!me|| !chatConversationId) return + + api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_approved`).then((response) => { + dispatch(approveChatConversationRequestSuccess(response.data)) + }).catch((error) => dispatch(approveChatConversationRequestFail(error))) +} + +export const approveChatConversationRequestSuccess = (chatConversation) => ({ + type: CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS, + chatConversation, }) -export const expandBlocksSuccess = (accounts, next) => ({ - type: BLOCKS_EXPAND_SUCCESS, - accounts, - next, -}) - -export const expandBlocksFail = (error) => ({ - type: BLOCKS_EXPAND_FAIL, - error, -}) +export const approveChatConversationRequestFail = () => ({ + type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL, +}) \ No newline at end of file diff --git a/app/javascript/gabsocial/actions/chat_messages.js b/app/javascript/gabsocial/actions/chat_messages.js index e65912c9..3c89cac0 100644 --- a/app/javascript/gabsocial/actions/chat_messages.js +++ b/app/javascript/gabsocial/actions/chat_messages.js @@ -1,86 +1,85 @@ +import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable' +import noop from 'lodash.noop' import api from '../api' import { me } from '../initial_state' +import { importFetchedChatMessages } from './importer' -export const MESSAGE_SEND_REQUEST = 'MESSAGE_SEND_REQUEST' -export const MESSAGE_SEND_SUCCESS = 'MESSAGE_SEND_SUCCESS' -export const MESSAGE_SEND_FAIL = 'MESSAGE_SEND_FAIL' +export const CHAT_MESSAGES_SEND_REQUEST = 'CHAT_MESSAGES_SEND_REQUEST' +export const CHAT_MESSAGES_SEND_SUCCESS = 'CHAT_MESSAGES_SEND_SUCCESS' +export const CHAT_MESSAGES_SEND_FAIL = 'CHAT_MESSAGES_SEND_FAIL' -export const MESSAGE_DELETE_REQUEST = 'MESSAGE_DELETE_REQUEST' -export const MESSAGE_DELETE_SUCCESS = 'MESSAGE_DELETE_SUCCESS' -export const MESSAGE_DELETE_FAIL = 'MESSAGE_DELETE_FAIL' +export const CHAT_MESSAGES_DELETE_REQUEST = 'CHAT_MESSAGES_DELETE_REQUEST' +export const CHAT_MESSAGES_DELETE_SUCCESS = 'CHAT_MESSAGES_DELETE_SUCCESS' +export const CHAT_MESSAGES_DELETE_FAIL = 'CHAT_MESSAGES_DELETE_FAIL' /** * */ -const sendMessage = (text, conversationId) => (dispatch, getState) => { - if (!me) return +export const sendChatMessage = (text = '', chatConversationId) => (dispatch, getState) => { + if (!me || !chatConversationId) return + if (text.length === 0) return - // : todo : - // let text = getState().getIn(['chat_messages', 'text'], '') - // let conversationId = getState().getIn(['chat_messags', 'conversation_id'], '') - dispatch(sendMessageRequest()) - api(getState).put('/api/v1/messages/chat', { + api(getState).post('/api/v1/chat_messages', { text, - conversationId, + chat_conversation_id: chatConversationId, }, { - headers: { - 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), - }, + // headers: { + // 'Idempotency-Key': getState().getIn(['chat_compose`', 'idempotencyKey']), + // }, }).then((response) => { - sendMessageSuccess(response) + dispatch(importFetchedChatMessages([response.data])) + dispatch(sendMessageSuccess(response.data, chatConversationId)) }).catch((error) => { dispatch(sendMessageFail(error)) }) } -const sendMessageRequest = (text, conversationId) => ({ - type: MESSAGE_SEND_REQUEST, - text, - conversationId, +const sendMessageRequest = () => ({ + type: CHAT_MESSAGES_SEND_REQUEST, }) -const sendMessageSuccess = () => ({ - type: MESSAGE_SEND_SUCCESS, +const sendMessageSuccess = (chatMessage, chatConversationId) => ({ + type: CHAT_MESSAGES_SEND_SUCCESS, + chatMessage, + chatConversationId, }) const sendMessageFail = (error) => ({ - type: MESSAGE_SEND_FAIL, + type: CHAT_MESSAGES_SEND_FAIL, error, }) /** * */ -const deleteMessage = (messageId) => (dispatch, getState) => { - if (!me || !messageId) return +const deleteMessage = (chatMessageId) => (dispatch, getState) => { + if (!me || !chatMessageId) return - // : todo : - - dispatch(sendMessageRequest()) + dispatch(deleteMessageRequest(chatMessageId)) - api(getState).delete(`/api/v1/messages/chat/${messageId}`, {}, { - headers: { - 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), - }, + api(getState).delete(`/api/v1/chat_messages/${chatMessageId}`, {}, { + // headers: { + // 'Idempotency-Key': getState().getIn(['chat_compose', 'idempotencyKey']), + // }, }).then((response) => { - sendMessageSuccess(response) + deleteMessageSuccess(response) }).catch((error) => { - dispatch(sendMessageFail(error)) + dispatch(deleteMessageFail(error)) }) } -const deleteMessageRequest = (messageId) => ({ - type: MESSAGE_DELETE_REQUEST, - messageId, +const deleteMessageRequest = (chatMessageId) => ({ + type: CHAT_MESSAGES_DELETE_REQUEST, + chatMessageId, }) const deleteMessageSuccess = () => ({ - type: MESSAGE_DELETE_SUCCESS, + type: CHAT_MESSAGES_DELETE_SUCCESS, }) const deleteMessageFail = (error) => ({ - type: MESSAGE_DELETE_FAIL, + type: CHAT_MESSAGES_DELETE_FAIL, error, }) \ No newline at end of file diff --git a/app/javascript/gabsocial/actions/chats.js b/app/javascript/gabsocial/actions/chats.js new file mode 100644 index 00000000..2aeff5a7 --- /dev/null +++ b/app/javascript/gabsocial/actions/chats.js @@ -0,0 +1,41 @@ +import throttle from 'lodash.throttle' +import api, { getLinks } from '../api' +import { importFetchedAccounts } from './importer' +import { me } from '../initial_state' + +export const CHAT_CONVERSATION_CREATE_SEARCH_ACCOUNTS_SUCCESS = 'CHAT_CONVERSATION_CREATE_SEARCH_ACCOUNTS_SUCCESS' + +export const SET_CHAT_CONVERSATION_SELECTED = 'SET_CHAT_CONVERSATION_SELECTED' + +/** + * + */ +export const fetchChatConversationAccountSuggestions = (query) => throttle((dispatch, getState) => { + api(getState).get('/api/v1/accounts/search', { + params: { + q: query, + resolve: false, + limit: 4, + }, + }).then((response) => { + dispatch(importFetchedAccounts(response.data)) + dispatch(fetchChatConversationAccountSuggestionsSuccess(response.data)) + }).catch((error) => { + // + }) +}, 200, { leading: true, trailing: true }) + +const fetchChatConversationAccountSuggestionsSuccess = (accounts) => ({ + type: CHAT_CONVERSATION_CREATE_SEARCH_ACCOUNTS_SUCCESS, + accounts, +}) + +/** + * + */ +export const setChatConversationSelected = (chatConversationId) => (dispatch) => { + dispatch({ + type: SET_CHAT_CONVERSATION_SELECTED, + chatConversationId, + }) +} \ No newline at end of file diff --git a/app/javascript/gabsocial/actions/importer/index.js b/app/javascript/gabsocial/actions/importer/index.js index c98a256b..e3caee22 100644 --- a/app/javascript/gabsocial/actions/importer/index.js +++ b/app/javascript/gabsocial/actions/importer/index.js @@ -12,6 +12,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT' export const STATUS_IMPORT = 'STATUS_IMPORT' export const STATUSES_IMPORT = 'STATUSES_IMPORT' export const POLLS_IMPORT = 'POLLS_IMPORT' +export const CHAT_MESSAGES_IMPORT = 'CHAT_MESSAGES_IMPORT' export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP' /** @@ -48,6 +49,11 @@ export const importPolls = (polls) => ({ polls, }) +export const importChatMessages = (chatMessages) => ({ + type: CHAT_MESSAGES_IMPORT, + chatMessages, +}) + export const importFetchedAccount = (account) => { return importFetchedAccounts([account]); } @@ -97,7 +103,7 @@ export const importFetchedStatuses = (statuses) => (dispatch, getState) => { } } - statuses.forEach(processStatus) + statuses.forEach(processStatus) dispatch(importPolls(polls)) dispatch(importFetchedAccounts(accounts)) @@ -113,3 +119,7 @@ export const importErrorWhileFetchingAccountByUsername = (username) => ({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username }) + +export const importFetchedChatMessages = (chatMessages) => (dispatch, getState) => { + dispatch(importChatMessages(chatMessages)) +} \ No newline at end of file diff --git a/app/javascript/gabsocial/actions/lists.js b/app/javascript/gabsocial/actions/lists.js index 27d77873..03c85ced 100644 --- a/app/javascript/gabsocial/actions/lists.js +++ b/app/javascript/gabsocial/actions/lists.js @@ -297,7 +297,7 @@ export const fetchListSuggestions = (q) => (dispatch, getState) => { /** * */ -export const fetchListSuggestionsReady = (query, accounts) => ({ +const fetchListSuggestionsReady = (query, accounts) => ({ type: LIST_EDITOR_SUGGESTIONS_READY, query, accounts, diff --git a/app/javascript/gabsocial/actions/streaming.js b/app/javascript/gabsocial/actions/streaming.js index e33caeb5..592b9e51 100644 --- a/app/javascript/gabsocial/actions/streaming.js +++ b/app/javascript/gabsocial/actions/streaming.js @@ -75,15 +75,18 @@ export const connectUserStream = () => connectTimelineStream('home', 'user') /** * */ -export const connectMessageStream = () => { - - return connectStream('chat_messages', null, (dispatch, getState) => { - +export const connectChatMessagesStream = (accountId) => { + return connectStream(`chat_messages:${accountId}`, null, (dispatch, getState) => { return { - onConnect() {}, - onDisconnect() {}, + onConnect() { + // console.log("chat messages connected") + }, + onDisconnect() { + // console.log("chat messages disconnected") + }, onReceive (data) { - // + // : todo : + console.log("chat messages onReceive:", data) }, } }) diff --git a/app/javascript/gabsocial/components/account.js b/app/javascript/gabsocial/components/account.js index 45f17804..9e586896 100644 --- a/app/javascript/gabsocial/components/account.js +++ b/app/javascript/gabsocial/components/account.js @@ -24,10 +24,13 @@ import Avatar from './avatar' import DisplayName from './display_name' import Button from './button' import Text from './text' + class Account extends ImmutablePureComponent { handleAction = (e) => { this.props.onActionClick(this.props.account, e) + e.preventDefault() + return false } handleUnrequest = () => { diff --git a/app/javascript/gabsocial/components/avatar_group.js b/app/javascript/gabsocial/components/avatar_group.js new file mode 100644 index 00000000..16b2e626 --- /dev/null +++ b/app/javascript/gabsocial/components/avatar_group.js @@ -0,0 +1,61 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ImmutablePropTypes from 'react-immutable-proptypes' +import ImmutablePureComponent from 'react-immutable-pure-component' +import Image from './image' + + +/** + * Renders an avatar component + * @param {list} [props.accounts] - the accounts for images + * @param {number} [props.size=40] - the size of the avatar + */ +class AvatarGroup extends ImmutablePureComponent { + + render() { + const { accounts, size } = this.props + + return ( +
+ { + accounts.map((account) => { + const isPro = account.get('is_pro') + const alt = `${account.get('display_name')} ${isPro ? '(PRO)' : ''}`.trim() + const className = [_s.d, _s.circle, _s.overflowHidden] + if (isPro) { + className.push(_s.boxShadowAvatarPro) + } + + const options = { + alt, + className, + src: account.get('avatar_static'), + style: { + width: `${size}px`, + height: `${size}px`, + }, + } + + return ( +
+ +
+ ) + }) + } +
+ ) + } + +} + +AvatarGroup.propTypes = { + accounts: ImmutablePropTypes.list, + size: PropTypes.number, +} + +AvatarGroup.defaultProps = { + size: 40, +} + +export default AvatarGroup \ No newline at end of file diff --git a/app/javascript/gabsocial/components/display_name_group.js b/app/javascript/gabsocial/components/display_name_group.js new file mode 100644 index 00000000..3ed02894 --- /dev/null +++ b/app/javascript/gabsocial/components/display_name_group.js @@ -0,0 +1,81 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import ImmutablePropTypes from 'react-immutable-proptypes' +import ImmutablePureComponent from 'react-immutable-pure-component' +import { me } from '../initial_state' +import { CX } from '../constants' +import Icon from './icon' +import Text from './text' + +class DisplayNameGroup extends ImmutablePureComponent { + + render() { + const { + accounts, + isMultiline, + isLarge, + noHover, + isSmall, + } = this.props + + if (!account) return null + + const containerClassName = CX({ + d: 1, + maxW100PC: 1, + aiCenter: !isMultiline, + flexRow: !isMultiline, + cursorPointer: !noHover, + aiCenter: isCentered, + }) + + const displayNameClasses = CX({ + text: 1, + overflowWrapBreakWord: 1, + whiteSpaceNoWrap: 1, + fw600: 1, + cPrimary: 1, + mr2: 1, + lineHeight125: !isSmall, + fs14PX: isSmall, + fs15PX: !isLarge, + fs24PX: isLarge && !isSmall, + }) + + const usernameClasses = CX({ + text: 1, + displayFlex: 1, + flexNormal: 1, + flexShrink1: 1, + overflowWrapBreakWord: 1, + textOverflowEllipsis: 1, + cSecondary: 1, + fw400: 1, + lineHeight15: isMultiline, + lineHeight125: !isMultiline, + ml5: !isMultiline, + fs14PX: isSmall, + fs15PX: !isLarge, + fs16PX: isLarge && !isSmall, + }) + + const iconSize = + !!isLarge ? 19 : + !!isComment ? 12 : + !!isSmall ? 14 : 15 + + return ( +
+ ) + } +} + +DisplayNameGroup.propTypes = { + accounts: ImmutablePropTypes.map, + isLarge: PropTypes.bool, + isMultiline: PropTypes.bool, + isSmall: PropTypes.bool, +} + +export default DisplayNameGroup \ No newline at end of file diff --git a/app/javascript/gabsocial/components/floating_action_button.js b/app/javascript/gabsocial/components/floating_action_button.js index b2de7475..6653591e 100644 --- a/app/javascript/gabsocial/components/floating_action_button.js +++ b/app/javascript/gabsocial/components/floating_action_button.js @@ -3,21 +3,44 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import { defineMessages, injectIntl } from 'react-intl' import { me } from '../initial_state' -import { CX } from '../constants' +import { getWindowDimension } from '../utils/is_mobile' +import { + CX, + MODAL_COMPOSE, + BREAKPOINT_EXTRA_SMALL, +} from '../constants' import { openModal } from '../actions/modal' import Button from './button' +const initialState = getWindowDimension() + class FloatingActionButton extends React.PureComponent { + state = { + width: initialState.width, + } + + componentDidMount() { + this.handleResize() + window.addEventListener('resize', this.handleResize, false) + } + + handleResize = () => { + const { width } = getWindowDimension() + this.setState({ width }) + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize, false) + } + render() { - const { - intl, - onOpenCompose, - isDesktop, - } = this.props + const { intl, onOpenCompose } = this.props + const { width } = this.state if (!me) return null + const isDesktop = width > BREAKPOINT_EXTRA_SMALL const message = intl.formatMessage(messages.gab) const containerClasses = CX({ @@ -56,13 +79,12 @@ const messages = defineMessages({ }) const mapDispatchToProps = (dispatch) => ({ - onOpenCompose: () => dispatch(openModal('COMPOSE')), + onOpenCompose: () => dispatch(openModal(MODAL_COMPOSE)), }) FloatingActionButton.propTypes = { intl: PropTypes.object.isRequired, onOpenCompose: PropTypes.func.isRequired, - isDesktop: PropTypes.bool, } export default injectIntl(connect(null, mapDispatchToProps)(FloatingActionButton)) \ No newline at end of file diff --git a/app/javascript/gabsocial/components/modal/chat_conversation_create_modal.js b/app/javascript/gabsocial/components/modal/chat_conversation_create_modal.js new file mode 100644 index 00000000..ae9d0f56 --- /dev/null +++ b/app/javascript/gabsocial/components/modal/chat_conversation_create_modal.js @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ModalLayout from './modal_layout' +import { ChatConversationCreate } from '../../features/ui/util/async_components' +import WrappedBundle from '../../features/ui/util/wrapped_bundle' + +class ChatConversationCreateModal extends React.PureComponent { + + render() { + const { onClose, chatConversationId } = this.props + + return ( + + + + ) + } + +} + +ChatConversationCreateModal.propTypes = { + onClose: PropTypes.func.isRequired, + chatConversationId: PropTypes.string, +} + +export default ChatConversationCreateModal \ No newline at end of file diff --git a/app/javascript/gabsocial/components/modal/chat_conversation_delete_modal.js b/app/javascript/gabsocial/components/modal/chat_conversation_delete_modal.js new file mode 100644 index 00000000..46fb7f96 --- /dev/null +++ b/app/javascript/gabsocial/components/modal/chat_conversation_delete_modal.js @@ -0,0 +1,40 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { deleteChatConversation } from '../../actions/chat_conversations' +import ConfirmationModal from './confirmation_modal' + +class ChatConversationDeleteModal extends React.PureComponent { + + handleClick = () => { + this.props.onConfirm(this.props.chatConversationId) + } + + render() { + const { onClose } = this.props + + return ( + + ) + } + +} + +const mapDispatchToProps = (dispatch) => ({ + onDeleteChatConversation: (chatConversationId) => { + dispatch(deleteChatConversation(chatConversationId)) + }, +}) + +ChatConversationDeleteModal.propTypes = { + chatConversationId: PropTypes.string.isRequired, + onDeleteChatConversation: PropTypes.func.isRequired, +} + +export default connect(null, mapDispatchToProps)(ChatConversationDeleteModal) \ No newline at end of file diff --git a/app/javascript/gabsocial/components/modal/modal_root.js b/app/javascript/gabsocial/components/modal/modal_root.js index 58bbe5c9..242663df 100644 --- a/app/javascript/gabsocial/components/modal/modal_root.js +++ b/app/javascript/gabsocial/components/modal/modal_root.js @@ -10,6 +10,8 @@ import LoadingModal from './loading_modal' import { MODAL_BLOCK_ACCOUNT, MODAL_BOOST, + MODAL_CHAT_CONVERSATION_CREATE, + MODAL_CHAT_CONVERSATION_DELETE, MODAL_COMMUNITY_TIMELINE_SETTINGS, MODAL_COMPOSE, MODAL_CONFIRM, @@ -42,6 +44,8 @@ import { import { BlockAccountModal, BoostModal, + ChatConversationCreateModal, + ChatConversationDeleteModal, CommunityTimelineSettingsModal, ComposeModal, ConfirmationModal, @@ -74,37 +78,40 @@ import { VideoModal, } from '../../features/ui/util/async_components' -const MODAL_COMPONENTS = {} -MODAL_COMPONENTS[MODAL_BLOCK_ACCOUNT] = BlockAccountModal -MODAL_COMPONENTS[MODAL_BOOST] = BoostModal -MODAL_COMPONENTS[MODAL_COMMUNITY_TIMELINE_SETTINGS] = CommunityTimelineSettingsModal -MODAL_COMPONENTS[MODAL_COMPOSE] = ComposeModal -MODAL_COMPONENTS[MODAL_CONFIRM] = ConfirmationModal -MODAL_COMPONENTS[MODAL_DISPLAY_OPTIONS] = DisplayOptionsModal -MODAL_COMPONENTS[MODAL_EDIT_SHORTCUTS] = EditShortcutsModal -MODAL_COMPONENTS[MODAL_EDIT_PROFILE] = EditProfileModal -MODAL_COMPONENTS[MODAL_EMAIL_CONFIRMATION_REMINDER] = EmailConfirmationReminderModal -MODAL_COMPONENTS[MODAL_GROUP_CREATE] = GroupCreateModal -MODAL_COMPONENTS[MODAL_GROUP_DELETE] = GroupDeleteModal -MODAL_COMPONENTS[MODAL_GROUP_PASSWORD] = GroupPasswordModal -MODAL_COMPONENTS[MODAL_HASHTAG_TIMELINE_SETTINGS] = HashtagTimelineSettingsModal -MODAL_COMPONENTS[MODAL_HOME_TIMELINE_SETTINGS] = HomeTimelineSettingsModal -MODAL_COMPONENTS[MODAL_HOTKEYS] = HotkeysModal -MODAL_COMPONENTS[MODAL_LIST_ADD_USER] = ListAddUserModal -MODAL_COMPONENTS[MODAL_LIST_CREATE] = ListCreateModal -MODAL_COMPONENTS[MODAL_LIST_DELETE] = ListDeleteModal -MODAL_COMPONENTS[MODAL_LIST_EDITOR] = ListEditorModal -MODAL_COMPONENTS[MODAL_LIST_TIMELINE_SETTINGS] = ListTimelineSettingsModal -MODAL_COMPONENTS[MODAL_MEDIA] = MediaModal -MODAL_COMPONENTS[MODAL_MUTE] = MuteModal -MODAL_COMPONENTS[MODAL_PRO_UPGRADE] = ProUpgradeModal -MODAL_COMPONENTS[MODAL_REPORT] = ReportModal -MODAL_COMPONENTS[MODAL_STATUS_LIKES] = StatusLikesModal -MODAL_COMPONENTS[MODAL_STATUS_REPOSTS] = StatusRepostsModal -MODAL_COMPONENTS[MODAL_STATUS_REVISIONS] = StatusRevisionsModal -MODAL_COMPONENTS[MODAL_UNAUTHORIZED] = UnauthorizedModal -MODAL_COMPONENTS[MODAL_UNFOLLOW] = UnfollowModal -MODAL_COMPONENTS[MODAL_VIDEO] = VideoModal +const MODAL_COMPONENTS = { + [MODAL_BLOCK_ACCOUNT]: BlockAccountModal, + [MODAL_BOOST]: BoostModal, + [MODAL_CHAT_CONVERSATION_CREATE]: ChatConversationCreateModal, + [MODAL_CHAT_CONVERSATION_DELETE]: ChatConversationDeleteModal, + [MODAL_COMMUNITY_TIMELINE_SETTINGS]: CommunityTimelineSettingsModal, + [MODAL_COMPOSE]: ComposeModal, + [MODAL_CONFIRM]: ConfirmationModal, + [MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal, + [MODAL_EDIT_SHORTCUTS]: EditShortcutsModal, + [MODAL_EDIT_PROFILE]: EditProfileModal, + [MODAL_EMAIL_CONFIRMATION_REMINDER]: EmailConfirmationReminderModal, + [MODAL_GROUP_CREATE]: GroupCreateModal, + [MODAL_GROUP_DELETE]: GroupDeleteModal, + [MODAL_GROUP_PASSWORD]: GroupPasswordModal, + [MODAL_HASHTAG_TIMELINE_SETTINGS]: HashtagTimelineSettingsModal, + [MODAL_HOME_TIMELINE_SETTINGS]: HomeTimelineSettingsModal, + [MODAL_HOTKEYS]: HotkeysModal, + [MODAL_LIST_ADD_USER]: ListAddUserModal, + [MODAL_LIST_CREATE]: ListCreateModal, + [MODAL_LIST_DELETE]: ListDeleteModal, + [MODAL_LIST_EDITOR]: ListEditorModal, + [MODAL_LIST_TIMELINE_SETTINGS]: ListTimelineSettingsModal, + [MODAL_MEDIA]: MediaModal, + [MODAL_MUTE]: MuteModal, + [MODAL_PRO_UPGRADE]: ProUpgradeModal, + [MODAL_REPORT]: ReportModal, + [MODAL_STATUS_LIKES]: StatusLikesModal, + [MODAL_STATUS_REPOSTS]: StatusRepostsModal, + [MODAL_STATUS_REVISIONS]: StatusRevisionsModal, + [MODAL_UNAUTHORIZED]: UnauthorizedModal, + [MODAL_UNFOLLOW]: UnfollowModal, + [MODAL_VIDEO]: VideoModal, +} const CENTERED_XS_MODALS = [ MODAL_BLOCK_ACCOUNT, diff --git a/app/javascript/gabsocial/components/placeholder/chat_message_placeholder.js b/app/javascript/gabsocial/components/placeholder/chat_message_placeholder.js new file mode 100644 index 00000000..025f8425 --- /dev/null +++ b/app/javascript/gabsocial/components/placeholder/chat_message_placeholder.js @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { getRandomInt } from '../../utils/numbers' +import PlaceholderLayout from './placeholder_layout' + +export default class ChatMessagePlaceholder extends React.PureComponent { + + render() { + const alt = getRandomInt(0, 1) === 1 + const width = getRandomInt(120, 240) + const height = getRandomInt(40, 110) + + if (alt) { + return ( + + + + + ) + } + + return ( + + + + + ) + + } + +} \ No newline at end of file diff --git a/app/javascript/gabsocial/components/placeholder/placeholder_layout.js b/app/javascript/gabsocial/components/placeholder/placeholder_layout.js index 18554c59..75aa5eab 100644 --- a/app/javascript/gabsocial/components/placeholder/placeholder_layout.js +++ b/app/javascript/gabsocial/components/placeholder/placeholder_layout.js @@ -12,6 +12,7 @@ class PlaceholderLayout extends React.PureComponent { intl, theme, viewBox, + preserveAspectRatio, } = this.props const isLight = ['light', 'white', ''].indexOf(theme) > -1 @@ -26,6 +27,7 @@ class PlaceholderLayout extends React.PureComponent { viewBox={viewBox} backgroundColor={backgroundColor} foregroundColor={foregroundColor} + preserveAspectRatio={preserveAspectRatio} > {this.props.children} @@ -47,6 +49,7 @@ PlaceholderLayout.propTypes = { intl: PropTypes.object.isRequired, theme: PropTypes.string.isRequired, viewBox: PropTypes.string.isRequired, + preserveAspectRatio: PropTypes.string, } export default injectIntl(connect(mapStateToProps)(PlaceholderLayout)) diff --git a/app/javascript/gabsocial/components/scrollable_list.js b/app/javascript/gabsocial/components/scrollable_list.js index b2c379b2..9c4da240 100644 --- a/app/javascript/gabsocial/components/scrollable_list.js +++ b/app/javascript/gabsocial/components/scrollable_list.js @@ -94,30 +94,32 @@ class ScrollableList extends React.PureComponent { handleScroll = throttle(() => { if (this.window) { - const { scrollTop, scrollHeight } = this.documentElement; - const { innerHeight } = this.window; - const offset = scrollHeight - scrollTop - innerHeight; + const { scrollTop, scrollHeight } = this.documentElement + const { innerHeight } = this.window + const offset = scrollHeight - scrollTop - innerHeight if (600 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading && !this.props.disableInfiniteScroll) { - this.props.onLoadMore(); + this.props.onLoadMore() } if (scrollTop < 100 && this.props.onScrollToTop) { - this.props.onScrollToTop(); + this.props.onScrollToTop() + } else if (scrollTop < 100 && this.props.onScrollToBottom) { + this.props.onScrollToBottom() } else if (this.props.onScroll) { - this.props.onScroll(); + this.props.onScroll() } if (!this.lastScrollWasSynthetic) { // If the last scroll wasn't caused by setScrollTop(), assume it was // intentional and cancel any pending scroll reset on mouse idle - this.scrollToTopOnMouseIdle = false; + this.scrollToTopOnMouseIdle = false } - this.lastScrollWasSynthetic = false; + this.lastScrollWasSynthetic = false } }, 150, { trailing: true, - }); + }) handleWheel = throttle(() => { this.scrollToTopOnMouseIdle = false; @@ -175,6 +177,11 @@ class ScrollableList extends React.PureComponent { this.props.onLoadMore(); } + setRef = (c) => { + this.node = c + if (this.props.scrollRef) this.props.scrollRef(c) + } + render() { const { children, @@ -186,6 +193,8 @@ class ScrollableList extends React.PureComponent { onLoadMore, placeholderComponent: Placeholder, placeholderCount, + onScrollToTop, + onScrollToBottom, } = this.props const childrenCount = React.Children.count(children); @@ -210,8 +219,18 @@ class ScrollableList extends React.PureComponent { return } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) { return ( -
+
+ { + (hasMore && onLoadMore && !isLoading) && !!onScrollToBottom && + + } + + { + isLoading && !!onScrollToBottom && + + } + { !!this.props.children && React.Children.map(this.props.children, (child, index) => ( @@ -234,12 +253,12 @@ class ScrollableList extends React.PureComponent { } { - (hasMore && onLoadMore && !isLoading) && + (hasMore && onLoadMore && !isLoading) && !!onScrollToTop && } { - isLoading && + isLoading && !!onScrollToTop && }
@@ -268,9 +287,10 @@ ScrollableList.propTypes = { ]), children: PropTypes.node, onScrollToTop: PropTypes.func, + onScrollToBottom: PropTypes.func, onScroll: PropTypes.func, placeholderComponent: PropTypes.node, - placeholderCount: PropTypes.node, + placeholderCount: PropTypes.number, disableInfiniteScroll: PropTypes.bool, } diff --git a/app/javascript/gabsocial/components/sidebar/sidebar_layout.js b/app/javascript/gabsocial/components/sidebar/sidebar_layout.js index e80dbeb7..1e30354c 100644 --- a/app/javascript/gabsocial/components/sidebar/sidebar_layout.js +++ b/app/javascript/gabsocial/components/sidebar/sidebar_layout.js @@ -1,27 +1,18 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { - CX, - BREAKPOINT_SMALL, -} from '../../constants' +import { CX } from '../../constants' import { me, emailConfirmed, } from '../../initial_state' import Button from '../button' -import { openModal } from '../../actions/modal' -import Responsive from '../../features/ui/util/responsive_component' import Heading from '../heading' import BackButton from '../back_button' import Pills from '../pills' class SidebarLayout extends React.PureComponent { - handleOpenComposeModal = () => { - this.props.onOpenComposeModal() - } - render() { const { actions, @@ -91,30 +82,6 @@ class SidebarLayout extends React.PureComponent { {children} - { - !!me && - -
@@ -124,18 +91,11 @@ class SidebarLayout extends React.PureComponent { } -const mapDispatchToProps = (dispatch) => ({ - onOpenComposeModal() { - dispatch(openModal('COMPOSE')) - }, -}) - SidebarLayout.propTypes = { - onOpenComposeModal: PropTypes.func.isRequired, actions: PropTypes.array, tabs: PropTypes.array, title: PropTypes.string, showBackBtn: PropTypes.bool, } -export default connect(null, mapDispatchToProps)(SidebarLayout) \ No newline at end of file +export default SidebarLayout \ No newline at end of file diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index cb49b8d9..cbf38c33 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -290,7 +290,7 @@ class StatusList extends ImmutablePureComponent { hasMore={hasMore} /> +
- + {title} -
{ @@ -90,12 +94,16 @@ class Toast extends React.PureComponent { /> }
- + {message} { date && - + + { + !image && + · + } } @@ -116,10 +124,6 @@ Toast.propTypes = { onDismiss: PropTypes.func.isRequired, title: PropTypes.string.isRequired, to: PropTypes.string, - type: PropTypes.oneOf([ - TOAST_TYPE_ERROR, - TOAST_TYPE_SUCCESS, - ]).isRequired, } export default Toast \ No newline at end of file diff --git a/app/javascript/gabsocial/constants.js b/app/javascript/gabsocial/constants.js index 59a02011..61c034de 100644 --- a/app/javascript/gabsocial/constants.js +++ b/app/javascript/gabsocial/constants.js @@ -43,6 +43,8 @@ export const POPOVER_VIDEO_STATS = 'VIDEO_STATS' export const MODAL_BLOCK_ACCOUNT = 'BLOCK_ACCOUNT' export const MODAL_BOOST = 'BOOST' +export const MODAL_CHAT_CONVERSATION_CREATE = 'CHAT_CONVERSATION_CREATE' +export const MODAL_CHAT_CONVERSATION_DELETE = 'CHAT_CONVERSATION_DELETE' export const MODAL_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS' export const MODAL_COMPOSE = 'COMPOSE' export const MODAL_CONFIRM = 'CONFIRM' diff --git a/app/javascript/gabsocial/containers/gabsocial.js b/app/javascript/gabsocial/containers/gabsocial.js index c51bf23d..615b7b39 100644 --- a/app/javascript/gabsocial/containers/gabsocial.js +++ b/app/javascript/gabsocial/containers/gabsocial.js @@ -16,6 +16,7 @@ import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants' import { connectUserStream, connectStatusUpdateStream, + connectChatMessagesStream, } from '../actions/streaming' import { getLocale } from '../locales' import initialState from '../initial_state' @@ -95,6 +96,7 @@ export default class GabSocial extends React.PureComponent { if (!!me) { this.disconnect = store.dispatch(connectUserStream()) store.dispatch(connectStatusUpdateStream()) + store.dispatch(connectChatMessagesStream(me)) } console.log('%cGab Social ', [ diff --git a/app/javascript/gabsocial/containers/toasts_container.js b/app/javascript/gabsocial/containers/toasts_container.js index c497c828..528da5b2 100644 --- a/app/javascript/gabsocial/containers/toasts_container.js +++ b/app/javascript/gabsocial/containers/toasts_container.js @@ -13,11 +13,58 @@ class ToastsContainer extends React.PureComponent { } render() { - const { notifications } = this.props + // const { notifications } = this.props + const notifications = [ + { + key: '1', + title: 'Error', + to: 'to', + image: 'https://gab.com/media/user/58077e8a49705.jpg', + message: 'Unable to follow @andrew', + date: new Date(), + isImageAccount: true, + }, + { + key: '2', + title: 'Success', + to: 'to', + image: 'https://gab.com/media/user/58077e8a49705.jpg', + message: 'Your gab was posted. Click here to view', + date: new Date(), + isImageAccount: false, + }, + { + key: '3', + title: '', + to: 'to', + image: 'https://gab.com/media/user/58077e8a49705.jpg', + message: 'Unable to follow @andrew', + date: new Date(), + isImageAccount: true, + }, + { + key: '4', + title: '', + to: 'to', + image: 'https://gab.com/media/user/58077e8a49705.jpg', + message: 'Your gab was posted. Click here to view', + date: new Date(), + isImageAccount: false, + }, + { + key: '5', + title: '', + to: 'to', + message: 'Your gab was deleted', + date: new Date(), + isImageAccount: false, + }, + ] + const hasNotifications = Array.isArray(notifications) && notifications.length > 0 const containerClasses = CX({ - default: 1, + d: 1, z5: 1, posFixed: 1, bottom0: 1, @@ -28,17 +75,17 @@ class ToastsContainer extends React.PureComponent { pb10: 1, displayNone: !hasNotifications }) - + + return (
{ - hasNotifications && notifications.map((notification) => ( + !hasNotifications && notifications.map((notification) => ( { + this.props.onExpandBlocks() + }, 300, { leading: true }) + + render() { + const { + intl, + accountIds, + hasMore, + isLoading, + } = this.props + + const emptyMessage = intl.formatMessage(messages.empty) + + return ( +
+
+ +
+ + { + accountIds && accountIds.map((id) => ( + + )) + } + +
+ ) + } + +} + +const messages = defineMessages({ + empty: { id: 'empty_column.blocks', defaultMessage: 'You haven\'t blocked any users yet.' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, +}) + +const mapStateToProps = (state) => ({ + accountIds: state.getIn(['user_lists', 'blocks', me, 'items']), + hasMore: !!state.getIn(['user_lists', 'blocks', me, 'next']), + isLoading: state.getIn(['user_lists', 'blocks', me, 'isLoading']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onFetchBlocks: () => dispatch(fetchBlocks()), + onExpandBlocks: () => dispatch(expandBlocks()), +}) + +MessagesBlockedAccounts.propTypes = { + accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + intl: PropTypes.object.isRequired, + isLoading: PropTypes.bool, + onExpandBlocks: PropTypes.func.isRequired, + onFetchBlocks: PropTypes.func.isRequired, +} + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MessagesBlockedAccounts)) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/chat_conversation_create.js b/app/javascript/gabsocial/features/chat_conversation_create.js new file mode 100644 index 00000000..6d99053f --- /dev/null +++ b/app/javascript/gabsocial/features/chat_conversation_create.js @@ -0,0 +1,86 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { fetchChatConversationAccountSuggestions } from '../actions/chats' +import { createChatConversation } from '../actions/chat_conversations' +import Account from '../components/account' +import Button from '../components/button' +import Input from '../components/input' +import Form from '../components/form' +import Text from '../components/text' + +class ChatConversationCreate extends React.PureComponent { + + state = { + query: '', + } + + onChange = (query) => { + this.setState({ query }) + this.props.onChange(query) + } + + handleOnCreateChatConversation = (accountId) => { + console.log("handleOnCreateChatConversation:", accountId) + this.props.onCreateChatConversation(accountId) + } + + render() { + const { query, suggestionsIds } = this.props + + return ( +
+
+ +
+ +
+
+ + Search results ({suggestionsIds.size}) + + { + suggestionsIds && + suggestionsIds.map((accountId) => ( + this.handleOnCreateChatConversation(accountId)} + actionIcon='add' + /> + )) + } +
+
+
+ ) + } + +} + +const mapStateToProps = (state) => ({ + suggestionsIds: state.getIn(['chats', 'createChatConversationSuggestionIds']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onChange: (value) => { + console.log("value", value) + dispatch(fetchChatConversationAccountSuggestions(value)) + }, + onCreateChatConversation: (accountId) => { + dispatch(createChatConversation(accountId)) + }, +}) + +ChatConversationCreate.propTypes = { + onChange: PropTypes.func.isRequired, + onCreateChatConversation: PropTypes.func.isRequired, + isModal: PropTypes.bool, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationCreate) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/chat_conversation_muted_accounts.js b/app/javascript/gabsocial/features/chat_conversation_muted_accounts.js new file mode 100644 index 00000000..dfaedd74 --- /dev/null +++ b/app/javascript/gabsocial/features/chat_conversation_muted_accounts.js @@ -0,0 +1,79 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { injectIntl, FormattedMessage } from 'react-intl' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import debounce from 'lodash.debounce' +import { me } from '../initial_state' +import { fetchMutes, expandMutes } from '../actions/mutes' +import Account from '../components/account' +import Block from '../components/block' +import BlockHeading from '../components/block_heading' +import ScrollableList from '../components/scrollable_list' + +class MessagesMutedAccounts extends ImmutablePureComponent { + + componentWillMount() { + this.props.onFetchMutes() + } + + handleLoadMore = debounce(() => { + this.props.onExpandMutes() + }, 300, { leading: true }) + + render() { + const { + accountIds, + hasMore, + isLoading, + } = this.props + + return ( +
+
+ } /> +
+ } + > + { + accountIds && accountIds.map((id) => + + ) + } + +
+ ) + } + +} + +const mapStateToProps = (state) => ({ + accountIds: state.getIn(['user_lists', 'mutes', me, 'items']), + hasMore: !!state.getIn(['user_lists', 'mutes', me, 'next']), + isLoading: state.getIn(['user_lists', 'mutes', me, 'isLoading']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onFetchMutes: () => dispatch(fetchMutes()), + onExpandMutes: () => dispatch(expandMutes()), +}) + +MessagesMutedAccounts.propTypes = { + accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + onExpandMutes: PropTypes.func.isRequired, + onFetchMutes: PropTypes.func.isRequired, +} + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MessagesMutedAccounts)) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/chat_conversation_requests.js b/app/javascript/gabsocial/features/chat_conversation_requests.js new file mode 100644 index 00000000..aa023a62 --- /dev/null +++ b/app/javascript/gabsocial/features/chat_conversation_requests.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' +import BlockHeading from '../components/block_heading' +import ChatConversationsList from './messages/components/chat_conversations_list' + +class ChatConversationRequests extends React.PureComponent { + + render() { + return ( +
+
+ +
+ +
+ ) + } + +} + +export default ChatConversationRequests \ No newline at end of file diff --git a/app/javascript/gabsocial/features/compose/components/poll_form.js b/app/javascript/gabsocial/features/compose/components/poll_form.js index e0ea95a9..9946a072 100644 --- a/app/javascript/gabsocial/features/compose/components/poll_form.js +++ b/app/javascript/gabsocial/features/compose/components/poll_form.js @@ -157,7 +157,7 @@ class PollFormOption extends ImmutablePureComponent { diff --git a/app/javascript/gabsocial/features/messages/components/chat_conversations_empty_block.js b/app/javascript/gabsocial/features/messages/components/chat_conversations_empty_block.js new file mode 100644 index 00000000..b0d37841 --- /dev/null +++ b/app/javascript/gabsocial/features/messages/components/chat_conversations_empty_block.js @@ -0,0 +1,47 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { openModal } from '../../../actions/modal' +import { MODAL_CHAT_CONVERSATION_CREATE } from '../../../constants' +import Text from '../../../components/text' +import Button from '../../../components/button' + +class ChatEmptyMessageBlock extends React.PureComponent { + + handleOnCreateNewChat = () => { + this.props.onOpenChatConversationCreateModal() + } + + render () { + return ( +
+
+ + You don’t have a message selected + + + Choose one from your existing messages, or start a new one. + + +
+
+ ) + } + +} + +const mapDispatchToProps = (dispatch) => ({ + onOpenChatConversationCreateModal() { + dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE)) + } +}) + +ChatEmptyMessageBlock.propTypes = { + onOpenChatConversationCreateModal: PropTypes.func.isRequired, +} + +export default connect(null, mapDispatchToProps)(ChatEmptyMessageBlock) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/messages/components/chat_conversations_list.js b/app/javascript/gabsocial/features/messages/components/chat_conversations_list.js new file mode 100644 index 00000000..50bee43a --- /dev/null +++ b/app/javascript/gabsocial/features/messages/components/chat_conversations_list.js @@ -0,0 +1,98 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import debounce from 'lodash.debounce' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import { + fetchChatConversations, + expandChatConversations, + fetchChatConversationRequested, + expandChatConversationRequested, +} from '../../../actions/chat_conversations' +import AccountPlaceholder from '../../../components/placeholder/account_placeholder' +import ChatConversationsListItem from './chat_conversations_list_item' +import ScrollableList from '../../../components/scrollable_list' + +class ChatConversationsList extends ImmutablePureComponent { + + componentDidMount() { + console.log("componentDidMount:", this.props.source) + this.props.onFetchChatConversations(this.props.source) + } + + handleLoadMore = debounce(() => { + this.props.onExpandChatConversations(this.props.source) + }, 300, { leading: true }) + + render() { + const { + hasMore, + isLoading, + source, + chatConversationIds, + } = this.props + + console.log("---source:", source, chatConversationIds) + + return ( +
+ + { + !!chatConversationIds && chatConversationIds.map((chatConversationId, i) => ( + + )) + } + +
+ ) + } + +} + +const mapStateToProps = (state, { source }) => ({ + chatConversationIds: state.getIn(['chat_conversation_lists', source, 'items']), + hasMore: !!state.getIn(['chat_conversation_lists', source, 'next']), + isLoading: state.getIn(['chat_conversation_lists', source, 'isLoading']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onFetchChatConversations(source) { + if (source ==='approved') { + dispatch(fetchChatConversations()) + } else if (source ==='requested') { + dispatch(fetchChatConversationRequested()) + } + }, + onExpandChatConversations(source) { + if (source ==='approved') { + dispatch(expandChatConversations()) + } else if (source ==='requested') { + dispatch(expandChatConversationRequested()) + } + }, +}) + +ChatConversationsList.propTypes = { + chatConversationIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + onFetchChatConversations: PropTypes.func.isRequired, + onExpandChatConversations: PropTypes.func.isRequired, + source: PropTypes.string.isRequired, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationsList) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/messages/components/chat_conversations_list_item.js b/app/javascript/gabsocial/features/messages/components/chat_conversations_list_item.js new file mode 100644 index 00000000..369c273b --- /dev/null +++ b/app/javascript/gabsocial/features/messages/components/chat_conversations_list_item.js @@ -0,0 +1,116 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import { makeGetChatConversation } from '../../../selectors' +import { setChatConversationSelected } from '../../../actions/chats' +import { CX } from '../../../constants' +import Input from '../../../components/input' +import DisplayNameGroup from '../../../components/display_name_group' +import DisplayName from '../../../components/display_name' +import AvatarGroup from '../../../components/avatar_group' +import Text from '../../../components/text' +import RelativeTimestamp from '../../../components/relative_timestamp' + +class ChatConversationsListItem extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + } + + handleOnClick = () => { + const { chatConversationId } = this.props + this.props.onSetChatConversationSelected(chatConversationId) + this.context.router.history.push(`/messages/${chatConversationId}`) + } + + render() { + const { + selected, + selectedId, + chatConversation, + chatConversationId, + } = this.props + + if (!chatConversation) return
+ + const containerClasses = CX({ + d: 1, + w100PC: 1, + bgTransparent: 1, + bgSubtle_onHover: 1, + borderBottom1PX: 1, + borderColorSecondary: 1, + noUnderline: 1, + outlineNone: 1, + cursorPointer: 1, + }) + + const innerContainerClasses = CX({ + d: 1, + flexRow: 1, + aiStart: 1, + aiCenter: 0, + px15: 1, + py15: 1, + borderRight4PX: selected, + borderColorBrand: selected, + }) + + const avatarSize = 46 + const otherAccounts = chatConversation.get('other_accounts') + const lastMessage = chatConversation.get('last_chat_message', null) + const content = { __html: !!lastMessage ? lastMessage.get('text', '') : '' } + const date = !!lastMessage ? lastMessage.get('created_at') : chatConversation.get('created_at') + + return ( + + ) + } + +} + +const mapStateToProps = (state, { chatConversationId }) => ({ + chatConversation: makeGetChatConversation()(state, { id: chatConversationId }), + selectedId: state.getIn(['chats', 'selectedChatConversationId'], null), + selected: state.getIn(['chats', 'selectedChatConversationId'], null) === chatConversationId, +}) + +const mapDispatchToProps = (dispatch) => ({ + onSetChatConversationSelected: (chatConversationId) => { + dispatch(setChatConversationSelected(chatConversationId)) + }, +}) + +ChatConversationsListItem.propTypes = { + chatConversationId: PropTypes.string.isRequired, + chatConversation: ImmutablePropTypes.map, + onSetChatConversationSelected: PropTypes.func.isRequired, + selected: PropTypes.bool.isRequired, + source: PropTypes.string.isRequired, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationsListItem) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/messages/components/chat_conversations_requests_list_item.js b/app/javascript/gabsocial/features/messages/components/chat_conversations_requests_list_item.js new file mode 100644 index 00000000..106fdcfc --- /dev/null +++ b/app/javascript/gabsocial/features/messages/components/chat_conversations_requests_list_item.js @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { NavLink } from 'react-router-dom' +import { shortNumberFormat } from '../../../utils/numbers' +import { fetchChatConversationRequestedCount } from '../../../actions/chat_conversations' +import Text from '../../../components/text' +import Icon from '../../../components/icon' + +class ChatConversationRequestsListItem extends React.PureComponent { + + componentDidMount() { + this.props.onFetchChatConversationRequestedCount() + } + + render() { + const { requestCount } = this.props + + if (!requestCount || requestCount < 1) return null + + return ( + +
+ Message Requests + + {shortNumberFormat(requestCount)} + + +
+
+ ) + } + +} + + +const mapStateToProps = (state) => ({ + requestCount: state.getIn(['chats', 'chatConversationRequestCount'], 0), +}) + +const mapDispatchToProps = (dispatch) => ({ + onFetchChatConversationRequestedCount: () => dispatch(fetchChatConversationRequestedCount()), +}) + +ChatConversationRequestsListItem.propTypes = { + requestCount: PropTypes.number, + onFetchChatConversationRequestedCount: PropTypes.func.isRequired, +} + +ChatConversationRequestsListItem.defaultProps = { + requestCount: 0, +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationRequestsListItem) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/messages/components/messages_search.js b/app/javascript/gabsocial/features/messages/components/chat_conversations_search.js similarity index 54% rename from app/javascript/gabsocial/features/messages/components/messages_search.js rename to app/javascript/gabsocial/features/messages/components/chat_conversations_search.js index 91345a95..08db977b 100644 --- a/app/javascript/gabsocial/features/messages/components/messages_search.js +++ b/app/javascript/gabsocial/features/messages/components/chat_conversations_search.js @@ -1,11 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import { defineMessages, injectIntl } from 'react-intl' -import ImmutablePureComponent from 'react-immutable-pure-component' -import ImmutablePropTypes from 'react-immutable-proptypes' import Input from '../../../components/input' -class MessagesSearch extends ImmutablePureComponent { +class ChatConversationsSearch extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -17,7 +14,7 @@ class MessagesSearch extends ImmutablePureComponent { render() { const { - intl, + children } = this.props return ( @@ -34,12 +31,8 @@ class MessagesSearch extends ImmutablePureComponent { } -const messages = defineMessages({ - placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" }, -}) - -MessagesSearch.propTypes = { - intl: PropTypes.object.isRequired, +ChatConversationsSearch.propTypes = { + // } -export default injectIntl(MessagesSearch) \ No newline at end of file +export default ChatConversationsSearch \ No newline at end of file diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js new file mode 100644 index 00000000..f3e0505d --- /dev/null +++ b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js @@ -0,0 +1,140 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import { connect } from 'react-redux' +import Textarea from 'react-textarea-autosize' +import { openModal } from '../../../actions/modal' +import { sendChatMessage } from '../../../actions/chat_messages' +import { CX } from '../../../constants' +import Button from '../../../components/button' +import Input from '../../../components/input' + +class ChatMessagesComposeForm extends React.PureComponent { + + state = { + focused: false, + value: '', + } + + handleOnSendChatMessage = () => { + this.props.onSendChatMessage(this.state.value, this.props.chatConversationId) + this.setState({ value: '' }) + } + + onChange = (e) => { + this.setState({ value: e.target.value }) + } + + onBlur = () => { + this.setState({ focused: false }); + } + + onFocus = () => { + this.setState({ focused: true }); + } + + onKeyDown = (e) => { + const { disabled } = this.props; + + if (disabled) { + e.preventDefault(); + return; + } + + // Ignore key events during text composition + // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) + if (e.which === 229 || e.isComposing) return; + + switch (e.key) { + case 'Escape': + document.querySelector('#gabsocial').focus() + break; + case 'Enter': + case 'Tab': + // + break; + } + + // if (e.defaultPrevented || !this.props.onKeyDown) return; + } + + setTextbox = (c) => { + this.textbox = c + } + + render () { + const { chatConversationId } = this.props + const { value } = this.state + const disabled = false + + const textareaContainerClasses = CX({ + d: 1, + maxW100PC: 1, + flexGrow1: 1, + jcCenter: 1, + py5: 1, + }) + + const textareaClasses = CX({ + d: 1, + font: 1, + wrap: 1, + resizeNone: 1, + bgTransparent: 1, + outlineNone: 1, + lineHeight125: 1, + cPrimary: 1, + px10: 1, + fs14PX: 1, + maxH200PX: 1, + borderColorSecondary: 1, + border1PX: 1, + radiusSmall: 1, + py10: 1, + }) + + return ( +
+
+