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)
|
||||
end
|
||||
|
||||
def enable
|
||||
authorize @custom_emoji, :enable?
|
||||
@custom_emoji.update!(disabled: false)
|
||||
log_action :enable, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
end
|
||||
|
||||
def disable
|
||||
authorize @custom_emoji, :disable?
|
||||
@custom_emoji.update!(disabled: true)
|
||||
log_action :disable, @custom_emoji
|
||||
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
|
||||
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_custom_emoji
|
||||
|
|
|
@ -3,12 +3,15 @@
|
|||
class Api::BaseController < ApplicationController
|
||||
DEFAULT_STATUSES_LIMIT = 20
|
||||
DEFAULT_ACCOUNTS_LIMIT = 20
|
||||
DEFAULT_CHAT_CONVERSATION_LIMIT = 12
|
||||
DEFAULT_CHAT_CONVERSATION_MESSAGE_LIMIT = 10
|
||||
|
||||
include RateLimitHeaders
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :check_user_permissions
|
||||
|
||||
before_action :block_if_doorkeeper
|
||||
before_action :set_cache_headers
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
|
@ -90,6 +93,14 @@ class Api::BaseController < ApplicationController
|
|||
doorkeeper_authorize!(*scopes) if doorkeeper_token
|
||||
end
|
||||
|
||||
def superapp?
|
||||
doorkeeper_token && doorkeeper_token.application.superapp? || false
|
||||
end
|
||||
|
||||
def block_if_doorkeeper
|
||||
raise GabSocial::NotPermittedError unless superapp?
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||
class Api::V1::ChatConversationAccountsController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:chats' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:chats' }, only: [:create]
|
||||
|
||||
before_action :require_user!, except: [:show, :create]
|
||||
before_action :require_user!
|
||||
before_action :set_account, except: [:create]
|
||||
before_action :check_account_suspension, only: [:show]
|
||||
|
||||
def show
|
||||
#
|
||||
end
|
||||
|
||||
def create
|
||||
#
|
||||
end
|
||||
|
||||
def block
|
||||
BlockMessengerService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
|
@ -42,18 +34,19 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
# @account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||
end
|
||||
# def relationships(**options)
|
||||
# AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||
# end
|
||||
|
||||
def check_account_suspension
|
||||
gone if @account.suspended?
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:username, :email, :password, :agreement, :locale)
|
||||
end
|
||||
# def account_params
|
||||
# params.permit(:username, :email, :password, :agreement, :locale)
|
||||
# end
|
||||
|
||||
end
|
|
@ -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
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -52,7 +52,7 @@ class ReactController < ApplicationController
|
|||
end
|
||||
|
||||
def find_route_matches
|
||||
request.path.match(/\A\/(home|news|suggestions|links|messages|shortcuts|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|mutes)/)
|
||||
request.path.match(/\A\/(home|news|api|suggestions|links|chat_conversations|chat_conversation_accounts|messages|shortcuts|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|mutes)/)
|
||||
end
|
||||
|
||||
def find_public_route_matches
|
||||
|
|
|
@ -163,6 +163,7 @@ export const followAccount = (id, reblogs = true) => (dispatch, getState) => {
|
|||
dispatch(followAccountRequest(id, locked))
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then((response) => {
|
||||
console.log("response:", response)
|
||||
dispatch(followAccountSuccess(response.data, alreadyFollowing))
|
||||
}).catch((error) => {
|
||||
dispatch(followAccountFail(error, locked))
|
||||
|
|
|
@ -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 })
|
||||
}
|
|
@ -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 { me } from '../initial_state'
|
||||
|
||||
export const CONVERSATION_BLOCKS_FETCH_REQUEST = 'CONVERSATION_BLOCKS_FETCH_REQUEST'
|
||||
export const CONVERSATION_BLOCKS_FETCH_SUCCESS = 'CONVERSATION_BLOCKS_FETCH_SUCCESS'
|
||||
export const CONVERSATION_BLOCKS_FETCH_FAIL = 'CONVERSATION_BLOCKS_FETCH_FAIL'
|
||||
//
|
||||
|
||||
export const CONVERSATION_BLOCKS_EXPAND_REQUEST = 'CONVERSATION_BLOCKS_EXPAND_REQUEST'
|
||||
export const CONVERSATION_BLOCKS_EXPAND_SUCCESS = 'CONVERSATION_BLOCKS_EXPAND_SUCCESS'
|
||||
export const CONVERSATION_BLOCKS_EXPAND_FAIL = 'CONVERSATION_BLOCKS_EXPAND_FAIL'
|
||||
export const CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL = 'CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL'
|
||||
|
||||
export const CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL'
|
||||
|
||||
//
|
||||
|
||||
export const CHAT_CONVERSATIONS_CREATE_REQUEST = 'CHAT_CONVERSATIONS_CREATE_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_CREATE_SUCCESS = 'CHAT_CONVERSATIONS_CREATE_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_CREATE_FAIL = 'CHAT_CONVERSATIONS_CREATE_FAIL'
|
||||
|
||||
export const CHAT_CONVERSATIONS_DELETE_REQUEST = 'CHAT_CONVERSATIONS_DELETE_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_DELETE_SUCCESS = 'CHAT_CONVERSATIONS_DELETE_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_DELETE_FAIL = 'CHAT_CONVERSATIONS_DELETE_FAIL'
|
||||
|
||||
//
|
||||
|
||||
export const CHAT_CONVERSATION_BLOCKS_FETCH_REQUEST = 'CHAT_CONVERSATION_BLOCKS_FETCH_REQUEST'
|
||||
export const CHAT_CONVERSATION_BLOCKS_FETCH_SUCCESS = 'CHAT_CONVERSATION_BLOCKS_FETCH_SUCCESS'
|
||||
export const CHAT_CONVERSATION_BLOCKS_FETCH_FAIL = 'CHAT_CONVERSATION_BLOCKS_FETCH_FAIL'
|
||||
|
||||
export const CHAT_CONVERSATION_BLOCKS_EXPAND_REQUEST = 'CHAT_CONVERSATION_BLOCKS_EXPAND_REQUEST'
|
||||
export const CHAT_CONVERSATION_BLOCKS_EXPAND_SUCCESS = 'CHAT_CONVERSATION_BLOCKS_EXPAND_SUCCESS'
|
||||
export const CHAT_CONVERSATION_BLOCKS_EXPAND_FAIL = 'CHAT_CONVERSATION_BLOCKS_EXPAND_FAIL'
|
||||
|
||||
export const BLOCK_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST'
|
||||
export const BLOCK_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS'
|
||||
|
@ -21,13 +43,13 @@ export const UNBLOCK_MESSAGER_FAIL = 'UNBLOCK_MESSAGER_FAIL'
|
|||
|
||||
//
|
||||
|
||||
export const CONVERSATION_MUTES_FETCH_REQUEST = 'CONVERSATION_MUTES_FETCH_REQUEST'
|
||||
export const CONVERSATION_MUTES_FETCH_SUCCESS = 'CONVERSATION_MUTES_FETCH_SUCCESS'
|
||||
export const CONVERSATION_MUTES_FETCH_FAIL = 'CONVERSATION_MUTES_FETCH_FAIL'
|
||||
export const CHAT_CONVERSATION_MUTES_FETCH_REQUEST = 'CHAT_CONVERSATION_MUTES_FETCH_REQUEST'
|
||||
export const CHAT_CONVERSATION_MUTES_FETCH_SUCCESS = 'CHAT_CONVERSATION_MUTES_FETCH_SUCCESS'
|
||||
export const CHAT_CONVERSATION_MUTES_FETCH_FAIL = 'CHAT_CONVERSATION_MUTES_FETCH_FAIL'
|
||||
|
||||
export const CONVERSATION_MUTES_EXPAND_REQUEST = 'CONVERSATION_MUTES_EXPAND_REQUEST'
|
||||
export const CONVERSATION_MUTES_EXPAND_SUCCESS = 'CONVERSATION_MUTES_EXPAND_SUCCESS'
|
||||
export const CONVERSATION_MUTES_EXPAND_FAIL = 'CONVERSATION_MUTES_EXPAND_FAIL'
|
||||
export const CHAT_CONVERSATION_MUTES_EXPAND_REQUEST = 'CHAT_CONVERSATION_MUTES_EXPAND_REQUEST'
|
||||
export const CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS = 'CHAT_CONVERSATION_MUTES_EXPAND_SUCCESS'
|
||||
export const CHAT_CONVERSATION_MUTES_EXPAND_FAIL = 'CHAT_CONVERSATION_MUTES_EXPAND_FAIL'
|
||||
|
||||
export const MUTE_MESSAGER_REQUEST = 'BLOCK_MESSAGER_REQUEST'
|
||||
export const MUTE_MESSAGER_SUCCESS = 'BLOCK_MESSAGER_SUCCESS'
|
||||
|
@ -39,25 +61,238 @@ export const UNMUTE_MESSAGER_FAIL = 'UNMUTE_MESSAGER_FAIL'
|
|||
|
||||
//
|
||||
|
||||
export const CONVERSATION_REQUEST_APPROVE_SUCCESS = 'CONVERSATION_REQUEST_APPROVE_SUCCESS'
|
||||
export const CONVERSATION_REQUEST_APPROVE_FAIL = 'CONVERSATION_REQUEST_APPROVE_FAIL'
|
||||
export const CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS = 'CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS'
|
||||
|
||||
export const CONVERSATION_REQUEST_REJECT_SUCCESS = 'CONVERSATION_REQUEST_REJECT_SUCCESS'
|
||||
export const CONVERSATION_REQUEST_REJECT_FAIL = 'CONVERSATION_REQUEST_REJECT_FAIL'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL = 'CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL'
|
||||
|
||||
export const CONVERSATION_DELETE_REQUEST = 'CONVERSATION_DELETE_REQUEST'
|
||||
export const CONVERSATION_DELETE_SUCCESS = 'CONVERSATION_DELETE_SUCCESS'
|
||||
export const CONVERSATION_DELETE_FAIL = 'CONVERSATION_DELETE_FAIL'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS'
|
||||
export const CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL'
|
||||
|
||||
//
|
||||
|
||||
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST'
|
||||
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS'
|
||||
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL'
|
||||
export const CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS = 'CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS'
|
||||
export const CHAT_CONVERSATION_REQUEST_APPROVE_FAIL = 'CHAT_CONVERSATION_REQUEST_APPROVE_FAIL'
|
||||
|
||||
export const CHAT_CONVERSATION_DELETE_REQUEST = 'CHAT_CONVERSATION_DELETE_REQUEST'
|
||||
export const CHAT_CONVERSATION_DELETE_SUCCESS = 'CHAT_CONVERSATION_DELETE_SUCCESS'
|
||||
export const CHAT_CONVERSATION_DELETE_FAIL = 'CHAT_CONVERSATION_DELETE_FAIL'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const fetchChatConversations = () => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
dispatch(fetchChatConversationsRequest())
|
||||
|
||||
api(getState).get('/api/v1/chat_conversations/approved_conversations').then((response) => {
|
||||
console.log("chat_conversations response: ", response)
|
||||
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts))
|
||||
const conversationsChatMessages = response.data.map((c) => c.last_chat_message)
|
||||
|
||||
dispatch(importFetchedAccounts(conversationsAccounts))
|
||||
// dispatch(importFetchedChatMessages(conversationsChatMessages))
|
||||
dispatch(fetchChatConversationsSuccess(response.data, next ? next.uri : null))
|
||||
}).catch((error) => {
|
||||
console.log("fetchChatConversationsFail:", error)
|
||||
dispatch(fetchChatConversationsFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchChatConversationsRequest = () => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_FETCH_REQUEST,
|
||||
})
|
||||
|
||||
export const fetchChatConversationsSuccess = (chatConversations, next) => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
||||
chatConversations,
|
||||
next,
|
||||
})
|
||||
|
||||
export const fetchChatConversationsFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_FETCH_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const expandChatConversations = () => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
const url = getState().getIn(['chat_conversations', 'approved', 'next'])
|
||||
const isLoading = getState().getIn(['chat_conversations', 'approved', 'isLoading'])
|
||||
|
||||
if (url === null || isLoading) return
|
||||
|
||||
dispatch(expandChatConversationsRequest())
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts))
|
||||
const conversationsChatMessages = response.data.map((c) => c.last_chat_message)
|
||||
|
||||
dispatch(importFetchedAccounts(conversationsAccounts))
|
||||
// dispatch(importFetchedChatMessages(conversationsChatMessages))
|
||||
dispatch(expandChatConversationsSuccess(response.data, next ? next.uri : null))
|
||||
}).catch(error => dispatch(expandChatConversationsFail(error)))
|
||||
}
|
||||
|
||||
export const expandChatConversationsRequest = () => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
|
||||
})
|
||||
|
||||
export const expandChatConversationsSuccess = (chatConversations, next) => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
|
||||
chatConversations,
|
||||
next,
|
||||
})
|
||||
|
||||
export const expandChatConversationsFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const fetchChatConversationRequested = () => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
dispatch(fetchChatConversationRequestedRequest())
|
||||
|
||||
api(getState).get('/api/v1/chat_conversations/requested_conversations').then((response) => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts))
|
||||
const conversationsChatMessages = response.data.map((c) => c.last_chat_message)
|
||||
|
||||
dispatch(importFetchedAccounts(conversationsAccounts))
|
||||
// dispatch(importFetchedChatMessages(conversationsChatMessages))
|
||||
dispatch(fetchChatConversationRequestedSuccess(response.data, next ? next.uri : null))
|
||||
}).catch((error) => {
|
||||
console.log("error:", error)
|
||||
dispatch(fetchChatConversationRequestedFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchChatConversationRequestedRequest = () => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_FETCH_REQUEST,
|
||||
})
|
||||
|
||||
export const fetchChatConversationRequestedSuccess = (chatConversations, next) => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
||||
chatConversations,
|
||||
next,
|
||||
})
|
||||
|
||||
export const fetchChatConversationRequestedFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_FETCH_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const expandChatConversationRequested = () => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
const url = getState().getIn(['chat_conversations', 'requested', 'next'])
|
||||
const isLoading = getState().getIn(['chat_conversations', 'requested', 'isLoading'])
|
||||
|
||||
if (url === null || isLoading) return
|
||||
|
||||
dispatch(expandChatConversationRequestedRequest())
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
const conversationsAccounts = [].concat.apply([], response.data.map((c) => c.other_accounts))
|
||||
const conversationsChatMessages = response.data.map((c) => c.last_chat_message)
|
||||
|
||||
dispatch(importFetchedAccounts(conversationsAccounts))
|
||||
// dispatch(importFetchedChatMessages(conversationsChatMessages))
|
||||
dispatch(expandChatConversationRequestedSuccess(response.data, next ? next.uri : null))
|
||||
}).catch(error => dispatch(expandChatConversationRequestedFail(error)))
|
||||
}
|
||||
|
||||
export const expandChatConversationRequestedRequest = () => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_REQUEST,
|
||||
})
|
||||
|
||||
export const expandChatConversationRequestedSuccess = (chatConversations, next) => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
||||
chatConversations,
|
||||
next,
|
||||
})
|
||||
|
||||
export const expandChatConversationRequestedFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_REQUESTED_EXPAND_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const createChatConversation = (accountId) => (dispatch, getState) => {
|
||||
if (!me || !accountId) return
|
||||
|
||||
dispatch(createChatConversationRequest())
|
||||
|
||||
api(getState).post('/api/v1/chat_conversation', { account_id: accountId }).then((response) => {
|
||||
dispatch(createChatConversationSuccess(response.data))
|
||||
}).catch((error) => {
|
||||
dispatch(createChatConversationFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const createChatConversationRequest = () => ({
|
||||
type: CHAT_CONVERSATIONS_CREATE_REQUEST,
|
||||
})
|
||||
|
||||
export const createChatConversationSuccess = (chatConversation) => ({
|
||||
type: CHAT_CONVERSATIONS_CREATE_SUCCESS,
|
||||
chatConversation,
|
||||
})
|
||||
|
||||
export const createChatConversationFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_CREATE_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const deleteChatConversation = (chatConversationId) => (dispatch, getState) => {
|
||||
if (!me || !chatConversationId) return
|
||||
|
||||
dispatch(deleteChatConversationRequest(conversationId))
|
||||
|
||||
api(getState).delete(`/api/v1/chat_conversation/${chatConversationId}`).then((response) => {
|
||||
console.log("chat_conversations delete response: ", response)
|
||||
dispatch(deleteChatConversationSuccess())
|
||||
}).catch((error) => {
|
||||
dispatch(deleteChatConversationFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteChatConversationRequest = (conversationId) => ({
|
||||
type: CHAT_CONVERSATIONS_DELETE_REQUEST,
|
||||
conversationId,
|
||||
})
|
||||
|
||||
export const deleteChatConversationSuccess = () => ({
|
||||
type: CHAT_CONVERSATIONS_DELETE_SUCCESS,
|
||||
})
|
||||
|
||||
export const deleteChatConversationFail = (error) => ({
|
||||
type: CHAT_CONVERSATIONS_DELETE_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
export const CONVERSATIONS_EXPAND_REQUEST = 'CONVERSATIONS_EXPAND_REQUEST'
|
||||
export const CONVERSATIONS_EXPAND_SUCCESS = 'CONVERSATIONS_EXPAND_SUCCESS'
|
||||
export const CONVERSATIONS_EXPAND_FAIL = 'CONVERSATIONS_EXPAND_FAIL'
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -110,12 +345,12 @@ const unblockMessengerRequest = (accountId) => ({
|
|||
accountId,
|
||||
})
|
||||
|
||||
const blockMessengerSuccess = (data) => ({
|
||||
const unblockMessengerSuccess = (data) => ({
|
||||
type: UNBLOCK_MESSAGER_REQUEST,
|
||||
data,
|
||||
})
|
||||
|
||||
const blockMessengerFail = (accountId, error) => ({
|
||||
const unblockMessengerFail = (accountId, error) => ({
|
||||
type: UNBLOCK_MESSAGER_REQUEST,
|
||||
accountId,
|
||||
error,
|
||||
|
@ -137,20 +372,20 @@ export const fetchBlocks = () => (dispatch, getState) => {
|
|||
}).catch(error => dispatch(fetchBlocksFail(error)))
|
||||
}
|
||||
|
||||
export const fetchBlocksRequest = () => ({
|
||||
type: BLOCKS_FETCH_REQUEST,
|
||||
})
|
||||
// export const fetchBlocksRequest = () => ({
|
||||
// type: BLOCKS_FETCH_REQUEST,
|
||||
// })
|
||||
|
||||
export const fetchBlocksSuccess = (accounts, next) => ({
|
||||
type: BLOCKS_FETCH_SUCCESS,
|
||||
accounts,
|
||||
next,
|
||||
})
|
||||
// export const fetchBlocksSuccess = (accounts, next) => ({
|
||||
// type: BLOCKS_FETCH_SUCCESS,
|
||||
// accounts,
|
||||
// next,
|
||||
// })
|
||||
|
||||
export const fetchBlocksFail = (error) => ({
|
||||
type: BLOCKS_FETCH_FAIL,
|
||||
error,
|
||||
})
|
||||
// export const fetchBlocksFail = (error) => ({
|
||||
// type: BLOCKS_FETCH_FAIL,
|
||||
// error,
|
||||
// })
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -173,17 +408,51 @@ export const expandBlocks = () => (dispatch, getState) => {
|
|||
}).catch(error => dispatch(expandBlocksFail(error)))
|
||||
}
|
||||
|
||||
export const expandBlocksRequest = () => ({
|
||||
type: BLOCKS_EXPAND_REQUEST,
|
||||
// export const expandBlocksRequest = () => ({
|
||||
// type: BLOCKS_EXPAND_REQUEST,
|
||||
// })
|
||||
|
||||
// export const expandBlocksSuccess = (accounts, next) => ({
|
||||
// type: BLOCKS_EXPAND_SUCCESS,
|
||||
// accounts,
|
||||
// next,
|
||||
// })
|
||||
|
||||
// export const expandBlocksFail = (error) => ({
|
||||
// type: BLOCKS_EXPAND_FAIL,
|
||||
// error,
|
||||
// })
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const fetchChatConversationRequestedCount = () => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
api(getState).get('/api/v1/chat_conversations/requested_conversations/count').then(response => {
|
||||
dispatch({
|
||||
type: CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
|
||||
count: response.data,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const approveChatConversationRequest = (chatConversationId) => (dispatch, getState) => {
|
||||
if (!me|| !chatConversationId) return
|
||||
|
||||
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_approved`).then((response) => {
|
||||
dispatch(approveChatConversationRequestSuccess(response.data))
|
||||
}).catch((error) => dispatch(approveChatConversationRequestFail(error)))
|
||||
}
|
||||
|
||||
export const approveChatConversationRequestSuccess = (chatConversation) => ({
|
||||
type: CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
||||
chatConversation,
|
||||
})
|
||||
|
||||
export const expandBlocksSuccess = (accounts, next) => ({
|
||||
type: BLOCKS_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
next,
|
||||
})
|
||||
|
||||
export const expandBlocksFail = (error) => ({
|
||||
type: BLOCKS_EXPAND_FAIL,
|
||||
error,
|
||||
})
|
||||
export const approveChatConversationRequestFail = () => ({
|
||||
type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL,
|
||||
})
|
|
@ -1,86 +1,85 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable'
|
||||
import noop from 'lodash.noop'
|
||||
import api from '../api'
|
||||
import { me } from '../initial_state'
|
||||
import { importFetchedChatMessages } from './importer'
|
||||
|
||||
export const MESSAGE_SEND_REQUEST = 'MESSAGE_SEND_REQUEST'
|
||||
export const MESSAGE_SEND_SUCCESS = 'MESSAGE_SEND_SUCCESS'
|
||||
export const MESSAGE_SEND_FAIL = 'MESSAGE_SEND_FAIL'
|
||||
export const CHAT_MESSAGES_SEND_REQUEST = 'CHAT_MESSAGES_SEND_REQUEST'
|
||||
export const CHAT_MESSAGES_SEND_SUCCESS = 'CHAT_MESSAGES_SEND_SUCCESS'
|
||||
export const CHAT_MESSAGES_SEND_FAIL = 'CHAT_MESSAGES_SEND_FAIL'
|
||||
|
||||
export const MESSAGE_DELETE_REQUEST = 'MESSAGE_DELETE_REQUEST'
|
||||
export const MESSAGE_DELETE_SUCCESS = 'MESSAGE_DELETE_SUCCESS'
|
||||
export const MESSAGE_DELETE_FAIL = 'MESSAGE_DELETE_FAIL'
|
||||
export const CHAT_MESSAGES_DELETE_REQUEST = 'CHAT_MESSAGES_DELETE_REQUEST'
|
||||
export const CHAT_MESSAGES_DELETE_SUCCESS = 'CHAT_MESSAGES_DELETE_SUCCESS'
|
||||
export const CHAT_MESSAGES_DELETE_FAIL = 'CHAT_MESSAGES_DELETE_FAIL'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const sendMessage = (text, conversationId) => (dispatch, getState) => {
|
||||
if (!me) return
|
||||
export const sendChatMessage = (text = '', chatConversationId) => (dispatch, getState) => {
|
||||
if (!me || !chatConversationId) return
|
||||
if (text.length === 0) return
|
||||
|
||||
// : todo :
|
||||
// let text = getState().getIn(['chat_messages', 'text'], '')
|
||||
// let conversationId = getState().getIn(['chat_messags', 'conversation_id'], '')
|
||||
|
||||
dispatch(sendMessageRequest())
|
||||
|
||||
api(getState).put('/api/v1/messages/chat', {
|
||||
api(getState).post('/api/v1/chat_messages', {
|
||||
text,
|
||||
conversationId,
|
||||
chat_conversation_id: chatConversationId,
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
// headers: {
|
||||
// 'Idempotency-Key': getState().getIn(['chat_compose`', 'idempotencyKey']),
|
||||
// },
|
||||
}).then((response) => {
|
||||
sendMessageSuccess(response)
|
||||
dispatch(importFetchedChatMessages([response.data]))
|
||||
dispatch(sendMessageSuccess(response.data, chatConversationId))
|
||||
}).catch((error) => {
|
||||
dispatch(sendMessageFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
const sendMessageRequest = (text, conversationId) => ({
|
||||
type: MESSAGE_SEND_REQUEST,
|
||||
text,
|
||||
conversationId,
|
||||
const sendMessageRequest = () => ({
|
||||
type: CHAT_MESSAGES_SEND_REQUEST,
|
||||
})
|
||||
|
||||
const sendMessageSuccess = () => ({
|
||||
type: MESSAGE_SEND_SUCCESS,
|
||||
const sendMessageSuccess = (chatMessage, chatConversationId) => ({
|
||||
type: CHAT_MESSAGES_SEND_SUCCESS,
|
||||
chatMessage,
|
||||
chatConversationId,
|
||||
})
|
||||
|
||||
const sendMessageFail = (error) => ({
|
||||
type: MESSAGE_SEND_FAIL,
|
||||
type: CHAT_MESSAGES_SEND_FAIL,
|
||||
error,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const deleteMessage = (messageId) => (dispatch, getState) => {
|
||||
if (!me || !messageId) return
|
||||
const deleteMessage = (chatMessageId) => (dispatch, getState) => {
|
||||
if (!me || !chatMessageId) return
|
||||
|
||||
// : todo :
|
||||
|
||||
dispatch(sendMessageRequest())
|
||||
dispatch(deleteMessageRequest(chatMessageId))
|
||||
|
||||
api(getState).delete(`/api/v1/messages/chat/${messageId}`, {}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
api(getState).delete(`/api/v1/chat_messages/${chatMessageId}`, {}, {
|
||||
// headers: {
|
||||
// 'Idempotency-Key': getState().getIn(['chat_compose', 'idempotencyKey']),
|
||||
// },
|
||||
}).then((response) => {
|
||||
sendMessageSuccess(response)
|
||||
deleteMessageSuccess(response)
|
||||
}).catch((error) => {
|
||||
dispatch(sendMessageFail(error))
|
||||
dispatch(deleteMessageFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMessageRequest = (messageId) => ({
|
||||
type: MESSAGE_DELETE_REQUEST,
|
||||
messageId,
|
||||
const deleteMessageRequest = (chatMessageId) => ({
|
||||
type: CHAT_MESSAGES_DELETE_REQUEST,
|
||||
chatMessageId,
|
||||
})
|
||||
|
||||
const deleteMessageSuccess = () => ({
|
||||
type: MESSAGE_DELETE_SUCCESS,
|
||||
type: CHAT_MESSAGES_DELETE_SUCCESS,
|
||||
})
|
||||
|
||||
const deleteMessageFail = (error) => ({
|
||||
type: MESSAGE_DELETE_FAIL,
|
||||
type: CHAT_MESSAGES_DELETE_FAIL,
|
||||
error,
|
||||
})
|
|
@ -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 STATUSES_IMPORT = 'STATUSES_IMPORT'
|
||||
export const POLLS_IMPORT = 'POLLS_IMPORT'
|
||||
export const CHAT_MESSAGES_IMPORT = 'CHAT_MESSAGES_IMPORT'
|
||||
export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'
|
||||
|
||||
/**
|
||||
|
@ -48,6 +49,11 @@ export const importPolls = (polls) => ({
|
|||
polls,
|
||||
})
|
||||
|
||||
export const importChatMessages = (chatMessages) => ({
|
||||
type: CHAT_MESSAGES_IMPORT,
|
||||
chatMessages,
|
||||
})
|
||||
|
||||
export const importFetchedAccount = (account) => {
|
||||
return importFetchedAccounts([account]);
|
||||
}
|
||||
|
@ -97,7 +103,7 @@ export const importFetchedStatuses = (statuses) => (dispatch, getState) => {
|
|||
}
|
||||
}
|
||||
|
||||
statuses.forEach(processStatus)
|
||||
statuses.forEach(processStatus)
|
||||
|
||||
dispatch(importPolls(polls))
|
||||
dispatch(importFetchedAccounts(accounts))
|
||||
|
@ -113,3 +119,7 @@ export const importErrorWhileFetchingAccountByUsername = (username) => ({
|
|||
type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
||||
username
|
||||
})
|
||||
|
||||
export const importFetchedChatMessages = (chatMessages) => (dispatch, getState) => {
|
||||
dispatch(importChatMessages(chatMessages))
|
||||
}
|
|
@ -297,7 +297,7 @@ export const fetchListSuggestions = (q) => (dispatch, getState) => {
|
|||
/**
|
||||
*
|
||||
*/
|
||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||
const fetchListSuggestionsReady = (query, accounts) => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
|
|
|
@ -75,15 +75,18 @@ export const connectUserStream = () => connectTimelineStream('home', 'user')
|
|||
/**
|
||||
*
|
||||
*/
|
||||
export const connectMessageStream = () => {
|
||||
|
||||
return connectStream('chat_messages', null, (dispatch, getState) => {
|
||||
|
||||
export const connectChatMessagesStream = (accountId) => {
|
||||
return connectStream(`chat_messages:${accountId}`, null, (dispatch, getState) => {
|
||||
return {
|
||||
onConnect() {},
|
||||
onDisconnect() {},
|
||||
onConnect() {
|
||||
// console.log("chat messages connected")
|
||||
},
|
||||
onDisconnect() {
|
||||
// console.log("chat messages disconnected")
|
||||
},
|
||||
onReceive (data) {
|
||||
//
|
||||
// : todo :
|
||||
console.log("chat messages onReceive:", data)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -24,10 +24,13 @@ import Avatar from './avatar'
|
|||
import DisplayName from './display_name'
|
||||
import Button from './button'
|
||||
import Text from './text'
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
handleAction = (e) => {
|
||||
this.props.onActionClick(this.props.account, e)
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
handleUnrequest = () => {
|
||||
|
|
|
@ -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
|
|
@ -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 { defineMessages, injectIntl } from 'react-intl'
|
||||
import { me } from '../initial_state'
|
||||
import { CX } from '../constants'
|
||||
import { getWindowDimension } from '../utils/is_mobile'
|
||||
import {
|
||||
CX,
|
||||
MODAL_COMPOSE,
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
} from '../constants'
|
||||
import { openModal } from '../actions/modal'
|
||||
import Button from './button'
|
||||
|
||||
const initialState = getWindowDimension()
|
||||
|
||||
class FloatingActionButton extends React.PureComponent {
|
||||
|
||||
state = {
|
||||
width: initialState.width,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleResize()
|
||||
window.addEventListener('resize', this.handleResize, false)
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
const { width } = getWindowDimension()
|
||||
this.setState({ width })
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize, false)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
onOpenCompose,
|
||||
isDesktop,
|
||||
} = this.props
|
||||
const { intl, onOpenCompose } = this.props
|
||||
const { width } = this.state
|
||||
|
||||
if (!me) return null
|
||||
|
||||
const isDesktop = width > BREAKPOINT_EXTRA_SMALL
|
||||
const message = intl.formatMessage(messages.gab)
|
||||
|
||||
const containerClasses = CX({
|
||||
|
@ -56,13 +79,12 @@ const messages = defineMessages({
|
|||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenCompose: () => dispatch(openModal('COMPOSE')),
|
||||
onOpenCompose: () => dispatch(openModal(MODAL_COMPOSE)),
|
||||
})
|
||||
|
||||
FloatingActionButton.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
onOpenCompose: PropTypes.func.isRequired,
|
||||
isDesktop: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default injectIntl(connect(null, mapDispatchToProps)(FloatingActionButton))
|
|
@ -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 {
|
||||
MODAL_BLOCK_ACCOUNT,
|
||||
MODAL_BOOST,
|
||||
MODAL_CHAT_CONVERSATION_CREATE,
|
||||
MODAL_CHAT_CONVERSATION_DELETE,
|
||||
MODAL_COMMUNITY_TIMELINE_SETTINGS,
|
||||
MODAL_COMPOSE,
|
||||
MODAL_CONFIRM,
|
||||
|
@ -42,6 +44,8 @@ import {
|
|||
import {
|
||||
BlockAccountModal,
|
||||
BoostModal,
|
||||
ChatConversationCreateModal,
|
||||
ChatConversationDeleteModal,
|
||||
CommunityTimelineSettingsModal,
|
||||
ComposeModal,
|
||||
ConfirmationModal,
|
||||
|
@ -74,37 +78,40 @@ import {
|
|||
VideoModal,
|
||||
} from '../../features/ui/util/async_components'
|
||||
|
||||
const MODAL_COMPONENTS = {}
|
||||
MODAL_COMPONENTS[MODAL_BLOCK_ACCOUNT] = BlockAccountModal
|
||||
MODAL_COMPONENTS[MODAL_BOOST] = BoostModal
|
||||
MODAL_COMPONENTS[MODAL_COMMUNITY_TIMELINE_SETTINGS] = CommunityTimelineSettingsModal
|
||||
MODAL_COMPONENTS[MODAL_COMPOSE] = ComposeModal
|
||||
MODAL_COMPONENTS[MODAL_CONFIRM] = ConfirmationModal
|
||||
MODAL_COMPONENTS[MODAL_DISPLAY_OPTIONS] = DisplayOptionsModal
|
||||
MODAL_COMPONENTS[MODAL_EDIT_SHORTCUTS] = EditShortcutsModal
|
||||
MODAL_COMPONENTS[MODAL_EDIT_PROFILE] = EditProfileModal
|
||||
MODAL_COMPONENTS[MODAL_EMAIL_CONFIRMATION_REMINDER] = EmailConfirmationReminderModal
|
||||
MODAL_COMPONENTS[MODAL_GROUP_CREATE] = GroupCreateModal
|
||||
MODAL_COMPONENTS[MODAL_GROUP_DELETE] = GroupDeleteModal
|
||||
MODAL_COMPONENTS[MODAL_GROUP_PASSWORD] = GroupPasswordModal
|
||||
MODAL_COMPONENTS[MODAL_HASHTAG_TIMELINE_SETTINGS] = HashtagTimelineSettingsModal
|
||||
MODAL_COMPONENTS[MODAL_HOME_TIMELINE_SETTINGS] = HomeTimelineSettingsModal
|
||||
MODAL_COMPONENTS[MODAL_HOTKEYS] = HotkeysModal
|
||||
MODAL_COMPONENTS[MODAL_LIST_ADD_USER] = ListAddUserModal
|
||||
MODAL_COMPONENTS[MODAL_LIST_CREATE] = ListCreateModal
|
||||
MODAL_COMPONENTS[MODAL_LIST_DELETE] = ListDeleteModal
|
||||
MODAL_COMPONENTS[MODAL_LIST_EDITOR] = ListEditorModal
|
||||
MODAL_COMPONENTS[MODAL_LIST_TIMELINE_SETTINGS] = ListTimelineSettingsModal
|
||||
MODAL_COMPONENTS[MODAL_MEDIA] = MediaModal
|
||||
MODAL_COMPONENTS[MODAL_MUTE] = MuteModal
|
||||
MODAL_COMPONENTS[MODAL_PRO_UPGRADE] = ProUpgradeModal
|
||||
MODAL_COMPONENTS[MODAL_REPORT] = ReportModal
|
||||
MODAL_COMPONENTS[MODAL_STATUS_LIKES] = StatusLikesModal
|
||||
MODAL_COMPONENTS[MODAL_STATUS_REPOSTS] = StatusRepostsModal
|
||||
MODAL_COMPONENTS[MODAL_STATUS_REVISIONS] = StatusRevisionsModal
|
||||
MODAL_COMPONENTS[MODAL_UNAUTHORIZED] = UnauthorizedModal
|
||||
MODAL_COMPONENTS[MODAL_UNFOLLOW] = UnfollowModal
|
||||
MODAL_COMPONENTS[MODAL_VIDEO] = VideoModal
|
||||
const MODAL_COMPONENTS = {
|
||||
[MODAL_BLOCK_ACCOUNT]: BlockAccountModal,
|
||||
[MODAL_BOOST]: BoostModal,
|
||||
[MODAL_CHAT_CONVERSATION_CREATE]: ChatConversationCreateModal,
|
||||
[MODAL_CHAT_CONVERSATION_DELETE]: ChatConversationDeleteModal,
|
||||
[MODAL_COMMUNITY_TIMELINE_SETTINGS]: CommunityTimelineSettingsModal,
|
||||
[MODAL_COMPOSE]: ComposeModal,
|
||||
[MODAL_CONFIRM]: ConfirmationModal,
|
||||
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
|
||||
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
|
||||
[MODAL_EDIT_PROFILE]: EditProfileModal,
|
||||
[MODAL_EMAIL_CONFIRMATION_REMINDER]: EmailConfirmationReminderModal,
|
||||
[MODAL_GROUP_CREATE]: GroupCreateModal,
|
||||
[MODAL_GROUP_DELETE]: GroupDeleteModal,
|
||||
[MODAL_GROUP_PASSWORD]: GroupPasswordModal,
|
||||
[MODAL_HASHTAG_TIMELINE_SETTINGS]: HashtagTimelineSettingsModal,
|
||||
[MODAL_HOME_TIMELINE_SETTINGS]: HomeTimelineSettingsModal,
|
||||
[MODAL_HOTKEYS]: HotkeysModal,
|
||||
[MODAL_LIST_ADD_USER]: ListAddUserModal,
|
||||
[MODAL_LIST_CREATE]: ListCreateModal,
|
||||
[MODAL_LIST_DELETE]: ListDeleteModal,
|
||||
[MODAL_LIST_EDITOR]: ListEditorModal,
|
||||
[MODAL_LIST_TIMELINE_SETTINGS]: ListTimelineSettingsModal,
|
||||
[MODAL_MEDIA]: MediaModal,
|
||||
[MODAL_MUTE]: MuteModal,
|
||||
[MODAL_PRO_UPGRADE]: ProUpgradeModal,
|
||||
[MODAL_REPORT]: ReportModal,
|
||||
[MODAL_STATUS_LIKES]: StatusLikesModal,
|
||||
[MODAL_STATUS_REPOSTS]: StatusRepostsModal,
|
||||
[MODAL_STATUS_REVISIONS]: StatusRevisionsModal,
|
||||
[MODAL_UNAUTHORIZED]: UnauthorizedModal,
|
||||
[MODAL_UNFOLLOW]: UnfollowModal,
|
||||
[MODAL_VIDEO]: VideoModal,
|
||||
}
|
||||
|
||||
const CENTERED_XS_MODALS = [
|
||||
MODAL_BLOCK_ACCOUNT,
|
||||
|
|
|
@ -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,
|
||||
theme,
|
||||
viewBox,
|
||||
preserveAspectRatio,
|
||||
} = this.props
|
||||
|
||||
const isLight = ['light', 'white', ''].indexOf(theme) > -1
|
||||
|
@ -26,6 +27,7 @@ class PlaceholderLayout extends React.PureComponent {
|
|||
viewBox={viewBox}
|
||||
backgroundColor={backgroundColor}
|
||||
foregroundColor={foregroundColor}
|
||||
preserveAspectRatio={preserveAspectRatio}
|
||||
>
|
||||
{this.props.children}
|
||||
</ContentLoader>
|
||||
|
@ -47,6 +49,7 @@ PlaceholderLayout.propTypes = {
|
|||
intl: PropTypes.object.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
viewBox: PropTypes.string.isRequired,
|
||||
preserveAspectRatio: PropTypes.string,
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(PlaceholderLayout))
|
||||
|
|
|
@ -94,30 +94,32 @@ class ScrollableList extends React.PureComponent {
|
|||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.window) {
|
||||
const { scrollTop, scrollHeight } = this.documentElement;
|
||||
const { innerHeight } = this.window;
|
||||
const offset = scrollHeight - scrollTop - innerHeight;
|
||||
const { scrollTop, scrollHeight } = this.documentElement
|
||||
const { innerHeight } = this.window
|
||||
const offset = scrollHeight - scrollTop - innerHeight
|
||||
|
||||
if (600 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading && !this.props.disableInfiniteScroll) {
|
||||
this.props.onLoadMore();
|
||||
this.props.onLoadMore()
|
||||
}
|
||||
|
||||
if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||
this.props.onScrollToTop();
|
||||
this.props.onScrollToTop()
|
||||
} else if (scrollTop < 100 && this.props.onScrollToBottom) {
|
||||
this.props.onScrollToBottom()
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
this.props.onScroll()
|
||||
}
|
||||
|
||||
if (!this.lastScrollWasSynthetic) {
|
||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
||||
// intentional and cancel any pending scroll reset on mouse idle
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
this.scrollToTopOnMouseIdle = false
|
||||
}
|
||||
this.lastScrollWasSynthetic = false;
|
||||
this.lastScrollWasSynthetic = false
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
})
|
||||
|
||||
handleWheel = throttle(() => {
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
|
@ -175,6 +177,11 @@ class ScrollableList extends React.PureComponent {
|
|||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c
|
||||
if (this.props.scrollRef) this.props.scrollRef(c)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
|
@ -186,6 +193,8 @@ class ScrollableList extends React.PureComponent {
|
|||
onLoadMore,
|
||||
placeholderComponent: Placeholder,
|
||||
placeholderCount,
|
||||
onScrollToTop,
|
||||
onScrollToBottom,
|
||||
} = this.props
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
|
@ -210,8 +219,18 @@ class ScrollableList extends React.PureComponent {
|
|||
return <ColumnIndicator type='loading' />
|
||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||
return (
|
||||
<div onMouseMove={this.handleMouseMove}>
|
||||
<div onMouseMove={this.handleMouseMove} ref={this.setRef}>
|
||||
<div role='feed'>
|
||||
{
|
||||
(hasMore && onLoadMore && !isLoading) && !!onScrollToBottom &&
|
||||
<LoadMore onClick={this.handleLoadMore} />
|
||||
}
|
||||
|
||||
{
|
||||
isLoading && !!onScrollToBottom &&
|
||||
<ColumnIndicator type='loading' />
|
||||
}
|
||||
|
||||
{
|
||||
!!this.props.children &&
|
||||
React.Children.map(this.props.children, (child, index) => (
|
||||
|
@ -234,12 +253,12 @@ class ScrollableList extends React.PureComponent {
|
|||
}
|
||||
|
||||
{
|
||||
(hasMore && onLoadMore && !isLoading) &&
|
||||
(hasMore && onLoadMore && !isLoading) && !!onScrollToTop &&
|
||||
<LoadMore onClick={this.handleLoadMore} />
|
||||
}
|
||||
|
||||
{
|
||||
isLoading &&
|
||||
isLoading && !!onScrollToTop &&
|
||||
<ColumnIndicator type='loading' />
|
||||
}
|
||||
</div>
|
||||
|
@ -268,9 +287,10 @@ ScrollableList.propTypes = {
|
|||
]),
|
||||
children: PropTypes.node,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScrollToBottom: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
placeholderComponent: PropTypes.node,
|
||||
placeholderCount: PropTypes.node,
|
||||
placeholderCount: PropTypes.number,
|
||||
disableInfiniteScroll: PropTypes.bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,18 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
CX,
|
||||
BREAKPOINT_SMALL,
|
||||
} from '../../constants'
|
||||
import { CX } from '../../constants'
|
||||
import {
|
||||
me,
|
||||
emailConfirmed,
|
||||
} from '../../initial_state'
|
||||
import Button from '../button'
|
||||
import { openModal } from '../../actions/modal'
|
||||
import Responsive from '../../features/ui/util/responsive_component'
|
||||
import Heading from '../heading'
|
||||
import BackButton from '../back_button'
|
||||
import Pills from '../pills'
|
||||
|
||||
class SidebarLayout extends React.PureComponent {
|
||||
|
||||
handleOpenComposeModal = () => {
|
||||
this.props.onOpenComposeModal()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
|
@ -91,30 +82,6 @@ class SidebarLayout extends React.PureComponent {
|
|||
{children}
|
||||
</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>
|
||||
|
@ -124,18 +91,11 @@ class SidebarLayout extends React.PureComponent {
|
|||
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenComposeModal() {
|
||||
dispatch(openModal('COMPOSE'))
|
||||
},
|
||||
})
|
||||
|
||||
SidebarLayout.propTypes = {
|
||||
onOpenComposeModal: PropTypes.func.isRequired,
|
||||
actions: PropTypes.array,
|
||||
tabs: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
showBackBtn: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SidebarLayout)
|
||||
export default SidebarLayout
|
|
@ -290,7 +290,7 @@ class StatusList extends ImmutablePureComponent {
|
|||
hasMore={hasMore}
|
||||
/>
|
||||
<ScrollableList
|
||||
ref={this.setRef}
|
||||
scrollRef={this.setRef}
|
||||
isLoading={isLoading || isRefreshing}
|
||||
showLoading={isRefreshing || (isLoading && statusIds.size === 0)}
|
||||
onLoadMore={onLoadMore && this.handleLoadOlder}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { CX } from '../constants'
|
|||
|
||||
// Define colors for enumeration for Text component `color` prop
|
||||
const COLORS = {
|
||||
alt: 'alt',
|
||||
primary: 'primary',
|
||||
secondary: 'secondary',
|
||||
tertiary: 'tertiary',
|
||||
|
@ -76,6 +77,7 @@ class Text extends React.PureComponent {
|
|||
lineHeight15: isBadge,
|
||||
px5: isBadge,
|
||||
|
||||
cAlt: color === COLORS.alt,
|
||||
cPrimary: color === COLORS.primary,
|
||||
cSecondary: color === COLORS.secondary,
|
||||
cTertiary: color === COLORS.tertiary,
|
||||
|
|
|
@ -35,19 +35,31 @@ class Toast extends React.PureComponent {
|
|||
message,
|
||||
date,
|
||||
to,
|
||||
type,
|
||||
} = this.props
|
||||
|
||||
const contentClasses = CX({
|
||||
default: 1,
|
||||
const containerClasses = CX({
|
||||
d: 1,
|
||||
radiusSmall: 1,
|
||||
w228PX: 1,
|
||||
mt5: 1,
|
||||
pt2: 1,
|
||||
maxWidth240PX: 1,
|
||||
mb5: 1,
|
||||
px15: 1,
|
||||
pt10: 1,
|
||||
pb15: !!title,
|
||||
pb10: !title,
|
||||
bgToast: 1,
|
||||
boxShadowToast: 1,
|
||||
})
|
||||
|
||||
const contentClasses = CX({
|
||||
d: 1,
|
||||
mt5: !!title,
|
||||
pt2: !!title,
|
||||
flexRow: !!image,
|
||||
})
|
||||
|
||||
const innerContentClasses = CX({
|
||||
default: 1,
|
||||
d: 1,
|
||||
flexNormal: 1,
|
||||
pl10: !!image,
|
||||
pt2: !!image,
|
||||
|
@ -65,19 +77,11 @@ class Toast extends React.PureComponent {
|
|||
})
|
||||
|
||||
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(' ')}>
|
||||
<Text size='media' weight='medium' className={[_s.mr15, _s.minWidth160PX].join(' ')}>
|
||||
<Text size='medium' color='alt' weight='bold'>
|
||||
{title}
|
||||
</Text>
|
||||
<Button
|
||||
backgroundColor='secondary'
|
||||
color='primary'
|
||||
icon='close'
|
||||
iconSize='6px'
|
||||
onClick={this.handleOnDismiss}
|
||||
className={[_s.mlAuto, _s.px10].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
<div className={contentClasses}>
|
||||
{
|
||||
|
@ -90,12 +94,16 @@ class Toast extends React.PureComponent {
|
|||
/>
|
||||
}
|
||||
<div className={innerContentClasses}>
|
||||
<Text size='small'>
|
||||
<Text size='small' color='alt'>
|
||||
{message}
|
||||
</Text>
|
||||
{
|
||||
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} />
|
||||
</Text>
|
||||
}
|
||||
|
@ -116,10 +124,6 @@ Toast.propTypes = {
|
|||
onDismiss: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
to: PropTypes.string,
|
||||
type: PropTypes.oneOf([
|
||||
TOAST_TYPE_ERROR,
|
||||
TOAST_TYPE_SUCCESS,
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
export default Toast
|
|
@ -43,6 +43,8 @@ export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
|
|||
|
||||
export const MODAL_BLOCK_ACCOUNT = 'BLOCK_ACCOUNT'
|
||||
export const MODAL_BOOST = 'BOOST'
|
||||
export const MODAL_CHAT_CONVERSATION_CREATE = 'CHAT_CONVERSATION_CREATE'
|
||||
export const MODAL_CHAT_CONVERSATION_DELETE = 'CHAT_CONVERSATION_DELETE'
|
||||
export const MODAL_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS'
|
||||
export const MODAL_COMPOSE = 'COMPOSE'
|
||||
export const MODAL_CONFIRM = 'CONFIRM'
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants'
|
|||
import {
|
||||
connectUserStream,
|
||||
connectStatusUpdateStream,
|
||||
connectChatMessagesStream,
|
||||
} from '../actions/streaming'
|
||||
import { getLocale } from '../locales'
|
||||
import initialState from '../initial_state'
|
||||
|
@ -95,6 +96,7 @@ export default class GabSocial extends React.PureComponent {
|
|||
if (!!me) {
|
||||
this.disconnect = store.dispatch(connectUserStream())
|
||||
store.dispatch(connectStatusUpdateStream())
|
||||
store.dispatch(connectChatMessagesStream(me))
|
||||
}
|
||||
|
||||
console.log('%cGab Social ', [
|
||||
|
|
|
@ -13,11 +13,58 @@ class ToastsContainer extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { notifications } = this.props
|
||||
// const { notifications } = this.props
|
||||
const notifications = [
|
||||
{
|
||||
key: '1',
|
||||
title: 'Error',
|
||||
to: 'to',
|
||||
image: 'https://gab.com/media/user/58077e8a49705.jpg',
|
||||
message: 'Unable to follow @andrew',
|
||||
date: new Date(),
|
||||
isImageAccount: true,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
title: 'Success',
|
||||
to: 'to',
|
||||
image: 'https://gab.com/media/user/58077e8a49705.jpg',
|
||||
message: 'Your gab was posted. Click here to view',
|
||||
date: new Date(),
|
||||
isImageAccount: false,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
title: '',
|
||||
to: 'to',
|
||||
image: 'https://gab.com/media/user/58077e8a49705.jpg',
|
||||
message: 'Unable to follow @andrew',
|
||||
date: new Date(),
|
||||
isImageAccount: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: '',
|
||||
to: 'to',
|
||||
image: 'https://gab.com/media/user/58077e8a49705.jpg',
|
||||
message: 'Your gab was posted. Click here to view',
|
||||
date: new Date(),
|
||||
isImageAccount: false,
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
title: '',
|
||||
to: 'to',
|
||||
message: 'Your gab was deleted',
|
||||
date: new Date(),
|
||||
isImageAccount: false,
|
||||
},
|
||||
]
|
||||
|
||||
const hasNotifications = Array.isArray(notifications) && notifications.length > 0
|
||||
|
||||
const containerClasses = CX({
|
||||
default: 1,
|
||||
d: 1,
|
||||
z5: 1,
|
||||
posFixed: 1,
|
||||
bottom0: 1,
|
||||
|
@ -28,17 +75,17 @@ class ToastsContainer extends React.PureComponent {
|
|||
pb10: 1,
|
||||
displayNone: !hasNotifications
|
||||
})
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{
|
||||
hasNotifications && notifications.map((notification) => (
|
||||
!hasNotifications && notifications.map((notification) => (
|
||||
<Toast
|
||||
onDismiss={this.handleOnDismiss}
|
||||
key={notification.key}
|
||||
id={notification.key}
|
||||
title={notification.title}
|
||||
type={notification.type}
|
||||
to={notification.to}
|
||||
image={notification.image}
|
||||
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
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
maxLength={160}
|
||||
value={title}
|
||||
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 PropTypes from 'prop-types'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import Input from '../../../components/input'
|
||||
|
||||
class MessagesSearch extends ImmutablePureComponent {
|
||||
class ChatConversationsSearch extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -17,7 +14,7 @@ class MessagesSearch extends ImmutablePureComponent {
|
|||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
children
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -34,12 +31,8 @@ class MessagesSearch extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" },
|
||||
})
|
||||
|
||||
MessagesSearch.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
ChatConversationsSearch.propTypes = {
|
||||
//
|
||||
}
|
||||
|
||||
export default injectIntl(MessagesSearch)
|
||||
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 Button from '../../../components/button'
|
||||
|
||||
class MessagesHeader extends ImmutablePureComponent {
|
||||
class ChatSettingsHeader extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -16,27 +16,19 @@ class MessagesHeader extends ImmutablePureComponent {
|
|||
return (
|
||||
<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(' ')}>
|
||||
<Button
|
||||
noClasses
|
||||
className={[_s.d, _s.noUnderline, _s.jcCenter, _s.mr5, _s.aiCenter, _s.bgTransparent, _s.cursorPointer, _s.outlineNone].join(' ')}
|
||||
to='/messages'
|
||||
color='primary'
|
||||
backgroundColor='none'
|
||||
icon='angle-left'
|
||||
iconSize='16px'
|
||||
iconClassName={[_s.mr5, _s.cPrimary].join(' ')}
|
||||
/>
|
||||
<Heading size='h1'>
|
||||
Messages
|
||||
Chat Settings
|
||||
</Heading>
|
||||
<div className={[_s.d, _s.bgTransparent, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.mlAuto].join(' ')}>
|
||||
<Button
|
||||
isNarrow
|
||||
onClick={undefined}
|
||||
className={[_s.ml5, _s.px15].join(' ')}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
<Button
|
||||
isNarrow
|
||||
onClick={undefined}
|
||||
color='brand'
|
||||
backgroundColor='none'
|
||||
className={_s.ml5}
|
||||
icon='cog'
|
||||
iconSize='18px'
|
||||
/>
|
||||
</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 PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { makeGetAccount } from '../../selectors'
|
||||
import Text from '../../components/text'
|
||||
import Button from '../../components/button'
|
||||
import Avatar from '../../components/avatar'
|
||||
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'
|
||||
|
||||
import ChatEmptyMessageBlock from './components/chat_conversations_empty_block'
|
||||
import ChatMessageHeader from './components/chat_message_header'
|
||||
import ChatMessageScrollingList from './components/chat_message_scrolling_list'
|
||||
import ChatMessagesComposeForm from './components/chat_message_compose_form'
|
||||
|
||||
class Messages extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { account } = this.props
|
||||
const {
|
||||
account,
|
||||
selectedChatConversationId,
|
||||
chatConverationIsRequest,
|
||||
} = this.props
|
||||
|
||||
const selectedMessage = true
|
||||
|
||||
return (
|
||||
<div className={[_s.d, _s.bgPrimary, _s.h100PC, _s.w100PC].join(' ')}>
|
||||
{
|
||||
!selectedMessage &&
|
||||
<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}>
|
||||
<Text color='inherit' weight='bold' className={_s.px15}>
|
||||
New Message
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
!selectedChatConversationId &&
|
||||
<ChatEmptyMessageBlock />
|
||||
}
|
||||
{
|
||||
selectedMessage &&
|
||||
!!selectedChatConversationId &&
|
||||
<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(' ')}>
|
||||
<Avatar account={account} size={34} />
|
||||
<div className={[_s.d, _s.pl10, _s.maxW100PC86PX, _s.overflowHidden].join(' ')}>
|
||||
<DisplayName account={account} isMultiline />
|
||||
</div>
|
||||
<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>
|
||||
<ChatMessageHeader chatConversationId={selectedChatConversationId} />
|
||||
<ChatMessageScrollingList chatConversationId={selectedChatConversationId} />
|
||||
{
|
||||
!chatConverationIsRequest &&
|
||||
<ChatMessagesComposeForm chatConversationId={selectedChatConversationId} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -91,13 +38,18 @@ class Messages extends React.PureComponent {
|
|||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: makeGetAccount()(state, '1'),
|
||||
})
|
||||
const mapStateToProps = (state, props) => {
|
||||
const selectedChatConversationId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||
const chatConverationIsRequest = selectedChatConversationId ? !state.getIn(['chat_conversations', selectedChatConversationId, 'is_approved'], null) : false
|
||||
return {
|
||||
selectedChatConversationId,
|
||||
chatConverationIsRequest,
|
||||
}
|
||||
}
|
||||
|
||||
Messages.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
selectedChatConversationId: PropTypes.string,
|
||||
chatConverationIsRequest: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Messages)
|
|
@ -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 ScrollableList from '../components/scrollable_list'
|
||||
|
||||
class Mutes extends ImmutablePureComponent {
|
||||
class MutedAccounts extends ImmutablePureComponent {
|
||||
|
||||
componentWillMount() {
|
||||
this.props.onFetchMutes()
|
||||
|
@ -66,7 +66,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
onExpandMutes: () => dispatch(expandMutes()),
|
||||
})
|
||||
|
||||
Mutes.propTypes = {
|
||||
MutedAccounts.propTypes = {
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -74,4 +74,4 @@ Mutes.propTypes = {
|
|||
onFetchMutes: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Mutes))
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MutedAccounts))
|
|
@ -56,6 +56,10 @@ import {
|
|||
Assets,
|
||||
BlockedAccounts,
|
||||
BookmarkedStatuses,
|
||||
ChatConversationCreate,
|
||||
ChatConversationRequests,
|
||||
ChatConversationBlockedAccounts,
|
||||
ChatConversationMutedAccounts,
|
||||
CommunityTimeline,
|
||||
Compose,
|
||||
DMCA,
|
||||
|
@ -86,7 +90,8 @@ import {
|
|||
ListEdit,
|
||||
ListTimeline,
|
||||
Messages,
|
||||
Mutes,
|
||||
MessagesSettings,
|
||||
MutedAccounts,
|
||||
News,
|
||||
NewsView,
|
||||
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/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/:conversationId' exact page={MessagesPage} component={Messages} content={children} />
|
||||
<WrappedRoute path='/messages' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} />
|
||||
<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/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='/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} />
|
||||
|
||||
<Redirect from='/@:username/comments' to='/:username/comments' />
|
||||
<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} />
|
||||
|
||||
<Redirect from='/@:username/following' to='/:username/following' />
|
||||
<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/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} />
|
||||
|
||||
<Redirect from='/@:username/bookmarks' to='/:username/bookmarks' />
|
||||
<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' }} />
|
||||
|
||||
<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' }} />
|
||||
|
||||
<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 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 BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
|
||||
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 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') }
|
||||
|
@ -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 MediaModal() { return import(/* webpackChunkName: "components/media_modal" */'../../../components/modal/media_modal') }
|
||||
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 NavSettingsPopover() { return import(/* webpackChunkName: "modals/nav_settings_popover" */'../../../components/popover/nav_settings_popover') }
|
||||
export function News() { return import(/* webpackChunkName: "features/news" */'../../news') }
|
||||
|
|
|
@ -142,9 +142,7 @@ class Layout extends React.PureComponent {
|
|||
|
||||
{
|
||||
!noComposeButton &&
|
||||
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
|
||||
<FloatingActionButton />
|
||||
</Responsive>
|
||||
<FloatingActionButton />
|
||||
}
|
||||
|
||||
</ResponsiveClassesComponent>
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { connect } from 'react-redux'
|
||||
import { me } from '../initial_state'
|
||||
import { openModal } from '../actions/modal'
|
||||
import {
|
||||
CX,
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
MODAL_CHAT_CONVERSATION_CREATE,
|
||||
} from '../constants'
|
||||
import Layout from './layout'
|
||||
import Responsive from '../features/ui/util/responsive_component'
|
||||
import List from '../components/list'
|
||||
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
||||
import MessagesSearch from '../features/messages/components/messages_search'
|
||||
import MessagesList from '../features/messages/components/messages_list'
|
||||
import MessagesHeader from '../features/messages/components/messages_header'
|
||||
import ChatConversationsSearch from '../features/messages/components/chat_conversations_search'
|
||||
import ChatConversationsList from '../features/messages/components/chat_conversations_list'
|
||||
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() {
|
||||
const {
|
||||
children,
|
||||
showBackBtn,
|
||||
title,
|
||||
children,
|
||||
isSettings,
|
||||
showBackBtn,
|
||||
source,
|
||||
currentConversationIsRequest,
|
||||
} = this.props
|
||||
|
||||
const mainBlockClasses = CX({
|
||||
|
@ -31,22 +41,25 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||
jcEnd: 1,
|
||||
})
|
||||
|
||||
console.log("currentConversationIsRequest:",currentConversationIsRequest)
|
||||
|
||||
return (
|
||||
<Layout
|
||||
showBackBtn
|
||||
showGlobalFooter
|
||||
noRightSidebar
|
||||
noComposeButton
|
||||
showGlobalFooter
|
||||
showLinkFooterInSidebar
|
||||
page='messages'
|
||||
title='Chats'
|
||||
actions={[
|
||||
{
|
||||
icon: 'cog',
|
||||
onClick: this.onOpenCommunityPageSettingsModal,
|
||||
to: '/messages/settings',
|
||||
},
|
||||
{
|
||||
icon: 'pencil',
|
||||
onClick: this.onOpenCommunityPageSettingsModal,
|
||||
onClick: () => this.onClickAdd(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
@ -63,15 +76,50 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||
|
||||
<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].join(' ')}>
|
||||
<div className={[_s.d, _s.h100PC, _s.overflowHidden, _s.w100PC, _s.boxShadowNone].join(' ')}>
|
||||
|
||||
{ /* <MessagesHeader /> */ }
|
||||
<MessagesSearch />
|
||||
<MessagesList />
|
||||
{
|
||||
(isSettings || currentConversationIsRequest) &&
|
||||
<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>
|
||||
</Responsive>
|
||||
|
||||
|
||||
<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(' ')}>
|
||||
|
@ -87,10 +135,26 @@ class MessagesLayout extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
MessagesLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
showBackBtn: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
const mapStateToProps = (state) => {
|
||||
const selectedId = state.getIn(['chats', 'selectedChatConversationId'], null)
|
||||
const currentConversationIsRequest = selectedId ? !state.getIn(['chat_conversations', selectedId, 'is_approved'], true) : false
|
||||
|
||||
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>
|
||||
|
||||
<FloatingActionButton isDesktop />
|
||||
<FloatingActionButton />
|
||||
|
||||
</main>
|
||||
</Responsive>
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
import React from 'react'
|
||||
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 MessagesLayout from '../layouts/messages_layout'
|
||||
|
||||
class MessagesPage extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const { children, intl } = this.props
|
||||
|
||||
const title = intl.formatMessage(messages.chats)
|
||||
const {
|
||||
children,
|
||||
isSettings,
|
||||
source,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<MessagesLayout
|
||||
showBackBtn
|
||||
title={title}
|
||||
isSettings={isSettings}
|
||||
title='Chats'
|
||||
source={source}
|
||||
>
|
||||
<PageTitle path={title} />
|
||||
|
||||
<PageTitle path='Chats' />
|
||||
{children}
|
||||
</MessagesLayout>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
chats: { id: 'chats', defaultMessage: 'Chats' },
|
||||
})
|
||||
|
||||
MessagesPage.propTypes = {
|
||||
intl: PropTypes.object.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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
REPOST_REQUEST,
|
||||
UNREPOST_REQUEST,
|
||||
REPOST_FAIL,
|
||||
FAVORITE_REQUEST,
|
||||
FAVORITE_FAIL,
|
||||
UNFAVORITE_REQUEST,
|
||||
} from '../actions/interactions';
|
||||
CHAT_MESSAGES_SEND_SUCCESS,
|
||||
CHAT_MESSAGES_DELETE_REQUEST,
|
||||
} from '../actions/chat_messages'
|
||||
import {
|
||||
STATUS_REVEAL,
|
||||
STATUS_HIDE,
|
||||
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';
|
||||
CHAT_MESSAGES_IMPORT,
|
||||
} from '../actions/importer'
|
||||
|
||||
const importStatus = (state, status) => state.set(status.id, fromJS(status));
|
||||
const importChatMessage = (state, chatMessage) => state.set(chatMessage.id, fromJS(chatMessage))
|
||||
|
||||
const importStatuses = (state, statuses) =>
|
||||
state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status)));
|
||||
const importChatMessages = (state, chatMessages) =>
|
||||
state.withMutations((mutable) => chatMessages.forEach((chatMessage) => importChatMessage(mutable, chatMessage)))
|
||||
|
||||
const deleteStatus = (state, id, references) => {
|
||||
references.forEach(ref => {
|
||||
state = deleteStatus(state, ref[0], []);
|
||||
});
|
||||
const deleteChatMessage = (state, id) => {
|
||||
return state.delete(id)
|
||||
}
|
||||
|
||||
return state.delete(id);
|
||||
};
|
||||
const initialState = ImmutableMap()
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function statuses(state = initialState, action) {
|
||||
export default function chat_messages(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STATUS_IMPORT:
|
||||
return importStatus(state, action.status);
|
||||
case STATUSES_IMPORT:
|
||||
return importStatuses(state, action.statuses);
|
||||
case FAVORITE_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
||||
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)
|
||||
})
|
||||
case CHAT_MESSAGES_IMPORT:
|
||||
return importChatMessages(state, action.chatMessages)
|
||||
case CHAT_MESSAGES_SEND_SUCCESS:
|
||||
return importChatMessage(state, action.chatMessage)
|
||||
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||
return deleteChatMessage(state, action.chatMessageId)
|
||||
default:
|
||||
return state;
|
||||
return state
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 accounts from './accounts'
|
||||
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_messages from './chat_messages'
|
||||
import compose from './compose'
|
||||
|
@ -49,6 +51,9 @@ import user_lists from './user_lists'
|
|||
const reducers = {
|
||||
accounts,
|
||||
accounts_counters,
|
||||
chats,
|
||||
chat_conversation_lists,
|
||||
chat_conversation_messages,
|
||||
chat_conversations,
|
||||
chat_messages,
|
||||
compose,
|
||||
|
@ -88,10 +93,10 @@ const reducers = {
|
|||
status_revisions,
|
||||
suggestions,
|
||||
timelines,
|
||||
timeline_injections,
|
||||
toasts,
|
||||
user,
|
||||
user_lists,
|
||||
// timeline_injections,
|
||||
// toasts,
|
||||
// user,
|
||||
// user_lists,
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (firstIndex < 0) {
|
||||
console.log("----2")
|
||||
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:
|
||||
return state
|
||||
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)
|
||||
case RESEND_USER_CONFIRMATION_EMAIL_SUCCESS:
|
||||
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 => {
|
||||
switch (columnType) {
|
||||
case 'home':
|
||||
|
|
|
@ -43,6 +43,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
|||
},
|
||||
|
||||
received(data) {
|
||||
console.log("received:", data)
|
||||
onReceive(data);
|
||||
},
|
||||
|
||||
|
|
|
@ -2,11 +2,15 @@ import React from 'react'
|
|||
import { FormattedNumber } from 'react-intl'
|
||||
|
||||
export const shortNumberFormat = (number) => {
|
||||
if (isNaN(number)) {
|
||||
return <FormattedNumber value={0} />
|
||||
}
|
||||
|
||||
if (number < 1000) {
|
||||
try {
|
||||
return (<FormattedNumber value={number} />).props.value
|
||||
} catch (error) {
|
||||
return <FormattedNumber value={number} />
|
||||
return <FormattedNumber value={0} />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,10 +35,12 @@
|
|||
--solid_color_tertiary: #f5f5f7;
|
||||
--solid_color_block: #f5f8fa;
|
||||
--solid_color_highlight: rgba(224, 234, 66, .125);
|
||||
--solid_color_toast: #232425;
|
||||
|
||||
--text_color_primary: #2d3436;
|
||||
--text_color_secondary: #4b4f55;
|
||||
--text_color_tertiary: #777;
|
||||
--text_color_alt: var(--solid_color_secondary);
|
||||
|
||||
--border_color_secondary: #ececed;
|
||||
|
||||
|
@ -91,10 +93,12 @@
|
|||
--solid_color_secondary-dark: #424343 !important;
|
||||
--solid_color_tertiary: #333 !important;
|
||||
--solid_color_block: #2d2d2d !important;
|
||||
|
||||
--solid_color_toast: #fcfcfc !important;
|
||||
|
||||
--text_color_primary: #fff !important;
|
||||
--text_color_secondary: #999 !important;
|
||||
--text_color_tertiary: #656565 !important;
|
||||
--text_color_alt: var(----solid_color_primary);
|
||||
|
||||
--border_color_secondary: #424141 !important;
|
||||
|
||||
|
@ -116,14 +120,16 @@
|
|||
--solid_color_secondary-dark: #282828 !important;
|
||||
--solid_color_tertiary: #000 !important;
|
||||
--solid_color_block: #202327 !important;
|
||||
--solid_color_toast: #fcfcfc !important;
|
||||
|
||||
--text_color_primary: #cccbcb !important;
|
||||
--text_color_secondary: #888 !important;
|
||||
--text_color_tertiary: #656565 !important;
|
||||
--text_color_alt: var(----solid_color_primary);
|
||||
|
||||
--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_background: #000 !important;
|
||||
|
@ -423,6 +429,8 @@ pre {
|
|||
|
||||
.bgTertiary { background-color: var(--solid_color_tertiary); }
|
||||
|
||||
.bgToast { background-color: var(--solid_color_toast); }
|
||||
|
||||
.bgPrimary { background-color: var(--solid_color_primary); }
|
||||
.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); }
|
||||
|
||||
.cBlack { color: var(--color_black); }
|
||||
|
@ -582,6 +591,7 @@ pre {
|
|||
.w330PX { width: 330px; }
|
||||
.w300PX { width: 300px; }
|
||||
.w240PX { width: 240px; }
|
||||
.w228PX { width: 228px; }
|
||||
.w208PX { width: 208px; }
|
||||
.w115PX { width: 115px; }
|
||||
.w84PX { width: 84px; }
|
||||
|
@ -892,6 +902,7 @@ pre {
|
|||
.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); }
|
||||
.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 {
|
||||
box-shadow: none !important;
|
||||
|
|
|
@ -21,7 +21,7 @@ class EntityCache
|
|||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
||||
|
|
|
@ -297,6 +297,11 @@ class Account < ApplicationRecord
|
|||
username
|
||||
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
|
||||
Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
def call(query, account = nil, options = {})
|
||||
puts "query:"+query.inspect
|
||||
@query = query.strip
|
||||
@limit = options[:limit].to_i
|
||||
@offset = options[:offset].to_i
|
||||
|
|
|
@ -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
|
||||
MAX_OPTIONS = 4
|
||||
MAX_OPTION_CHARS = 25
|
||||
MAX_OPTION_CHARS = 160
|
||||
MAX_EXPIRATION = 1.month.freeze
|
||||
MIN_EXPIRATION = 5.minutes.freeze
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ Doorkeeper.configure do
|
|||
:'write:notifications',
|
||||
:'write:reports',
|
||||
:'write:statuses',
|
||||
:'write:chats',
|
||||
:read,
|
||||
:'read:accounts',
|
||||
:'read:blocks',
|
||||
|
@ -81,6 +82,7 @@ Doorkeeper.configure do
|
|||
:'read:notifications',
|
||||
:'read:search',
|
||||
:'read:statuses',
|
||||
:'read:chats',
|
||||
:follow,
|
||||
:push
|
||||
|
||||
|
|
|
@ -221,15 +221,33 @@ Rails.application.routes.draw do
|
|||
resource :explore, only: :show, controller: :explore
|
||||
end
|
||||
|
||||
namespace :messages do
|
||||
resource :conversations, only: [:show, :create]
|
||||
resource :chats, only: [:show, :create]
|
||||
|
||||
resources :chat_conversation_accounts, only: :show do
|
||||
resources :blocked_accounts, only: :index
|
||||
resources :muted_accounts, only: :index
|
||||
|
||||
member do
|
||||
post :block
|
||||
post :unblock
|
||||
post :mute
|
||||
post :unmute
|
||||
post :block_messenger
|
||||
post :unblock_messenger
|
||||
post :mute_messenger
|
||||
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
|
||||
|
||||
|
@ -241,6 +259,7 @@ Rails.application.routes.draw do
|
|||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||
resources :preferences, only: [:index]
|
||||
resources :group_categories, only: [:index]
|
||||
resources :chat_messages, only: [:create, :destroy]
|
||||
|
||||
get '/search', to: 'search#index', as: :search
|
||||
get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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