Progress on dms, code cleanup
Progress on dms, code cleanup
This commit is contained in:
parent
20d4fc09af
commit
9a43c51085
@ -51,22 +51,6 @@ module Admin
|
|||||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def set_custom_emoji
|
def set_custom_emoji
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
class Api::BaseController < ApplicationController
|
class Api::BaseController < ApplicationController
|
||||||
DEFAULT_STATUSES_LIMIT = 20
|
DEFAULT_STATUSES_LIMIT = 20
|
||||||
DEFAULT_ACCOUNTS_LIMIT = 20
|
DEFAULT_ACCOUNTS_LIMIT = 20
|
||||||
|
DEFAULT_CHAT_CONVERSATION_LIMIT = 12
|
||||||
|
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 10
|
||||||
|
|
||||||
include RateLimitHeaders
|
include RateLimitHeaders
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :check_user_permissions
|
skip_before_action :check_user_permissions
|
||||||
|
|
||||||
|
before_action :block_if_doorkeeper
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
@ -90,6 +93,14 @@ class Api::BaseController < ApplicationController
|
|||||||
doorkeeper_authorize!(*scopes) if doorkeeper_token
|
doorkeeper_authorize!(*scopes) if doorkeeper_token
|
||||||
end
|
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
|
def set_cache_headers
|
||||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||||
end
|
end
|
||||||
|
24
app/controllers/api/v1/accounts/search_controller.rb
Normal file
24
app/controllers/api/v1/accounts/search_controller.rb
Normal file
@ -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
|
@ -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
|
@ -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
|
@ -1,24 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::AccountsController < Api::BaseController
|
class Api::V1::ChatConversationAccountsController < Api::BaseController
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
before_action -> { authorize_if_got_token! :read, :'read:chats' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
before_action -> { doorkeeper_authorize! :write, :'write:chats' }, only: [:create]
|
||||||
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]
|
|
||||||
|
|
||||||
before_action :require_user!, except: [:show, :create]
|
before_action :require_user!
|
||||||
before_action :set_account, except: [:create]
|
before_action :set_account, except: [:create]
|
||||||
before_action :check_account_suspension, only: [:show]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
#
|
#
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
|
||||||
#
|
|
||||||
end
|
|
||||||
|
|
||||||
def block
|
def block
|
||||||
BlockMessengerService.new.call(current_user.account, @account)
|
BlockMessengerService.new.call(current_user.account, @account)
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
@ -42,18 +34,19 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find(params[:id])
|
# @account = Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
# def relationships(**options)
|
||||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
# AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||||
end
|
# end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
gone if @account.suspended?
|
gone if @account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
# def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale)
|
# params.permit(:username, :email, :password, :agreement, :locale)
|
||||||
end
|
# end
|
||||||
|
|
||||||
end
|
end
|
82
app/controllers/api/v1/chat_conversation_controller.rb
Normal file
82
app/controllers/api/v1/chat_conversation_controller.rb
Normal file
@ -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
|
@ -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
|
@ -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
|
@ -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
|
46
app/controllers/api/v1/chat_messages_controller.rb
Normal file
46
app/controllers/api/v1/chat_messages_controller.rb
Normal file
@ -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
|
@ -48,4 +48,16 @@ class EmptyController < ActionController::Base
|
|||||||
nil
|
nil
|
||||||
end
|
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
|
end
|
||||||
|
@ -52,7 +52,7 @@ class ReactController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_route_matches
|
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
|
end
|
||||||
|
|
||||||
def find_public_route_matches
|
def find_public_route_matches
|
||||||
|
@ -163,6 +163,7 @@ export const followAccount = (id, reblogs = true) => (dispatch, getState) => {
|
|||||||
dispatch(followAccountRequest(id, locked))
|
dispatch(followAccountRequest(id, locked))
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then((response) => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then((response) => {
|
||||||
|
console.log("response:", response)
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing))
|
dispatch(followAccountSuccess(response.data, alreadyFollowing))
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
dispatch(followAccountFail(error, locked))
|
dispatch(followAccountFail(error, locked))
|
||||||
|
@ -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 })
|
|
||||||
}
|
|
111
app/javascript/gabsocial/actions/chat_conversation_messages.js
Normal file
111
app/javascript/gabsocial/actions/chat_conversation_messages.js
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
|
@ -3,13 +3,35 @@ import { fetchRelationships } from './accounts'
|
|||||||
import { importFetchedAccounts } from './importer'
|
import { importFetchedAccounts } from './importer'
|
||||||
import { me } from '../initial_state'
|
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 CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST'
|
||||||
export const CONVERSATION_BLOCKS_EXPAND_SUCCESS = 'CONVERSATION_BLOCKS_EXPAND_SUCCESS'
|
export const CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS'
|
||||||
export const CONVERSATION_BLOCKS_EXPAND_FAIL = 'CONVERSATION_BLOCKS_EXPAND_FAIL'
|
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_REQUEST = 'BLOCK_MESSAGER_REQUEST'
|
||||||
export const BLOCK_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS'
|
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 CHAT_CONVERSATION_MUTES_FETCH_REQUEST = 'CHAT_CONVERSATION_MUTES_FETCH_REQUEST'
|
||||||
export const CONVERSATION_MUTES_FETCH_SUCCESS = 'CONVERSATION_MUTES_FETCH_SUCCESS'
|
export const CHAT_CONVERSATION_MUTES_FETCH_SUCCESS = 'CHAT_CONVERSATION_MUTES_FETCH_SUCCESS'
|
||||||
export const CONVERSATION_MUTES_FETCH_FAIL = 'CONVERSATION_MUTES_FETCH_FAIL'
|
export const CHAT_CONVERSATION_MUTES_FETCH_FAIL = 'CHAT_CONVERSATION_MUTES_FETCH_FAIL'
|
||||||
|
|
||||||
export const CONVERSATION_MUTES_EXPAND_REQUEST = 'CONVERSATION_MUTES_EXPAND_REQUEST'
|
export const CHAT_CONVERSATION_MUTES_EXPAND_REQUEST = 'CHAT_CONVERSATION_MUTES_EXPAND_REQUEST'
|
||||||
export const CONVERSATION_MUTES_EXPAND_SUCCESS = 'CONVERSATION_MUTES_EXPAND_SUCCESS'
|
export const CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS = 'CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS'
|
||||||
export const CONVERSATION_MUTES_EXPAND_FAIL = 'CONVERSATION_MUTES_EXPAND_FAIL'
|
export const CHAT_CONVERSATION_MUTES_EXPAND_FAIL = 'CHAT_CONVERSATION_MUTES_EXPAND_FAIL'
|
||||||
|
|
||||||
export const MUTE_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST'
|
export const MUTE_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST'
|
||||||
export const MUTE_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS'
|
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 CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS = 'CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS'
|
||||||
export const CONVERSATION_REQUEST_APPROVE_FAIL = 'CONVERSATION_REQUEST_APPROVE_FAIL'
|
|
||||||
|
|
||||||
export const CONVERSATION_REQUEST_REJECT_SUCCESS = 'CONVERSATION_REQUEST_REJECT_SUCCESS'
|
export const CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST'
|
||||||
export const CONVERSATION_REQUEST_REJECT_FAIL = 'CONVERSATION_REQUEST_REJECT_FAIL'
|
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 CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST'
|
||||||
export const CONVERSATION_DELETE_SUCCESS = 'CONVERSATION_DELETE_SUCCESS'
|
export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS'
|
||||||
export const CONVERSATION_DELETE_FAIL = 'CONVERSATION_DELETE_FAIL'
|
export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL'
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST'
|
export const CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS = 'CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS'
|
||||||
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS'
|
export const CHAT_CONVERSATION_REQUEST_APPROVE_FAIL = 'CHAT_CONVERSATION_REQUEST_APPROVE_FAIL'
|
||||||
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_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,
|
accountId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const blockMessengerSuccess = (data) => ({
|
const unblockMessengerSuccess = (data) => ({
|
||||||
type: UNBLOCK_MESSAGER_REQUEST,
|
type: UNBLOCK_MESSAGER_REQUEST,
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const blockMessengerFail = (accountId, error) => ({
|
const unblockMessengerFail = (accountId, error) => ({
|
||||||
type: UNBLOCK_MESSAGER_REQUEST,
|
type: UNBLOCK_MESSAGER_REQUEST,
|
||||||
accountId,
|
accountId,
|
||||||
error,
|
error,
|
||||||
@ -137,20 +372,20 @@ export const fetchBlocks = () => (dispatch, getState) => {
|
|||||||
}).catch(error => dispatch(fetchBlocksFail(error)))
|
}).catch(error => dispatch(fetchBlocksFail(error)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchBlocksRequest = () => ({
|
// export const fetchBlocksRequest = () => ({
|
||||||
type: BLOCKS_FETCH_REQUEST,
|
// type: BLOCKS_FETCH_REQUEST,
|
||||||
})
|
// })
|
||||||
|
|
||||||
export const fetchBlocksSuccess = (accounts, next) => ({
|
// export const fetchBlocksSuccess = (accounts, next) => ({
|
||||||
type: BLOCKS_FETCH_SUCCESS,
|
// type: BLOCKS_FETCH_SUCCESS,
|
||||||
accounts,
|
// accounts,
|
||||||
next,
|
// next,
|
||||||
})
|
// })
|
||||||
|
|
||||||
export const fetchBlocksFail = (error) => ({
|
// export const fetchBlocksFail = (error) => ({
|
||||||
type: BLOCKS_FETCH_FAIL,
|
// type: BLOCKS_FETCH_FAIL,
|
||||||
error,
|
// error,
|
||||||
})
|
// })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -173,17 +408,51 @@ export const expandBlocks = () => (dispatch, getState) => {
|
|||||||
}).catch(error => dispatch(expandBlocksFail(error)))
|
}).catch(error => dispatch(expandBlocksFail(error)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expandBlocksRequest = () => ({
|
// export const expandBlocksRequest = () => ({
|
||||||
type: BLOCKS_EXPAND_REQUEST,
|
// 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) => ({
|
export const approveChatConversationRequestFail = () => ({
|
||||||
type: BLOCKS_EXPAND_SUCCESS,
|
type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL,
|
||||||
accounts,
|
|
||||||
next,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const expandBlocksFail = (error) => ({
|
|
||||||
type: BLOCKS_EXPAND_FAIL,
|
|
||||||
error,
|
|
||||||
})
|
})
|
@ -1,86 +1,85 @@
|
|||||||
|
import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable'
|
||||||
|
import noop from 'lodash.noop'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
import { me } from '../initial_state'
|
import { me } from '../initial_state'
|
||||||
|
import { importFetchedChatMessages } from './importer'
|
||||||
|
|
||||||
export const MESSAGE_SEND_REQUEST = 'MESSAGE_SEND_REQUEST'
|
export const CHAT_MESSAGES_SEND_REQUEST = 'CHAT_MESSAGES_SEND_REQUEST'
|
||||||
export const MESSAGE_SEND_SUCCESS = 'MESSAGE_SEND_SUCCESS'
|
export const CHAT_MESSAGES_SEND_SUCCESS = 'CHAT_MESSAGES_SEND_SUCCESS'
|
||||||
export const MESSAGE_SEND_FAIL = 'MESSAGE_SEND_FAIL'
|
export const CHAT_MESSAGES_SEND_FAIL = 'CHAT_MESSAGES_SEND_FAIL'
|
||||||
|
|
||||||
export const MESSAGE_DELETE_REQUEST = 'MESSAGE_DELETE_REQUEST'
|
export const CHAT_MESSAGES_DELETE_REQUEST = 'CHAT_MESSAGES_DELETE_REQUEST'
|
||||||
export const MESSAGE_DELETE_SUCCESS = 'MESSAGE_DELETE_SUCCESS'
|
export const CHAT_MESSAGES_DELETE_SUCCESS = 'CHAT_MESSAGES_DELETE_SUCCESS'
|
||||||
export const MESSAGE_DELETE_FAIL = 'MESSAGE_DELETE_FAIL'
|
export const CHAT_MESSAGES_DELETE_FAIL = 'CHAT_MESSAGES_DELETE_FAIL'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const sendMessage = (text, conversationId) => (dispatch, getState) => {
|
export const sendChatMessage = (text = '', chatConversationId) => (dispatch, getState) => {
|
||||||
if (!me) return
|
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())
|
dispatch(sendMessageRequest())
|
||||||
|
|
||||||
api(getState).put('/api/v1/messages/chat', {
|
api(getState).post('/api/v1/chat_messages', {
|
||||||
text,
|
text,
|
||||||
conversationId,
|
chat_conversation_id: chatConversationId,
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
// headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
// 'Idempotency-Key': getState().getIn(['chat_compose`', 'idempotencyKey']),
|
||||||
},
|
// },
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
sendMessageSuccess(response)
|
dispatch(importFetchedChatMessages([response.data]))
|
||||||
|
dispatch(sendMessageSuccess(response.data, chatConversationId))
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
dispatch(sendMessageFail(error))
|
dispatch(sendMessageFail(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessageRequest = (text, conversationId) => ({
|
const sendMessageRequest = () => ({
|
||||||
type: MESSAGE_SEND_REQUEST,
|
type: CHAT_MESSAGES_SEND_REQUEST,
|
||||||
text,
|
|
||||||
conversationId,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendMessageSuccess = () => ({
|
const sendMessageSuccess = (chatMessage, chatConversationId) => ({
|
||||||
type: MESSAGE_SEND_SUCCESS,
|
type: CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
|
chatMessage,
|
||||||
|
chatConversationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendMessageFail = (error) => ({
|
const sendMessageFail = (error) => ({
|
||||||
type: MESSAGE_SEND_FAIL,
|
type: CHAT_MESSAGES_SEND_FAIL,
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const deleteMessage = (messageId) => (dispatch, getState) => {
|
const deleteMessage = (chatMessageId) => (dispatch, getState) => {
|
||||||
if (!me || !messageId) return
|
if (!me || !chatMessageId) return
|
||||||
|
|
||||||
// : todo :
|
dispatch(deleteMessageRequest(chatMessageId))
|
||||||
|
|
||||||
dispatch(sendMessageRequest())
|
api(getState).delete(`/api/v1/chat_messages/${chatMessageId}`, {}, {
|
||||||
|
// headers: {
|
||||||
api(getState).delete(`/api/v1/messages/chat/${messageId}`, {}, {
|
// 'Idempotency-Key': getState().getIn(['chat_compose', 'idempotencyKey']),
|
||||||
headers: {
|
// },
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
sendMessageSuccess(response)
|
deleteMessageSuccess(response)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
dispatch(sendMessageFail(error))
|
dispatch(deleteMessageFail(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteMessageRequest = (messageId) => ({
|
const deleteMessageRequest = (chatMessageId) => ({
|
||||||
type: MESSAGE_DELETE_REQUEST,
|
type: CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
messageId,
|
chatMessageId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMessageSuccess = () => ({
|
const deleteMessageSuccess = () => ({
|
||||||
type: MESSAGE_DELETE_SUCCESS,
|
type: CHAT_MESSAGES_DELETE_SUCCESS,
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMessageFail = (error) => ({
|
const deleteMessageFail = (error) => ({
|
||||||
type: MESSAGE_DELETE_FAIL,
|
type: CHAT_MESSAGES_DELETE_FAIL,
|
||||||
error,
|
error,
|
||||||
})
|
})
|
41
app/javascript/gabsocial/actions/chats.js
Normal file
41
app/javascript/gabsocial/actions/chats.js
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
}
|
@ -12,6 +12,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'
|
|||||||
export const STATUS_IMPORT = 'STATUS_IMPORT'
|
export const STATUS_IMPORT = 'STATUS_IMPORT'
|
||||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT'
|
export const STATUSES_IMPORT = 'STATUSES_IMPORT'
|
||||||
export const POLLS_IMPORT = 'POLLS_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'
|
export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +49,11 @@ export const importPolls = (polls) => ({
|
|||||||
polls,
|
polls,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const importChatMessages = (chatMessages) => ({
|
||||||
|
type: CHAT_MESSAGES_IMPORT,
|
||||||
|
chatMessages,
|
||||||
|
})
|
||||||
|
|
||||||
export const importFetchedAccount = (account) => {
|
export const importFetchedAccount = (account) => {
|
||||||
return importFetchedAccounts([account]);
|
return importFetchedAccounts([account]);
|
||||||
}
|
}
|
||||||
@ -113,3 +119,7 @@ export const importErrorWhileFetchingAccountByUsername = (username) => ({
|
|||||||
type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const importFetchedChatMessages = (chatMessages) => (dispatch, getState) => {
|
||||||
|
dispatch(importChatMessages(chatMessages))
|
||||||
|
}
|
@ -297,7 +297,7 @@ export const fetchListSuggestions = (q) => (dispatch, getState) => {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
const fetchListSuggestionsReady = (query, accounts) => ({
|
||||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
type: LIST_EDITOR_SUGGESTIONS_READY,
|
||||||
query,
|
query,
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -75,15 +75,18 @@ export const connectUserStream = () => connectTimelineStream('home', 'user')
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const connectMessageStream = () => {
|
export const connectChatMessagesStream = (accountId) => {
|
||||||
|
return connectStream(`chat_messages:${accountId}`, null, (dispatch, getState) => {
|
||||||
return connectStream('chat_messages', null, (dispatch, getState) => {
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onConnect() {},
|
onConnect() {
|
||||||
onDisconnect() {},
|
// console.log("chat messages connected")
|
||||||
|
},
|
||||||
|
onDisconnect() {
|
||||||
|
// console.log("chat messages disconnected")
|
||||||
|
},
|
||||||
onReceive (data) {
|
onReceive (data) {
|
||||||
//
|
// : todo :
|
||||||
|
console.log("chat messages onReceive:", data)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -24,10 +24,13 @@ import Avatar from './avatar'
|
|||||||
import DisplayName from './display_name'
|
import DisplayName from './display_name'
|
||||||
import Button from './button'
|
import Button from './button'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
|
|
||||||
class Account extends ImmutablePureComponent {
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleAction = (e) => {
|
handleAction = (e) => {
|
||||||
this.props.onActionClick(this.props.account, e)
|
this.props.onActionClick(this.props.account, e)
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnrequest = () => {
|
handleUnrequest = () => {
|
||||||
|
61
app/javascript/gabsocial/components/avatar_group.js
Normal file
61
app/javascript/gabsocial/components/avatar_group.js
Normal file
@ -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 (
|
||||||
|
<div className={[_s.d].join(' ')}>
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className={[_s.d].join(' ')}>
|
||||||
|
<Image {...options} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarGroup.propTypes = {
|
||||||
|
accounts: ImmutablePropTypes.list,
|
||||||
|
size: PropTypes.number,
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarGroup.defaultProps = {
|
||||||
|
size: 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarGroup
|
81
app/javascript/gabsocial/components/display_name_group.js
Normal file
81
app/javascript/gabsocial/components/display_name_group.js
Normal file
@ -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 (
|
||||||
|
<div />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayNameGroup.propTypes = {
|
||||||
|
accounts: ImmutablePropTypes.map,
|
||||||
|
isLarge: PropTypes.bool,
|
||||||
|
isMultiline: PropTypes.bool,
|
||||||
|
isSmall: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DisplayNameGroup
|
@ -3,21 +3,44 @@ import PropTypes from 'prop-types'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
import { me } from '../initial_state'
|
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 { openModal } from '../actions/modal'
|
||||||
import Button from './button'
|
import Button from './button'
|
||||||
|
|
||||||
|
const initialState = getWindowDimension()
|
||||||
|
|
||||||
class FloatingActionButton extends React.PureComponent {
|
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() {
|
render() {
|
||||||
const {
|
const { intl, onOpenCompose } = this.props
|
||||||
intl,
|
const { width } = this.state
|
||||||
onOpenCompose,
|
|
||||||
isDesktop,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
|
||||||
|
const isDesktop = width > BREAKPOINT_EXTRA_SMALL
|
||||||
const message = intl.formatMessage(messages.gab)
|
const message = intl.formatMessage(messages.gab)
|
||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
@ -56,13 +79,12 @@ const messages = defineMessages({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
onOpenCompose: () => dispatch(openModal('COMPOSE')),
|
onOpenCompose: () => dispatch(openModal(MODAL_COMPOSE)),
|
||||||
})
|
})
|
||||||
|
|
||||||
FloatingActionButton.propTypes = {
|
FloatingActionButton.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onOpenCompose: PropTypes.func.isRequired,
|
onOpenCompose: PropTypes.func.isRequired,
|
||||||
isDesktop: PropTypes.bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(null, mapDispatchToProps)(FloatingActionButton))
|
export default injectIntl(connect(null, mapDispatchToProps)(FloatingActionButton))
|
@ -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 (
|
||||||
|
<ModalLayout
|
||||||
|
title='New Conversation'
|
||||||
|
width={440}
|
||||||
|
onClose={onClose}
|
||||||
|
noPadding
|
||||||
|
>
|
||||||
|
<WrappedBundle component={ChatConversationCreate} componentParams={{ chatConversationId, onCloseModal: onClose }} />
|
||||||
|
</ModalLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatConversationCreateModal.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
chatConversationId: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatConversationCreateModal
|
@ -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 (
|
||||||
|
<ConfirmationModal
|
||||||
|
title='Delete Conversation'
|
||||||
|
message='Are you sure you want to delete this chat conversation? The messages will not be deleted and you the other participant can still view messages.'
|
||||||
|
confirm='Delete'
|
||||||
|
onConfirm={this.handleClick}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onDeleteChatConversation: (chatConversationId) => {
|
||||||
|
dispatch(deleteChatConversation(chatConversationId))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatConversationDeleteModal.propTypes = {
|
||||||
|
chatConversationId: PropTypes.string.isRequired,
|
||||||
|
onDeleteChatConversation: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(ChatConversationDeleteModal)
|
@ -10,6 +10,8 @@ import LoadingModal from './loading_modal'
|
|||||||
import {
|
import {
|
||||||
MODAL_BLOCK_ACCOUNT,
|
MODAL_BLOCK_ACCOUNT,
|
||||||
MODAL_BOOST,
|
MODAL_BOOST,
|
||||||
|
MODAL_CHAT_CONVERSATION_CREATE,
|
||||||
|
MODAL_CHAT_CONVERSATION_DELETE,
|
||||||
MODAL_COMMUNITY_TIMELINE_SETTINGS,
|
MODAL_COMMUNITY_TIMELINE_SETTINGS,
|
||||||
MODAL_COMPOSE,
|
MODAL_COMPOSE,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
@ -42,6 +44,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
BlockAccountModal,
|
BlockAccountModal,
|
||||||
BoostModal,
|
BoostModal,
|
||||||
|
ChatConversationCreateModal,
|
||||||
|
ChatConversationDeleteModal,
|
||||||
CommunityTimelineSettingsModal,
|
CommunityTimelineSettingsModal,
|
||||||
ComposeModal,
|
ComposeModal,
|
||||||
ConfirmationModal,
|
ConfirmationModal,
|
||||||
@ -74,37 +78,40 @@ import {
|
|||||||
VideoModal,
|
VideoModal,
|
||||||
} from '../../features/ui/util/async_components'
|
} from '../../features/ui/util/async_components'
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {}
|
const MODAL_COMPONENTS = {
|
||||||
MODAL_COMPONENTS[MODAL_BLOCK_ACCOUNT] = BlockAccountModal
|
[MODAL_BLOCK_ACCOUNT]: BlockAccountModal,
|
||||||
MODAL_COMPONENTS[MODAL_BOOST] = BoostModal
|
[MODAL_BOOST]: BoostModal,
|
||||||
MODAL_COMPONENTS[MODAL_COMMUNITY_TIMELINE_SETTINGS] = CommunityTimelineSettingsModal
|
[MODAL_CHAT_CONVERSATION_CREATE]: ChatConversationCreateModal,
|
||||||
MODAL_COMPONENTS[MODAL_COMPOSE] = ComposeModal
|
[MODAL_CHAT_CONVERSATION_DELETE]: ChatConversationDeleteModal,
|
||||||
MODAL_COMPONENTS[MODAL_CONFIRM] = ConfirmationModal
|
[MODAL_COMMUNITY_TIMELINE_SETTINGS]: CommunityTimelineSettingsModal,
|
||||||
MODAL_COMPONENTS[MODAL_DISPLAY_OPTIONS] = DisplayOptionsModal
|
[MODAL_COMPOSE]: ComposeModal,
|
||||||
MODAL_COMPONENTS[MODAL_EDIT_SHORTCUTS] = EditShortcutsModal
|
[MODAL_CONFIRM]: ConfirmationModal,
|
||||||
MODAL_COMPONENTS[MODAL_EDIT_PROFILE] = EditProfileModal
|
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
|
||||||
MODAL_COMPONENTS[MODAL_EMAIL_CONFIRMATION_REMINDER] = EmailConfirmationReminderModal
|
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
|
||||||
MODAL_COMPONENTS[MODAL_GROUP_CREATE] = GroupCreateModal
|
[MODAL_EDIT_PROFILE]: EditProfileModal,
|
||||||
MODAL_COMPONENTS[MODAL_GROUP_DELETE] = GroupDeleteModal
|
[MODAL_EMAIL_CONFIRMATION_REMINDER]: EmailConfirmationReminderModal,
|
||||||
MODAL_COMPONENTS[MODAL_GROUP_PASSWORD] = GroupPasswordModal
|
[MODAL_GROUP_CREATE]: GroupCreateModal,
|
||||||
MODAL_COMPONENTS[MODAL_HASHTAG_TIMELINE_SETTINGS] = HashtagTimelineSettingsModal
|
[MODAL_GROUP_DELETE]: GroupDeleteModal,
|
||||||
MODAL_COMPONENTS[MODAL_HOME_TIMELINE_SETTINGS] = HomeTimelineSettingsModal
|
[MODAL_GROUP_PASSWORD]: GroupPasswordModal,
|
||||||
MODAL_COMPONENTS[MODAL_HOTKEYS] = HotkeysModal
|
[MODAL_HASHTAG_TIMELINE_SETTINGS]: HashtagTimelineSettingsModal,
|
||||||
MODAL_COMPONENTS[MODAL_LIST_ADD_USER] = ListAddUserModal
|
[MODAL_HOME_TIMELINE_SETTINGS]: HomeTimelineSettingsModal,
|
||||||
MODAL_COMPONENTS[MODAL_LIST_CREATE] = ListCreateModal
|
[MODAL_HOTKEYS]: HotkeysModal,
|
||||||
MODAL_COMPONENTS[MODAL_LIST_DELETE] = ListDeleteModal
|
[MODAL_LIST_ADD_USER]: ListAddUserModal,
|
||||||
MODAL_COMPONENTS[MODAL_LIST_EDITOR] = ListEditorModal
|
[MODAL_LIST_CREATE]: ListCreateModal,
|
||||||
MODAL_COMPONENTS[MODAL_LIST_TIMELINE_SETTINGS] = ListTimelineSettingsModal
|
[MODAL_LIST_DELETE]: ListDeleteModal,
|
||||||
MODAL_COMPONENTS[MODAL_MEDIA] = MediaModal
|
[MODAL_LIST_EDITOR]: ListEditorModal,
|
||||||
MODAL_COMPONENTS[MODAL_MUTE] = MuteModal
|
[MODAL_LIST_TIMELINE_SETTINGS]: ListTimelineSettingsModal,
|
||||||
MODAL_COMPONENTS[MODAL_PRO_UPGRADE] = ProUpgradeModal
|
[MODAL_MEDIA]: MediaModal,
|
||||||
MODAL_COMPONENTS[MODAL_REPORT] = ReportModal
|
[MODAL_MUTE]: MuteModal,
|
||||||
MODAL_COMPONENTS[MODAL_STATUS_LIKES] = StatusLikesModal
|
[MODAL_PRO_UPGRADE]: ProUpgradeModal,
|
||||||
MODAL_COMPONENTS[MODAL_STATUS_REPOSTS] = StatusRepostsModal
|
[MODAL_REPORT]: ReportModal,
|
||||||
MODAL_COMPONENTS[MODAL_STATUS_REVISIONS] = StatusRevisionsModal
|
[MODAL_STATUS_LIKES]: StatusLikesModal,
|
||||||
MODAL_COMPONENTS[MODAL_UNAUTHORIZED] = UnauthorizedModal
|
[MODAL_STATUS_REPOSTS]: StatusRepostsModal,
|
||||||
MODAL_COMPONENTS[MODAL_UNFOLLOW] = UnfollowModal
|
[MODAL_STATUS_REVISIONS]: StatusRevisionsModal,
|
||||||
MODAL_COMPONENTS[MODAL_VIDEO] = VideoModal
|
[MODAL_UNAUTHORIZED]: UnauthorizedModal,
|
||||||
|
[MODAL_UNFOLLOW]: UnfollowModal,
|
||||||
|
[MODAL_VIDEO]: VideoModal,
|
||||||
|
}
|
||||||
|
|
||||||
const CENTERED_XS_MODALS = [
|
const CENTERED_XS_MODALS = [
|
||||||
MODAL_BLOCK_ACCOUNT,
|
MODAL_BLOCK_ACCOUNT,
|
||||||
|
@ -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 (
|
||||||
|
<PlaceholderLayout viewBox='0 0 400 110' preserveAspectRatio='xMaxYMin meet'>
|
||||||
|
<rect x='80' y='0' rx='20' ry='20' width='260' height={height} />
|
||||||
|
<circle cx='380' cy='20' r='20' />
|
||||||
|
</PlaceholderLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlaceholderLayout viewBox='0 0 400 110' preserveAspectRatio='xMinYMax meet'>
|
||||||
|
<circle cx='20' cy='20' r='20' />
|
||||||
|
<rect x='60' y='0' rx='20' ry='20' width={width} height={height} />
|
||||||
|
</PlaceholderLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,6 +12,7 @@ class PlaceholderLayout extends React.PureComponent {
|
|||||||
intl,
|
intl,
|
||||||
theme,
|
theme,
|
||||||
viewBox,
|
viewBox,
|
||||||
|
preserveAspectRatio,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const isLight = ['light', 'white', ''].indexOf(theme) > -1
|
const isLight = ['light', 'white', ''].indexOf(theme) > -1
|
||||||
@ -26,6 +27,7 @@ class PlaceholderLayout extends React.PureComponent {
|
|||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
foregroundColor={foregroundColor}
|
foregroundColor={foregroundColor}
|
||||||
|
preserveAspectRatio={preserveAspectRatio}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</ContentLoader>
|
</ContentLoader>
|
||||||
@ -47,6 +49,7 @@ PlaceholderLayout.propTypes = {
|
|||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
viewBox: PropTypes.string.isRequired,
|
viewBox: PropTypes.string.isRequired,
|
||||||
|
preserveAspectRatio: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps)(PlaceholderLayout))
|
export default injectIntl(connect(mapStateToProps)(PlaceholderLayout))
|
||||||
|
@ -94,30 +94,32 @@ class ScrollableList extends React.PureComponent {
|
|||||||
|
|
||||||
handleScroll = throttle(() => {
|
handleScroll = throttle(() => {
|
||||||
if (this.window) {
|
if (this.window) {
|
||||||
const { scrollTop, scrollHeight } = this.documentElement;
|
const { scrollTop, scrollHeight } = this.documentElement
|
||||||
const { innerHeight } = this.window;
|
const { innerHeight } = this.window
|
||||||
const offset = scrollHeight - scrollTop - innerHeight;
|
const offset = scrollHeight - scrollTop - innerHeight
|
||||||
|
|
||||||
if (600 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading && !this.props.disableInfiniteScroll) {
|
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) {
|
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) {
|
} else if (this.props.onScroll) {
|
||||||
this.props.onScroll();
|
this.props.onScroll()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.lastScrollWasSynthetic) {
|
if (!this.lastScrollWasSynthetic) {
|
||||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
||||||
// intentional and cancel any pending scroll reset on mouse idle
|
// intentional and cancel any pending scroll reset on mouse idle
|
||||||
this.scrollToTopOnMouseIdle = false;
|
this.scrollToTopOnMouseIdle = false
|
||||||
}
|
}
|
||||||
this.lastScrollWasSynthetic = false;
|
this.lastScrollWasSynthetic = false
|
||||||
}
|
}
|
||||||
}, 150, {
|
}, 150, {
|
||||||
trailing: true,
|
trailing: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
handleWheel = throttle(() => {
|
handleWheel = throttle(() => {
|
||||||
this.scrollToTopOnMouseIdle = false;
|
this.scrollToTopOnMouseIdle = false;
|
||||||
@ -175,6 +177,11 @@ class ScrollableList extends React.PureComponent {
|
|||||||
this.props.onLoadMore();
|
this.props.onLoadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c
|
||||||
|
if (this.props.scrollRef) this.props.scrollRef(c)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
@ -186,6 +193,8 @@ class ScrollableList extends React.PureComponent {
|
|||||||
onLoadMore,
|
onLoadMore,
|
||||||
placeholderComponent: Placeholder,
|
placeholderComponent: Placeholder,
|
||||||
placeholderCount,
|
placeholderCount,
|
||||||
|
onScrollToTop,
|
||||||
|
onScrollToBottom,
|
||||||
} = this.props
|
} = this.props
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
@ -210,8 +219,18 @@ class ScrollableList extends React.PureComponent {
|
|||||||
return <ColumnIndicator type='loading' />
|
return <ColumnIndicator type='loading' />
|
||||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||||
return (
|
return (
|
||||||
<div onMouseMove={this.handleMouseMove}>
|
<div onMouseMove={this.handleMouseMove} ref={this.setRef}>
|
||||||
<div role='feed'>
|
<div role='feed'>
|
||||||
|
{
|
||||||
|
(hasMore && onLoadMore && !isLoading) && !!onScrollToBottom &&
|
||||||
|
<LoadMore onClick={this.handleLoadMore} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading && !!onScrollToBottom &&
|
||||||
|
<ColumnIndicator type='loading' />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!this.props.children &&
|
!!this.props.children &&
|
||||||
React.Children.map(this.props.children, (child, index) => (
|
React.Children.map(this.props.children, (child, index) => (
|
||||||
@ -234,12 +253,12 @@ class ScrollableList extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
(hasMore && onLoadMore && !isLoading) &&
|
(hasMore && onLoadMore && !isLoading) && !!onScrollToTop &&
|
||||||
<LoadMore onClick={this.handleLoadMore} />
|
<LoadMore onClick={this.handleLoadMore} />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isLoading &&
|
isLoading && !!onScrollToTop &&
|
||||||
<ColumnIndicator type='loading' />
|
<ColumnIndicator type='loading' />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -268,9 +287,10 @@ ScrollableList.propTypes = {
|
|||||||
]),
|
]),
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
|
onScrollToBottom: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
placeholderComponent: PropTypes.node,
|
placeholderComponent: PropTypes.node,
|
||||||
placeholderCount: PropTypes.node,
|
placeholderCount: PropTypes.number,
|
||||||
disableInfiniteScroll: PropTypes.bool,
|
disableInfiniteScroll: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import {
|
import { CX } from '../../constants'
|
||||||
CX,
|
|
||||||
BREAKPOINT_SMALL,
|
|
||||||
} from '../../constants'
|
|
||||||
import {
|
import {
|
||||||
me,
|
me,
|
||||||
emailConfirmed,
|
emailConfirmed,
|
||||||
} from '../../initial_state'
|
} from '../../initial_state'
|
||||||
import Button from '../button'
|
import Button from '../button'
|
||||||
import { openModal } from '../../actions/modal'
|
|
||||||
import Responsive from '../../features/ui/util/responsive_component'
|
|
||||||
import Heading from '../heading'
|
import Heading from '../heading'
|
||||||
import BackButton from '../back_button'
|
import BackButton from '../back_button'
|
||||||
import Pills from '../pills'
|
import Pills from '../pills'
|
||||||
|
|
||||||
class SidebarLayout extends React.PureComponent {
|
class SidebarLayout extends React.PureComponent {
|
||||||
|
|
||||||
handleOpenComposeModal = () => {
|
|
||||||
this.props.onOpenComposeModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
actions,
|
actions,
|
||||||
@ -91,30 +82,6 @@ class SidebarLayout extends React.PureComponent {
|
|||||||
{children}
|
{children}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{
|
|
||||||
!!me &&
|
|
||||||
<Responsive min={BREAKPOINT_SMALL}>
|
|
||||||
<Button
|
|
||||||
onClick={this.handleOpenComposeModal}
|
|
||||||
className={_s.py15}
|
|
||||||
icon='pencil'
|
|
||||||
iconSize='18px'
|
|
||||||
iconClassName={[_s.py5, _s.px5].join(' ')}
|
|
||||||
/>
|
|
||||||
</Responsive>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!me &&
|
|
||||||
<Responsive max={BREAKPOINT_SMALL}>
|
|
||||||
<Button
|
|
||||||
onClick={this.handleOpenComposeModal}
|
|
||||||
className={_s.py15}
|
|
||||||
icon='pencil'
|
|
||||||
/>
|
|
||||||
</Responsive>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -124,18 +91,11 @@ class SidebarLayout extends React.PureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onOpenComposeModal() {
|
|
||||||
dispatch(openModal('COMPOSE'))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
SidebarLayout.propTypes = {
|
SidebarLayout.propTypes = {
|
||||||
onOpenComposeModal: PropTypes.func.isRequired,
|
|
||||||
actions: PropTypes.array,
|
actions: PropTypes.array,
|
||||||
tabs: PropTypes.array,
|
tabs: PropTypes.array,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
showBackBtn: PropTypes.bool,
|
showBackBtn: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(SidebarLayout)
|
export default SidebarLayout
|
@ -290,7 +290,7 @@ class StatusList extends ImmutablePureComponent {
|
|||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
/>
|
/>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
ref={this.setRef}
|
scrollRef={this.setRef}
|
||||||
isLoading={isLoading || isRefreshing}
|
isLoading={isLoading || isRefreshing}
|
||||||
showLoading={isRefreshing || (isLoading && statusIds.size === 0)}
|
showLoading={isRefreshing || (isLoading && statusIds.size === 0)}
|
||||||
onLoadMore={onLoadMore && this.handleLoadOlder}
|
onLoadMore={onLoadMore && this.handleLoadOlder}
|
||||||
|
@ -4,6 +4,7 @@ import { CX } from '../constants'
|
|||||||
|
|
||||||
// Define colors for enumeration for Text component `color` prop
|
// Define colors for enumeration for Text component `color` prop
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
|
alt: 'alt',
|
||||||
primary: 'primary',
|
primary: 'primary',
|
||||||
secondary: 'secondary',
|
secondary: 'secondary',
|
||||||
tertiary: 'tertiary',
|
tertiary: 'tertiary',
|
||||||
@ -76,6 +77,7 @@ class Text extends React.PureComponent {
|
|||||||
lineHeight15: isBadge,
|
lineHeight15: isBadge,
|
||||||
px5: isBadge,
|
px5: isBadge,
|
||||||
|
|
||||||
|
cAlt: color === COLORS.alt,
|
||||||
cPrimary: color === COLORS.primary,
|
cPrimary: color === COLORS.primary,
|
||||||
cSecondary: color === COLORS.secondary,
|
cSecondary: color === COLORS.secondary,
|
||||||
cTertiary: color === COLORS.tertiary,
|
cTertiary: color === COLORS.tertiary,
|
||||||
|
@ -35,19 +35,31 @@ class Toast extends React.PureComponent {
|
|||||||
message,
|
message,
|
||||||
date,
|
date,
|
||||||
to,
|
to,
|
||||||
type,
|
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const contentClasses = CX({
|
const containerClasses = CX({
|
||||||
default: 1,
|
d: 1,
|
||||||
|
radiusSmall: 1,
|
||||||
|
w228PX: 1,
|
||||||
mt5: 1,
|
mt5: 1,
|
||||||
pt2: 1,
|
mb5: 1,
|
||||||
maxWidth240PX: 1,
|
px15: 1,
|
||||||
|
pt10: 1,
|
||||||
|
pb15: !!title,
|
||||||
|
pb10: !title,
|
||||||
|
bgToast: 1,
|
||||||
|
boxShadowToast: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const contentClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
mt5: !!title,
|
||||||
|
pt2: !!title,
|
||||||
flexRow: !!image,
|
flexRow: !!image,
|
||||||
})
|
})
|
||||||
|
|
||||||
const innerContentClasses = CX({
|
const innerContentClasses = CX({
|
||||||
default: 1,
|
d: 1,
|
||||||
flexNormal: 1,
|
flexNormal: 1,
|
||||||
pl10: !!image,
|
pl10: !!image,
|
||||||
pt2: !!image,
|
pt2: !!image,
|
||||||
@ -65,19 +77,11 @@ class Toast extends React.PureComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.default, _s.radiusSmall, _s.mb5, _s.px15, _s.pt10, _s.pb15, _s.bgPrimary, _s.boxShadowToast].join(' ')}>
|
<div className={containerClasses}>
|
||||||
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
|
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
|
||||||
<Text size='media' weight='medium' className={[_s.mr15, _s.minWidth160PX].join(' ')}>
|
<Text size='medium' color='alt' weight='bold'>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
|
||||||
backgroundColor='secondary'
|
|
||||||
color='primary'
|
|
||||||
icon='close'
|
|
||||||
iconSize='6px'
|
|
||||||
onClick={this.handleOnDismiss}
|
|
||||||
className={[_s.mlAuto, _s.px10].join(' ')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={contentClasses}>
|
<div className={contentClasses}>
|
||||||
{
|
{
|
||||||
@ -90,12 +94,16 @@ class Toast extends React.PureComponent {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div className={innerContentClasses}>
|
<div className={innerContentClasses}>
|
||||||
<Text size='small'>
|
<Text size='small' color='alt'>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
{
|
{
|
||||||
date &&
|
date &&
|
||||||
<Text color='secondary' size='extraSmall' className={dateClasses}>
|
<Text color='tertiary' size='extraSmall' className={dateClasses}>
|
||||||
|
{
|
||||||
|
!image &&
|
||||||
|
<Text size='small' color='tertiary' className={[_s.ml5, _s.mr5].join(' ')}>·</Text>
|
||||||
|
}
|
||||||
<RelativeTimestamp timestamp={date} />
|
<RelativeTimestamp timestamp={date} />
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@ -116,10 +124,6 @@ Toast.propTypes = {
|
|||||||
onDismiss: PropTypes.func.isRequired,
|
onDismiss: PropTypes.func.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
type: PropTypes.oneOf([
|
|
||||||
TOAST_TYPE_ERROR,
|
|
||||||
TOAST_TYPE_SUCCESS,
|
|
||||||
]).isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Toast
|
export default Toast
|
@ -43,6 +43,8 @@ export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
|
|||||||
|
|
||||||
export const MODAL_BLOCK_ACCOUNT = 'BLOCK_ACCOUNT'
|
export const MODAL_BLOCK_ACCOUNT = 'BLOCK_ACCOUNT'
|
||||||
export const MODAL_BOOST = 'BOOST'
|
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_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS'
|
||||||
export const MODAL_COMPOSE = 'COMPOSE'
|
export const MODAL_COMPOSE = 'COMPOSE'
|
||||||
export const MODAL_CONFIRM = 'CONFIRM'
|
export const MODAL_CONFIRM = 'CONFIRM'
|
||||||
|
@ -16,6 +16,7 @@ import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants'
|
|||||||
import {
|
import {
|
||||||
connectUserStream,
|
connectUserStream,
|
||||||
connectStatusUpdateStream,
|
connectStatusUpdateStream,
|
||||||
|
connectChatMessagesStream,
|
||||||
} from '../actions/streaming'
|
} from '../actions/streaming'
|
||||||
import { getLocale } from '../locales'
|
import { getLocale } from '../locales'
|
||||||
import initialState from '../initial_state'
|
import initialState from '../initial_state'
|
||||||
@ -95,6 +96,7 @@ export default class GabSocial extends React.PureComponent {
|
|||||||
if (!!me) {
|
if (!!me) {
|
||||||
this.disconnect = store.dispatch(connectUserStream())
|
this.disconnect = store.dispatch(connectUserStream())
|
||||||
store.dispatch(connectStatusUpdateStream())
|
store.dispatch(connectStatusUpdateStream())
|
||||||
|
store.dispatch(connectChatMessagesStream(me))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('%cGab Social ', [
|
console.log('%cGab Social ', [
|
||||||
|
@ -13,11 +13,58 @@ class ToastsContainer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 hasNotifications = Array.isArray(notifications) && notifications.length > 0
|
||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
default: 1,
|
d: 1,
|
||||||
z5: 1,
|
z5: 1,
|
||||||
posFixed: 1,
|
posFixed: 1,
|
||||||
bottom0: 1,
|
bottom0: 1,
|
||||||
@ -29,16 +76,16 @@ class ToastsContainer extends React.PureComponent {
|
|||||||
displayNone: !hasNotifications
|
displayNone: !hasNotifications
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{
|
{
|
||||||
hasNotifications && notifications.map((notification) => (
|
!hasNotifications && notifications.map((notification) => (
|
||||||
<Toast
|
<Toast
|
||||||
onDismiss={this.handleOnDismiss}
|
onDismiss={this.handleOnDismiss}
|
||||||
key={notification.key}
|
key={notification.key}
|
||||||
id={notification.key}
|
id={notification.key}
|
||||||
title={notification.title}
|
title={notification.title}
|
||||||
type={notification.type}
|
|
||||||
to={notification.to}
|
to={notification.to}
|
||||||
image={notification.image}
|
image={notification.image}
|
||||||
message={notification.message}
|
message={notification.message}
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { defineMessages, injectIntl } 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 { fetchBlocks, expandBlocks } from '../actions/blocks'
|
||||||
|
import Account from '../components/account'
|
||||||
|
import Block from '../components/block'
|
||||||
|
import BlockHeading from '../components/block_heading'
|
||||||
|
import Divider from '../components/divider'
|
||||||
|
import ScrollableList from '../components/scrollable_list'
|
||||||
|
import AccountPlaceholder from '../components/placeholder/account_placeholder'
|
||||||
|
|
||||||
|
class MessagesBlockedAccounts extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.onFetchBlocks()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.onExpandBlocks()
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
intl,
|
||||||
|
accountIds,
|
||||||
|
hasMore,
|
||||||
|
isLoading,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const emptyMessage = intl.formatMessage(messages.empty)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.h60PX, _s.w100PC, _s.px10, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
|
<BlockHeading title={intl.formatMessage(messages.blocks)} />
|
||||||
|
</div>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='blocked_accounts'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
placeholderComponent={AccountPlaceholder}
|
||||||
|
placeholderCount={3}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
accountIds && accountIds.map((id) => (
|
||||||
|
<Account
|
||||||
|
key={`blocked-accounts-${id}`}
|
||||||
|
id={id}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ScrollableList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
@ -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 (
|
||||||
|
<Form>
|
||||||
|
<div className={[_s.d, _s.px15, _s.pt10].join(' ')}>
|
||||||
|
<Input
|
||||||
|
title='Search for a user'
|
||||||
|
value={query}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={[_s.d, _s.pt10].join(' ')}>
|
||||||
|
<div className={[_s.d].join(' ')}>
|
||||||
|
<Text weight='bold' size='small' color='secondary' className={[_s.d, _s.px15, _s.ml15, _s.mt5, _s.mb15].join(' ')}>
|
||||||
|
Search results ({suggestionsIds.size})
|
||||||
|
</Text>
|
||||||
|
{
|
||||||
|
suggestionsIds &&
|
||||||
|
suggestionsIds.map((accountId) => (
|
||||||
|
<Account
|
||||||
|
compact
|
||||||
|
key={`remove-from-list-${accountId}`}
|
||||||
|
id={accountId}
|
||||||
|
onActionClick={() => this.handleOnCreateChatConversation(accountId)}
|
||||||
|
actionIcon='add'
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
@ -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 (
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.h60PX, _s.w100PC, _s.px10, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
|
<BlockHeading title={<FormattedMessage id='navigation_bar.mutes' defaultMessage='Muted users' />} />
|
||||||
|
</div>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='mutes'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
accountIds && accountIds.map((id) =>
|
||||||
|
<Account
|
||||||
|
key={`mutes-${id}`}
|
||||||
|
id={id}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ScrollableList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
@ -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 (
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.h60PX, _s.w100PC, _s.px10, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
|
<BlockHeading title={'Message Requests'} />
|
||||||
|
</div>
|
||||||
|
<ChatConversationsList source='requested' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatConversationRequests
|
@ -157,7 +157,7 @@ class PollFormOption extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||||
maxLength={25}
|
maxLength={160}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={this.handleOptionTitleChange}
|
onChange={this.handleOptionTitleChange}
|
||||||
/>
|
/>
|
||||||
|
@ -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 (
|
||||||
|
<div className={[_s.d, _s.bgPrimary, _s.h100PC, _s.w100PC].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
|
<Text weight='bold' size='extraLarge'>
|
||||||
|
You don’t have a message selected
|
||||||
|
</Text>
|
||||||
|
<Text size='medium' color='secondary' className={_s.py10}>
|
||||||
|
Choose one from your existing messages, or start a new one.
|
||||||
|
</Text>
|
||||||
|
<Button className={_s.mt10} onClick={this.handleOnCreateNewChat}>
|
||||||
|
<Text color='inherit' weight='bold' className={_s.px15}>
|
||||||
|
New Message
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onOpenChatConversationCreateModal() {
|
||||||
|
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatEmptyMessageBlock.propTypes = {
|
||||||
|
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(ChatEmptyMessageBlock)
|
@ -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 (
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.overflowHidden, _s.boxShadowNone].join(' ')}>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='chat-conversations'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading}
|
||||||
|
placeholderComponent={AccountPlaceholder}
|
||||||
|
placeholderCount={3}
|
||||||
|
emptyMessage='Empty'
|
||||||
|
>
|
||||||
|
{
|
||||||
|
!!chatConversationIds && chatConversationIds.map((chatConversationId, i) => (
|
||||||
|
<ChatConversationsListItem
|
||||||
|
key={`chat-conversation-${i}`}
|
||||||
|
chatConversationId={chatConversationId}
|
||||||
|
source={source}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ScrollableList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
@ -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 <div/>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
className={containerClasses}
|
||||||
|
onClick={this.handleOnClick}
|
||||||
|
>
|
||||||
|
<div className={innerContainerClasses}>
|
||||||
|
<AvatarGroup accounts={otherAccounts} size={avatarSize} noHover />
|
||||||
|
|
||||||
|
<div className={[_s.d, _s.pl10, _s.overflowHidden, _s.flexNormal].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.pt2, _s.pr5, _s.noUnderline, _s.overflowHidden, _s.flexNormal, _s.flexRow, _s.aiStart, _s.aiCenter].join(' ')}>
|
||||||
|
<div className={_s.maxW100PC42PX}>
|
||||||
|
<DisplayName account={otherAccounts.get(0)} noHover />
|
||||||
|
</div>
|
||||||
|
<Text size='extraSmall' color='secondary' className={_s.mlAuto}>
|
||||||
|
<RelativeTimestamp timestamp={date} />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={[_s.py5, _s.dangerousContent, _s.textAlignLeft].join(' ')} dangerouslySetInnerHTML={content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
@ -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 (
|
||||||
|
<NavLink
|
||||||
|
className={[_s.d, _s.w100PC, _s.bgTransparent, _s.bgSubtle_onHover, _s.borderBottom1PX, _s.borderColorSecondary, _s.noUnderline, _s.outlineNone, _s.cursorPointer].join(' ')}
|
||||||
|
to='/messages/requests'
|
||||||
|
>
|
||||||
|
<div className={[_s.d, _s.px15, _s.py15, _s.aiCenter, _s.flexRow].join(' ')}>
|
||||||
|
<Text size='medium'>Message Requests</Text>
|
||||||
|
<Text size='medium' className={[_s.mlAuto, _s.mr15].join(' ')}>
|
||||||
|
{shortNumberFormat(requestCount)}
|
||||||
|
</Text>
|
||||||
|
<Icon id='angle-right' size='10px' className={_s.cPrimary} />
|
||||||
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
@ -1,11 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
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'
|
import Input from '../../../components/input'
|
||||||
|
|
||||||
class MessagesSearch extends ImmutablePureComponent {
|
class ChatConversationsSearch extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
@ -17,7 +14,7 @@ class MessagesSearch extends ImmutablePureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
intl,
|
children
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,12 +31,8 @@ class MessagesSearch extends ImmutablePureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
ChatConversationsSearch.propTypes = {
|
||||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" },
|
//
|
||||||
})
|
|
||||||
|
|
||||||
MessagesSearch.propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(MessagesSearch)
|
export default ChatConversationsSearch
|
@ -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 (
|
||||||
|
<div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.bgPrimary, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15, _s.py5].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.pr15, _s.flexGrow1].join(' ')}>
|
||||||
|
<Textarea
|
||||||
|
id='chat-message-compose-input'
|
||||||
|
inputRef={this.setTextbox}
|
||||||
|
className={textareaClasses}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder='Type a new message...'
|
||||||
|
autoFocus={false}
|
||||||
|
value={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
aria-autocomplete='list'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={[_s.d, _s.h100PC, _s.mtAuto, _s.pb5].join(' ')}>
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={this.handleOnSendChatMessage}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onSendChatMessage(text, chatConversationId) {
|
||||||
|
dispatch(sendChatMessage(text, chatConversationId))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatMessagesComposeForm.propTypes = {
|
||||||
|
chatConversationId: PropTypes.string,
|
||||||
|
onSendMessage: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(ChatMessagesComposeForm)
|
@ -0,0 +1,84 @@
|
|||||||
|
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 { makeGetChatConversation } from '../../../selectors'
|
||||||
|
import { openModal } from '../../../actions/modal'
|
||||||
|
import { approveChatConversationRequest } from '../../../actions/chat_conversations'
|
||||||
|
import { MODAL_CHAT_CONVERSATION_CREATE } from '../../../constants'
|
||||||
|
import Button from '../../../components/button'
|
||||||
|
import AvatarGroup from '../../../components/avatar_group'
|
||||||
|
import DisplayName from '../../../components/display_name'
|
||||||
|
import Text from '../../../components/text'
|
||||||
|
|
||||||
|
class ChatMessageHeader extends React.PureComponent {
|
||||||
|
|
||||||
|
handleOnApproveMessageRequest = () => {
|
||||||
|
this.props.onApproveChatConversationRequest(this.props.chatConversationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { chatConversation } = this.props
|
||||||
|
|
||||||
|
const isChatConversationRequest = !!chatConversation ? !chatConversation.get('is_approved') : false
|
||||||
|
const otherAccounts = !!chatConversation ? chatConversation.get('other_accounts') : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[_s.d, _s.posAbs, _s.top0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.h60PX, _s.w100PC, _s.borderBottom1PX, _s.borderColorSecondary, _s.bgPrimary, _s.px15, _s.py5].join(' ')}>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!otherAccounts &&
|
||||||
|
<React.Fragment>
|
||||||
|
<AvatarGroup accounts={otherAccounts} size={34} noHover />
|
||||||
|
<div className={[_s.d, _s.pl10, _s.maxW100PC86PX, _s.overflowHidden].join(' ')}>
|
||||||
|
<DisplayName account={otherAccounts.get(0)} isMultiline />
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
isNarrow
|
||||||
|
onClick={undefined}
|
||||||
|
color='primary'
|
||||||
|
backgroundColor='secondary'
|
||||||
|
className={[_s.mlAuto, _s.px5].join(' ')}
|
||||||
|
icon='ellipsis'
|
||||||
|
iconSize='18px'
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
isChatConversationRequest &&
|
||||||
|
<Button
|
||||||
|
isNarrow
|
||||||
|
onClick={this.handleOnApproveMessageRequest}
|
||||||
|
className={_s.ml10}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
Approve Message Request
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { chatConversationId }) => ({
|
||||||
|
chatConversation: makeGetChatConversation()(state, { id: chatConversationId }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onOpenChatConversationCreateModal() {
|
||||||
|
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
|
||||||
|
},
|
||||||
|
onApproveChatConversationRequest(chatConversationId) {
|
||||||
|
dispatch(approveChatConversationRequest(chatConversationId))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatMessageHeader.propTypes = {
|
||||||
|
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
|
||||||
|
chatConversationId: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageHeader)
|
@ -0,0 +1,179 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import moment from 'moment-mini'
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import { CX } from '../../../constants'
|
||||||
|
import { me } from '../../../initial_state'
|
||||||
|
import Input from '../../../components/input'
|
||||||
|
import Avatar from '../../../components/avatar'
|
||||||
|
import Button from '../../../components/button'
|
||||||
|
import Text from '../../../components/text'
|
||||||
|
import { makeGetChatMessage } from '../../../selectors'
|
||||||
|
|
||||||
|
class ChatMessageItem extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hovering: false,
|
||||||
|
isNewDay: false,
|
||||||
|
isCloseToMyLast: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { lastChatMessageSameSender, lastChatMessageDate } = this.props
|
||||||
|
if (lastChatMessageDate) {
|
||||||
|
const createdAt = this.props.chatMessage.get('created_at')
|
||||||
|
const isNewDay = moment(createdAt).format('L') !== moment(lastChatMessageDate).format('L')
|
||||||
|
const isCloseToMyLast = moment(lastChatMessageDate).diff(createdAt, 'minutes') < 60 && lastChatMessageSameSender && !isNewDay
|
||||||
|
this.setState({
|
||||||
|
isNewDay,
|
||||||
|
isCloseToMyLast,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseEnter = () => {
|
||||||
|
this.setState({ isHovering: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseLeave = () => {
|
||||||
|
this.setState({ isHovering: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoreClick = () => {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
chatMessage,
|
||||||
|
isHidden,
|
||||||
|
lastChatMessageDate,
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
isCloseToMyLast,
|
||||||
|
isHovering,
|
||||||
|
isNewDay,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
if (!chatMessage) return <div />
|
||||||
|
|
||||||
|
const account = chatMessage.get('account')
|
||||||
|
const content = { __html: chatMessage.get('text') }
|
||||||
|
const alt = account.get('id', null) === me
|
||||||
|
const createdAt = chatMessage.get('created_at')
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{account.get('display_name')}
|
||||||
|
<div dangerouslySetInnerHTML={content} />
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageContainerClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
flexRow: !alt,
|
||||||
|
flexRowReverse: alt,
|
||||||
|
pb5: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const messageInnerContainerClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
px15: 1,
|
||||||
|
py5: 1,
|
||||||
|
bgTertiary: !alt,
|
||||||
|
bgSecondary: alt,
|
||||||
|
circle: 1,
|
||||||
|
ml10: 1,
|
||||||
|
mr10: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const lowerContainerClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
pt10: 1,
|
||||||
|
posAbs: 1,
|
||||||
|
bottom0: 1,
|
||||||
|
right0: alt,
|
||||||
|
left0: !alt,
|
||||||
|
displayNone: !isHovering,
|
||||||
|
pl50: !alt,
|
||||||
|
pr50: alt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonContainerClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
flexRow: 1,
|
||||||
|
displayNone: !isHovering && alt,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[_s.d, _s.w100PC, _s.pb10].join(' ')}
|
||||||
|
onMouseEnter={this.handleOnMouseEnter}
|
||||||
|
onMouseLeave={this.handleOnMouseLeave}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
!!lastChatMessageDate && isNewDay &&
|
||||||
|
<Text color='secondary' size='small' align='center' className={[_s.d, _s.py10].join(' ')}>
|
||||||
|
{moment(createdAt).format('lll')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.pb15].join(' ')}>
|
||||||
|
|
||||||
|
<div className={messageContainerClasses}>
|
||||||
|
<Avatar account={chatMessage.get('account')} size={38} />
|
||||||
|
<div className={messageInnerContainerClasses}>
|
||||||
|
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
alt &&
|
||||||
|
<div className={buttonContainerClasses}>
|
||||||
|
<Button
|
||||||
|
onClick={this.handleMoreClick}
|
||||||
|
color='tertiary'
|
||||||
|
backgroundColor='none'
|
||||||
|
icon='ellipsis'
|
||||||
|
iconSize='18px'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={lowerContainerClasses}>
|
||||||
|
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
|
||||||
|
{moment(createdAt).format('lll')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
|
||||||
|
chatMessage: makeGetChatMessage()(state, { id: chatMessageId }),
|
||||||
|
lastChatMessageDate: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'created_at'], null) : null,
|
||||||
|
lastChatMessageSameSender: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'from_account_id'], null) === state.getIn(['chat_messages', `${chatMessageId}`, 'from_account_id'], null) : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatMessageItem.propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
lastChatMessageId: PropTypes.string,
|
||||||
|
lastChatMessageDate: PropTypes.string,
|
||||||
|
lastChatMessageSameSender: PropTypes.string,
|
||||||
|
chatMessageId: PropTypes.string.isRequired,
|
||||||
|
chatMessage: ImmutablePropTypes.map,
|
||||||
|
isHidden: PropTypes.bool,
|
||||||
|
alt: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ChatMessageItem)
|
@ -0,0 +1,212 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import moment from 'moment-mini'
|
||||||
|
import { List as ImmutableList } from 'immutable'
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
import debounce from 'lodash.debounce'
|
||||||
|
import { me } from '../../../initial_state'
|
||||||
|
import { setChatConversationSelected } from '../../../actions/chats'
|
||||||
|
import {
|
||||||
|
expandChatMessages,
|
||||||
|
scrollBottomChatMessageConversation,
|
||||||
|
} from '../../../actions/chat_conversation_messages'
|
||||||
|
import ScrollableList from '../../../components/scrollable_list'
|
||||||
|
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
|
||||||
|
import ChatMessageItem from './chat_message_item'
|
||||||
|
|
||||||
|
class ChatMessageScrollingList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isRefreshing: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { chatConversationId } = this.props
|
||||||
|
this.props.onExpandChatMessages(chatConversationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.onSetChatConversationSelected(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
const { chatConversationId } = nextProps
|
||||||
|
|
||||||
|
if (chatConversationId !== this.props.chatConversationId) {
|
||||||
|
this.props.onExpandChatMessages(chatConversationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = (sinceId) => {
|
||||||
|
const { chatConversationId, dispatch } = this.props
|
||||||
|
this.props.onExpandChatMessages(chatConversationId, { sinceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this.state.isRefreshing) {
|
||||||
|
this.setState({ isRefreshing: false })
|
||||||
|
}
|
||||||
|
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0) {
|
||||||
|
this.containerNode.scrollTop = this.containerNode.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentChatMessageIndex = (id) => {
|
||||||
|
// : todo :
|
||||||
|
return this.props.chatMessageIds.indexOf(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoveUp = (id) => {
|
||||||
|
const elementIndex = this.getCurrentChatMessageIndex(id) - 1
|
||||||
|
this._selectChild(elementIndex, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoveDown = (id) => {
|
||||||
|
const elementIndex = this.getCurrentChatMessageIndex(id) + 1
|
||||||
|
this._selectChild(elementIndex, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadOlder = debounce(() => {
|
||||||
|
this.handleLoadMore(this.props.chatMessageIds.size > 0 ? this.props.chatMessageIds.last() : undefined)
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
handleOnReload = debounce(() => {
|
||||||
|
this.handleLoadMore()
|
||||||
|
this.setState({ isRefreshing: true })
|
||||||
|
}, 300, { trailing: true })
|
||||||
|
|
||||||
|
_selectChild(index, align_top) {
|
||||||
|
const container = this.node.node
|
||||||
|
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`)
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (align_top && container.scrollTop > element.offsetTop) {
|
||||||
|
element.scrollIntoView(true)
|
||||||
|
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||||
|
element.scrollIntoView(false)
|
||||||
|
}
|
||||||
|
element.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c
|
||||||
|
}
|
||||||
|
|
||||||
|
containerRef = (c) => {
|
||||||
|
this.containerNode = c
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
chatConversationId,
|
||||||
|
chatMessageIds,
|
||||||
|
isLoading,
|
||||||
|
isPartial,
|
||||||
|
hasMore,
|
||||||
|
onScrollToBottom,
|
||||||
|
onScroll,
|
||||||
|
} = this.props
|
||||||
|
const { isRefreshing } = this.state
|
||||||
|
|
||||||
|
if (isPartial || (isLoading && chatMessageIds.size === 0)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollableContent = []
|
||||||
|
let emptyContent = []
|
||||||
|
|
||||||
|
if (isLoading || chatMessageIds.size > 0) {
|
||||||
|
for (let i = 0; i < chatMessageIds.count(); i++) {
|
||||||
|
const chatMessageId = chatMessageIds.get(i)
|
||||||
|
const lastChatMessageId = i > 0 ? chatMessageIds.get(i - 1) : null
|
||||||
|
if (!chatMessageId) {
|
||||||
|
scrollableContent.unshift(
|
||||||
|
<div
|
||||||
|
key={`chat-message-gap:${(i + 1)}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
sinceId={i > 0 ? chatMessageIds.get(i - 1) : null}
|
||||||
|
onClick={this.handleLoadMore}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scrollableContent.unshift(
|
||||||
|
<ChatMessageItem
|
||||||
|
key={`chat-message-${chatConversationId}-${i}`}
|
||||||
|
chatMessageId={chatMessageId}
|
||||||
|
lastChatMessageId={lastChatMessageId}
|
||||||
|
onMoveUp={this.handleMoveUp}
|
||||||
|
onMoveDown={this.handleMoveDown}
|
||||||
|
commentsLimited
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[_s.d, _s.boxShadowNone, _s.posAbs, _s.bottom60PX, _s.left0, _s.right0, _s.px15, _s.py15, _s.top60PX, _s.w100PC, _s.overflowYScroll].join(' ')}
|
||||||
|
ref={this.containerRef}
|
||||||
|
>
|
||||||
|
<ScrollableList
|
||||||
|
scrollRef={this.setRef}
|
||||||
|
onLoadMore={this.handleLoadMore && this.handleLoadOlder}
|
||||||
|
scrollKey='chat_messages'
|
||||||
|
hasMore={hasMore}
|
||||||
|
emptyMessage='No chats found'
|
||||||
|
onScrollToBottom={onScrollToBottom}
|
||||||
|
onScroll={onScroll}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{scrollableContent}
|
||||||
|
</ScrollableList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { chatConversationId }) => {
|
||||||
|
if (!chatConversationId) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatMessageIds: state.getIn(['chat_conversation_messages', chatConversationId, 'items'], ImmutableList()),
|
||||||
|
isLoading: state.getIn(['chat_conversation_messages', chatConversationId, 'isLoading'], true),
|
||||||
|
isPartial: state.getIn(['chat_conversation_messages', chatConversationId, 'isPartial'], false),
|
||||||
|
hasMore: state.getIn(['chat_conversation_messages', chatConversationId, 'hasMore']),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
|
onScrollToBottom: debounce(() => {
|
||||||
|
dispatch(scrollBottomChatMessageConversation(ownProps.chatConversationId, true))
|
||||||
|
}, 100),
|
||||||
|
onScroll: debounce(() => {
|
||||||
|
dispatch(scrollBottomChatMessageConversation(ownProps.chatConversationId, false))
|
||||||
|
}, 100),
|
||||||
|
onExpandChatMessages(chatConversationId, params) {
|
||||||
|
dispatch(expandChatMessages(chatConversationId, params))
|
||||||
|
},
|
||||||
|
onSetChatConversationSelected: (chatConversationId) => {
|
||||||
|
dispatch(setChatConversationSelected(chatConversationId))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatMessageScrollingList.propTypes = {
|
||||||
|
chatMessageIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
chatConversationId: PropTypes.string.isRequired,
|
||||||
|
onExpandChatMessages: PropTypes.func.isRequired,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
isPartial: PropTypes.bool,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
onClearTimeline: PropTypes.func.isRequired,
|
||||||
|
onScrollToTop: PropTypes.func.isRequired,
|
||||||
|
onScroll: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageScrollingList)
|
@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
|
|||||||
import Heading from '../../../components/heading'
|
import Heading from '../../../components/heading'
|
||||||
import Button from '../../../components/button'
|
import Button from '../../../components/button'
|
||||||
|
|
||||||
class MessagesHeader extends ImmutablePureComponent {
|
class ChatSettingsHeader extends ImmutablePureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@ -16,27 +16,19 @@ class MessagesHeader extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.w100PC, _s.h60PX, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
<div className={[_s.d, _s.w100PC, _s.h60PX, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
<div className={[_s.d, _s.flexRow, _s.pl15, _s.pr5, _s.py15].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.pl15, _s.pr5, _s.py15].join(' ')}>
|
||||||
<Heading size='h1'>
|
|
||||||
Messages
|
|
||||||
</Heading>
|
|
||||||
<div className={[_s.d, _s.bgTransparent, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.mlAuto].join(' ')}>
|
|
||||||
<Button
|
<Button
|
||||||
isNarrow
|
noClasses
|
||||||
onClick={undefined}
|
className={[_s.d, _s.noUnderline, _s.jcCenter, _s.mr5, _s.aiCenter, _s.bgTransparent, _s.cursorPointer, _s.outlineNone].join(' ')}
|
||||||
className={[_s.ml5, _s.px15].join(' ')}
|
to='/messages'
|
||||||
>
|
color='primary'
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isNarrow
|
|
||||||
onClick={undefined}
|
|
||||||
color='brand'
|
|
||||||
backgroundColor='none'
|
backgroundColor='none'
|
||||||
className={_s.ml5}
|
icon='angle-left'
|
||||||
icon='cog'
|
iconSize='16px'
|
||||||
iconSize='18px'
|
iconClassName={[_s.mr5, _s.cPrimary].join(' ')}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Heading size='h1'>
|
||||||
|
Chat Settings
|
||||||
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -44,4 +36,4 @@ class MessagesHeader extends ImmutablePureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessagesHeader
|
export default ChatSettingsHeader
|
@ -1,116 +0,0 @@
|
|||||||
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 { NavLink } from 'react-router-dom'
|
|
||||||
import { CX } from '../../../constants'
|
|
||||||
import Input from '../../../components/input'
|
|
||||||
import Avatar from '../../../components/avatar'
|
|
||||||
import Button from '../../../components/button'
|
|
||||||
import Text from '../../../components/text'
|
|
||||||
import RelativeTimestamp from '../../../components/relative_timestamp'
|
|
||||||
import { makeGetAccount } from '../../../selectors'
|
|
||||||
|
|
||||||
class MessagesItem extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hovering: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnMouseEnter = () => {
|
|
||||||
this.setState({ isHovering: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnMouseLeave = () => {
|
|
||||||
this.setState({ isHovering: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
account,
|
|
||||||
intl,
|
|
||||||
alt,
|
|
||||||
} = this.props
|
|
||||||
const { isHovering } = this.state
|
|
||||||
|
|
||||||
const content = { __html: 'REEEE i heard you have the sauce2?' }
|
|
||||||
const messageContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
flexRow: !alt,
|
|
||||||
flexRowReverse: alt,
|
|
||||||
})
|
|
||||||
const messageInnerContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
px15: 1,
|
|
||||||
py5: 1,
|
|
||||||
bgSecondary: !alt,
|
|
||||||
bgBrandLight: alt,
|
|
||||||
circle: 1,
|
|
||||||
ml10: 1,
|
|
||||||
mr10: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const lowerContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
pt10: 1,
|
|
||||||
pl50: !alt,
|
|
||||||
pr50: alt,
|
|
||||||
})
|
|
||||||
|
|
||||||
const buttonContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
flexRow: 1,
|
|
||||||
displayNone: !isHovering,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={[_s.d, _s.w100PC, _s.pb10].join(' ')}
|
|
||||||
onMouseEnter={this.handleOnMouseEnter}
|
|
||||||
onMouseLeave={this.handleOnMouseLeave}
|
|
||||||
>
|
|
||||||
<div className={[_s.d, _s.w100PC, _s.pb15].join(' ')}>
|
|
||||||
|
|
||||||
<div className={messageContainerClasses}>
|
|
||||||
<Avatar account={account} size={38} />
|
|
||||||
<div className={messageInnerContainerClasses}>
|
|
||||||
<div className={[_s.py5, _s.dangerousContent].join(' ')} dangerouslySetInnerHTML={content} />
|
|
||||||
</div>
|
|
||||||
<div className={buttonContainerClasses}>
|
|
||||||
<Button
|
|
||||||
onClick={undefined}
|
|
||||||
color='tertiary'
|
|
||||||
backgroundColor='none'
|
|
||||||
icon='ellipsis'
|
|
||||||
iconSize='18px'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={lowerContainerClasses}>
|
|
||||||
<Text size='small' color='tertiary' align={alt ? 'right' : 'left'}>
|
|
||||||
Apr 16, 2020, 8:20 AM
|
|
||||||
{ /* <RelativeTimestamp timestamp={'2020-20-10'} /> */ }
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
account: makeGetAccount()(state, '1'),
|
|
||||||
})
|
|
||||||
|
|
||||||
MessagesItem.propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
alt: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(MessagesItem)
|
|
@ -1,36 +0,0 @@
|
|||||||
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 MessagesListItem from './messages_list_item'
|
|
||||||
import { makeGetAccount } from '../../../selectors'
|
|
||||||
|
|
||||||
class MessagesList extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
account,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={[_s.d, _s.w100PC].join(' ')}>
|
|
||||||
<MessagesListItem />
|
|
||||||
<MessagesListItem selected />
|
|
||||||
<MessagesListItem />
|
|
||||||
<MessagesListItem />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
account: makeGetAccount()(state, '1'),
|
|
||||||
})
|
|
||||||
|
|
||||||
MessagesList.propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(MessagesList)
|
|
@ -1,107 +0,0 @@
|
|||||||
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 { NavLink } from 'react-router-dom'
|
|
||||||
import { CX } from '../../../constants'
|
|
||||||
import Input from '../../../components/input'
|
|
||||||
import DisplayName from '../../../components/display_name'
|
|
||||||
import Avatar from '../../../components/avatar'
|
|
||||||
import Text from '../../../components/text'
|
|
||||||
import RelativeTimestamp from '../../../components/relative_timestamp'
|
|
||||||
import { makeGetAccount } from '../../../selectors'
|
|
||||||
|
|
||||||
class MessagesListItem extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
composeFocused: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
account,
|
|
||||||
intl,
|
|
||||||
selected,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
const buttonClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
pt2: 1,
|
|
||||||
pr5: 1,
|
|
||||||
noUnderline: 1,
|
|
||||||
overflowHidden: 1,
|
|
||||||
flexNormal: 1,
|
|
||||||
flexRow: 1,
|
|
||||||
aiStart: 1,
|
|
||||||
aiCenter: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const containerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
bgSubtle_onHover: 1,
|
|
||||||
borderBottom1PX: 1,
|
|
||||||
borderColorSecondary: 1,
|
|
||||||
noUnderline: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const innerContainerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
flexRow: 1,
|
|
||||||
aiStart: 1,
|
|
||||||
aiCenter: 0,
|
|
||||||
px15: 1,
|
|
||||||
py15: 1,
|
|
||||||
borderRight4PX: selected,
|
|
||||||
borderColorBrand: selected,
|
|
||||||
})
|
|
||||||
|
|
||||||
const avatarSize = 49
|
|
||||||
const content = { __html: 'REEEE i heard you have the sauce?' }
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
className={containerClasses}
|
|
||||||
title={account.get('acct')}
|
|
||||||
to={`/messages/conversation-id`}
|
|
||||||
>
|
|
||||||
<div className={innerContainerClasses}>
|
|
||||||
|
|
||||||
<Avatar account={account} size={avatarSize} noHover />
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.pl10, _s.overflowHidden, _s.flexNormal].join(' ')}>
|
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
|
|
||||||
<div className={buttonClasses}>
|
|
||||||
<div className={_s.maxW100PC42PX}>
|
|
||||||
<DisplayName account={account} noHover />
|
|
||||||
</div>
|
|
||||||
<Text size='extraSmall' color='secondary' className={_s.mlAuto}>
|
|
||||||
May 1
|
|
||||||
{ /* <RelativeTimestamp timestamp={'2020-20-10'} /> */ }
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={[_s.py5, _s.dangerousContent].join(' ')} dangerouslySetInnerHTML={content} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NavLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
account: makeGetAccount()(state, '1'),
|
|
||||||
})
|
|
||||||
|
|
||||||
MessagesListItem.propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
selected: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(MessagesListItem)
|
|
@ -1,88 +1,35 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { makeGetAccount } from '../../selectors'
|
import ChatEmptyMessageBlock from './components/chat_conversations_empty_block'
|
||||||
import Text from '../../components/text'
|
import ChatMessageHeader from './components/chat_message_header'
|
||||||
import Button from '../../components/button'
|
import ChatMessageScrollingList from './components/chat_message_scrolling_list'
|
||||||
import Avatar from '../../components/avatar'
|
import ChatMessagesComposeForm from './components/chat_message_compose_form'
|
||||||
import DisplayName from '../../components/display_name'
|
|
||||||
import Input from '../../components/input'
|
|
||||||
import EmojiPickerButton from '../compose/components/emoji_picker_button'
|
|
||||||
import UploadButton from '../compose/components/media_upload_button'
|
|
||||||
import MessageItem from './components/message_item'
|
|
||||||
|
|
||||||
// import MessagesContainer from './containers/messages_container'
|
|
||||||
|
|
||||||
class Messages extends React.PureComponent {
|
class Messages extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account } = this.props
|
const {
|
||||||
|
account,
|
||||||
const selectedMessage = true
|
selectedChatConversationId,
|
||||||
|
chatConverationIsRequest,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.bgPrimary, _s.h100PC, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.bgPrimary, _s.h100PC, _s.w100PC].join(' ')}>
|
||||||
{
|
{
|
||||||
!selectedMessage &&
|
!selectedChatConversationId &&
|
||||||
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
<ChatEmptyMessageBlock />
|
||||||
<Text weight='bold' size='extraLarge'>
|
|
||||||
You don’t have a message selected
|
|
||||||
</Text>
|
|
||||||
<Text size='medium' color='secondary' className={_s.py10}>
|
|
||||||
Choose one from your existing messages, or start a new one.
|
|
||||||
</Text>
|
|
||||||
<Button className={_s.mt10}>
|
|
||||||
<Text color='inherit' weight='bold' className={_s.px15}>
|
|
||||||
New Message
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
selectedMessage &&
|
!!selectedChatConversationId &&
|
||||||
<div className={[_s.d, _s.h100PC, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.h100PC, _s.w100PC].join(' ')}>
|
||||||
<div className={[_s.d, _s.posAbs, _s.top0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.h60PX, _s.w100PC, _s.borderBottom1PX, _s.borderColorSecondary, _s.px15, _s.py5].join(' ')}>
|
<ChatMessageHeader chatConversationId={selectedChatConversationId} />
|
||||||
<Avatar account={account} size={34} />
|
<ChatMessageScrollingList chatConversationId={selectedChatConversationId} />
|
||||||
<div className={[_s.d, _s.pl10, _s.maxW100PC86PX, _s.overflowHidden].join(' ')}>
|
{
|
||||||
<DisplayName account={account} isMultiline />
|
!chatConverationIsRequest &&
|
||||||
</div>
|
<ChatMessagesComposeForm chatConversationId={selectedChatConversationId} />
|
||||||
<Button
|
}
|
||||||
isNarrow
|
|
||||||
onClick={undefined}
|
|
||||||
color='brand'
|
|
||||||
backgroundColor='none'
|
|
||||||
className={_s.mlAuto}
|
|
||||||
icon='more'
|
|
||||||
iconSize='18px'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.posAbs, _s.bottom60PX, _s.left0, _s.right0, _s.px15, _s.py15, _s.top60PX, _s.w100PC, _s.overflowYScroll].join(' ')}>
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem alt />
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem alt />
|
|
||||||
<MessageItem alt />
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem />
|
|
||||||
<MessageItem alt />
|
|
||||||
<MessageItem />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.h60PX, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15, _s.py5].join(' ')}>
|
|
||||||
<EmojiPickerButton />
|
|
||||||
<UploadButton />
|
|
||||||
<div className={[_s.d, _s.px15, _s.flexGrow1].join(' ')}>
|
|
||||||
<Input
|
|
||||||
placeholder='Type a message...'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button>
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -91,13 +38,18 @@ class Messages extends React.PureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => {
|
||||||
account: makeGetAccount()(state, '1'),
|
const selectedChatConversationId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||||
})
|
const chatConverationIsRequest = selectedChatConversationId ? !state.getIn(['chat_conversations', selectedChatConversationId, 'is_approved'], null) : false
|
||||||
|
return {
|
||||||
|
selectedChatConversationId,
|
||||||
|
chatConverationIsRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Messages.propTypes = {
|
Messages.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
selectedChatConversationId: PropTypes.string,
|
||||||
selected: PropTypes.bool,
|
chatConverationIsRequest: PropTypes.bool.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Messages)
|
export default connect(mapStateToProps)(Messages)
|
87
app/javascript/gabsocial/features/messages_settings.js
Normal file
87
app/javascript/gabsocial/features/messages_settings.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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 BlockHeading from '../components/block_heading'
|
||||||
|
import Button from '../components/button'
|
||||||
|
import Form from '../components/form'
|
||||||
|
import Switch from '../components/switch'
|
||||||
|
import Text from '../components/text'
|
||||||
|
import Divider from '../components/divider'
|
||||||
|
|
||||||
|
class MessagesSettings extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.onFetchMutes()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.onExpandMutes()
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
accountIds,
|
||||||
|
hasMore,
|
||||||
|
isLoading,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.h60PX, _s.w100PC, _s.px10, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
|
<BlockHeading title={'Chat Preferences'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={[_s.d, _s.px15, _s.py15].join(' ')}>
|
||||||
|
<Form>
|
||||||
|
<Switch
|
||||||
|
label='Restrict messages from people you dont follow'
|
||||||
|
checked={true}
|
||||||
|
onChange={this.handleLockedChange}
|
||||||
|
/>
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.my10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} />
|
||||||
|
<Switch
|
||||||
|
label='Show when you are active'
|
||||||
|
checked={false}
|
||||||
|
onChange={this.handleLockedChange}
|
||||||
|
/>
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.my10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} />
|
||||||
|
<Switch
|
||||||
|
label='Notification sound enabled'
|
||||||
|
checked={false}
|
||||||
|
onChange={this.handleLockedChange}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
})
|
||||||
|
|
||||||
|
MessagesSettings.propTypes = {
|
||||||
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onExpandMutes: PropTypes.func.isRequired,
|
||||||
|
onFetchMutes: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MessagesSettings))
|
@ -12,7 +12,7 @@ import Block from '../components/block'
|
|||||||
import BlockHeading from '../components/block_heading'
|
import BlockHeading from '../components/block_heading'
|
||||||
import ScrollableList from '../components/scrollable_list'
|
import ScrollableList from '../components/scrollable_list'
|
||||||
|
|
||||||
class Mutes extends ImmutablePureComponent {
|
class MutedAccounts extends ImmutablePureComponent {
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.props.onFetchMutes()
|
this.props.onFetchMutes()
|
||||||
@ -66,7 +66,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
onExpandMutes: () => dispatch(expandMutes()),
|
onExpandMutes: () => dispatch(expandMutes()),
|
||||||
})
|
})
|
||||||
|
|
||||||
Mutes.propTypes = {
|
MutedAccounts.propTypes = {
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
@ -74,4 +74,4 @@ Mutes.propTypes = {
|
|||||||
onFetchMutes: PropTypes.func.isRequired,
|
onFetchMutes: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Mutes))
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MutedAccounts))
|
@ -56,6 +56,10 @@ import {
|
|||||||
Assets,
|
Assets,
|
||||||
BlockedAccounts,
|
BlockedAccounts,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
|
ChatConversationCreate,
|
||||||
|
ChatConversationRequests,
|
||||||
|
ChatConversationBlockedAccounts,
|
||||||
|
ChatConversationMutedAccounts,
|
||||||
CommunityTimeline,
|
CommunityTimeline,
|
||||||
Compose,
|
Compose,
|
||||||
DMCA,
|
DMCA,
|
||||||
@ -86,7 +90,8 @@ import {
|
|||||||
ListEdit,
|
ListEdit,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Messages,
|
Messages,
|
||||||
Mutes,
|
MessagesSettings,
|
||||||
|
MutedAccounts,
|
||||||
News,
|
News,
|
||||||
NewsView,
|
NewsView,
|
||||||
Notifications,
|
Notifications,
|
||||||
@ -198,8 +203,12 @@ class SwitchingArea extends React.PureComponent {
|
|||||||
<WrappedRoute path='/news' exact publicRoute page={NewsPage} component={News} content={children} componentParams={{ title: 'News' }} />
|
<WrappedRoute path='/news' exact publicRoute page={NewsPage} component={News} content={children} componentParams={{ title: 'News' }} />
|
||||||
<WrappedRoute path='/news/view/:trendsRSSId' page={NewsPage} component={NewsView} content={children} componentParams={{ title: 'News RSS Feed' }} />
|
<WrappedRoute path='/news/view/:trendsRSSId' page={NewsPage} component={NewsView} content={children} componentParams={{ title: 'News RSS Feed' }} />
|
||||||
|
|
||||||
<WrappedRoute path='/messages' exact page={MessagesPage} component={Messages} content={children} />
|
<WrappedRoute path='/messages' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} />
|
||||||
<WrappedRoute path='/messages/:conversationId' exact page={MessagesPage} component={Messages} content={children} />
|
<WrappedRoute path='/messages/settings' exact page={MessagesPage} component={MessagesSettings} content={children} componentParams={{ isSettings: true }} />
|
||||||
|
<WrappedRoute path='/messages/requests' exact page={MessagesPage} component={ChatConversationRequests} content={children} componentParams={{ isSettings: true, source: 'requested' }} />
|
||||||
|
<WrappedRoute path='/messages/blocks' exact page={MessagesPage} component={ChatConversationBlockedAccounts} content={children} componentParams={{ isSettings: true }} />
|
||||||
|
<WrappedRoute path='/messages/mutes' exact page={MessagesPage} component={ChatConversationMutedAccounts} content={children} componentParams={{ isSettings: true }} />
|
||||||
|
<WrappedRoute path='/messages/:conversationId' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} />
|
||||||
|
|
||||||
<WrappedRoute path='/timeline/all' exact page={CommunityPage} component={CommunityTimeline} content={children} componentParams={{ title: 'Community Feed' }} />
|
<WrappedRoute path='/timeline/all' exact page={CommunityPage} component={CommunityTimeline} content={children} componentParams={{ title: 'Community Feed' }} />
|
||||||
<WrappedRoute path='/timeline/pro' exact page={ProPage} component={ProTimeline} content={children} componentParams={{ title: 'Pro Feed' }} />
|
<WrappedRoute path='/timeline/pro' exact page={ProPage} component={ProTimeline} content={children} componentParams={{ title: 'Pro Feed' }} />
|
||||||
@ -244,39 +253,24 @@ class SwitchingArea extends React.PureComponent {
|
|||||||
<WrappedRoute path='/search/links' exact page={SearchPage} component={Search} content={children} />
|
<WrappedRoute path='/search/links' exact page={SearchPage} component={Search} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/settings/blocks' exact page={SettingsPage} component={BlockedAccounts} content={children} componentParams={{ title: 'Blocked Users' }} />
|
<WrappedRoute path='/settings/blocks' exact page={SettingsPage} component={BlockedAccounts} content={children} componentParams={{ title: 'Blocked Users' }} />
|
||||||
<WrappedRoute path='/settings/mutes' exact page={SettingsPage} component={Mutes} content={children} componentParams={{ title: 'Muted Users' }} />
|
<WrappedRoute path='/settings/mutes' exact page={SettingsPage} component={MutedAccounts} content={children} componentParams={{ title: 'Muted Users' }} />
|
||||||
|
|
||||||
<Redirect from='/@:username' to='/:username' exact />
|
|
||||||
<WrappedRoute path='/:username' publicRoute exact page={ProfilePage} component={AccountTimeline} content={children} />
|
<WrappedRoute path='/:username' publicRoute exact page={ProfilePage} component={AccountTimeline} content={children} />
|
||||||
|
|
||||||
<Redirect from='/@:username/comments' to='/:username/comments' />
|
|
||||||
<WrappedRoute path='/:username/comments' page={ProfilePage} component={AccountTimeline} content={children} componentParams={{ commentsOnly: true }} />
|
<WrappedRoute path='/:username/comments' page={ProfilePage} component={AccountTimeline} content={children} componentParams={{ commentsOnly: true }} />
|
||||||
|
|
||||||
<Redirect from='/@:username/followers' to='/:username/followers' />
|
|
||||||
<WrappedRoute path='/:username/followers' page={ProfilePage} component={Followers} content={children} />
|
<WrappedRoute path='/:username/followers' page={ProfilePage} component={Followers} content={children} />
|
||||||
|
|
||||||
<Redirect from='/@:username/following' to='/:username/following' />
|
|
||||||
<WrappedRoute path='/:username/following' page={ProfilePage} component={Following} content={children} />
|
<WrappedRoute path='/:username/following' page={ProfilePage} component={Following} content={children} />
|
||||||
|
|
||||||
<Redirect from='/@:username/media' to='/:username/photos' />
|
|
||||||
<Redirect from='/@:username/photos' to='/:username/photos' />
|
|
||||||
<Redirect from='/:username/media' to='/:username/photos' />
|
|
||||||
<WrappedRoute path='/:username/photos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
|
<WrappedRoute path='/:username/photos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
|
||||||
<WrappedRoute path='/:username/videos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'video' }} />
|
<WrappedRoute path='/:username/videos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'video' }} />
|
||||||
|
|
||||||
<Redirect from='/@:username/likes' to='/:username/likes' />
|
|
||||||
<WrappedRoute path='/:username/likes' page={ProfilePage} component={LikedStatuses} content={children} />
|
<WrappedRoute path='/:username/likes' page={ProfilePage} component={LikedStatuses} content={children} />
|
||||||
|
|
||||||
<Redirect from='/@:username/bookmarks' to='/:username/bookmarks' />
|
|
||||||
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
|
||||||
|
|
||||||
<Redirect from='/@:username/posts/:statusId' to='/:username/posts/:statusId' exact />
|
|
||||||
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />
|
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />
|
||||||
|
|
||||||
<Redirect from='/@:username/posts/:statusId/reposts' to='/:username/posts/:statusId/reposts' />
|
|
||||||
<WrappedRoute path='/:username/posts/:statusId/reposts' publicRoute page={ModalPage} component={StatusReposts} content={children} componentParams={{ title: 'Reposts' }} />
|
<WrappedRoute path='/:username/posts/:statusId/reposts' publicRoute page={ModalPage} component={StatusReposts} content={children} componentParams={{ title: 'Reposts' }} />
|
||||||
|
|
||||||
<Redirect from='/@:username/posts/:statusId/likes' to='/:username/posts/:statusId/likes' />
|
|
||||||
<WrappedRoute path='/:username/posts/:statusId/likes' page={ModalPage} component={StatusLikes} content={children} componentParams={{ title: 'Likes' }} />
|
<WrappedRoute path='/:username/posts/:statusId/likes' page={ModalPage} component={StatusLikes} content={children} componentParams={{ title: 'Likes' }} />
|
||||||
|
|
||||||
<WrappedRoute page={ErrorPage} component={GenericNotFound} content={children} />
|
<WrappedRoute page={ErrorPage} component={GenericNotFound} content={children} />
|
||||||
|
@ -7,6 +7,12 @@ export function BlockAccountModal() { return import(/* webpackChunkName: "compon
|
|||||||
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
|
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
|
||||||
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
|
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
|
||||||
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
|
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
|
||||||
|
export function ChatConversationBlockedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_blocked_accounts" */'../../chat_conversation_blocked_accounts') }
|
||||||
|
export function ChatConversationCreate() { return import(/* webpackChunkName: "features/chat_conversation_create" */'../../chat_conversation_create') }
|
||||||
|
export function ChatConversationCreateModal() { return import(/* webpackChunkName: "components/chat_conversation_create_modal" */'../../../components/modal/chat_conversation_create_modal') }
|
||||||
|
export function ChatConversationDeleteModal() { return import(/* webpackChunkName: "components/chat_conversation_delete_modal" */'../../../components/modal/chat_conversation_delete_modal') }
|
||||||
|
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
|
||||||
|
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
|
||||||
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
|
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
|
||||||
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
|
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
|
||||||
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
|
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
|
||||||
@ -78,7 +84,8 @@ export function MediaGallery() { return import(/* webpackChunkName: "components/
|
|||||||
export function MediaGalleryPanel() { return import(/* webpackChunkName: "components/media_gallery_panel" */'../../../components/panel/media_gallery_panel') }
|
export function MediaGalleryPanel() { return import(/* webpackChunkName: "components/media_gallery_panel" */'../../../components/panel/media_gallery_panel') }
|
||||||
export function MediaModal() { return import(/* webpackChunkName: "components/media_modal" */'../../../components/modal/media_modal') }
|
export function MediaModal() { return import(/* webpackChunkName: "components/media_modal" */'../../../components/modal/media_modal') }
|
||||||
export function Messages() { return import(/* webpackChunkName: "features/messages" */'../../messages') }
|
export function Messages() { return import(/* webpackChunkName: "features/messages" */'../../messages') }
|
||||||
export function Mutes() { return import(/* webpackChunkName: "features/mutes" */'../../mutes') }
|
export function MessagesSettings() { return import(/* webpackChunkName: "features/messages_settings" */'../../messages_settings') }
|
||||||
|
export function MutedAccounts() { return import(/* webpackChunkName: "features/muted_accounts" */'../../muted_accounts') }
|
||||||
export function MuteModal() { return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal/mute_modal') }
|
export function MuteModal() { return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal/mute_modal') }
|
||||||
export function NavSettingsPopover() { return import(/* webpackChunkName: "modals/nav_settings_popover" */'../../../components/popover/nav_settings_popover') }
|
export function NavSettingsPopover() { return import(/* webpackChunkName: "modals/nav_settings_popover" */'../../../components/popover/nav_settings_popover') }
|
||||||
export function News() { return import(/* webpackChunkName: "features/news" */'../../news') }
|
export function News() { return import(/* webpackChunkName: "features/news" */'../../news') }
|
||||||
|
@ -142,9 +142,7 @@ class Layout extends React.PureComponent {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!noComposeButton &&
|
!noComposeButton &&
|
||||||
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
|
|
||||||
<FloatingActionButton />
|
<FloatingActionButton />
|
||||||
</Responsive>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</ResponsiveClassesComponent>
|
</ResponsiveClassesComponent>
|
||||||
|
@ -1,26 +1,36 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
import { connect } from 'react-redux'
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
|
||||||
import { me } from '../initial_state'
|
import { me } from '../initial_state'
|
||||||
|
import { openModal } from '../actions/modal'
|
||||||
import {
|
import {
|
||||||
CX,
|
CX,
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
|
MODAL_CHAT_CONVERSATION_CREATE,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import Layout from './layout'
|
import Layout from './layout'
|
||||||
import Responsive from '../features/ui/util/responsive_component'
|
import Responsive from '../features/ui/util/responsive_component'
|
||||||
|
import List from '../components/list'
|
||||||
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
||||||
import MessagesSearch from '../features/messages/components/messages_search'
|
import ChatConversationsSearch from '../features/messages/components/chat_conversations_search'
|
||||||
import MessagesList from '../features/messages/components/messages_list'
|
import ChatConversationsList from '../features/messages/components/chat_conversations_list'
|
||||||
import MessagesHeader from '../features/messages/components/messages_header'
|
import ChatSettingsHeader from '../features/messages/components/chat_settings_header'
|
||||||
|
import ChatConversationRequestsListItem from '../features/messages/components/chat_conversations_requests_list_item'
|
||||||
|
|
||||||
class MessagesLayout extends ImmutablePureComponent {
|
class MessagesLayout extends React.PureComponent {
|
||||||
|
|
||||||
|
onClickAdd = () => {
|
||||||
|
this.props.onOpenChatConversationCreateModal()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
|
||||||
showBackBtn,
|
|
||||||
title,
|
title,
|
||||||
|
children,
|
||||||
|
isSettings,
|
||||||
|
showBackBtn,
|
||||||
|
source,
|
||||||
|
currentConversationIsRequest,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const mainBlockClasses = CX({
|
const mainBlockClasses = CX({
|
||||||
@ -31,22 +41,25 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||||||
jcEnd: 1,
|
jcEnd: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("currentConversationIsRequest:",currentConversationIsRequest)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
showBackBtn
|
showBackBtn
|
||||||
showGlobalFooter
|
|
||||||
noRightSidebar
|
noRightSidebar
|
||||||
|
noComposeButton
|
||||||
|
showGlobalFooter
|
||||||
showLinkFooterInSidebar
|
showLinkFooterInSidebar
|
||||||
page='messages'
|
page='messages'
|
||||||
title='Chats'
|
title='Chats'
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
icon: 'cog',
|
icon: 'cog',
|
||||||
onClick: this.onOpenCommunityPageSettingsModal,
|
to: '/messages/settings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pencil',
|
icon: 'pencil',
|
||||||
onClick: this.onOpenCommunityPageSettingsModal,
|
onClick: () => this.onClickAdd(),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@ -63,16 +76,51 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||||
<div className={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}>
|
<div className={[_s.d, _s.w340PX, _s.h100PC, _s.bgPrimary, _s.borderLeft1PX, _s.borderRight1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
<div className={[_s.d, _s.w340PX].join(' ')}>
|
<div className={[_s.d, _s.h100PC, _s.overflowHidden, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||||
|
|
||||||
{ /* <MessagesHeader /> */ }
|
{
|
||||||
<MessagesSearch />
|
(isSettings || currentConversationIsRequest) &&
|
||||||
<MessagesList />
|
<React.Fragment>
|
||||||
|
<ChatSettingsHeader />
|
||||||
|
<List
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: 'Preferences',
|
||||||
|
to: '/messages/settings',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Message Requests',
|
||||||
|
to: '/messages/requests',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Blocked Chats',
|
||||||
|
to: '/messages/blocks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Muted Chats',
|
||||||
|
to: '/messages/mutes',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSettings && !currentConversationIsRequest &&
|
||||||
|
<React.Fragment>
|
||||||
|
<ChatConversationsSearch />
|
||||||
|
<div className={[_s.d, _s.w100PC, _s.posAbs, _s.bottom0, _s.top60PX, _s.overflowYScroll].join(' ')}>
|
||||||
|
<ChatConversationRequestsListItem />
|
||||||
|
<ChatConversationsList source={source} />
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Responsive>
|
</Responsive>
|
||||||
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.flexGrow1, _s.h100PC, _s.bgPrimary, _s.borderColorSecondary, _s.borderRight1PX, _s.z1].join(' ')}>
|
<div className={[_s.d, _s.flexGrow1, _s.h100PC, _s.bgPrimary, _s.borderColorSecondary, _s.borderRight1PX, _s.z1].join(' ')}>
|
||||||
<div className={[_s.d, _s.w100PC, _s.h100PC].join(' ')}>
|
<div className={[_s.d, _s.w100PC, _s.h100PC].join(' ')}>
|
||||||
{children}
|
{children}
|
||||||
@ -87,10 +135,26 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessagesLayout.propTypes = {
|
const mapStateToProps = (state) => {
|
||||||
children: PropTypes.node,
|
const selectedId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||||
showBackBtn: PropTypes.bool,
|
const currentConversationIsRequest = selectedId ? !state.getIn(['chat_conversations', selectedId, 'is_approved'], true) : false
|
||||||
title: PropTypes.string,
|
|
||||||
|
return { currentConversationIsRequest }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessagesLayout
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onOpenChatConversationCreateModal() {
|
||||||
|
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
MessagesLayout.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
isSettings: PropTypes.bool,
|
||||||
|
showBackBtn: PropTypes.bool,
|
||||||
|
source: PropTypes.string,
|
||||||
|
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MessagesLayout)
|
@ -177,7 +177,7 @@ class ProfileLayout extends ImmutablePureComponent {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FloatingActionButton isDesktop />
|
<FloatingActionButton />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</Responsive>
|
</Responsive>
|
||||||
|
@ -1,38 +1,37 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
|
||||||
import PageTitle from '../features/ui/util/page_title'
|
import PageTitle from '../features/ui/util/page_title'
|
||||||
import MessagesLayout from '../layouts/messages_layout'
|
import MessagesLayout from '../layouts/messages_layout'
|
||||||
|
|
||||||
class MessagesPage extends React.PureComponent {
|
class MessagesPage extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, intl } = this.props
|
const {
|
||||||
|
children,
|
||||||
const title = intl.formatMessage(messages.chats)
|
isSettings,
|
||||||
|
source,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagesLayout
|
<MessagesLayout
|
||||||
showBackBtn
|
showBackBtn
|
||||||
title={title}
|
isSettings={isSettings}
|
||||||
|
title='Chats'
|
||||||
|
source={source}
|
||||||
>
|
>
|
||||||
<PageTitle path={title} />
|
<PageTitle path='Chats' />
|
||||||
|
{children}
|
||||||
</MessagesLayout>
|
</MessagesLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
chats: { id: 'chats', defaultMessage: 'Chats' },
|
|
||||||
})
|
|
||||||
|
|
||||||
MessagesPage.propTypes = {
|
MessagesPage.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
isSettings: PropTypes.func,
|
||||||
|
source: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect()(MessagesPage))
|
export default MessagesPage
|
@ -1,43 +0,0 @@
|
|||||||
import {
|
|
||||||
MESSAGE_INPUT_CHANGE,
|
|
||||||
MESSAGE_INPUT_RESET,
|
|
||||||
MESSAGE_SEND_REQUEST,
|
|
||||||
MESSAGE_SEND_SUCCESS,
|
|
||||||
MESSAGE_SEND_FAIL,
|
|
||||||
MESSAGE_DELETE_REQUEST,
|
|
||||||
MESSAGE_DELETE_SUCCESS,
|
|
||||||
MESSAGE_DELETE_FAIL,
|
|
||||||
} from '../actions/lists'
|
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable'
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
|
||||||
text: '',
|
|
||||||
conversationId: null,
|
|
||||||
idempotencyKey: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const normalizeList = (state, list) => state.set(list.id, fromJS(list))
|
|
||||||
|
|
||||||
const normalizeLists = (state, lists) => {
|
|
||||||
lists.forEach(list => {
|
|
||||||
state = normalizeList(state, list)
|
|
||||||
})
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function lists(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case LIST_FETCH_SUCCESS:
|
|
||||||
case LIST_CREATE_SUCCESS:
|
|
||||||
case LIST_UPDATE_SUCCESS:
|
|
||||||
return normalizeList(state, action.list);
|
|
||||||
case LISTS_FETCH_SUCCESS:
|
|
||||||
return normalizeLists(state, action.lists);
|
|
||||||
case LIST_DELETE_SUCCESS:
|
|
||||||
case LIST_FETCH_FAIL:
|
|
||||||
return state.set(action.id, false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
85
app/javascript/gabsocial/reducers/chat_conversation_lists.js
Normal file
85
app/javascript/gabsocial/reducers/chat_conversation_lists.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL,
|
||||||
|
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL,
|
||||||
|
|
||||||
|
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
||||||
|
} from '../actions/chat_conversations'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
approved: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
items: ImmutableList(),
|
||||||
|
}),
|
||||||
|
requested: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
items: ImmutableList(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeList = (state, source, chatConversations, next) => {
|
||||||
|
return state.update(source, listMap => listMap.withMutations(map => {
|
||||||
|
map.set('next', next)
|
||||||
|
map.set('loaded', true)
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('items', ImmutableList(chatConversations.map(chatConversation => chatConversation.chat_conversation_id)))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendToList = (state, source, chatConversations, next) => {
|
||||||
|
return state.update(source, listMap => listMap.withMutations(map => {
|
||||||
|
map.set('next', next)
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('items', map.get('items').concat(chatConversations.map(chatConversation => chatConversation.chat_conversation_id)))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeOneFromList = (state, source, chatConversationId) => {
|
||||||
|
return state.update(source, listMap => listMap.withMutations(map => {
|
||||||
|
map.set('items', map.get('items').filter(id => id !== chatConversationId))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function chat_conversation_lists(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST:
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['approved', 'isLoading'], true)
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL:
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL:
|
||||||
|
return state.setIn(['approved', 'isLoading'], false)
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'approved', action.chatConversations, action.next)
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, 'approved', action.chatConversations, action.next)
|
||||||
|
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST:
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['requested', 'isLoading'], true)
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL:
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL:
|
||||||
|
return state.setIn(['requested', 'isLoading'], false)
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'requested', action.chatConversations, action.next)
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, 'requested', action.chatConversations, action.next)
|
||||||
|
|
||||||
|
case CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS:
|
||||||
|
return removeOneFromList(state, 'requested', action.chatConversation.chat_conversation_id)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
105
app/javascript/gabsocial/reducers/chat_conversation_messages.js
Normal file
105
app/javascript/gabsocial/reducers/chat_conversation_messages.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
List as ImmutableList,
|
||||||
|
Map as ImmutableMap,
|
||||||
|
fromJS,
|
||||||
|
} from 'immutable'
|
||||||
|
import compareId from '../utils/compare_id'
|
||||||
|
import {
|
||||||
|
CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
|
CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
|
} from '../actions/chat_messages'
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATION_MESSAGES_EXPAND_REQUEST,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_EXPAND_FAIL,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_CONNECT,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_DISCONNECT,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_CLEAR,
|
||||||
|
} from '../actions/chat_conversation_messages'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap()
|
||||||
|
|
||||||
|
const initialConversation = ImmutableMap({
|
||||||
|
unread: 0,
|
||||||
|
online: false,
|
||||||
|
top: true,
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
hasMore: true,
|
||||||
|
items: ImmutableList(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const expandNormalizedChatConversation = (state, chatConversationId, chatMessages, next, isPartial, isLoadingRecent) => {
|
||||||
|
return state.update(chatConversationId, initialConversation, map => map.withMutations((mMap) => {
|
||||||
|
mMap.set('isLoading', false)
|
||||||
|
mMap.set('isPartial', isPartial)
|
||||||
|
|
||||||
|
if (!next && !isLoadingRecent) mMap.set('hasMore', false)
|
||||||
|
|
||||||
|
if (!!chatMessages && !chatMessages.isEmpty()) {
|
||||||
|
mMap.update('items', ImmutableList(), oldIds => {
|
||||||
|
const newIds = chatMessages.map(chatMessage => chatMessage.get('id'));
|
||||||
|
|
||||||
|
const lastIndex = oldIds.findLastIndex((id) => id !== null && compareId(id, newIds.last()) >= 0) + 1;
|
||||||
|
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
|
||||||
|
|
||||||
|
if (firstIndex < 0) {
|
||||||
|
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldIds.take(firstIndex + 1).concat(
|
||||||
|
isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
|
||||||
|
oldIds.skip(lastIndex)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChatMessageConversation = (state, chatConversationId, chatMessage) => {
|
||||||
|
const top = state.getIn([chatConversationId, 'top']);
|
||||||
|
const ids = state.getIn([chatConversationId, 'items'], ImmutableList());
|
||||||
|
const includesId = ids.includes(chatMessage.get('id'));
|
||||||
|
const unread = state.getIn([chatConversationId, 'unread'], 0);
|
||||||
|
|
||||||
|
if (includesId) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newIds = ids;
|
||||||
|
|
||||||
|
return state.update(chatConversationId, initialConversation, map => map.withMutations(mMap => {
|
||||||
|
if (!top) mMap.set('unread', unread + 1);
|
||||||
|
// if (top && ids.size > 40) newIds = newIds.take(20);
|
||||||
|
mMap.set('items', newIds.unshift(chatMessage.get('id')));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function chat_conversation_messages(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_CONNECT:
|
||||||
|
return state.update(action.chatConversationId, initialConversation, map => map.set('online', true))
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_DISCONNECT:
|
||||||
|
return state.update(
|
||||||
|
action.chatConversationId,
|
||||||
|
initialConversation,
|
||||||
|
map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
|
||||||
|
)
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_CLEAR:
|
||||||
|
return state.set(chatConversationId, initialTimeline)
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_EXPAND_REQUEST:
|
||||||
|
return state.update(action.chatConversationId, initialConversation, map => map.set('isLoading', true))
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_EXPAND_FAIL:
|
||||||
|
return state.update(action.chatConversationId, initialConversation, map => map.withMutations((mMap) => {
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('isError', true)
|
||||||
|
}))
|
||||||
|
case CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS:
|
||||||
|
return expandNormalizedChatConversation(state, action.chatConversationId, fromJS(action.chatMessages), action.next, action.partial, action.isLoadingRecent)
|
||||||
|
case CHAT_MESSAGES_SEND_SUCCESS:
|
||||||
|
return updateChatMessageConversation(state, action.chatConversationId, fromJS(action.chatMessage))
|
||||||
|
// CHAT_MESSAGES_DELETE_REQUEST
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Map as ImmutableMap,
|
||||||
|
List as ImmutableList,
|
||||||
|
fromJS,
|
||||||
|
} from 'immutable'
|
||||||
|
import { me } from '../initial_state'
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
||||||
|
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
||||||
|
} from '../actions/chat_conversations'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap()
|
||||||
|
|
||||||
|
export const normalizeChatConversation = (chatConversation) => {
|
||||||
|
const { other_accounts, ...rest } = chatConversation
|
||||||
|
return fromJS({
|
||||||
|
...rest,
|
||||||
|
other_account_ids: other_accounts.map((a) => a.id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const importChatConversation = (state, chatConversation) => state.set(chatConversation.chat_conversation_id, normalizeChatConversation(chatConversation))
|
||||||
|
|
||||||
|
const importChatConversations = (state, chatConversations) => {
|
||||||
|
return state.withMutations((mutable) => chatConversations.forEach((chatConversation) => importChatConversation(mutable, chatConversation)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function chat_conversations(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS:
|
||||||
|
return importChatConversation(state, action.chatConversation)
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS:
|
||||||
|
case CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS:
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS:
|
||||||
|
case CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS:
|
||||||
|
return importChatConversations(state, action.chatConversations)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
@ -1,84 +1,32 @@
|
|||||||
|
import { Map as ImmutableMap, fromJS } from 'immutable'
|
||||||
import {
|
import {
|
||||||
REPOST_REQUEST,
|
CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
UNREPOST_REQUEST,
|
CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
REPOST_FAIL,
|
} from '../actions/chat_messages'
|
||||||
FAVORITE_REQUEST,
|
|
||||||
FAVORITE_FAIL,
|
|
||||||
UNFAVORITE_REQUEST,
|
|
||||||
} from '../actions/interactions';
|
|
||||||
import {
|
import {
|
||||||
STATUS_REVEAL,
|
CHAT_MESSAGES_IMPORT,
|
||||||
STATUS_HIDE,
|
} from '../actions/importer'
|
||||||
UPDATE_STATUS_STATS,
|
|
||||||
} from '../actions/statuses';
|
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
|
||||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
const importChatMessage = (state, chatMessage) => state.set(chatMessage.id, fromJS(chatMessage))
|
||||||
|
|
||||||
const importStatuses = (state, statuses) =>
|
const importChatMessages = (state, chatMessages) =>
|
||||||
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
|
state.withMutations((mutable) => chatMessages.forEach((chatMessage) => importChatMessage(mutable, chatMessage)))
|
||||||
|
|
||||||
const deleteStatus = (state, id, references) => {
|
const deleteChatMessage = (state, id) => {
|
||||||
references.forEach(ref => {
|
return state.delete(id)
|
||||||
state = deleteStatus(state, ref[0], []);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return state.delete(id);
|
const initialState = ImmutableMap()
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
export default function chat_messages(state = initialState, action) {
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STATUS_IMPORT:
|
case CHAT_MESSAGES_IMPORT:
|
||||||
return importStatus(state, action.status);
|
return importChatMessages(state, action.chatMessages)
|
||||||
case STATUSES_IMPORT:
|
case CHAT_MESSAGES_SEND_SUCCESS:
|
||||||
return importStatuses(state, action.statuses);
|
return importChatMessage(state, action.chatMessage)
|
||||||
case FAVORITE_REQUEST:
|
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
return deleteChatMessage(state, action.chatMessageId)
|
||||||
case FAVORITE_FAIL:
|
|
||||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
|
||||||
case UNFAVORITE_REQUEST:
|
|
||||||
return state.setIn([action.status.get('id'), 'favourited'], false);
|
|
||||||
case REPOST_REQUEST:
|
|
||||||
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
|
||||||
case UNREPOST_REQUEST:
|
|
||||||
return state.setIn([action.status.get('id'), 'reblogged'], false);
|
|
||||||
case REPOST_FAIL:
|
|
||||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
|
|
||||||
case STATUS_REVEAL:
|
|
||||||
return state.withMutations((map) => {
|
|
||||||
action.ids.forEach(id => {
|
|
||||||
if (!(state.get(id) === undefined)) {
|
|
||||||
map.setIn([id, 'hidden'], false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
case STATUS_HIDE:
|
|
||||||
return state.withMutations((map) => {
|
|
||||||
action.ids.forEach(id => {
|
|
||||||
if (!(state.get(id) === undefined)) {
|
|
||||||
map.setIn([id, 'hidden'], true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
case TIMELINE_DELETE:
|
|
||||||
return deleteStatus(state, action.id, action.references);
|
|
||||||
case UPDATE_STATUS_STATS:
|
|
||||||
const { status_id } = action.data
|
|
||||||
return state.withMutations((map) => {
|
|
||||||
if (action.data.favourited !== undefined) map.setIn([status_id, 'favourited'], action.data.favourited)
|
|
||||||
if (action.data.favourites_count !== undefined) map.setIn([status_id, 'favourites_count'], action.data.favourites_count)
|
|
||||||
if (action.data.reblogged !== undefined) map.setIn([status_id, 'reblogged'], action.data.reblogged)
|
|
||||||
if (action.data.reblogs_count !== undefined) map.setIn([status_id, 'reblogs_count'], action.data.reblogs_count)
|
|
||||||
if (action.data.replies_count !== undefined) map.setIn([status_id, 'replies_count'], action.data.replies_count)
|
|
||||||
if (action.data.pinned !== undefined) map.setIn([status_id, 'pinned'], action.data.pinned)
|
|
||||||
if (action.data.pinned_by_group !== undefined) map.setIn([status_id, 'pinned_by_group'], action.data.pinned_by_group)
|
|
||||||
if (action.data.bookmarked !== undefined) map.setIn([status_id, 'bookmarked'], action.data.bookmarked)
|
|
||||||
})
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
38
app/javascript/gabsocial/reducers/chats.js
Normal file
38
app/javascript/gabsocial/reducers/chats.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Map as ImmutableMap,
|
||||||
|
List as ImmutableList,
|
||||||
|
fromJS,
|
||||||
|
} from 'immutable'
|
||||||
|
import { me } from '../initial_state'
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATION_CREATE_SEARCH_ACCOUNTS_SUCCESS,
|
||||||
|
SET_CHAT_CONVERSATION_SELECTED,
|
||||||
|
} from '../actions/chats'
|
||||||
|
import {
|
||||||
|
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
|
||||||
|
} from '../actions/chat_conversations'
|
||||||
|
import {
|
||||||
|
CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
|
CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
|
CHAT_MESSAGES_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS,
|
||||||
|
} from '../actions/chat_messages'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
createChatConversationSuggestionIds: ImmutableList(),
|
||||||
|
selectedChatConversationId: null,
|
||||||
|
chatConversationRequestCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function chats(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case CHAT_CONVERSATION_CREATE_SEARCH_ACCOUNTS_SUCCESS:
|
||||||
|
return state.set('createChatConversationSuggestionIds', ImmutableList(action.accounts.map((item) => item.id)))
|
||||||
|
case SET_CHAT_CONVERSATION_SELECTED:
|
||||||
|
return state.set('selectedChatConversationId', action.chatConversationId)
|
||||||
|
case CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS:
|
||||||
|
return state.set('chatConversationRequestCount', action.count)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,9 @@ import { combineReducers } from 'redux-immutable'
|
|||||||
import { loadingBarReducer } from 'react-redux-loading-bar'
|
import { loadingBarReducer } from 'react-redux-loading-bar'
|
||||||
import accounts from './accounts'
|
import accounts from './accounts'
|
||||||
import accounts_counters from './accounts_counters'
|
import accounts_counters from './accounts_counters'
|
||||||
import chat_compose from './chat_compose'
|
import chats from './chats'
|
||||||
|
import chat_conversation_lists from './chat_conversation_lists'
|
||||||
|
import chat_conversation_messages from './chat_conversation_messages'
|
||||||
import chat_conversations from './chat_conversations'
|
import chat_conversations from './chat_conversations'
|
||||||
import chat_messages from './chat_messages'
|
import chat_messages from './chat_messages'
|
||||||
import compose from './compose'
|
import compose from './compose'
|
||||||
@ -49,6 +51,9 @@ import user_lists from './user_lists'
|
|||||||
const reducers = {
|
const reducers = {
|
||||||
accounts,
|
accounts,
|
||||||
accounts_counters,
|
accounts_counters,
|
||||||
|
chats,
|
||||||
|
chat_conversation_lists,
|
||||||
|
chat_conversation_messages,
|
||||||
chat_conversations,
|
chat_conversations,
|
||||||
chat_messages,
|
chat_messages,
|
||||||
compose,
|
compose,
|
||||||
@ -88,10 +93,10 @@ const reducers = {
|
|||||||
status_revisions,
|
status_revisions,
|
||||||
suggestions,
|
suggestions,
|
||||||
timelines,
|
timelines,
|
||||||
timeline_injections,
|
// timeline_injections,
|
||||||
toasts,
|
// toasts,
|
||||||
user,
|
// user,
|
||||||
user_lists,
|
// user_lists,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default combineReducers(reducers)
|
export default combineReducers(reducers)
|
||||||
|
@ -62,7 +62,6 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
|||||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
|
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
|
||||||
|
|
||||||
if (firstIndex < 0) {
|
if (firstIndex < 0) {
|
||||||
console.log("----2")
|
|
||||||
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
|
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ export default function (state = initialState, action) {
|
|||||||
case SAVE_USER_PROFILE_INFORMATION_FETCH_SUCCESS:
|
case SAVE_USER_PROFILE_INFORMATION_FETCH_SUCCESS:
|
||||||
return state
|
return state
|
||||||
case SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL:
|
case SAVE_USER_PROFILE_INFORMATION_FETCH_FAIL:
|
||||||
alert('Failed to update your profile. Max profile image size is 2MB. Please update and try again.')
|
|
||||||
return state.set('isError', true)
|
return state.set('isError', true)
|
||||||
case RESEND_USER_CONFIRMATION_EMAIL_SUCCESS:
|
case RESEND_USER_CONFIRMATION_EMAIL_SUCCESS:
|
||||||
return state.set('emailConfirmationResends', state.get('emailConfirmationResends') + 1)
|
return state.set('emailConfirmationResends', state.get('emailConfirmationResends') + 1)
|
||||||
|
@ -20,6 +20,49 @@ export const makeGetAccount = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const makeGetChatMessage = () => {
|
||||||
|
return createSelector(
|
||||||
|
[
|
||||||
|
(state) => state,
|
||||||
|
(state, { id }) => state.getIn(['chat_messages', id]),
|
||||||
|
(state, { id }) => state.getIn(['accounts', `${state.getIn(['chat_messages', `${id}`, 'from_account_id'])}`]),
|
||||||
|
],
|
||||||
|
(state, base, account) => {
|
||||||
|
if (!base) return null
|
||||||
|
return base.withMutations((map) => {
|
||||||
|
map.set('account', account)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeGetChatConversation = () => {
|
||||||
|
return createSelector(
|
||||||
|
[
|
||||||
|
(state) => state,
|
||||||
|
(state, { id }) => state.getIn(['chat_conversations', `${id}`]),
|
||||||
|
(state) => state.get('accounts'),
|
||||||
|
],
|
||||||
|
(state, base, allAccounts) => {
|
||||||
|
if (!base) return null
|
||||||
|
|
||||||
|
let otherAccounts = ImmutableList()
|
||||||
|
if (allAccounts) {
|
||||||
|
base.get('other_account_ids').forEach((acctId) => {
|
||||||
|
const acct = allAccounts.get(`${acctId}`, null)
|
||||||
|
if (acct) {
|
||||||
|
otherAccounts = otherAccounts.set(otherAccounts.size, acct)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.withMutations((map) => {
|
||||||
|
map.set('other_accounts', otherAccounts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const toServerSideType = columnType => {
|
const toServerSideType = columnType => {
|
||||||
switch (columnType) {
|
switch (columnType) {
|
||||||
case 'home':
|
case 'home':
|
||||||
|
@ -43,6 +43,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
received(data) {
|
received(data) {
|
||||||
|
console.log("received:", data)
|
||||||
onReceive(data);
|
onReceive(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2,11 +2,15 @@ import React from 'react'
|
|||||||
import { FormattedNumber } from 'react-intl'
|
import { FormattedNumber } from 'react-intl'
|
||||||
|
|
||||||
export const shortNumberFormat = (number) => {
|
export const shortNumberFormat = (number) => {
|
||||||
|
if (isNaN(number)) {
|
||||||
|
return <FormattedNumber value={0} />
|
||||||
|
}
|
||||||
|
|
||||||
if (number < 1000) {
|
if (number < 1000) {
|
||||||
try {
|
try {
|
||||||
return (<FormattedNumber value={number} />).props.value
|
return (<FormattedNumber value={number} />).props.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return <FormattedNumber value={number} />
|
return <FormattedNumber value={0} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,10 +35,12 @@
|
|||||||
--solid_color_tertiary: #f5f5f7;
|
--solid_color_tertiary: #f5f5f7;
|
||||||
--solid_color_block: #f5f8fa;
|
--solid_color_block: #f5f8fa;
|
||||||
--solid_color_highlight: rgba(224, 234, 66, .125);
|
--solid_color_highlight: rgba(224, 234, 66, .125);
|
||||||
|
--solid_color_toast: #232425;
|
||||||
|
|
||||||
--text_color_primary: #2d3436;
|
--text_color_primary: #2d3436;
|
||||||
--text_color_secondary: #4b4f55;
|
--text_color_secondary: #4b4f55;
|
||||||
--text_color_tertiary: #777;
|
--text_color_tertiary: #777;
|
||||||
|
--text_color_alt: var(--solid_color_secondary);
|
||||||
|
|
||||||
--border_color_secondary: #ececed;
|
--border_color_secondary: #ececed;
|
||||||
|
|
||||||
@ -91,10 +93,12 @@
|
|||||||
--solid_color_secondary-dark: #424343 !important;
|
--solid_color_secondary-dark: #424343 !important;
|
||||||
--solid_color_tertiary: #333 !important;
|
--solid_color_tertiary: #333 !important;
|
||||||
--solid_color_block: #2d2d2d !important;
|
--solid_color_block: #2d2d2d !important;
|
||||||
|
--solid_color_toast: #fcfcfc !important;
|
||||||
|
|
||||||
--text_color_primary: #fff !important;
|
--text_color_primary: #fff !important;
|
||||||
--text_color_secondary: #999 !important;
|
--text_color_secondary: #999 !important;
|
||||||
--text_color_tertiary: #656565 !important;
|
--text_color_tertiary: #656565 !important;
|
||||||
|
--text_color_alt: var(----solid_color_primary);
|
||||||
|
|
||||||
--border_color_secondary: #424141 !important;
|
--border_color_secondary: #424141 !important;
|
||||||
|
|
||||||
@ -116,14 +120,16 @@
|
|||||||
--solid_color_secondary-dark: #282828 !important;
|
--solid_color_secondary-dark: #282828 !important;
|
||||||
--solid_color_tertiary: #000 !important;
|
--solid_color_tertiary: #000 !important;
|
||||||
--solid_color_block: #202327 !important;
|
--solid_color_block: #202327 !important;
|
||||||
|
--solid_color_toast: #fcfcfc !important;
|
||||||
|
|
||||||
--text_color_primary: #cccbcb !important;
|
--text_color_primary: #cccbcb !important;
|
||||||
--text_color_secondary: #888 !important;
|
--text_color_secondary: #888 !important;
|
||||||
--text_color_tertiary: #656565 !important;
|
--text_color_tertiary: #656565 !important;
|
||||||
|
--text_color_alt: var(----solid_color_primary);
|
||||||
|
|
||||||
--border_color_secondary: #343434 !important;
|
--border_color_secondary: #343434 !important;
|
||||||
|
|
||||||
--color_input_placeholder: var(--text_color_secondary);
|
--color_input_placeholder: var(--text_color_secondary) !important;
|
||||||
|
|
||||||
/* Navigation bar. Only themes. Non-editable */
|
/* Navigation bar. Only themes. Non-editable */
|
||||||
--navigation_background: #000 !important;
|
--navigation_background: #000 !important;
|
||||||
@ -423,6 +429,8 @@ pre {
|
|||||||
|
|
||||||
.bgTertiary { background-color: var(--solid_color_tertiary); }
|
.bgTertiary { background-color: var(--solid_color_tertiary); }
|
||||||
|
|
||||||
|
.bgToast { background-color: var(--solid_color_toast); }
|
||||||
|
|
||||||
.bgPrimary { background-color: var(--solid_color_primary); }
|
.bgPrimary { background-color: var(--solid_color_primary); }
|
||||||
.bgPrimaryOpaque { background-color: var(--solid_color_primary-opaque) }
|
.bgPrimaryOpaque { background-color: var(--solid_color_primary-opaque) }
|
||||||
|
|
||||||
@ -459,6 +467,7 @@ pre {
|
|||||||
|
|
||||||
/* */
|
/* */
|
||||||
|
|
||||||
|
.cAlt { color: var(--text_color_alt); }
|
||||||
.cPrimary { color: var(--text_color_primary); }
|
.cPrimary { color: var(--text_color_primary); }
|
||||||
|
|
||||||
.cBlack { color: var(--color_black); }
|
.cBlack { color: var(--color_black); }
|
||||||
@ -582,6 +591,7 @@ pre {
|
|||||||
.w330PX { width: 330px; }
|
.w330PX { width: 330px; }
|
||||||
.w300PX { width: 300px; }
|
.w300PX { width: 300px; }
|
||||||
.w240PX { width: 240px; }
|
.w240PX { width: 240px; }
|
||||||
|
.w228PX { width: 228px; }
|
||||||
.w208PX { width: 208px; }
|
.w208PX { width: 208px; }
|
||||||
.w115PX { width: 115px; }
|
.w115PX { width: 115px; }
|
||||||
.w84PX { width: 84px; }
|
.w84PX { width: 84px; }
|
||||||
@ -892,6 +902,7 @@ pre {
|
|||||||
.boxShadowPopover { box-shadow: 0 0 15px -5px rgba(0,0,0,0.15); }
|
.boxShadowPopover { box-shadow: 0 0 15px -5px rgba(0,0,0,0.15); }
|
||||||
.boxShadowBlock { box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
|
.boxShadowBlock { box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
|
||||||
.boxShadowDot { box-shadow: inset 0 0 0 3px #fff, inset 0 0 0 6px #000; }
|
.boxShadowDot { box-shadow: inset 0 0 0 3px #fff, inset 0 0 0 6px #000; }
|
||||||
|
.boxShadowToast { box-shadow: 0px 0px 10px -2px rgba(0, 0, 0, .2), 0px 0px 2px -1px rgba(0, 0, 0, 0.4); }
|
||||||
|
|
||||||
.boxShadowNone .boxShadowBlock {
|
.boxShadowNone .boxShadowBlock {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
@ -21,7 +21,7 @@ class EntityCache
|
|||||||
end
|
end
|
||||||
|
|
||||||
unless uncached_ids.empty?
|
unless uncached_ids.empty?
|
||||||
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).each_with_object({}) { |item, h| h[item.shortcode] = item }
|
uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain).each_with_object({}) { |item, h| h[item.shortcode] = item }
|
||||||
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
|
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -297,6 +297,11 @@ class Account < ApplicationRecord
|
|||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def excluded_from_chat_account_ids
|
||||||
|
# : todo :
|
||||||
|
# Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
||||||
|
end
|
||||||
|
|
||||||
def excluded_from_timeline_account_ids
|
def excluded_from_timeline_account_ids
|
||||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
||||||
end
|
end
|
||||||
|
30
app/models/chat_block.rb
Normal file
30
app/models/chat_block.rb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: chat_blocks
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# target_account_id :bigint(8) not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class ChatBlock < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
include RelationshipCacheable
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :target_account, class_name: 'Account'
|
||||||
|
|
||||||
|
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||||
|
|
||||||
|
after_commit :remove_blocking_cache
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_blocking_cache
|
||||||
|
Rails.cache.delete("exclude_chat_account_ids_for:#{account_id}")
|
||||||
|
Rails.cache.delete("exclude_chat_account_ids_for:#{target_account_id}")
|
||||||
|
end
|
||||||
|
end
|
13
app/models/chat_conversation.rb
Normal file
13
app/models/chat_conversation.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: chat_conversations
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class ChatConversation < ApplicationRecord
|
||||||
|
|
||||||
|
end
|
43
app/models/chat_conversation_account.rb
Normal file
43
app/models/chat_conversation_account.rb
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: chat_conversation_accounts
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8)
|
||||||
|
# chat_conversation_id :bigint(8)
|
||||||
|
# participant_account_ids :bigint(8) default([]), not null, is an Array
|
||||||
|
# last_chat_message_id :bigint(8)
|
||||||
|
# is_unread :boolean default(FALSE), not null
|
||||||
|
# is_hidden :boolean default(FALSE), not null
|
||||||
|
# is_approved :boolean default(FALSE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class ChatConversationAccount < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :chat_conversation
|
||||||
|
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
|
||||||
|
|
||||||
|
# before_validation :set_last_chat_message
|
||||||
|
|
||||||
|
def participant_accounts
|
||||||
|
if participant_account_ids.empty?
|
||||||
|
[account]
|
||||||
|
else
|
||||||
|
# : todo : dont include current_account
|
||||||
|
participants = Account.where(id: participant_account_ids)
|
||||||
|
participants.empty? ? [account] : participants
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_last_chat_message
|
||||||
|
self.last_chat_message_id = nil # : todo :
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
27
app/models/chat_message.rb
Normal file
27
app/models/chat_message.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: chat_messages
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# chat_conversation_id :bigint(8) not null
|
||||||
|
# text :text default(""), not null
|
||||||
|
# language :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class ChatMessage < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
|
||||||
|
belongs_to :from_account, class_name: 'Account'
|
||||||
|
belongs_to :chat_conversation
|
||||||
|
|
||||||
|
validates_with ChatMessageLengthValidator
|
||||||
|
|
||||||
|
default_scope { recent }
|
||||||
|
|
||||||
|
scope :recent, -> { reorder(created_at: :desc) }
|
||||||
|
|
||||||
|
end
|
29
app/models/chat_mute.rb
Normal file
29
app/models/chat_mute.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: chat_mutes
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# target_account_id :bigint(8) not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class ChatMute < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
include RelationshipCacheable
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :target_account, class_name: 'Account'
|
||||||
|
|
||||||
|
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||||
|
|
||||||
|
after_commit :remove_blocking_cache
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_blocking_cache
|
||||||
|
Rails.cache.delete("exclude_chat_account_ids_for:#{account_id}")
|
||||||
|
end
|
||||||
|
end
|
21
app/serializers/rest/chat_conversation_account_serializer.rb
Normal file
21
app/serializers/rest/chat_conversation_account_serializer.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :is_hidden, :is_approved, :is_unread, :chat_conversation_id, :created_at
|
||||||
|
|
||||||
|
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
|
||||||
|
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def chat_conversation_id
|
||||||
|
object.chat_conversation_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_chat_message_id?
|
||||||
|
object.last_chat_message_id.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
10
app/serializers/rest/chat_message_serializer.rb
Normal file
10
app/serializers/rest/chat_message_serializer.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :text, :language, :from_account_id,
|
||||||
|
:chat_conversation_id, :created_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
@ -4,6 +4,7 @@ class AccountSearchService < BaseService
|
|||||||
attr_reader :query, :limit, :offset, :options, :account
|
attr_reader :query, :limit, :offset, :options, :account
|
||||||
|
|
||||||
def call(query, account = nil, options = {})
|
def call(query, account = nil, options = {})
|
||||||
|
puts "query:"+query.inspect
|
||||||
@query = query.strip
|
@query = query.strip
|
||||||
@limit = options[:limit].to_i
|
@limit = options[:limit].to_i
|
||||||
@offset = options[:offset].to_i
|
@offset = options[:offset].to_i
|
||||||
|
10
app/validators/chat_message_length_validator.rb
Normal file
10
app/validators/chat_message_length_validator.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ChatMessageLengthValidator < ActiveModel::Validator
|
||||||
|
MAX_CHARS = 1600
|
||||||
|
|
||||||
|
def validate(chatMessage)
|
||||||
|
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if chatMessage.text.length > MAX_CHARS
|
||||||
|
status.errors.add(:text, I18n.t('statuses.over_character_limit', max: MAX_CHARS)) if chatMessage.text.length === 0
|
||||||
|
end
|
||||||
|
end
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class PollValidator < ActiveModel::Validator
|
class PollValidator < ActiveModel::Validator
|
||||||
MAX_OPTIONS = 4
|
MAX_OPTIONS = 4
|
||||||
MAX_OPTION_CHARS = 25
|
MAX_OPTION_CHARS = 160
|
||||||
MAX_EXPIRATION = 1.month.freeze
|
MAX_EXPIRATION = 1.month.freeze
|
||||||
MIN_EXPIRATION = 5.minutes.freeze
|
MIN_EXPIRATION = 5.minutes.freeze
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ Doorkeeper.configure do
|
|||||||
:'write:notifications',
|
:'write:notifications',
|
||||||
:'write:reports',
|
:'write:reports',
|
||||||
:'write:statuses',
|
:'write:statuses',
|
||||||
|
:'write:chats',
|
||||||
:read,
|
:read,
|
||||||
:'read:accounts',
|
:'read:accounts',
|
||||||
:'read:blocks',
|
:'read:blocks',
|
||||||
@ -81,6 +82,7 @@ Doorkeeper.configure do
|
|||||||
:'read:notifications',
|
:'read:notifications',
|
||||||
:'read:search',
|
:'read:search',
|
||||||
:'read:statuses',
|
:'read:statuses',
|
||||||
|
:'read:chats',
|
||||||
:follow,
|
:follow,
|
||||||
:push
|
:push
|
||||||
|
|
||||||
|
@ -221,15 +221,33 @@ Rails.application.routes.draw do
|
|||||||
resource :explore, only: :show, controller: :explore
|
resource :explore, only: :show, controller: :explore
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :messages do
|
resources :chat_conversation_accounts, only: :show do
|
||||||
resource :conversations, only: [:show, :create]
|
resources :blocked_accounts, only: :index
|
||||||
resource :chats, only: [:show, :create]
|
resources :muted_accounts, only: :index
|
||||||
|
|
||||||
member do
|
member do
|
||||||
post :block
|
post :block_messenger
|
||||||
post :unblock
|
post :unblock_messenger
|
||||||
post :mute
|
post :mute_messenger
|
||||||
post :unmute
|
post :unmute_messenger
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :chat_conversations do
|
||||||
|
resources :messages, only: :show
|
||||||
|
resources :approved_conversations
|
||||||
|
resources :requested_conversations, only: :index do
|
||||||
|
collection do
|
||||||
|
get :count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :chat_conversation, only: [:show, :create] do
|
||||||
|
member do
|
||||||
|
post :mark_chat_conversation_approved
|
||||||
|
post :mark_chat_conversation_unread
|
||||||
|
post :mark_chat_conversation_hidden
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -241,6 +259,7 @@ Rails.application.routes.draw do
|
|||||||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||||
resources :preferences, only: [:index]
|
resources :preferences, only: [:index]
|
||||||
resources :group_categories, only: [:index]
|
resources :group_categories, only: [:index]
|
||||||
|
resources :chat_messages, only: [:create, :destroy]
|
||||||
|
|
||||||
get '/search', to: 'search#index', as: :search
|
get '/search', to: 'search#index', as: :search
|
||||||
get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex
|
get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex
|
||||||
|
9
db/migrate/20201127051070_create_chat_conversations.rb
Normal file
9
db/migrate/20201127051070_create_chat_conversations.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class CreateChatConversations < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :chat_conversations do |t|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
14
db/migrate/20201127051071_create_chat_blocks.rb
Normal file
14
db/migrate/20201127051071_create_chat_blocks.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class CreateChatBlocks < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :chat_blocks do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.integer :target_account_id, null: false
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :chat_blocks, [:account_id, :target_account_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
12
db/migrate/20201127051072_create_chat_mutes.rb
Normal file
12
db/migrate/20201127051072_create_chat_mutes.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class CreateChatMutes < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :chat_mutes do |t|
|
||||||
|
t.integer :account_id, null: false
|
||||||
|
t.integer :target_account_id, null: false
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :chat_mutes, [:account_id, :target_account_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,16 @@
|
|||||||
|
class CreateChatConversationAccounts < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :chat_conversation_accounts do |t|
|
||||||
|
t.belongs_to :account, foreign_key: { on_delete: :cascade }
|
||||||
|
t.belongs_to :chat_conversation, foreign_key: { on_delete: :cascade }
|
||||||
|
t.bigint :participant_account_ids, array: true, null: false, default: []
|
||||||
|
t.bigint :last_chat_message_id, null: true, default: nil
|
||||||
|
t.boolean :is_unread, null: false, default: false
|
||||||
|
t.boolean :is_hidden, null: false, default: false
|
||||||
|
t.boolean :is_approved, null: false, default: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
14
db/migrate/20201127051074_create_chat_messages.rb
Normal file
14
db/migrate/20201127051074_create_chat_messages.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class CreateChatMessages < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :chat_messages do |t|
|
||||||
|
t.text :text, null: false, default: ''
|
||||||
|
t.text :language, null: false, default: ''
|
||||||
|
t.integer :from_account_id, null: false
|
||||||
|
t.integer :chat_conversation_id, null: false
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :chat_messages, [:from_account_id, :chat_conversation_id]
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user