Progress
This commit is contained in:
parent
de0c977950
commit
75d52c841e
18
app/controllers/admin/chat_conversations_controller.rb
Normal file
18
app/controllers/admin/chat_conversations_controller.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ChatConversationsController < BaseController
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
PER_PAGE = 20
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :account, :index?
|
||||||
|
@chatConversationAccounts = ChatConversationAccount.where(account: @account).page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
app/controllers/admin/chat_messages_controller.rb
Normal file
18
app/controllers/admin/chat_messages_controller.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ChatMessagesController < BaseController
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
PER_PAGE = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :account, :index?
|
||||||
|
@followers = ChatMessage.where(from_account: @account).page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,6 +5,9 @@ module Admin
|
|||||||
class DashboardController < BaseController
|
class DashboardController < BaseController
|
||||||
def index
|
def index
|
||||||
@users_count = User.count
|
@users_count = User.count
|
||||||
|
@statuses_count = Status.count
|
||||||
|
@pro_accounts_count = Account.where(is_pro: true).count
|
||||||
|
@donor_accounts_count = Account.where(is_donor: true).count
|
||||||
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
||||||
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
||||||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
||||||
|
@ -1,67 +1,75 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class GroupsController < BaseController
|
class GroupsController < BaseController
|
||||||
before_action :set_group, except: [:index]
|
before_action :set_group, except: [:index]
|
||||||
before_action :set_filter_params
|
before_action :set_filter_params
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :group, :index?
|
authorize :group, :index?
|
||||||
@groups = filtered_groups.page(params[:page])
|
@groups = filtered_groups.page(params[:page])
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
authorize @group, :destroy?
|
|
||||||
@group.destroy!
|
|
||||||
log_action :destroy, @group
|
|
||||||
flash[:notice] = I18n.t('admin.groups.destroyed_msg')
|
|
||||||
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def enable_featured
|
|
||||||
authorize @group, :update?
|
|
||||||
@group.is_featured = true
|
|
||||||
@group.save!
|
|
||||||
log_action :update, @group
|
|
||||||
flash[:notice] = I18n.t('admin.groups.updated_msg')
|
|
||||||
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def disable_featured
|
|
||||||
authorize @group, :update?
|
|
||||||
@group.is_featured = false
|
|
||||||
@group.save!
|
|
||||||
log_action :update, @group
|
|
||||||
flash[:notice] = I18n.t('admin.groups.updated_msg')
|
|
||||||
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_group
|
|
||||||
@group = Group.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_filter_params
|
|
||||||
@filter_params = filter_params.to_hash.symbolize_keys
|
|
||||||
end
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:group).permit(:is_featured, :is_nsfw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_groups
|
|
||||||
query = Group.order('is_featured DESC, member_count DESC')
|
|
||||||
|
|
||||||
if params[:title]
|
|
||||||
query = query.where("LOWER(title) LIKE LOWER(?)", "%#{params[:title]}%")
|
|
||||||
end
|
|
||||||
|
|
||||||
return query
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_params
|
|
||||||
params.permit(:sort,)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize :group, :index?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
#
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @group, :destroy?
|
||||||
|
@group.destroy!
|
||||||
|
log_action :destroy, @group
|
||||||
|
flash[:notice] = I18n.t('admin.groups.destroyed_msg')
|
||||||
|
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_featured
|
||||||
|
authorize @group, :update?
|
||||||
|
@group.is_featured = true
|
||||||
|
@group.save!
|
||||||
|
log_action :update, @group
|
||||||
|
flash[:notice] = I18n.t('admin.groups.updated_msg')
|
||||||
|
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_featured
|
||||||
|
authorize @group, :update?
|
||||||
|
@group.is_featured = false
|
||||||
|
@group.save!
|
||||||
|
log_action :update, @group
|
||||||
|
flash[:notice] = I18n.t('admin.groups.updated_msg')
|
||||||
|
redirect_to admin_groups_path(page: params[:page], **@filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_group
|
||||||
|
@group = Group.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_filter_params
|
||||||
|
@filter_params = filter_params.to_hash.symbolize_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:group).permit(:is_featured, :is_nsfw)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_groups
|
||||||
|
query = Group.order('is_featured DESC, member_count DESC')
|
||||||
|
|
||||||
|
if params[:title]
|
||||||
|
query = query.where("LOWER(title) LIKE LOWER(?)", "%#{params[:title]}%")
|
||||||
|
end
|
||||||
|
return query
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.permit(:sort,)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
18
app/controllers/admin/joined_groups_controller.rb
Normal file
18
app/controllers/admin/joined_groups_controller.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class JoinedGroupsController < BaseController
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
PER_PAGE = 25
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :account, :index?
|
||||||
|
@groups = @account.groups.page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -12,6 +12,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@account = current_account
|
@account = current_account
|
||||||
|
# : todo : add link blocking check for bio
|
||||||
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||||
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
|
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
|
44
app/controllers/api/v1/albums_controller.rb
Normal file
44
app/controllers/api/v1/albums_controller.rb
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::AlbumsController < Api::BaseController
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_albums, only: :index
|
||||||
|
before_action :set_album, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @albums, each_serializer: REST::AlbumSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@album = "" #current_account.custom_filters.create!(resource_params)
|
||||||
|
render json: @album, serializer: REST::AlbumSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @album, serializer: REST::AlbumSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@album.update!(resource_params)
|
||||||
|
render json: @album, serializer: REST::AlbumSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@album.destroy!
|
||||||
|
render_empty_success
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_albums
|
||||||
|
@albums = "" #current_account.custom_filters
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_album
|
||||||
|
@album = "" # current_account.custom_filters.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:title, :description, :visibility)
|
||||||
|
end
|
||||||
|
end
|
44
app/controllers/api/v1/bookmark_collections_controller.rb
Normal file
44
app/controllers/api/v1/bookmark_collections_controller.rb
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::BookmarkCollectionsController < Api::BaseController
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_bookmark_collections, only: :index
|
||||||
|
before_action :set_bookmark_collection, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @bookmark_collections, each_serializer: REST::BookmarkCollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@bookmark_collection = "" #current_account.custom_filters.create!(resource_params)
|
||||||
|
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@bookmark_collection.update!(resource_params)
|
||||||
|
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@bookmark_collection.destroy!
|
||||||
|
render_empty_success
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_bookmark_collections
|
||||||
|
@bookmark_collections = "" #current_account.custom_filters
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_bookmark_collection
|
||||||
|
@bookmark_collection = "" # current_account.custom_filters.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(:title)
|
||||||
|
end
|
||||||
|
end
|
@ -35,6 +35,15 @@ class Api::V1::ChatConversationAccountsController < Api::BaseController
|
|||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_expiration_policy
|
||||||
|
if current_user.account.is_pro
|
||||||
|
#
|
||||||
|
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||||
|
else
|
||||||
|
render json: { error: 'You need to be a GabPRO member to access this' }, status: 422
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
@ -6,7 +6,7 @@ class Api::V1::ChatConversationController < Api::BaseController
|
|||||||
|
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_account, only: :create
|
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]
|
before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_read]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: {}, each_serializer: REST::ChatConversationAccountSerializer
|
render json: {}, each_serializer: REST::ChatConversationAccountSerializer
|
||||||
@ -23,8 +23,8 @@ class Api::V1::ChatConversationController < Api::BaseController
|
|||||||
render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer
|
render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_chat_conversation_unread
|
def mark_chat_conversation_read
|
||||||
@chat_conversation_account.update!(unread_count: 1)
|
@chat_conversation_account.update!(unread_count: 0)
|
||||||
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -34,8 +34,13 @@ class Api::V1::ChatConversationController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mark_chat_conversation_approved
|
def mark_chat_conversation_approved
|
||||||
@chat_conversation_account.update!(is_approved: true)
|
approved_conversation_count = ChatConversationAccount.where(account: @account, is_hidden: false, is_approved: true).count
|
||||||
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
if approved_conversation_count >= ChatConversationAccount::PER_ACCOUNT_APPROVED_LIMIT
|
||||||
|
render json: { error: true, message: "You have #{approved_conversation_count} active chat conversations. The limit is #{ChatConversationAccount::PER_ACCOUNT_APPROVED_LIMIT}. Delete some conversations first before approving any more requests." }
|
||||||
|
else
|
||||||
|
@chat_conversation_account.update!(is_approved: true)
|
||||||
|
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -11,24 +11,16 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers, unless: -> { @chats.empty? }
|
after_action :insert_pagination_headers, unless: -> { @chats.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
puts "tilly chat_message_conversations - 1: " + @chats.count.inspect
|
|
||||||
render json: @chats, each_serializer: REST::ChatMessageSerializer
|
render json: @chats, each_serializer: REST::ChatMessageSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy_all
|
def destroy_all
|
||||||
puts "tilly destry all chat"
|
if current_user.account.is_pro
|
||||||
# : todo :
|
@chat_conversation_account = PurgeChatMessagesService.new.call(current_user.account, @chat_conversation)
|
||||||
# check if is pro
|
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||||
# @chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
|
else
|
||||||
|
render json: { error: 'You need to be a GabPRO member to access this' }, status: 422
|
||||||
puts "tilly @chat: " + @chat.inspect
|
end
|
||||||
|
|
||||||
# : todo :
|
|
||||||
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
|
||||||
|
|
||||||
# @chat.destroy!
|
|
||||||
|
|
||||||
# render json: @chat, serializer: REST::ChatMessageSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -5,50 +5,20 @@ class Api::V1::ChatMessagesController < Api::BaseController
|
|||||||
before_action -> { doorkeeper_authorize! :write, :'write:chats' }
|
before_action -> { doorkeeper_authorize! :write, :'write:chats' }
|
||||||
|
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_chat_conversation, only: :create
|
|
||||||
before_action :set_chat_conversation_recipients, only: :create
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@chat = ChatMessage.create!(
|
@chat_conversation = ChatConversation.find(chat_params[:chat_conversation_id])
|
||||||
from_account: current_account,
|
@chat = PostChatMessageService.new.call(current_user.account, text: chat_params[:text], chat_conversation: @chat_conversation)
|
||||||
chat_conversation: @chat_conversation,
|
|
||||||
text: ActionController::Base.helpers.strip_tags(params[:text])
|
|
||||||
)
|
|
||||||
|
|
||||||
# : todo :
|
|
||||||
# check if blocked
|
|
||||||
# update unread_count++ if offline
|
|
||||||
|
|
||||||
@chat_conversation_recipients.each do |account|
|
|
||||||
payload = InlineRenderer.render(@chat, account, :chat_message)
|
|
||||||
Redis.current.publish("chat_messages:#{account.id}", Oj.dump(event: :notification, payload: payload))
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: @chat, serializer: REST::ChatMessageSerializer
|
render json: @chat, serializer: REST::ChatMessageSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
|
@chat = DeleteChatMessageService.new.call(current_user.account, params[:id])
|
||||||
|
|
||||||
# : todo :
|
|
||||||
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
|
||||||
|
|
||||||
@chat.destroy!
|
|
||||||
|
|
||||||
render json: @chat, serializer: REST::ChatMessageSerializer
|
render json: @chat, serializer: REST::ChatMessageSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_chat_conversation
|
|
||||||
@chat_conversation = ChatConversation.find(params[:chat_conversation_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_chat_conversation_recipients
|
|
||||||
account_conversation = ChatConversationAccount.where(account: current_user.account, chat_conversation: @chat_conversation).first
|
|
||||||
@chat_conversation_recipients = Account.where(id: account_conversation.participant_account_ids)
|
|
||||||
end
|
|
||||||
|
|
||||||
def chat_params
|
def chat_params
|
||||||
params.permit(:text, :chat_conversation_id)
|
params.permit(:text, :chat_conversation_id)
|
||||||
end
|
end
|
||||||
|
@ -46,7 +46,7 @@ class Api::V1::GroupsController < Api::BaseController
|
|||||||
|
|
||||||
@groups = []
|
@groups = []
|
||||||
if !@groupCategory.nil?
|
if !@groupCategory.nil?
|
||||||
@groups = Group.where(is_archived: false, group_categories: @groupCategory).all
|
@groups = Group.where(is_archived: false, group_categories: @groupCategory).order('member_count DESC').all
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @groups, each_serializer: REST::GroupSerializer
|
render json: @groups, each_serializer: REST::GroupSerializer
|
||||||
@ -59,7 +59,7 @@ class Api::V1::GroupsController < Api::BaseController
|
|||||||
|
|
||||||
@groups = []
|
@groups = []
|
||||||
if !params[:tag].empty?
|
if !params[:tag].empty?
|
||||||
@groups = Group.where(is_archived: false).where("array_to_string(tags, '||') ILIKE :tag", tag: "%#{params[:tag]}%").all
|
@groups = Group.where(is_archived: false).where("array_to_string(tags, '||') ILIKE :tag", tag: "%#{params[:tag]}%").order('member_count DESC').all
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @groups, each_serializer: REST::GroupSerializer
|
render json: @groups, each_serializer: REST::GroupSerializer
|
||||||
|
@ -62,6 +62,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def regeneration_in_progress?
|
def regeneration_in_progress?
|
||||||
Redis.current.exists("account:#{current_account.id}:regeneration")
|
Redis.current.exists?("account:#{current_account.id}:regeneration")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -48,7 +48,25 @@ class EmptyController < ActionController::Base
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
def cache_collection(raw, klass)
|
||||||
|
return raw unless klass.respond_to?(:with_includes)
|
||||||
|
|
||||||
|
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||||
|
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
||||||
|
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
||||||
|
|
||||||
|
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
||||||
|
|
||||||
|
unless uncached_ids.empty?
|
||||||
|
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
|
||||||
|
|
||||||
|
uncached.each_value do |item|
|
||||||
|
Rails.cache.write(item, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
|
||||||
|
end
|
||||||
|
|
||||||
def limit_param(default_limit)
|
def limit_param(default_limit)
|
||||||
return default_limit unless params[:limit]
|
return default_limit unless params[:limit]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
class ManifestsController < EmptyController
|
class ManifestsController < EmptyController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: InstancePresenter.new, serializer: ManifestSerializer
|
render json:{} # InstancePresenter.new, serializer: ManifestSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -20,7 +20,13 @@ class Settings::ProfilesController < Settings::BaseController
|
|||||||
if @account.is_verified && params[:account][:display_name] && @account.display_name != params[:account][:display_name]
|
if @account.is_verified && params[:account][:display_name] && @account.display_name != params[:account][:display_name]
|
||||||
flash[:alert] = 'Unable to change Display name for verified account'
|
flash[:alert] = 'Unable to change Display name for verified account'
|
||||||
redirect_to settings_profile_path
|
redirect_to settings_profile_path
|
||||||
|
elsif !@account.is_pro && params[:account][:username] && @account.username != params[:account][:username]
|
||||||
|
flash[:alert] = 'Unable to change username for your account. You are not GabPRO'
|
||||||
|
redirect_to settings_profile_path
|
||||||
else
|
else
|
||||||
|
# : todo :
|
||||||
|
# only allowed to change username once per day
|
||||||
|
|
||||||
if UpdateAccountService.new.call(@account, account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
@ -33,7 +39,7 @@ class Settings::ProfilesController < Settings::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
params.require(:account).permit(:display_name, :username, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
@ -46,11 +46,11 @@ class Settings::PromotionsController < Admin::BaseController
|
|||||||
@promotion = Promotion.find(params[:id])
|
@promotion = Promotion.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_filter_params
|
def set_filter_params
|
||||||
@filter_params = filter_params.to_hash.symbolize_keys
|
@filter_params = filter_params.to_hash.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
|
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
10
app/controllers/settings/trending_hashtags_controller.rb
Normal file
10
app/controllers/settings/trending_hashtags_controller.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class Settings::TrendingHashtagsController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
@trending_hashtags = Redis.current.get("admin_trending_hashtags") || ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
Redis.current.set("admin_trending_hashtags", params[:trending_hashtags])
|
||||||
|
redirect_to settings_trending_hashtags_path
|
||||||
|
end
|
||||||
|
end
|
@ -1,10 +1,10 @@
|
|||||||
class Settings::Verifications::ModerationController < Admin::BaseController
|
class Settings::Verifications::ModerationController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
@verification_requests = AccountVerificationRequest.all
|
@verification_requests = AccountVerificationRequest.order('created_at DESC').all
|
||||||
end
|
end
|
||||||
|
|
||||||
def approve
|
def approve
|
||||||
verification_request = AccountVerificationRequest.find params[:id]
|
verification_request = AccountVerificationRequest.find(params[:id])
|
||||||
|
|
||||||
# Mark user as verified
|
# Mark user as verified
|
||||||
account = verification_request.account
|
account = verification_request.account
|
||||||
@ -22,6 +22,8 @@ class Settings::Verifications::ModerationController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
@verification_requests = AccountVerificationRequest.find params[:id]
|
verification_request = AccountVerificationRequest.find(params[:id])
|
||||||
|
verification_request.destroy()
|
||||||
|
redirect_to settings_verifications_moderation_url, notice: I18n.t('verifications.moderation.rejected_msg')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -38,12 +38,6 @@ module StatusesHelper
|
|||||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_badge(account, all: false)
|
|
||||||
if account.bot?
|
|
||||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def link_to_more(url)
|
def link_to_more(url)
|
||||||
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
||||||
end
|
end
|
||||||
|
1
app/javascript/gabsocial/actions/albums.js
Normal file
1
app/javascript/gabsocial/actions/albums.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
//
|
@ -10,6 +10,20 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_RE
|
|||||||
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'
|
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'
|
||||||
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'
|
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const BOOKMARK_COLLECTIONS_FETCH_REQUEST = 'BOOKMARK_COLLECTIONS_FETCH_REQUEST'
|
||||||
|
export const BOOKMARK_COLLECTIONS_FETCH_SUCCESS = 'BOOKMARK_COLLECTIONS_FETCH_SUCCESS'
|
||||||
|
export const BOOKMARK_COLLECTIONS_FETCH_FAIL = 'BOOKMARK_COLLECTIONS_FETCH_FAIL'
|
||||||
|
|
||||||
|
export const BOOKMARK_COLLECTIONS_CREATE_REQUEST = 'BOOKMARK_COLLECTIONS_CREATE_REQUEST'
|
||||||
|
export const BOOKMARK_COLLECTIONS_CREATE_SUCCESS = 'BOOKMARK_COLLECTIONS_CREATE_SUCCESS'
|
||||||
|
export const BOOKMARK_COLLECTIONS_CREATE_FAIL = 'BOOKMARK_COLLECTIONS_CREATE_FAIL'
|
||||||
|
|
||||||
|
export const BOOKMARK_COLLECTIONS_REMOVE_REQUEST = 'BOOKMARK_COLLECTIONS_REMOVE_REQUEST'
|
||||||
|
export const BOOKMARK_COLLECTIONS_REMOVE_SUCCESS = 'BOOKMARK_COLLECTIONS_REMOVE_SUCCESS'
|
||||||
|
export const BOOKMARK_COLLECTIONS_REMOVE_FAIL = 'BOOKMARK_COLLECTIONS_REMOVE_FAIL'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -22,11 +36,11 @@ export const fetchBookmarkedStatuses = () => (dispatch, getState) => {
|
|||||||
|
|
||||||
dispatch(fetchBookmarkedStatusesRequest())
|
dispatch(fetchBookmarkedStatusesRequest())
|
||||||
|
|
||||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
api(getState).get('/api/v1/bookmarks').then((response) => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||||
dispatch(importFetchedStatuses(response.data))
|
dispatch(importFetchedStatuses(response.data))
|
||||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
||||||
}).catch(error => {
|
}).catch((error) => {
|
||||||
dispatch(fetchBookmarkedStatusesFail(error))
|
dispatch(fetchBookmarkedStatusesFail(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -61,11 +75,11 @@ export const expandBookmarkedStatuses = () => (dispatch, getState) => {
|
|||||||
|
|
||||||
dispatch(expandBookmarkedStatusesRequest())
|
dispatch(expandBookmarkedStatusesRequest())
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then((response) => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||||
dispatch(importFetchedStatuses(response.data))
|
dispatch(importFetchedStatuses(response.data))
|
||||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
||||||
}).catch(error => {
|
}).catch((error) => {
|
||||||
dispatch(expandBookmarkedStatusesFail(error))
|
dispatch(expandBookmarkedStatusesFail(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -85,3 +99,95 @@ const expandBookmarkedStatusesFail = (error) => ({
|
|||||||
showToast: true,
|
showToast: true,
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const fetchBookmarkCollections = () => (dispatch, getState) => {
|
||||||
|
if (!me) return
|
||||||
|
|
||||||
|
if (getState().getIn(['bookmark_collections', 'isLoading'])) return
|
||||||
|
|
||||||
|
dispatch(fetchBookmarkCollectionsRequest())
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/bookmark_collections').then((response) => {
|
||||||
|
dispatch(fetchBookmarkCollectionsSuccess(response.data))
|
||||||
|
}).catch((error) => {
|
||||||
|
dispatch(fetchBookmarkCollectionsFail(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchBookmarkCollectionsRequest = () => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_FETCH_REQUEST,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchBookmarkCollectionsSuccess = (bookmarkCollections) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
|
||||||
|
bookmarkCollections,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchBookmarkCollectionsFail = (error) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_FETCH_FAIL,
|
||||||
|
showToast: true,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const createBookmarkCollection = (title) => (dispatch, getState) => {
|
||||||
|
if (!me || !title) return
|
||||||
|
|
||||||
|
dispatch(createBookmarkCollectionRequest())
|
||||||
|
|
||||||
|
api(getState).post('/api/v1/bookmark_collections', { title }).then((response) => {
|
||||||
|
dispatch(createBookmarkCollectionSuccess(response.data))
|
||||||
|
}).catch((error) => {
|
||||||
|
dispatch(createBookmarkCollectionFail(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBookmarkCollectionRequest = () => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_REQUEST,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createBookmarkCollectionSuccess = (bookmarkCollection) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_SUCCESS,
|
||||||
|
bookmarkCollection,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createBookmarkCollectionFail = (error) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_FAIL,
|
||||||
|
showToast: true,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const removeBookmarkCollection = (bookmarkCollectionId) => (dispatch, getState) => {
|
||||||
|
if (!me || !bookmarkCollectionId) return
|
||||||
|
|
||||||
|
dispatch(removeBookmarkCollectionRequest(bookmarkCollectionId))
|
||||||
|
|
||||||
|
api(getState).delete(`/api/v1/bookmark_collection/${bookmarkCollectionId}`).then((response) => {
|
||||||
|
dispatch(removeBookmarkCollectionSuccess(response.data))
|
||||||
|
}).catch((error) => {
|
||||||
|
dispatch(removeBookmarkCollectionFail(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBookmarkCollectionRequest = (bookmarkCollectionId) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_REQUEST,
|
||||||
|
bookmarkCollectionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeBookmarkCollectionSuccess = () => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_SUCCESS,
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeBookmarkCollectionFail = (error) => ({
|
||||||
|
type: BOOKMARK_COLLECTIONS_CREATE_FAIL,
|
||||||
|
showToast: true,
|
||||||
|
error,
|
||||||
|
})
|
@ -46,6 +46,7 @@ export const IS_CHAT_MESSENGER_MUTED_SUCCESS = 'IS_CHAT_MESSENGER_MUTED_SUCCESS'
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const blockChatMessenger = (accountId) => (dispatch, getState) => {
|
export const blockChatMessenger = (accountId) => (dispatch, getState) => {
|
||||||
|
console.log("blockChatMessenger:", accountId)
|
||||||
if (!me || !accountId) return
|
if (!me || !accountId) return
|
||||||
|
|
||||||
dispatch(blockChatMessengerRequest(accountId))
|
dispatch(blockChatMessengerRequest(accountId))
|
||||||
|
@ -44,10 +44,10 @@ export const clearChatMessageConversation = (chatConversationId) => (dispatch) =
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const scrollBottomChatMessageConversation = (chatConversationId, top) => ({
|
export const scrollBottomChatMessageConversation = (chatConversationId, bottom) => ({
|
||||||
type: CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM,
|
type: CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM,
|
||||||
chatConversationId,
|
chatConversationId,
|
||||||
top,
|
bottom,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,7 +56,7 @@ export const scrollBottomChatMessageConversation = (chatConversationId, top) =>
|
|||||||
export const expandChatMessages = (chatConversationId, params = {}, done = noop) => (dispatch, getState) => {
|
export const expandChatMessages = (chatConversationId, params = {}, done = noop) => (dispatch, getState) => {
|
||||||
if (!me || !chatConversationId) return
|
if (!me || !chatConversationId) return
|
||||||
|
|
||||||
const chatConversation = getState().getIn(['chat_messages', chatConversationId], ImmutableMap())
|
const chatConversation = getState().getIn(['chat_conversations', chatConversationId], ImmutableMap())
|
||||||
const isLoadingMore = !!params.maxId
|
const isLoadingMore = !!params.maxId
|
||||||
|
|
||||||
if (!!chatConversation && (chatConversation.get('isLoading') || chatConversation.get('isError'))) {
|
if (!!chatConversation && (chatConversation.get('isLoading') || chatConversation.get('isError'))) {
|
||||||
|
@ -45,6 +45,22 @@ export const CHAT_CONVERSATION_DELETE_REQUEST = 'CHAT_CONVERSATION_DELETE_REQUES
|
|||||||
export const CHAT_CONVERSATION_DELETE_SUCCESS = 'CHAT_CONVERSATION_DELETE_SUCCESS'
|
export const CHAT_CONVERSATION_DELETE_SUCCESS = 'CHAT_CONVERSATION_DELETE_SUCCESS'
|
||||||
export const CHAT_CONVERSATION_DELETE_FAIL = 'CHAT_CONVERSATION_DELETE_FAIL'
|
export const CHAT_CONVERSATION_DELETE_FAIL = 'CHAT_CONVERSATION_DELETE_FAIL'
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const CHAT_CONVERSATION_MARK_READ_FETCH = 'CHAT_CONVERSATION_MARK_READ_FETCH'
|
||||||
|
export const CHAT_CONVERSATION_MARK_READ_SUCCESS = 'CHAT_CONVERSATION_MARK_READ_SUCCESS'
|
||||||
|
export const CHAT_CONVERSATION_MARK_READ_FAIL = 'CHAT_CONVERSATION_MARK_READ_FAIL'
|
||||||
|
|
||||||
|
export const CHAT_CONVERSATION_HIDE_FETCH = 'CHAT_CONVERSATION_HIDE_FETCH'
|
||||||
|
export const CHAT_CONVERSATION_HIDE_SUCCESS = 'CHAT_CONVERSATION_HIDE_SUCCESS'
|
||||||
|
export const CHAT_CONVERSATION_HIDE_FAIL = 'CHAT_CONVERSATION_HIDE_FAIL'
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const SET_CHAT_CONVERSATION_EXPIRATION_REQUEST = 'SET_CHAT_CONVERSATION_EXPIRATION_REQUEST'
|
||||||
|
export const SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS = 'SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS'
|
||||||
|
export const SET_CHAT_CONVERSATION_EXPIRATION_FAIL = 'SET_CHAT_CONVERSATION_EXPIRATION_FAIL'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Fetch paginated active chat conversations, import accounts and set chat converations
|
* @description Fetch paginated active chat conversations, import accounts and set chat converations
|
||||||
*/
|
*/
|
||||||
@ -310,3 +326,92 @@ export const approveChatConversationRequestSuccess = (chatConversation) => ({
|
|||||||
export const approveChatConversationRequestFail = () => ({
|
export const approveChatConversationRequestFail = () => ({
|
||||||
type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL,
|
type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const hideChatConversation = (chatConversationId) => (dispatch, getState) => {
|
||||||
|
if (!me|| !chatConversationId) return
|
||||||
|
|
||||||
|
dispatch(hideChatConversationFetch(chatConversationId))
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_hidden`).then((response) => {
|
||||||
|
dispatch(approveChatConversationRequestSuccess(chatConversationId))
|
||||||
|
}).catch((error) => dispatch(approveChatConversationRequestFail(error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideChatConversationFetch = (chatConversationId) => ({
|
||||||
|
type: CHAT_CONVERSATION_HIDE_SUCCESS,
|
||||||
|
chatConversationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hideChatConversationSuccess = (chatConversationId) => ({
|
||||||
|
type: CHAT_CONVERSATION_HIDE_SUCCESS,
|
||||||
|
chatConversationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hideChatConversationFail = () => ({
|
||||||
|
type: CHAT_CONVERSATION_HIDE_FAIL,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const readChatConversation = (chatConversationId) => (dispatch, getState) => {
|
||||||
|
if (!me|| !chatConversationId) return
|
||||||
|
|
||||||
|
const chatConversation = getState().getIn(['chat_conversations', chatConversationId])
|
||||||
|
if (!chatConversation) return
|
||||||
|
if (chatConversation.get('unread_count') < 1) return
|
||||||
|
|
||||||
|
dispatch(readChatConversationFetch(chatConversation))
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_read`).then((response) => {
|
||||||
|
dispatch(readChatConversationSuccess(response.data))
|
||||||
|
}).catch((error) => dispatch(readChatConversationFail(error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readChatConversationFetch = (chatConversation) => ({
|
||||||
|
type: CHAT_CONVERSATION_MARK_READ_FETCH,
|
||||||
|
chatConversation,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const readChatConversationSuccess = (chatConversation) => ({
|
||||||
|
type: CHAT_CONVERSATION_MARK_READ_SUCCESS,
|
||||||
|
chatConversation,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const readChatConversationFail = () => ({
|
||||||
|
type: CHAT_CONVERSATION_MARK_READ_FAIL,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const setChatConversationExpiration = (chatConversationId, expiration) => (dispatch, getState) => {
|
||||||
|
if (!me|| !chatConversationId || !expiration) return
|
||||||
|
|
||||||
|
dispatch(setChatConversationExpirationFetch(chatConversation))
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/set_expiration_policy`, {
|
||||||
|
expiration,
|
||||||
|
}).then((response) => {
|
||||||
|
dispatch(setChatConversationExpirationSuccess(response.data))
|
||||||
|
}).catch((error) => dispatch(setChatConversationExpirationFail(error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setChatConversationExpirationFetch = (chatConversation) => ({
|
||||||
|
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
|
||||||
|
chatConversation,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setChatConversationExpirationSuccess = (chatConversation) => ({
|
||||||
|
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
|
||||||
|
chatConversation,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setChatConversationExpirationFail = (error) => ({
|
||||||
|
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
|
||||||
|
@ -56,6 +56,22 @@ const sendChatMessageFail = (error) => ({
|
|||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const manageIncomingChatMessage = (chatMessage) => (dispatch, getState) => {
|
||||||
|
if (!chatMessage) return
|
||||||
|
|
||||||
|
console.log("chatMessage:", chatMessage)
|
||||||
|
dispatch(sendChatMessageSuccess(chatMessage))
|
||||||
|
|
||||||
|
const isOnline = getState().getIn(['chat_conversation_messages', chatMessage.chat_conversation_id, 'online'])
|
||||||
|
console.log("isOnline: ", isOnline)
|
||||||
|
|
||||||
|
// : todo :
|
||||||
|
// Check if is online for conversation, if not increase total/convo unread count
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -99,24 +115,25 @@ export const purgeChatMessages = (chatConversationId) => (dispatch, getState) =>
|
|||||||
|
|
||||||
dispatch(deleteChatMessagesRequest(chatConversationId))
|
dispatch(deleteChatMessagesRequest(chatConversationId))
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/chat_conversations/${chatConversationId}/messages/destroy_all`).then((response) => {
|
api(getState).delete(`/api/v1/chat_conversations/messages/${chatConversationId}/destroy_all`).then((response) => {
|
||||||
dispatch(deleteChatMessagesSuccess(response.data))
|
dispatch(deleteChatMessagesSuccess(response.data))
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
dispatch(deleteChatMessagesFail(error))
|
dispatch(deleteChatMessagesFail(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteChatMessagesRequest = () => ({
|
const purgeChatMessagesRequest = (chatConversationId) => ({
|
||||||
type: CHAT_MESSAGES_PURGE_REQUEST,
|
type: CHAT_MESSAGES_PURGE_REQUEST,
|
||||||
|
chatConversationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteChatMessagesSuccess = (chatConversationId) => ({
|
const purgeChatMessagesSuccess = (chatConversationId) => ({
|
||||||
type: CHAT_MESSAGES_PURGE_SUCCESS,
|
type: CHAT_MESSAGES_PURGE_SUCCESS,
|
||||||
chatConversationId,
|
chatConversationId,
|
||||||
showToast: true,
|
showToast: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteChatMessagesFail = (error) => ({
|
const purgeChatMessagesFail = (error) => ({
|
||||||
type: CHAT_MESSAGES_PURGE_FAIL,
|
type: CHAT_MESSAGES_PURGE_FAIL,
|
||||||
showToast: true,
|
showToast: true,
|
||||||
error,
|
error,
|
||||||
|
@ -20,12 +20,12 @@ import { defineMessages } from 'react-intl'
|
|||||||
import { openModal, closeModal } from './modal'
|
import { openModal, closeModal } from './modal'
|
||||||
import {
|
import {
|
||||||
MODAL_COMPOSE,
|
MODAL_COMPOSE,
|
||||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
EXPIRATION_OPTION_5_MINUTES,
|
||||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
EXPIRATION_OPTION_60_MINUTES,
|
||||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
EXPIRATION_OPTION_6_HOURS,
|
||||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
EXPIRATION_OPTION_24_HOURS,
|
||||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
EXPIRATION_OPTION_3_DAYS,
|
||||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
EXPIRATION_OPTION_7_DAYS,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import { me } from '../initial_state'
|
import { me } from '../initial_state'
|
||||||
import { makeGetStatus } from '../selectors'
|
import { makeGetStatus } from '../selectors'
|
||||||
@ -347,17 +347,17 @@ export const submitCompose = (groupId, replyToId = null, router, isStandalone, a
|
|||||||
let expires_at = getState().getIn(['compose', 'expires_at'], null)
|
let expires_at = getState().getIn(['compose', 'expires_at'], null)
|
||||||
|
|
||||||
if (expires_at) {
|
if (expires_at) {
|
||||||
if (expires_at === STATUS_EXPIRATION_OPTION_5_MINUTES) {
|
if (expires_at === EXPIRATION_OPTION_5_MINUTES) {
|
||||||
expires_at = moment.utc().add('5', 'minute').toDate()
|
expires_at = moment.utc().add('5', 'minute').toDate()
|
||||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_60_MINUTES) {
|
} else if (expires_at === EXPIRATION_OPTION_60_MINUTES) {
|
||||||
expires_at = moment.utc().add('60', 'minute').toDate()
|
expires_at = moment.utc().add('60', 'minute').toDate()
|
||||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_6_HOURS) {
|
} else if (expires_at === EXPIRATION_OPTION_6_HOURS) {
|
||||||
expires_at = moment.utc().add('6', 'hour').toDate()
|
expires_at = moment.utc().add('6', 'hour').toDate()
|
||||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_24_HOURS) {
|
} else if (expires_at === EXPIRATION_OPTION_24_HOURS) {
|
||||||
expires_at = moment.utc().add('24', 'hour').toDate()
|
expires_at = moment.utc().add('24', 'hour').toDate()
|
||||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_3_DAYS) {
|
} else if (expires_at === EXPIRATION_OPTION_3_DAYS) {
|
||||||
expires_at = moment.utc().add('3', 'day').toDate()
|
expires_at = moment.utc().add('3', 'day').toDate()
|
||||||
} else if (expires_at === STATUS_EXPIRATION_OPTION_7_DAYS) {
|
} else if (expires_at === EXPIRATION_OPTION_7_DAYS) {
|
||||||
expires_at = moment.utc().add('7', 'day').toDate()
|
expires_at = moment.utc().add('7', 'day').toDate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
updateTimelineQueue,
|
updateTimelineQueue,
|
||||||
} from './timelines'
|
} from './timelines'
|
||||||
import { updateNotificationsQueue } from './notifications'
|
import { updateNotificationsQueue } from './notifications'
|
||||||
import { sendChatMessageSuccess } from './chat_messages'
|
import { manageIncomingChatMessage } from './chat_messages'
|
||||||
import { fetchFilters } from './filters'
|
import { fetchFilters } from './filters'
|
||||||
import { getLocale } from '../locales'
|
import { getLocale } from '../locales'
|
||||||
import { handleComposeSubmit } from './compose'
|
import { handleComposeSubmit } from './compose'
|
||||||
@ -84,7 +84,7 @@ export const connectChatMessagesStream = (accountId) => {
|
|||||||
onReceive (data) {
|
onReceive (data) {
|
||||||
if (!data['event'] || !data['payload']) return
|
if (!data['event'] || !data['payload']) return
|
||||||
if (data.event === 'notification') {
|
if (data.event === 'notification') {
|
||||||
dispatch(sendChatMessageSuccess(JSON.parse(data.payload)))
|
dispatch(manageIncomingChatMessage(JSON.parse(data.payload)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
0
app/javascript/gabsocial/components/album.js
Normal file
0
app/javascript/gabsocial/components/album.js
Normal file
@ -189,6 +189,7 @@ class AutosuggestTextbox extends ImmutablePureComponent {
|
|||||||
id,
|
id,
|
||||||
isPro,
|
isPro,
|
||||||
isEdit,
|
isEdit,
|
||||||
|
isModalOpen,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const { suggestionsHidden } = this.state
|
const { suggestionsHidden } = this.state
|
||||||
|
@ -1,29 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import {
|
import {
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
injectIntl,
|
injectIntl,
|
||||||
} from 'react-intl'
|
} from 'react-intl'
|
||||||
import { openModal } from '../actions/modal'
|
|
||||||
import {
|
import {
|
||||||
|
me,
|
||||||
repository,
|
repository,
|
||||||
source_url,
|
source_url,
|
||||||
me,
|
|
||||||
} from '../initial_state'
|
} from '../initial_state'
|
||||||
import { CX, DEFAULT_REL } from '../constants'
|
import { DEFAULT_REL } from '../constants'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import Button from './button'
|
import Button from './button'
|
||||||
|
import DotTextSeperator from './dot_text_seperator'
|
||||||
|
|
||||||
class LinkFooter extends React.PureComponent {
|
class LinkFooter extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { intl } = this.props
|
||||||
intl,
|
|
||||||
noPadding,
|
|
||||||
onOpenHotkeys,
|
|
||||||
} = this.props
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
@ -32,12 +27,6 @@ class LinkFooter extends React.PureComponent {
|
|||||||
href: 'https://help.gab.com',
|
href: 'https://help.gab.com',
|
||||||
text: intl.formatMessage(messages.help),
|
text: intl.formatMessage(messages.help),
|
||||||
},
|
},
|
||||||
// : todo :
|
|
||||||
// {
|
|
||||||
// onClick: onOpenHotkeys,
|
|
||||||
// text: intl.formatMessage(messages.hotkeys),
|
|
||||||
// requiresUser: true,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
href: '/auth/edit',
|
href: '/auth/edit',
|
||||||
text: intl.formatMessage(messages.security),
|
text: intl.formatMessage(messages.security),
|
||||||
@ -52,16 +41,16 @@ class LinkFooter extends React.PureComponent {
|
|||||||
text: intl.formatMessage(messages.investors),
|
text: intl.formatMessage(messages.investors),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/about/tos',
|
to: '/about/sales',
|
||||||
text: intl.formatMessage(messages.terms),
|
text: intl.formatMessage(messages.salesTerms),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/about/dmca',
|
to: '/about/dmca',
|
||||||
text: intl.formatMessage(messages.dmca),
|
text: intl.formatMessage(messages.dmca),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/about/sales',
|
to: '/about/tos',
|
||||||
text: intl.formatMessage(messages.salesTerms),
|
text: intl.formatMessage(messages.terms),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/about/privacy',
|
to: '/about/privacy',
|
||||||
@ -75,36 +64,33 @@ class LinkFooter extends React.PureComponent {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const containerClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
px10: !noPadding,
|
|
||||||
mb15: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={[_s.d, _s.mb15].join(' ')}>
|
||||||
<nav aria-label='Footer' role='navigation' className={[_s.d, _s.flexWrap, _s.flexRow].join(' ')}>
|
<nav aria-label='Footer' role='navigation' className={[_s.d, _s.flexWrap, _s.flexRow].join(' ')}>
|
||||||
{
|
{
|
||||||
linkFooterItems.map((linkFooterItem, i) => {
|
linkFooterItems.map((linkFooterItem, i) => {
|
||||||
if (linkFooterItem.requiresUser && !me) return null
|
if (linkFooterItem.requiresUser && !me) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
isText
|
<Button
|
||||||
underlineOnHover
|
isText
|
||||||
color='none'
|
underlineOnHover
|
||||||
backgroundColor='none'
|
color='none'
|
||||||
key={`link-footer-item-${i}`}
|
backgroundColor='none'
|
||||||
to={linkFooterItem.to}
|
key={`link-footer-item-${i}`}
|
||||||
href={linkFooterItem.href}
|
to={linkFooterItem.to}
|
||||||
data-method={linkFooterItem.logout ? 'delete' : null}
|
href={linkFooterItem.href}
|
||||||
onClick={linkFooterItem.onClick || null}
|
data-method={linkFooterItem.logout ? 'delete' : null}
|
||||||
className={[_s.mt5, _s.mb5, _s.pr15].join(' ')}
|
onClick={linkFooterItem.onClick || null}
|
||||||
>
|
className={[_s.mt5].join(' ')}
|
||||||
<Text size='small' color='tertiary'>
|
>
|
||||||
{linkFooterItem.text}
|
<Text size='small' color='tertiary'>
|
||||||
</Text>
|
{linkFooterItem.text}
|
||||||
</Button>
|
</Text>
|
||||||
|
</Button>
|
||||||
|
{ !linkFooterItem.logout && <Text size='small' color='secondary' className={[_s.pt2, _s.mr5, _s.ml5].join(' ')}>·</Text> }
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -120,7 +106,7 @@ class LinkFooter extends React.PureComponent {
|
|||||||
defaultMessage='Gab Social is open source software. You can contribute or report issues on our self-hosted GitLab at {gitlab}.'
|
defaultMessage='Gab Social is open source software. You can contribute or report issues on our self-hosted GitLab at {gitlab}.'
|
||||||
values={{
|
values={{
|
||||||
gitlab: (
|
gitlab: (
|
||||||
<a href={source_url} className={[_s.displayBlock, _s.inherit].join(' ')} rel={DEFAULT_REL} target='_blank'>
|
<a href={source_url} className={[_s.displayInlineBlock, _s.inherit].join(' ')} rel={DEFAULT_REL} target='_blank'>
|
||||||
{repository}
|
{repository}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
@ -136,8 +122,6 @@ class LinkFooter extends React.PureComponent {
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
investors: { id: 'getting_started.investors', defaultMessage: 'Investors' },
|
investors: { id: 'getting_started.investors', defaultMessage: 'Investors' },
|
||||||
help: { id: 'getting_started.help', defaultMessage: 'Help' },
|
help: { id: 'getting_started.help', defaultMessage: 'Help' },
|
||||||
invite: { id: 'getting_started.invite', defaultMessage: 'Invite people' },
|
|
||||||
hotkeys: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' },
|
|
||||||
security: { id: 'getting_started.security', defaultMessage: 'Security' },
|
security: { id: 'getting_started.security', defaultMessage: 'Security' },
|
||||||
about: { id: 'navigation_bar.info', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.info', defaultMessage: 'About' },
|
||||||
developers: { id: 'getting_started.developers', defaultMessage: 'Developers' },
|
developers: { id: 'getting_started.developers', defaultMessage: 'Developers' },
|
||||||
@ -148,16 +132,8 @@ const messages = defineMessages({
|
|||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onOpenHotkeys() {
|
|
||||||
dispatch(openModal('HOTKEYS'))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
LinkFooter.propTypes = {
|
LinkFooter.propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
noPadding: PropTypes.bool,
|
|
||||||
onOpenHotkeys: PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter))
|
export default injectIntl(LinkFooter)
|
@ -79,7 +79,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||||||
posAbs: 1,
|
posAbs: 1,
|
||||||
top0: 1,
|
top0: 1,
|
||||||
h100PC: 1,
|
h100PC: 1,
|
||||||
w100PC: 1,
|
// w100PC: 1,
|
||||||
py2: !isSmall,
|
py2: !isSmall,
|
||||||
px2: !isSmall,
|
px2: !isSmall,
|
||||||
})
|
})
|
||||||
@ -87,7 +87,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||||||
const linkClasses = CX({
|
const linkClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
w100PC: 1,
|
w100PC: 1,
|
||||||
h100PC: 1,
|
// h100PC: 1,
|
||||||
overflowHidden: 1,
|
overflowHidden: 1,
|
||||||
border1PX: 1,
|
border1PX: 1,
|
||||||
borderColorPrimary: 1,
|
borderColorPrimary: 1,
|
||||||
@ -96,7 +96,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||||||
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
|
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.w25PC, _s.pt25PC].join(' ')}>
|
<div className={[_s.d, _s.pt25PC].join(' ')}>
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={statusUrl}
|
to={statusUrl}
|
||||||
@ -117,6 +117,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||||||
visible &&
|
visible &&
|
||||||
<Image
|
<Image
|
||||||
height='100%'
|
height='100%'
|
||||||
|
width=''
|
||||||
src={attachment.get('preview_url')}
|
src={attachment.get('preview_url')}
|
||||||
alt={attachment.get('description')}
|
alt={attachment.get('description')}
|
||||||
title={attachment.get('description')}
|
title={attachment.get('description')}
|
||||||
|
@ -49,9 +49,9 @@ class ComposeModal extends ImmutablePureComponent {
|
|||||||
const title = isEditing ? messages.edit : isComment ? messages.comment : messages.title
|
const title = isEditing ? messages.edit : isComment ? messages.comment : messages.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{width: '512px'}} className={[_s.d, _s.modal].join(' ')}>
|
<div style={{width: '580px'}} className={[_s.d, _s.modal].join(' ')}>
|
||||||
<Block>
|
<Block>
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl10, _s.pr15].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl5, _s.pr10].join(' ')}>
|
||||||
<div className={[_s.d, _s.w115PX, _s.aiStart, _s.jcCenter, _s.mrAuto].join(' ')}>
|
<div className={[_s.d, _s.w115PX, _s.aiStart, _s.jcCenter, _s.mrAuto].join(' ')}>
|
||||||
<Button
|
<Button
|
||||||
backgroundColor='none'
|
backgroundColor='none'
|
||||||
@ -69,8 +69,8 @@ class ComposeModal extends ImmutablePureComponent {
|
|||||||
<ComposeFormSubmitButton type='header' />
|
<ComposeFormSubmitButton type='header' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={[_s.d].join(' ')}>
|
<div className={[_s.d, _s.pt5].join(' ')}>
|
||||||
<TimelineComposeBlock isModal />
|
<TimelineComposeBlock isModal formLocation='modal' />
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { MODAL_DECK_COLUMN_ADD_OPTIONS } from '../../constants'
|
||||||
import { setDeckColumnAtIndex } from '../../actions/deck'
|
import { setDeckColumnAtIndex } from '../../actions/deck'
|
||||||
import { openModal } from '../../actions/modal'
|
import { openModal } from '../../actions/modal'
|
||||||
import ModalLayout from './modal_layout'
|
import ModalLayout from './modal_layout'
|
||||||
@ -10,27 +11,19 @@ import Text from '../text'
|
|||||||
class DeckColumnAddModal extends React.PureComponent {
|
class DeckColumnAddModal extends React.PureComponent {
|
||||||
|
|
||||||
onAdd = (column) => {
|
onAdd = (column) => {
|
||||||
console.log("onAdd column: ", column)
|
const moreOptions = ['user', 'list', 'group', 'hashtag']
|
||||||
switch (column) {
|
if (moreOptions.indexOf(column) > -1) {
|
||||||
case 'user':
|
this.openOptionsModal(column)
|
||||||
//
|
} else {
|
||||||
break
|
this.props.dispatch(setDeckColumnAtIndex(column))
|
||||||
case 'list':
|
this.props.onClose()
|
||||||
//
|
|
||||||
break
|
|
||||||
case 'group':
|
|
||||||
//
|
|
||||||
break
|
|
||||||
case 'hashtag':
|
|
||||||
//
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.props.dispatch(setDeckColumnAtIndex(column))
|
|
||||||
this.props.onClose()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openOptionsModal = (column) => {
|
||||||
|
this.props.dispatch(openModal(MODAL_DECK_COLUMN_ADD_OPTIONS, { column }))
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
intl,
|
intl,
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { openModal } from '../../actions/modal'
|
||||||
|
import { MODAL_DECK_COLUMN_ADD } from '../../constants'
|
||||||
|
import Heading from '../heading'
|
||||||
|
import Button from '../button'
|
||||||
|
import Block from '../block'
|
||||||
|
|
||||||
|
class DeckColumnAddOptionsModal extends React.PureComponent {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
selectedItem: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickClose = () => {
|
||||||
|
this.props.onClose()
|
||||||
|
this.props.dispatch(openModal(MODAL_DECK_COLUMN_ADD))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAdd = () => {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { column } = this.props
|
||||||
|
const { selectedItem } = this.state
|
||||||
|
|
||||||
|
// user, hashtag, list, groups
|
||||||
|
|
||||||
|
if (!column) return <div />
|
||||||
|
const title = `Select a ${column}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{width: '520px'}} className={[_s.d, _s.modal].join(' ')}>
|
||||||
|
<Block>
|
||||||
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl10, _s.pr15].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.w115PX, _s.aiStart, _s.jcCenter, _s.mrAuto].join(' ')}>
|
||||||
|
<Button
|
||||||
|
backgroundColor='none'
|
||||||
|
title='Back'
|
||||||
|
onClick={this.onClickClose}
|
||||||
|
color='secondary'
|
||||||
|
icon='back'
|
||||||
|
iconSize='16px'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Heading size='h2'>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
<div className={[_s.d, _s.w115PX, _s.aiEnd, _s.jcCenter, _s.mlAuto].join(' ')}>
|
||||||
|
<Button
|
||||||
|
isDisabled={!selectedItem}
|
||||||
|
onClick={this.handleAdd}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={[_s.d].join(' ')}>
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckColumnAddOptionsModal.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
column: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect()(DeckColumnAddOptionsModal)
|
@ -16,6 +16,7 @@ import {
|
|||||||
MODAL_COMPOSE,
|
MODAL_COMPOSE,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
MODAL_DECK_COLUMN_ADD,
|
MODAL_DECK_COLUMN_ADD,
|
||||||
|
MODAL_DECK_COLUMN_ADD_OPTIONS,
|
||||||
MODAL_DISPLAY_OPTIONS,
|
MODAL_DISPLAY_OPTIONS,
|
||||||
MODAL_EDIT_PROFILE,
|
MODAL_EDIT_PROFILE,
|
||||||
MODAL_EDIT_SHORTCUTS,
|
MODAL_EDIT_SHORTCUTS,
|
||||||
@ -51,6 +52,7 @@ import {
|
|||||||
ComposeModal,
|
ComposeModal,
|
||||||
ConfirmationModal,
|
ConfirmationModal,
|
||||||
DeckColumnAddModal,
|
DeckColumnAddModal,
|
||||||
|
DeckColumnAddOptionsModal,
|
||||||
DisplayOptionsModal,
|
DisplayOptionsModal,
|
||||||
EditProfileModal,
|
EditProfileModal,
|
||||||
EditShortcutsModal,
|
EditShortcutsModal,
|
||||||
@ -89,6 +91,7 @@ const MODAL_COMPONENTS = {
|
|||||||
[MODAL_COMPOSE]: ComposeModal,
|
[MODAL_COMPOSE]: ComposeModal,
|
||||||
[MODAL_CONFIRM]: ConfirmationModal,
|
[MODAL_CONFIRM]: ConfirmationModal,
|
||||||
[MODAL_DECK_COLUMN_ADD]: DeckColumnAddModal,
|
[MODAL_DECK_COLUMN_ADD]: DeckColumnAddModal,
|
||||||
|
[MODAL_DECK_COLUMN_ADD_OPTIONS]: DeckColumnAddOptionsModal,
|
||||||
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
|
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
|
||||||
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
|
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
|
||||||
[MODAL_EDIT_PROFILE]: EditProfileModal,
|
[MODAL_EDIT_PROFILE]: EditProfileModal,
|
||||||
|
@ -49,7 +49,7 @@ class ChatNavigationBar extends React.PureComponent {
|
|||||||
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
|
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
|
||||||
<AvatarGroup accounts={otherAccounts} size={35} noHover />
|
<AvatarGroup accounts={otherAccounts} size={35} noHover />
|
||||||
<Heading size='h1'>
|
<Heading size='h1'>
|
||||||
<div className={[_s.dangerousContent, _s.pl10, _s.fs19PX].join(' ')} dangerouslySetInnerHTML={{ __html: nameHTML }} />
|
<div className={[_s.dangerousContent, _s.colorNavigation, _s.pl10, _s.fs19PX].join(' ')} dangerouslySetInnerHTML={{ __html: nameHTML }} />
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import Heading from '../heading'
|
|||||||
import Button from '../button'
|
import Button from '../button'
|
||||||
import BackButton from '../back_button'
|
import BackButton from '../back_button'
|
||||||
import Text from '../text'
|
import Text from '../text'
|
||||||
import CharacterCounter from '../character_counter'
|
import ComposeFormSubmitButton from '../../features/compose/components/compose_form_submit_button'
|
||||||
|
|
||||||
class ComposeNavigationBar extends React.PureComponent {
|
class ComposeNavigationBar extends React.PureComponent {
|
||||||
|
|
||||||
@ -26,12 +26,6 @@ class ComposeNavigationBar extends React.PureComponent {
|
|||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
|
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
|
||||||
const buttonOptions = {
|
|
||||||
backgroundColor: disabledButton ? 'tertiary' : 'brand',
|
|
||||||
color: disabledButton ? 'tertiary' : 'white',
|
|
||||||
isDisabled: disabledButton,
|
|
||||||
onClick: this.handleOnPost,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}>
|
||||||
@ -48,18 +42,14 @@ class ComposeNavigationBar extends React.PureComponent {
|
|||||||
|
|
||||||
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
|
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
|
||||||
<Heading size='h1'>
|
<Heading size='h1'>
|
||||||
Compose
|
<span className={[_s.dangerousContent, _s.fs24PX, _s.colorNavigation].join(' ')}>
|
||||||
|
Compose
|
||||||
|
</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.mlAuto, _s.aiCenter, _s.jcCenter, _s.mr15].join(' ')}>
|
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.mlAuto, _s.aiCenter, _s.jcCenter, _s.mr15].join(' ')}>
|
||||||
<CharacterCounter max={MAX_POST_CHARACTER_COUNT} text={text} />
|
<ComposeFormSubmitButton type='navigation' />
|
||||||
|
|
||||||
<Button {...buttonOptions}>
|
|
||||||
<Text color='inherit' weight='bold' size='medium' className={_s.px5}>
|
|
||||||
POST
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +61,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
|
|||||||
noPadding
|
noPadding
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
|
headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
|
||||||
headerButtonTo={!!account ? `/${account.get('acct')}/media` : undefined}
|
headerButtonTo={!!account ? `/${account.get('acct')}/photos` : undefined}
|
||||||
>
|
>
|
||||||
<div className={[_s.d, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}>
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
|
import { closePopover } from '../../actions/popover'
|
||||||
|
import { changeExpiresAt } from '../../actions/compose'
|
||||||
|
import {
|
||||||
|
EXPIRATION_OPTION_5_MINUTES,
|
||||||
|
EXPIRATION_OPTION_60_MINUTES,
|
||||||
|
EXPIRATION_OPTION_6_HOURS,
|
||||||
|
EXPIRATION_OPTION_24_HOURS,
|
||||||
|
EXPIRATION_OPTION_3_DAYS,
|
||||||
|
EXPIRATION_OPTION_7_DAYS,
|
||||||
|
} from '../../constants'
|
||||||
|
import PopoverLayout from './popover_layout'
|
||||||
|
import List from '../list'
|
||||||
|
|
||||||
|
class ChatConversationExpirationOptionsPopover extends React.PureComponent {
|
||||||
|
|
||||||
|
handleOnSetExpiration = (expiresAt) => {
|
||||||
|
this.props.onChangeExpiresAt(expiresAt)
|
||||||
|
this.handleOnClosePopover()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClosePopover = () => {
|
||||||
|
this.props.onClosePopover()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
expiresAtValue,
|
||||||
|
intl,
|
||||||
|
isXS,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const listItems = [
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'None',
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(null),
|
||||||
|
isActive: !expiresAtValue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: intl.formatMessage(messages.minutes, { number: 5 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: intl.formatMessage(messages.minutes, { number: 60 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: '6 hours',
|
||||||
|
title: intl.formatMessage(messages.hours, { number: 6 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: intl.formatMessage(messages.hours, { number: 24 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: '3 days',
|
||||||
|
title: intl.formatMessage(messages.days, { number: 3 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: intl.formatMessage(messages.days, { number: 7 }),
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
|
||||||
|
isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (expiresAtValue) {
|
||||||
|
listItems.unshift({
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'Remove expiration',
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(null),
|
||||||
|
},)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverLayout
|
||||||
|
width={210}
|
||||||
|
isXS={isXS}
|
||||||
|
onClose={this.handleOnClosePopover}
|
||||||
|
>
|
||||||
|
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>This chats delete after:</Text>
|
||||||
|
<List
|
||||||
|
scrollKey='chat_conversation_expiration'
|
||||||
|
items={listItems}
|
||||||
|
size={isXS ? 'large' : 'small'}
|
||||||
|
/>
|
||||||
|
</PopoverLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||||
|
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||||
|
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
expiresAtValue: state.getIn(['compose', 'expires_at']),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onChangeExpiresAt(expiresAt) {
|
||||||
|
dispatch(changeExpiresAt(expiresAt))
|
||||||
|
},
|
||||||
|
onClosePopover() {
|
||||||
|
dispatch(closePopover())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatConversationExpirationOptionsPopover.defaultProps = {
|
||||||
|
expiresAtValue: PropTypes.string.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
isXS: PropTypes.bool,
|
||||||
|
onChangeExpiresAt: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ChatConversationExpirationOptionsPopover))
|
@ -5,14 +5,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { closePopover } from '../../actions/popover'
|
import { closePopover } from '../../actions/popover'
|
||||||
import { openModal } from '../../actions/modal'
|
import { openModal } from '../../actions/modal'
|
||||||
import {
|
import { hideChatConversation } from '../../actions/chat_conversations'
|
||||||
isChatMessengerBlocked,
|
import { purgeChatMessages } from '../../actions/chat_messages'
|
||||||
isChatMessengerMuted,
|
|
||||||
blockChatMessenger,
|
|
||||||
unblockChatMessenger,
|
|
||||||
muteChatMessenger,
|
|
||||||
unmuteChatMessenger,
|
|
||||||
} from '../../actions/chat_conversation_accounts'
|
|
||||||
import { MODAL_PRO_UPGRADE } from '../../constants'
|
import { MODAL_PRO_UPGRADE } from '../../constants'
|
||||||
import { me } from '../../initial_state'
|
import { me } from '../../initial_state'
|
||||||
import { makeGetChatConversation } from '../../selectors'
|
import { makeGetChatConversation } from '../../selectors'
|
||||||
@ -27,21 +21,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||||||
this.handleOnClosePopover()
|
this.handleOnClosePopover()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnBlock = () => {
|
|
||||||
this.props.onBlock()
|
|
||||||
this.handleOnClosePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnUnblock = () => {
|
|
||||||
this.props.onUnblock()
|
|
||||||
this.handleOnClosePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnMute = () => {
|
|
||||||
this.props.onMute()
|
|
||||||
this.handleOnClosePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnUnmute = () => {
|
handleOnUnmute = () => {
|
||||||
this.props.onUnute()
|
this.props.onUnute()
|
||||||
this.handleOnClosePopover()
|
this.handleOnClosePopover()
|
||||||
@ -51,7 +30,7 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||||||
if (!this.props.isPro) {
|
if (!this.props.isPro) {
|
||||||
this.props.openProUpgradeModal()
|
this.props.openProUpgradeModal()
|
||||||
} else {
|
} else {
|
||||||
this.props.onPurge()
|
this.props.onPurge(this.props.chatConversationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleOnClosePopover()
|
this.handleOnClosePopover()
|
||||||
@ -68,18 +47,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
|
||||||
hideArrow: true,
|
|
||||||
title: 'Block Messenger',
|
|
||||||
subtitle: 'The messenger will not be able to message you.',
|
|
||||||
onClick: () => this.handleOnBlock(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hideArrow: true,
|
|
||||||
title: 'Mute Messenger',
|
|
||||||
subtitle: 'You will not be notified of new messsages',
|
|
||||||
onClick: () => this.handleOnMute(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: 'Hide Conversation',
|
title: 'Hide Conversation',
|
||||||
@ -123,6 +90,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
onSetCommentSortingSetting(type) {
|
onSetCommentSortingSetting(type) {
|
||||||
dispatch(closePopover())
|
dispatch(closePopover())
|
||||||
},
|
},
|
||||||
|
onPurge(chatConversationId) {
|
||||||
|
dispatch(purgeChatMessages(chatConversationId))
|
||||||
|
},
|
||||||
|
onHide(chatConversationId) {
|
||||||
|
dispatch(hideChatConversation(chatConversationId))
|
||||||
|
},
|
||||||
onClosePopover: () => dispatch(closePopover()),
|
onClosePopover: () => dispatch(closePopover()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { closePopover } from '../../actions/popover'
|
|
||||||
import { deleteChatMessage } from '../../actions/chat_messages'
|
|
||||||
import PopoverLayout from './popover_layout'
|
|
||||||
import Button from '../button'
|
|
||||||
import Text from '../text'
|
|
||||||
|
|
||||||
class ChatMessageDeletePopover extends React.PureComponent {
|
|
||||||
|
|
||||||
handleOnClick = () => {
|
|
||||||
this.props.onDeleteChatMessage(this.props.chatMessageId)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnClosePopover = () => {
|
|
||||||
this.props.onClosePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isXS } = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopoverLayout
|
|
||||||
width={96}
|
|
||||||
isXS={isXS}
|
|
||||||
onClose={this.handleOnClosePopover}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={this.handleOnClick}
|
|
||||||
color='primary'
|
|
||||||
backgroundColor='tertiary'
|
|
||||||
className={[_s.radiusSmall].join(' ')}
|
|
||||||
>
|
|
||||||
<Text align='center' color='inherit'>Remove</Text>
|
|
||||||
</Button>
|
|
||||||
</PopoverLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onDeleteChatMessage(chatMessageId) {
|
|
||||||
dispatch(deleteChatMessage(chatMessageId))
|
|
||||||
dispatch(closePopover())
|
|
||||||
},
|
|
||||||
onClosePopover() {
|
|
||||||
dispatch(closePopover())
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ChatMessageDeletePopover.propTypes = {
|
|
||||||
isXS: PropTypes.bool,
|
|
||||||
chatMessageId: PropTypes.string.isRequired,
|
|
||||||
onDeleteChatMessage: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(ChatMessageDeletePopover)
|
|
@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { closePopover } from '../../actions/popover'
|
||||||
|
import { deleteChatMessage } from '../../actions/chat_messages'
|
||||||
|
import {
|
||||||
|
isChatMessengerBlocked,
|
||||||
|
isChatMessengerMuted,
|
||||||
|
blockChatMessenger,
|
||||||
|
unblockChatMessenger,
|
||||||
|
muteChatMessenger,
|
||||||
|
unmuteChatMessenger,
|
||||||
|
reportChatMessage,
|
||||||
|
} from '../../actions/chat_conversation_accounts'
|
||||||
|
import { makeGetChatMessage } from '../../selectors'
|
||||||
|
import { me } from '../../initial_state'
|
||||||
|
import PopoverLayout from './popover_layout'
|
||||||
|
import Button from '../button'
|
||||||
|
import List from '../list'
|
||||||
|
import Text from '../text'
|
||||||
|
|
||||||
|
class ChatMessageOptionsPopover extends React.PureComponent {
|
||||||
|
|
||||||
|
handleOnDelete = () => {
|
||||||
|
this.props.onDeleteChatMessage(this.props.chatMessageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnReport = () => {
|
||||||
|
this.props.onReportChatMessage(this.props.chatMessageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnBlock = () => {
|
||||||
|
if (this.props.isBlocked) {
|
||||||
|
this.props.unblockChatMessenger(this.props.fromAccountId)
|
||||||
|
} else {
|
||||||
|
this.props.blockChatMessenger(this.props.fromAccountId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMute = () => {
|
||||||
|
if (this.props.isMuted) {
|
||||||
|
this.props.unmuteChatMessenger(this.props.fromAccountId)
|
||||||
|
} else {
|
||||||
|
this.props.muteChatMessenger(this.props.fromAccountId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClosePopover = () => {
|
||||||
|
this.props.onClosePopover()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isXS,
|
||||||
|
isMine,
|
||||||
|
isMuted,
|
||||||
|
isBlocked,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const items = isMine ? [
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'Delete Message',
|
||||||
|
onClick: () => this.handleOnDelete(),
|
||||||
|
}
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'Report Messenger',
|
||||||
|
onClick: () => this.handleOnReport(),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: isBlocked ? 'Unblock Messenger' : 'Block Messenger',
|
||||||
|
subtitle: isBlocked ? '' : 'The messenger will not be able to message you.',
|
||||||
|
onClick: () => this.handleOnBlock(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: isMuted ? 'Unmute Messenger' : 'Mute Messenger',
|
||||||
|
subtitle: isMuted ? '' : 'You will not be notified of new messsages',
|
||||||
|
onClick: () => this.handleOnMute(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverLayout
|
||||||
|
width={isMine ? 160 : 200}
|
||||||
|
isXS={isXS}
|
||||||
|
onClose={this.handleOnClosePopover}
|
||||||
|
>
|
||||||
|
<List items={items} />
|
||||||
|
</PopoverLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { chatMessageId }) => ({
|
||||||
|
isMine: state.getIn(['chat_messages', chatMessageId, 'from_account_id']) === me,
|
||||||
|
fromAccountId: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
|
||||||
|
isBlocked: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
|
||||||
|
isMuted: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onDeleteChatMessage(chatMessageId) {
|
||||||
|
dispatch(deleteChatMessage(chatMessageId))
|
||||||
|
dispatch(closePopover())
|
||||||
|
},
|
||||||
|
onBlock(accountId) {
|
||||||
|
dispatch(blockChatMessenger(accountId))
|
||||||
|
},
|
||||||
|
onUnblock(accountId) {
|
||||||
|
dispatch(unblockChatMessenger(accountId))
|
||||||
|
},
|
||||||
|
onMute(accountId) {
|
||||||
|
dispatch(muteChatMessenger(accountId))
|
||||||
|
},
|
||||||
|
onUnmute(accountId) {
|
||||||
|
dispatch(unmuteChatMessenger(accountId))
|
||||||
|
},
|
||||||
|
onReportChatMessage(chatMessageId) {
|
||||||
|
dispatch(reportChatMessage(chatMessageId))
|
||||||
|
},
|
||||||
|
onClosePopover() {
|
||||||
|
dispatch(closePopover())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ChatMessageOptionsPopover.propTypes = {
|
||||||
|
isXS: PropTypes.bool,
|
||||||
|
chatMessageId: PropTypes.string.isRequired,
|
||||||
|
isBlocked: PropTypes.bool.isRequired,
|
||||||
|
isMuted: PropTypes.bool.isRequired,
|
||||||
|
onDeleteChatMessage: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageOptionsPopover)
|
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { closePopover } from '../../actions/popover'
|
||||||
|
import PopoverLayout from './popover_layout'
|
||||||
|
import List from '../list'
|
||||||
|
import Text from '../text'
|
||||||
|
|
||||||
|
class ComposePostDesinationPopover extends React.PureComponent {
|
||||||
|
|
||||||
|
handleOnClosePopover = () => {
|
||||||
|
this.props.onClosePopover()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isXS,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
// TIMELINE
|
||||||
|
// GROUP - MY GROUPS
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'Timeline',
|
||||||
|
onClick: () => this.handleOnDelete(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Group',
|
||||||
|
onClick: () => this.handleOnReport(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverLayout
|
||||||
|
width={180}
|
||||||
|
isXS={isXS}
|
||||||
|
onClose={this.handleOnClosePopover}
|
||||||
|
>
|
||||||
|
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>Post to:</Text>
|
||||||
|
<List items={items} />
|
||||||
|
</PopoverLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
//
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onClosePopover: () => dispatch(closePopover()),
|
||||||
|
})
|
||||||
|
|
||||||
|
ComposePostDesinationPopover.propTypes = {
|
||||||
|
isXS: PropTypes.bool,
|
||||||
|
onClosePopover: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ComposePostDesinationPopover)
|
@ -1,8 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
POPOVER_CHAT_CONVERSATION_OPTIONS,
|
POPOVER_CHAT_CONVERSATION_OPTIONS,
|
||||||
POPOVER_CHAT_MESSAGE_DELETE,
|
POPOVER_CHAT_MESSAGE_OPTIONS,
|
||||||
POPOVER_COMMENT_SORTING_OPTIONS,
|
POPOVER_COMMENT_SORTING_OPTIONS,
|
||||||
|
POPOVER_COMPOSE_POST_DESTINATION,
|
||||||
POPOVER_DATE_PICKER,
|
POPOVER_DATE_PICKER,
|
||||||
POPOVER_EMOJI_PICKER,
|
POPOVER_EMOJI_PICKER,
|
||||||
POPOVER_GROUP_LIST_SORT_OPTIONS,
|
POPOVER_GROUP_LIST_SORT_OPTIONS,
|
||||||
@ -23,8 +24,9 @@ import {
|
|||||||
} from '../../constants'
|
} from '../../constants'
|
||||||
import {
|
import {
|
||||||
ChatConversationOptionsPopover,
|
ChatConversationOptionsPopover,
|
||||||
ChatMessageDeletePopover,
|
ChatMessageOptionsPopover,
|
||||||
CommentSortingOptionsPopover,
|
CommentSortingOptionsPopover,
|
||||||
|
ComposePostDesinationPopover,
|
||||||
DatePickerPopover,
|
DatePickerPopover,
|
||||||
EmojiPickerPopover,
|
EmojiPickerPopover,
|
||||||
GroupListSortOptionsPopover,
|
GroupListSortOptionsPopover,
|
||||||
@ -59,8 +61,9 @@ const initialState = getWindowDimension()
|
|||||||
|
|
||||||
const POPOVER_COMPONENTS = {
|
const POPOVER_COMPONENTS = {
|
||||||
[POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
|
[POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
|
||||||
[POPOVER_CHAT_MESSAGE_DELETE]: ChatMessageDeletePopover,
|
[POPOVER_CHAT_MESSAGE_OPTIONS]: ChatMessageOptionsPopover,
|
||||||
[POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
|
[POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
|
||||||
|
[POPOVER_COMPOSE_POST_DESTINATION]: ComposePostDesinationPopover,
|
||||||
[POPOVER_DATE_PICKER]: DatePickerPopover,
|
[POPOVER_DATE_PICKER]: DatePickerPopover,
|
||||||
[POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
|
[POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
|
||||||
[POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,
|
[POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,
|
||||||
|
@ -5,15 +5,16 @@ import { defineMessages, injectIntl } from 'react-intl'
|
|||||||
import { closePopover } from '../../actions/popover'
|
import { closePopover } from '../../actions/popover'
|
||||||
import { changeExpiresAt } from '../../actions/compose'
|
import { changeExpiresAt } from '../../actions/compose'
|
||||||
import {
|
import {
|
||||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
EXPIRATION_OPTION_5_MINUTES,
|
||||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
EXPIRATION_OPTION_60_MINUTES,
|
||||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
EXPIRATION_OPTION_6_HOURS,
|
||||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
EXPIRATION_OPTION_24_HOURS,
|
||||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
EXPIRATION_OPTION_3_DAYS,
|
||||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
EXPIRATION_OPTION_7_DAYS,
|
||||||
} from '../../constants'
|
} from '../../constants'
|
||||||
import PopoverLayout from './popover_layout'
|
import PopoverLayout from './popover_layout'
|
||||||
import List from '../list'
|
import List from '../list'
|
||||||
|
import Text from '../text'
|
||||||
|
|
||||||
class StatusExpirationOptionsPopover extends React.PureComponent {
|
class StatusExpirationOptionsPopover extends React.PureComponent {
|
||||||
|
|
||||||
@ -34,43 +35,49 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
|
|||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const listItems = [
|
const listItems = [
|
||||||
|
{
|
||||||
|
hideArrow: true,
|
||||||
|
title: 'None',
|
||||||
|
onClick: () => this.handleOnSetStatusExpiration(null),
|
||||||
|
isActive: !expiresAtValue,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: intl.formatMessage(messages.minutes, { number: 5 }),
|
title: intl.formatMessage(messages.minutes, { number: 5 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_5_MINUTES),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_5_MINUTES,
|
isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: intl.formatMessage(messages.minutes, { number: 60 }),
|
title: intl.formatMessage(messages.minutes, { number: 60 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_60_MINUTES),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_60_MINUTES,
|
isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: '6 hours',
|
title: '6 hours',
|
||||||
title: intl.formatMessage(messages.hours, { number: 6 }),
|
title: intl.formatMessage(messages.hours, { number: 6 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_6_HOURS),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_6_HOURS,
|
isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: intl.formatMessage(messages.hours, { number: 24 }),
|
title: intl.formatMessage(messages.hours, { number: 24 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_24_HOURS),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_24_HOURS,
|
isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: '3 days',
|
title: '3 days',
|
||||||
title: intl.formatMessage(messages.days, { number: 3 }),
|
title: intl.formatMessage(messages.days, { number: 3 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_3_DAYS),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_3_DAYS,
|
isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
title: intl.formatMessage(messages.days, { number: 7 }),
|
title: intl.formatMessage(messages.days, { number: 7 }),
|
||||||
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_7_DAYS),
|
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
|
||||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_7_DAYS,
|
isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -88,8 +95,9 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
|
|||||||
isXS={isXS}
|
isXS={isXS}
|
||||||
onClose={this.handleOnClosePopover}
|
onClose={this.handleOnClosePopover}
|
||||||
>
|
>
|
||||||
|
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>This gab deletes after:</Text>
|
||||||
<List
|
<List
|
||||||
scrollKey='group_list_sort_options'
|
scrollKey='status_expiration'
|
||||||
items={listItems}
|
items={listItems}
|
||||||
size={isXS ? 'large' : 'small'}
|
size={isXS ? 'large' : 'small'}
|
||||||
/>
|
/>
|
||||||
|
@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
import { openModal } from '../../actions/modal'
|
import { openModal } from '../../actions/modal'
|
||||||
|
import { showToast } from '../../actions/toasts'
|
||||||
import { closePopover } from '../../actions/popover'
|
import { closePopover } from '../../actions/popover'
|
||||||
import PopoverLayout from './popover_layout'
|
import PopoverLayout from './popover_layout'
|
||||||
import Button from '../button'
|
import Button from '../button'
|
||||||
@ -31,6 +32,7 @@ class StatusSharePopover extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.body.removeChild(textarea)
|
document.body.removeChild(textarea)
|
||||||
|
this.props.onShowCopyToast()
|
||||||
this.handleClosePopover()
|
this.handleClosePopover()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +159,9 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
onClosePopover: () => dispatch(closePopover()),
|
onClosePopover: () => dispatch(closePopover()),
|
||||||
|
onShowCopyToast() {
|
||||||
|
dispatch(showToast())
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
StatusSharePopover.propTypes = {
|
StatusSharePopover.propTypes = {
|
||||||
|
@ -49,6 +49,7 @@ class StatusVisibilityDropdown extends React.PureComponent {
|
|||||||
isXS={isXS}
|
isXS={isXS}
|
||||||
onClose={this.handleOnClosePopover}
|
onClose={this.handleOnClosePopover}
|
||||||
>
|
>
|
||||||
|
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>Status Visibility:</Text>
|
||||||
<div className={[_s.d].join(' ')}>
|
<div className={[_s.d].join(' ')}>
|
||||||
{
|
{
|
||||||
options.map((option, i) => {
|
options.map((option, i) => {
|
||||||
|
@ -148,7 +148,7 @@ class StyleButton extends React.PureComponent {
|
|||||||
px10: 1,
|
px10: 1,
|
||||||
mr5: 1,
|
mr5: 1,
|
||||||
noSelect: 1,
|
noSelect: 1,
|
||||||
bgSecondaryDark_onHover: 1,
|
bgSubtle_onHover: 1,
|
||||||
bgBrandLight: active,
|
bgBrandLight: active,
|
||||||
bgTransparent: 1,
|
bgTransparent: 1,
|
||||||
radiusSmall: 1,
|
radiusSmall: 1,
|
||||||
@ -162,7 +162,7 @@ class StyleButton extends React.PureComponent {
|
|||||||
onMouseDown={this.handleOnClick}
|
onMouseDown={this.handleOnClick}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<Icon id={icon} size='12px' className={_s[iconColor]} />
|
<Icon id={icon} size='16px' className={_s[iconColor]} />
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,16 @@ class DeckSidebar extends ImmutablePureComponent {
|
|||||||
this.props.onOpenComposeModal()
|
this.props.onOpenComposeModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToItem = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
setAvatarNode = (c) => {
|
setAvatarNode = (c) => {
|
||||||
this.avatarNode = c
|
this.avatarNode = c
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account, logoDisabled } = this.props
|
const { account, gabDeckOrder, logoDisabled } = this.props
|
||||||
|
|
||||||
const isPro = !!account ? account.get('is_pro') : false
|
const isPro = !!account ? account.get('is_pro') : false
|
||||||
|
|
||||||
@ -83,6 +87,22 @@ class DeckSidebar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<Divider isSmall />
|
<Divider isSmall />
|
||||||
|
|
||||||
|
<div className={[_s.d, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
|
{
|
||||||
|
!!gabDeckOrder && gabDeckOrder.map((item, i) => (
|
||||||
|
<Button
|
||||||
|
isText
|
||||||
|
key={`gab-deck-sidebar-dot-${i}`}
|
||||||
|
onClick={this.scrollToItem}
|
||||||
|
backgroundColor='secondary'
|
||||||
|
className={[_s.mt5, _s.mb5, _s.px10, _s.py10, _s.circle].join(' ')}
|
||||||
|
icon='notifications'
|
||||||
|
iconClassName={_s.cPrimary}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Divider isSmall />
|
<Divider isSmall />
|
||||||
|
|
||||||
{ isPro && <NavigationBarButton title=' ' icon='add' onClick={this.handleOnOpenNewColumnModel} /> }
|
{ isPro && <NavigationBarButton title=' ' icon='add' onClick={this.handleOnOpenNewColumnModel} /> }
|
||||||
@ -119,6 +139,7 @@ const mapStateToProps = (state) => ({
|
|||||||
account: makeGetAccount()(state, me),
|
account: makeGetAccount()(state, me),
|
||||||
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
|
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
|
||||||
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
|
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
|
||||||
|
gabDeckOrder: state.getIn(['settings', 'gabDeckOrder']),
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
@ -10,12 +10,14 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co
|
|||||||
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
|
||||||
import Responsive from '../features/ui/util/responsive_component'
|
import Responsive from '../features/ui/util/responsive_component'
|
||||||
import Avatar from './avatar'
|
import Avatar from './avatar'
|
||||||
import Heading from './heading'
|
import Button from './button'
|
||||||
|
import Text from './text'
|
||||||
|
|
||||||
class TimelineComposeBlock extends ImmutablePureComponent {
|
class TimelineComposeBlock extends ImmutablePureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
formLocation,
|
||||||
account,
|
account,
|
||||||
size,
|
size,
|
||||||
intl,
|
intl,
|
||||||
@ -27,7 +29,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<section className={_s.d}>
|
<section className={_s.d}>
|
||||||
<div className={[_s.d, _s.flexRow].join(' ')}>
|
<div className={[_s.d, _s.flexRow].join(' ')}>
|
||||||
<ComposeFormContainer {...rest} isModal={isModal} />
|
<ComposeFormContainer {...rest} isModal={isModal} formLocation={formLocation} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -39,17 +41,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
|
|||||||
classNames={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden, _s.radiusSmall].join(' ')}
|
classNames={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden, _s.radiusSmall].join(' ')}
|
||||||
classNamesXS={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden].join(' ')}
|
classNamesXS={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden].join(' ')}
|
||||||
>
|
>
|
||||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
<ComposeFormContainer {...rest} formLocation={formLocation} />
|
||||||
<div className={[_s.d, _s.bgSubtle, _s.borderTop1PX, _s.borderBottom1PX, _s.borderColorSecondary, _s.px15, _s.py2, _s.aiCenter, _s.flexRow].join(' ')}>
|
|
||||||
<div className={_s.mr10}>
|
|
||||||
<Avatar account={account} size={20} noHover />
|
|
||||||
</div>
|
|
||||||
<Heading size='h5'>
|
|
||||||
{intl.formatMessage(messages.createPost)}
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
</Responsive>
|
|
||||||
<ComposeFormContainer {...rest} />
|
|
||||||
</ResponsiveClassesComponent>
|
</ResponsiveClassesComponent>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -70,10 +62,12 @@ TimelineComposeBlock.propTypes = {
|
|||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
isModal: PropTypes.bool,
|
isModal: PropTypes.bool,
|
||||||
|
formLocation: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineComposeBlock.defaultProps = {
|
TimelineComposeBlock.defaultProps = {
|
||||||
size: 32,
|
size: 32,
|
||||||
|
formLocation: 'timeline',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps)(TimelineComposeBlock))
|
export default injectIntl(connect(mapStateToProps)(TimelineComposeBlock))
|
@ -10,7 +10,6 @@ export const BREAKPOINT_LARGE = 1280
|
|||||||
export const BREAKPOINT_MEDIUM = 1160
|
export const BREAKPOINT_MEDIUM = 1160
|
||||||
export const BREAKPOINT_SMALL = 1080
|
export const BREAKPOINT_SMALL = 1080
|
||||||
export const BREAKPOINT_EXTRA_SMALL = 992
|
export const BREAKPOINT_EXTRA_SMALL = 992
|
||||||
export const BREAKPOINT_EXTRA_EXTRA_SMALL = 767
|
|
||||||
|
|
||||||
export const MOUSE_IDLE_DELAY = 300
|
export const MOUSE_IDLE_DELAY = 300
|
||||||
|
|
||||||
@ -26,8 +25,9 @@ export const URL_GAB_PRO = 'https://pro.gab.com'
|
|||||||
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
|
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
|
||||||
|
|
||||||
export const POPOVER_CHAT_CONVERSATION_OPTIONS = 'CHAT_CONVERSATION_OPTIONS'
|
export const POPOVER_CHAT_CONVERSATION_OPTIONS = 'CHAT_CONVERSATION_OPTIONS'
|
||||||
export const POPOVER_CHAT_MESSAGE_DELETE = 'CHAT_MESSAGE_DELETE'
|
export const POPOVER_CHAT_MESSAGE_OPTIONS = 'CHAT_MESSAGE_OPTIONS'
|
||||||
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
|
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
|
||||||
|
export const POPOVER_COMPOSE_POST_DESTINATION = 'COMPOSE_POST_DESTINATION'
|
||||||
export const POPOVER_DATE_PICKER = 'DATE_PICKER'
|
export const POPOVER_DATE_PICKER = 'DATE_PICKER'
|
||||||
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
|
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
|
||||||
export const POPOVER_GROUP_LIST_SORT_OPTIONS = 'GROUP_LIST_SORT_OPTIONS'
|
export const POPOVER_GROUP_LIST_SORT_OPTIONS = 'GROUP_LIST_SORT_OPTIONS'
|
||||||
@ -54,6 +54,7 @@ export const MODAL_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS'
|
|||||||
export const MODAL_COMPOSE = 'COMPOSE'
|
export const MODAL_COMPOSE = 'COMPOSE'
|
||||||
export const MODAL_CONFIRM = 'CONFIRM'
|
export const MODAL_CONFIRM = 'CONFIRM'
|
||||||
export const MODAL_DECK_COLUMN_ADD = 'DECK_COLUMN_ADD'
|
export const MODAL_DECK_COLUMN_ADD = 'DECK_COLUMN_ADD'
|
||||||
|
export const MODAL_DECK_COLUMN_ADD_OPTIONS = 'DECK_COLUMN_ADD_OPTIONS'
|
||||||
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
|
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
|
||||||
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
|
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
|
||||||
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
|
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
|
||||||
@ -130,12 +131,12 @@ export const GAB_COM_INTRODUCE_YOURSELF_GROUP_ID = '12'
|
|||||||
|
|
||||||
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
|
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
|
||||||
|
|
||||||
export const STATUS_EXPIRATION_OPTION_5_MINUTES = '5-minutes'
|
export const EXPIRATION_OPTION_5_MINUTES = 'five_minutes'
|
||||||
export const STATUS_EXPIRATION_OPTION_60_MINUTES = '60-minutes'
|
export const EXPIRATION_OPTION_60_MINUTES = 'one_hour'
|
||||||
export const STATUS_EXPIRATION_OPTION_6_HOURS = '6-hours'
|
export const EXPIRATION_OPTION_6_HOURS = 'six_hours'
|
||||||
export const STATUS_EXPIRATION_OPTION_24_HOURS = '24-hours'
|
export const EXPIRATION_OPTION_24_HOURS = 'one_day'
|
||||||
export const STATUS_EXPIRATION_OPTION_3_DAYS = '3-days'
|
export const EXPIRATION_OPTION_3_DAYS = 'three_days'
|
||||||
export const STATUS_EXPIRATION_OPTION_7_DAYS = '7-days'
|
export const EXPIRATION_OPTION_7_DAYS = 'one_week'
|
||||||
|
|
||||||
export const GROUP_TIMELINE_SORTING_TYPE_HOT = 'hot'
|
export const GROUP_TIMELINE_SORTING_TYPE_HOT = 'hot'
|
||||||
export const GROUP_TIMELINE_SORTING_TYPE_NEWEST = 'newest'
|
export const GROUP_TIMELINE_SORTING_TYPE_NEWEST = 'newest'
|
||||||
|
64
app/javascript/gabsocial/features/bookmark_collections.js
Normal file
64
app/javascript/gabsocial/features/bookmark_collections.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 { fetchBookmarkCollections } from '../actions/bookmarks'
|
||||||
|
import ColumnIndicator from '../components/column_indicator'
|
||||||
|
import List from '../components/list'
|
||||||
|
|
||||||
|
class BookmarkCollections extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.onFetchBookmarkCollections()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
bookmarkCollections,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ColumnIndicator type='error' message='Error fetching bookmark collections' />
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItems = shortcuts.map((s) => ({
|
||||||
|
to: s.get('to'),
|
||||||
|
title: s.get('title'),
|
||||||
|
image: s.get('image'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
scrollKey='bookmark-collections'
|
||||||
|
emptyMessage='You have no bookmark collections'
|
||||||
|
items={listItems}
|
||||||
|
showLoading={isLoading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
isError: state.getIn(['bookmark_collections', 'isError']),
|
||||||
|
isLoading: state.getIn(['bookmark_collections', 'isLoading']),
|
||||||
|
shortcuts: state.getIn(['bookmark_collections', 'items']),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onFetchBookmarkCollections() {
|
||||||
|
dispatch(fetchBookmarkCollections())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
BookmarkCollections.propTypes = {
|
||||||
|
isLoading: PropTypes.bool.isRequired,
|
||||||
|
isError: PropTypes.bool.isRequired,
|
||||||
|
onFetchBookmarkCollections: PropTypes.func.isRequired,
|
||||||
|
bookmarkCollections: ImmutablePropTypes.list,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(BookmarkCollections)
|
@ -1,75 +1,94 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
import { connect } from 'react-redux'
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
import { length } from 'stringz'
|
|
||||||
import { isMobile } from '../../../utils/is_mobile'
|
|
||||||
import { countableText } from '../../ui/util/counter'
|
|
||||||
import {
|
import {
|
||||||
CX,
|
CX,
|
||||||
MAX_POST_CHARACTER_COUNT,
|
MODAL_COMPOSE,
|
||||||
ALLOWED_AROUND_SHORT_CODE,
|
POPOVER_COMPOSE_POST_DESTINATION,
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
|
||||||
BREAKPOINT_EXTRA_EXTRA_SMALL,
|
|
||||||
BREAKPOINT_MEDIUM,
|
|
||||||
} from '../../../constants'
|
} from '../../../constants'
|
||||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
import { openModal } from '../../../actions/modal'
|
||||||
import Responsive from '../../ui/util/responsive_component'
|
import { openPopover } from '../../../actions/popover'
|
||||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
|
||||||
import Avatar from '../../../components/avatar'
|
import Avatar from '../../../components/avatar'
|
||||||
import Button from '../../../components/button'
|
import Button from '../../../components/button'
|
||||||
import EmojiPickerButton from './emoji_picker_button'
|
|
||||||
import PollButton from './poll_button'
|
|
||||||
import PollForm from './poll_form'
|
|
||||||
import SchedulePostButton from './schedule_post_button'
|
|
||||||
import SpoilerButton from './spoiler_button'
|
|
||||||
import ExpiresPostButton from './expires_post_button'
|
|
||||||
import RichTextEditorButton from './rich_text_editor_button'
|
|
||||||
import StatusContainer from '../../../containers/status_container'
|
|
||||||
import StatusVisibilityButton from './status_visibility_button'
|
|
||||||
import UploadButton from './media_upload_button'
|
|
||||||
import UploadForm from './upload_form'
|
|
||||||
import Input from '../../../components/input'
|
|
||||||
import Text from '../../../components/text'
|
|
||||||
import Icon from '../../../components/icon'
|
import Icon from '../../../components/icon'
|
||||||
import ComposeExtraButtonList from './compose_extra_button_list'
|
import Text from '../../../components/text'
|
||||||
|
|
||||||
class ComposeDestinationHeader extends ImmutablePureComponent {
|
class ComposeDestinationHeader extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleOnClick = () => {
|
handleOnClick = () => {
|
||||||
|
this.props.onOpenPopover(this.desinationBtn)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnExpand = () => {
|
||||||
|
this.props.onOpenModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
setDestinationBtn = (c) => {
|
||||||
|
this.desinationBtn = c
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account } = this.props
|
const { account, isModal } = this.props
|
||||||
|
|
||||||
const title = 'Post to timeline'
|
const title = 'Post to timeline'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.bgPrimary, _s.borderBottom1PX, _s.borderTop1PX, _s.borderColorSecondary, _s.mb5, _s.mt5, _s.px15, _s.w100PC, _s.h40PX].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.bgPrimary, _s.w100PC, _s.h40PX, _s.pr15].join(' ')}>
|
||||||
<Avatar account={account} size={28} />
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.pl15, _s.flexGrow1, _s.mrAuto, _s.h40PX].join(' ')}>
|
||||||
<div className={[_s.ml15].join(' ')}>
|
<Avatar account={account} size={28} />
|
||||||
<Button
|
<div className={[_s.ml15].join(' ')}>
|
||||||
isNarrow
|
<Button
|
||||||
radiusSmall
|
isNarrow
|
||||||
backgroundColor='tertiary'
|
isOutline
|
||||||
color='primary'
|
radiusSmall
|
||||||
onClick={this.handleOnClick}
|
buttonRef={this.setDestinationBtn}
|
||||||
>
|
backgroundColor='secondary'
|
||||||
<Text color='inherit' size='small' className={_s.jcCenter}>
|
color='primary'
|
||||||
{title}
|
onClick={this.handleOnClick}
|
||||||
<Icon id='caret-down' size='8px' className={_s.ml5} />
|
className={[_s.border1PX, _s.borderColorPrimary].join(' ')}
|
||||||
</Text>
|
>
|
||||||
</Button>
|
<Text color='inherit' size='small' className={_s.jcCenter}>
|
||||||
|
{title}
|
||||||
|
<Icon id='caret-down' size='8px' className={_s.ml5} />
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
!isModal &&
|
||||||
|
<Button
|
||||||
|
isText
|
||||||
|
isNarrow
|
||||||
|
backgroundColor='none'
|
||||||
|
color='tertiary'
|
||||||
|
icon='fullscreen'
|
||||||
|
onClick={this.handleOnExpand}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onOpenModal() {
|
||||||
|
dispatch(openModal(MODAL_COMPOSE))
|
||||||
|
},
|
||||||
|
onOpenPopover(targetRef) {
|
||||||
|
dispatch(openPopover(POPOVER_COMPOSE_POST_DESTINATION, {
|
||||||
|
targetRef,
|
||||||
|
position: 'bottom',
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
ComposeDestinationHeader.propTypes = {
|
ComposeDestinationHeader.propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
|
isModal: PropTypes.bool,
|
||||||
|
onOpenModal: PropTypes.func.isRequired,
|
||||||
|
onOpenPopover: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ComposeDestinationHeader
|
export default connect(null, mapDispatchToProps)(ComposeDestinationHeader)
|
@ -22,14 +22,14 @@ class ComposeExtraButton extends React.PureComponent {
|
|||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
mr5: 1,
|
|
||||||
jcCenter: 1,
|
jcCenter: 1,
|
||||||
h40PX: 1,
|
h40PX: 1,
|
||||||
|
mr5: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const btnClasses = CX({
|
const btnClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
circle: 1,
|
circle: small,
|
||||||
noUnderline: 1,
|
noUnderline: 1,
|
||||||
font: 1,
|
font: 1,
|
||||||
cursorPointer: 1,
|
cursorPointer: 1,
|
||||||
@ -37,21 +37,25 @@ class ComposeExtraButton extends React.PureComponent {
|
|||||||
outlineNone: 1,
|
outlineNone: 1,
|
||||||
bgTransparent: 1,
|
bgTransparent: 1,
|
||||||
flexRow: 1,
|
flexRow: 1,
|
||||||
|
aiCenter: 1,
|
||||||
|
// jcCenter: !small,
|
||||||
bgSubtle_onHover: !active,
|
bgSubtle_onHover: !active,
|
||||||
bgBrandLight: active,
|
bgBrandLight: active,
|
||||||
py10: 1,
|
py10: 1,
|
||||||
px10: 1,
|
px10: small,
|
||||||
|
radiusSmall: !small,
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClasses = CX(iconClassName, {
|
const iconClasses = CX(active ? null : iconClassName, {
|
||||||
cSecondary: !active,
|
cSecondary: !active,
|
||||||
cWhite: active,
|
cWhite: active,
|
||||||
mr10: 1,
|
mr10: !small,
|
||||||
py2: small,
|
py2: small,
|
||||||
ml10: small,
|
ml10: !small,
|
||||||
|
px2: small,
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconSize = !small ? '18px' : '16px'
|
const iconSize = '16px'
|
||||||
const textColor = !active ? 'primary' : 'white'
|
const textColor = !active ? 'primary' : 'white'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -65,13 +69,13 @@ class ComposeExtraButton extends React.PureComponent {
|
|||||||
backgroundColor='none'
|
backgroundColor='none'
|
||||||
iconClassName={iconClasses}
|
iconClassName={iconClasses}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
iconSize={iconSize}
|
iconSize='16px'
|
||||||
buttonRef={!children ? buttonRef : undefined}
|
buttonRef={!children ? buttonRef : undefined}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
{
|
{
|
||||||
!small &&
|
!small &&
|
||||||
<Text color={textColor} weight='medium' className={[_s.pr5].join(' ')}>
|
<Text color={textColor} weight='medium' className={[_s.pr10].join(' ')}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '../../../constants'
|
} from '../../../constants'
|
||||||
import Responsive from '../../ui/util/responsive_component'
|
import Responsive from '../../ui/util/responsive_component'
|
||||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
||||||
|
import Text from '../../../components/text'
|
||||||
import EmojiPickerButton from './emoji_picker_button'
|
import EmojiPickerButton from './emoji_picker_button'
|
||||||
import PollButton from './poll_button'
|
import PollButton from './poll_button'
|
||||||
import SchedulePostButton from './schedule_post_button'
|
import SchedulePostButton from './schedule_post_button'
|
||||||
@ -22,6 +23,7 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
height: initialState.height,
|
height: initialState.height,
|
||||||
|
width: initialState.width,
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -31,9 +33,9 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = () => {
|
||||||
const { height } = getWindowDimension()
|
const { height, width } = getWindowDimension()
|
||||||
|
|
||||||
this.setState({ height })
|
this.setState({ height, width })
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -48,26 +50,33 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||||||
edit,
|
edit,
|
||||||
hidePro,
|
hidePro,
|
||||||
isModal,
|
isModal,
|
||||||
isStandalone,
|
formLocation,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { height } = this.state
|
const { height, width } = this.state
|
||||||
|
|
||||||
const small = (height <= 660 || isModal) && !isStandalone
|
const isXS = width <= BREAKPOINT_EXTRA_SMALL
|
||||||
|
const isStandalone = formLocation === 'standalone'
|
||||||
|
const isTimeline = formLocation === 'timeline'
|
||||||
|
const small = (!isModal && isXS && !isStandalone) || isTimeline
|
||||||
|
|
||||||
|
console.log("small, formLocation:", small, formLocation)
|
||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
w100PC: 1,
|
w100PC: 1,
|
||||||
bgPrimary: 1,
|
bgPrimary: 1,
|
||||||
px15: 1,
|
px5: 1,
|
||||||
py10: 1,
|
py5: 1,
|
||||||
|
mb10: 1,
|
||||||
mtAuto: 1,
|
mtAuto: 1,
|
||||||
boxShadowBlockY: 1,
|
radiusSmall: 1,
|
||||||
topLeftRadiusSmall: 1,
|
borderTop1PX: 1,
|
||||||
|
borderBottom1PX: 1,
|
||||||
|
boxShadowBlock: 1,
|
||||||
borderColorSecondary: 1,
|
borderColorSecondary: 1,
|
||||||
topRightRadiusSmall: 1,
|
flexWrap: 1,
|
||||||
flexRow: small,
|
flexRow: (small || !isTimeline || isXS) && !isStandalone,
|
||||||
overflowXScroll: small,
|
jcSpaceAround: isXS,
|
||||||
noScrollbar: small,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -79,7 +88,7 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||||||
<SpoilerButton small={small} />
|
<SpoilerButton small={small} />
|
||||||
{ !hidePro && !edit && <SchedulePostButton small={small} /> }
|
{ !hidePro && !edit && <SchedulePostButton small={small} /> }
|
||||||
{ !hidePro && !edit && <ExpiresPostButton small={small} /> }
|
{ !hidePro && !edit && <ExpiresPostButton small={small} /> }
|
||||||
{ !hidePro && <RichTextEditorButton small={small} /> }
|
{ !hidePro && !isXS && <RichTextEditorButton small={small} /> }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -90,7 +99,7 @@ ComposeExtraButtonList.propTypes = {
|
|||||||
edit: PropTypes.bool,
|
edit: PropTypes.bool,
|
||||||
isMatch: PropTypes.bool,
|
isMatch: PropTypes.bool,
|
||||||
isModal: PropTypes.bool,
|
isModal: PropTypes.bool,
|
||||||
isStandalone: PropTypes.bool,
|
formLocation: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ComposeExtraButtonList
|
export default ComposeExtraButtonList
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||||
@ -11,7 +12,6 @@ import {
|
|||||||
MAX_POST_CHARACTER_COUNT,
|
MAX_POST_CHARACTER_COUNT,
|
||||||
ALLOWED_AROUND_SHORT_CODE,
|
ALLOWED_AROUND_SHORT_CODE,
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
BREAKPOINT_EXTRA_EXTRA_SMALL,
|
|
||||||
BREAKPOINT_MEDIUM,
|
BREAKPOINT_MEDIUM,
|
||||||
} from '../../../constants'
|
} from '../../../constants'
|
||||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
||||||
@ -62,19 +62,17 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleComposeFocus = () => {
|
handleComposeFocus = () => {
|
||||||
this.setState({
|
this.setState({ composeFocused: true })
|
||||||
composeFocused: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
handleKeyDown = (e) => {
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
this.handleSubmit();
|
this.handleSubmit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = (e) => {
|
handleClick = (e) => {
|
||||||
const { isStandalone, isModalOpen, shouldCondense } = this.props
|
const { isModalOpen, shouldCondense } = this.props
|
||||||
|
|
||||||
if (!this.form) return false
|
if (!this.form) return false
|
||||||
if (e.target) {
|
if (e.target) {
|
||||||
@ -82,60 +80,52 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
if (!this.form.contains(e.target)) {
|
if (!this.form.contains(e.target)) {
|
||||||
this.handleClickOutside()
|
this.handleClickOutside()
|
||||||
} else {
|
|
||||||
// : todo :
|
|
||||||
// if mobile go to /compose else openModal
|
|
||||||
if (!isStandalone && !isModalOpen && !shouldCondense) {
|
|
||||||
this.props.openComposeModal()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside = () => {
|
handleClickOutside = () => {
|
||||||
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props;
|
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props
|
||||||
const condensed = shouldCondense && !text;
|
const condensed = shouldCondense && !text
|
||||||
|
|
||||||
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
|
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
|
||||||
this.props.setScheduledAt(null);
|
this.props.setScheduledAt(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({ composeFocused: false })
|
||||||
composeFocused: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
|
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
|
||||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||||
// Update the state to match the current text
|
// Update the state to match the current text
|
||||||
// this.props.onChange(this.autosuggestTextarea.textbox.value);
|
// this.props.onChange(this.autosuggestTextarea.textbox.value)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Submit disabled:
|
// Submit disabled:
|
||||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia, groupId, autoJoinGroup } = this.props
|
const { isSubmitting, isChangingUpload, isUploading, anyMedia, groupId, autoJoinGroup } = this.props
|
||||||
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
|
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('')
|
||||||
|
|
||||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > MAX_POST_CHARACTER_COUNT || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > MAX_POST_CHARACTER_COUNT || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSubmit(groupId, this.props.replyToId, this.context.router, autoJoinGroup)
|
this.props.onSubmit(groupId, this.props.replyToId, this.context.router, autoJoinGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
onSuggestionsClearRequested = () => {
|
||||||
this.props.onClearSuggestions();
|
this.props.onClearSuggestions()
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionsFetchRequested = (token) => {
|
onSuggestionsFetchRequested = (token) => {
|
||||||
this.props.onFetchSuggestions(token);
|
this.props.onFetchSuggestions(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionSelected = (tokenStart, token, value) => {
|
onSuggestionSelected = (tokenStart, token, value) => {
|
||||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
this.props.onSuggestionSelected(tokenStart, token, value, ['text'])
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
||||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
|
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text'])
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeSpoilerText = (value) => {
|
handleChangeSpoilerText = (value) => {
|
||||||
@ -143,11 +133,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('click', this.handleClick, false);
|
document.addEventListener('click', this.handleClick, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.handleClick, false);
|
document.removeEventListener('click', this.handleClick, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -156,24 +146,24 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
// This statement does several things:
|
// This statement does several things:
|
||||||
// - If we're beginning a reply, and,
|
// - If we're beginning a reply, and,
|
||||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||||
// - Replying to more than one user, selects any usernames past the first;
|
// - Replying to more than one user, selects any usernames past the first
|
||||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||||
if (this.props.focusDate !== prevProps.focusDate) {
|
if (this.props.focusDate !== prevProps.focusDate) {
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart
|
||||||
|
|
||||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||||
selectionEnd = this.props.text.length;
|
selectionEnd = this.props.text.length
|
||||||
selectionStart = this.props.text.search(/\s/) + 1;
|
selectionStart = this.props.text.search(/\s/) + 1
|
||||||
} else if (typeof this.props.caretPosition === 'number') {
|
} else if (typeof this.props.caretPosition === 'number') {
|
||||||
selectionStart = this.props.caretPosition;
|
selectionStart = this.props.caretPosition
|
||||||
selectionEnd = this.props.caretPosition;
|
selectionEnd = this.props.caretPosition
|
||||||
} else {
|
} else {
|
||||||
selectionEnd = this.props.text.length;
|
selectionEnd = this.props.text.length
|
||||||
selectionStart = selectionEnd;
|
selectionStart = selectionEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd);
|
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd)
|
||||||
// this.autosuggestTextarea.textbox.focus();
|
// this.autosuggestTextarea.textbox.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +180,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
intl,
|
intl,
|
||||||
account,
|
account,
|
||||||
onPaste,
|
onPaste,
|
||||||
showSearch,
|
|
||||||
anyMedia,
|
anyMedia,
|
||||||
shouldCondense,
|
shouldCondense,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
@ -208,218 +197,151 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
isPro,
|
isPro,
|
||||||
hidePro,
|
hidePro,
|
||||||
isStandalone,
|
dontOpenModal,
|
||||||
|
formLocation,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const disabled = isSubmitting
|
const disabled = isSubmitting
|
||||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
const text = [this.props.spoilerText, countableText(this.props.text)].join('')
|
||||||
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
|
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
|
||||||
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
|
const shouldAutoFocus = autoFocus && !isMobile(window.innerWidth)
|
||||||
|
|
||||||
const parentContainerClasses = CX({
|
const containerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
w100PC: 1,
|
pb10: 1,
|
||||||
flexRow: !shouldCondense,
|
calcMaxH410PX: 1,
|
||||||
pb10: !shouldCondense,
|
minH200PX: isModalOpen && isMatch,
|
||||||
|
overflowYScroll: 1,
|
||||||
|
boxShadowBlock: 1,
|
||||||
|
borderTop1PX: 1,
|
||||||
|
borderColorSecondary: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const childContainerClasses = CX({
|
const textbox = (
|
||||||
d: 1,
|
<AutosuggestTextbox
|
||||||
flexWrap: 1,
|
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
||||||
overflowHidden: 1,
|
placeholder={intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
|
||||||
flex1: 1,
|
disabled={disabled}
|
||||||
minH28PX: 1,
|
value={this.props.text}
|
||||||
py2: shouldCondense,
|
valueMarkdown={this.props.markdown}
|
||||||
aiEnd: shouldCondense,
|
onChange={this.handleChange}
|
||||||
flexRow: shouldCondense,
|
suggestions={this.props.suggestions}
|
||||||
radiusSmall: shouldCondense,
|
onKeyDown={this.handleKeyDown}
|
||||||
bgSubtle: shouldCondense,
|
onFocus={this.handleComposeFocus}
|
||||||
px5: shouldCondense,
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
})
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
const actionsContainerClasses = CX({
|
onPaste={onPaste}
|
||||||
d: 1,
|
autoFocus={shouldAutoFocus}
|
||||||
flexRow: 1,
|
small={shouldCondense}
|
||||||
aiCenter: !shouldCondense,
|
isModalOpen={isModalOpen}
|
||||||
aiStart: shouldCondense,
|
isPro={isPro}
|
||||||
mt10: !shouldCondense,
|
isEdit={!!edit}
|
||||||
px10: !shouldCondense,
|
id='main-composer'
|
||||||
mlAuto: shouldCondense,
|
/>
|
||||||
flexWrap: !shouldCondense,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const commentPublishBtnClasses = CX({
|
|
||||||
d: 1,
|
|
||||||
jcCenter: 1,
|
|
||||||
displayNone: length(this.props.text) === 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (shouldCondense) {
|
if (shouldCondense) {
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.w100PC].join(' ')}>
|
||||||
<div className={[_s.d, _s.flexRow, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.w100PC, _s.aiCenter].join(' ')}>
|
||||||
<div className={[_s.d, _s.mr10].join(' ')}>
|
<div className={[_s.d, _s.mr10].join(' ')}>
|
||||||
<Avatar account={account} size={28} noHover />
|
<Avatar account={account} size={30} noHover />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={[_s.d, _s.flexWrap, _s.overflowHidden, _s.flex1, _s.minH28PX, _s.py2, _s.aiEnd, _s.flexRow, _s.radiusSmall, _s.bgSubtle, _s.px5].join(' ')}
|
className={[_s.d, _s.flexWrap, _s.overflowHidden, _s.flex1, _s.minH28PX, _s.py5, _s.aiEnd, _s.flexRow, _s.radiusSmall, _s.bgSubtle, _s.px5].join(' ')}
|
||||||
ref={this.setForm}
|
ref={this.setForm}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
|
{ textbox }
|
||||||
<AutosuggestTextbox
|
{ isMatch && <ComposeFormSubmitButton type='comment' /> }
|
||||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
|
||||||
placeholder={intl.formatMessage(messages.commentPlaceholder)}
|
|
||||||
disabled={disabled}
|
|
||||||
value={this.props.text}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
suggestions={this.props.suggestions}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
onFocus={this.handleComposeFocus}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
onPaste={onPaste}
|
|
||||||
autoFocus={shouldAutoFocus}
|
|
||||||
small={shouldCondense}
|
|
||||||
isPro={isPro}
|
|
||||||
isEdit={!!edit}
|
|
||||||
id='comment-composer'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiStart, _s.mlAuto].join(' ')}>
|
|
||||||
<div className={[_s.d, _s.flexRow, _s.mrAuto].join(' ')}>
|
|
||||||
<div className={commentPublishBtnClasses}>
|
|
||||||
<Button
|
|
||||||
isNarrow
|
|
||||||
onClick={this.handleSubmit}
|
|
||||||
isDisabled={disabledButton}
|
|
||||||
className={_s.px10}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.post)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
(isUploading || anyMedia) &&
|
(isUploading || anyMedia) && isMatch &&
|
||||||
<div className={[_s.d, _s.w100PC, _s.pl35, _s.mt5].join(' ')}>
|
<div className={[_s.d, _s.w100PC, _s.pl35, _s.mt5].join(' ')}>
|
||||||
<UploadForm replyToId={replyToId} isModalOpen={isModalOpen} />
|
<UploadForm isModalOpen={isModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStandalone || isModalOpen) {
|
|
||||||
return (
|
|
||||||
<div className={[_s.d, _s.w100PC, _s.flexGrow1, _s.bgTertiary].join(' ')}>
|
|
||||||
|
|
||||||
<div className={[_s.d, _s.pb10, _s.calcMaxH370PX, _s.overflowYScroll, _s.boxShadowBlock, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
|
||||||
<ComposeDestinationHeader account={account} />
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={[_s.d, _s.bgPrimary, _s.boxShadowBlock, _s.w100PC, _s.minH200PX, _s.pb10, _s.borderBottom1PX, _s.borderTop1PX, _s.borderColorSecondary].join(' ')}
|
|
||||||
ref={this.setForm}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!reduxReplyToId && isModalOpen && isMatch &&
|
|
||||||
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
|
|
||||||
<StatusContainer contextType='compose' id={reduxReplyToId} isChild />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!spoiler &&
|
|
||||||
<div className={[_s.d, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
|
||||||
<Input
|
|
||||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
|
||||||
value={this.props.spoilerText}
|
|
||||||
onChange={this.handleChangeSpoilerText}
|
|
||||||
disabled={!this.props.spoiler}
|
|
||||||
prependIcon='warning'
|
|
||||||
maxLength={256}
|
|
||||||
id='cw-spoiler-input'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<AutosuggestTextbox
|
|
||||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
|
||||||
placeholder={intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
|
|
||||||
disabled={disabled}
|
|
||||||
value={this.props.text}
|
|
||||||
valueMarkdown={this.props.markdown}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
suggestions={this.props.suggestions}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
onFocus={this.handleComposeFocus}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
onPaste={onPaste}
|
|
||||||
autoFocus={shouldAutoFocus}
|
|
||||||
small={shouldCondense}
|
|
||||||
isPro={isPro}
|
|
||||||
isEdit={!!edit}
|
|
||||||
id='main-composer'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
(isUploading || anyMedia) &&
|
|
||||||
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
|
|
||||||
<UploadForm replyToId={replyToId} isModalOpen={isModalOpen} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!edit && hasPoll &&
|
|
||||||
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
|
|
||||||
<PollForm replyToId={replyToId} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!quoteOfId && isModalOpen && isMatch &&
|
|
||||||
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
|
|
||||||
<StatusContainer contextType='compose' id={quoteOfId} isChild />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ !isModalOpen && <ComposeFormSubmitButton /> }
|
|
||||||
|
|
||||||
<ComposeExtraButtonList isStandalone={isStandalone} isMatch={isMatch} hidePro={hidePro} edit={edit} isModal={isModalOpen} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={[_s.d, _s.w100PC, _s.flexGrow1, _s.bgPrimary].join(' ')}>
|
||||||
className={[_s.d, _s.w100PC, _s.pb10, _s.flexWrap, _s.overflowHidden, _s.flex1].join(' ')}
|
<div className={[_s.d, _s.calcMaxH410PX, _s.overflowYScroll].join(' ')}>
|
||||||
ref={this.setForm}
|
|
||||||
onClick={this.handleClick}
|
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||||
>
|
<ComposeDestinationHeader account={account} isModal={isModalOpen} />
|
||||||
<Text className={[_s.d, _s.px15, _s.pt15, _s.pb10].join(' ')} size='medium' color='tertiary'>
|
</Responsive>
|
||||||
{intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
|
|
||||||
</Text>
|
<div className={containerClasses} ref={this.setForm} onClick={this.handleClick}>
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.mt5, _s.px10, _s.flexWrap].join(' ')}>
|
{
|
||||||
<UploadButton />
|
!!reduxReplyToId && isModalOpen && isMatch &&
|
||||||
<EmojiPickerButton isMatch={isMatch} />
|
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
|
||||||
<PollButton />
|
<StatusContainer contextType='compose' id={reduxReplyToId} isChild />
|
||||||
<MoreButton />
|
</div>
|
||||||
<ComposeFormSubmitButton />
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!spoiler &&
|
||||||
|
<div className={[_s.d, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||||
|
value={this.props.spoilerText}
|
||||||
|
onChange={this.handleChangeSpoilerText}
|
||||||
|
disabled={!this.props.spoiler}
|
||||||
|
prependIcon='warning'
|
||||||
|
maxLength={256}
|
||||||
|
id='cw-spoiler-input'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{ textbox }
|
||||||
|
|
||||||
|
{
|
||||||
|
(isUploading || anyMedia) &&
|
||||||
|
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.borderTop1PX, _s.borderColorSecondary].join(' ')} />
|
||||||
|
<UploadForm />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!edit && hasPoll &&
|
||||||
|
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
|
||||||
|
<PollForm />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!quoteOfId && isModalOpen && isMatch &&
|
||||||
|
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
|
||||||
|
<StatusContainer contextType='compose' id={quoteOfId} isChild />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={[_s.d, _s.posAbs, _s.z2, _s.left0, _s.right0, _s.bottom0, _s.top0].join(' ')} />
|
|
||||||
|
<div className={[_s.d, _s.px10].join(' ')}>
|
||||||
|
<ComposeExtraButtonList formLocation={formLocation} isMatch={isMatch} hidePro={hidePro} edit={edit} isModal={isModalOpen} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
(!disabledButton || isModalOpen) && isMatch &&
|
||||||
|
<div className={[_s.d, _s.mb10, _s.px10].join(' ')}>
|
||||||
|
<ComposeFormSubmitButton type='block' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
|
||||||
|
{
|
||||||
|
formLocation === 'timeline' &&
|
||||||
|
<NavLink to='/compose' className={[_s.d, _s.z4, _s.posAbs, _s.top0, _s.left0, _s.right0, _s.bottom0].join(' ')} />
|
||||||
|
}
|
||||||
|
</Responsive>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -450,9 +372,7 @@ ComposeForm.propTypes = {
|
|||||||
onFetchSuggestions: PropTypes.func.isRequired,
|
onFetchSuggestions: PropTypes.func.isRequired,
|
||||||
onSuggestionSelected: PropTypes.func.isRequired,
|
onSuggestionSelected: PropTypes.func.isRequired,
|
||||||
onChangeSpoilerText: PropTypes.func.isRequired,
|
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||||
openComposeModal: PropTypes.func.isRequired,
|
|
||||||
onPaste: PropTypes.func.isRequired,
|
onPaste: PropTypes.func.isRequired,
|
||||||
showSearch: PropTypes.bool,
|
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
shouldCondense: PropTypes.bool,
|
shouldCondense: PropTypes.bool,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
@ -466,11 +386,7 @@ ComposeForm.propTypes = {
|
|||||||
isPro: PropTypes.bool,
|
isPro: PropTypes.bool,
|
||||||
hidePro: PropTypes.bool,
|
hidePro: PropTypes.bool,
|
||||||
autoJoinGroup: PropTypes.bool,
|
autoJoinGroup: PropTypes.bool,
|
||||||
isStandalone: PropTypes.bool,
|
formLocation: PropTypes.string,
|
||||||
}
|
|
||||||
|
|
||||||
ComposeForm.defaultProps = {
|
|
||||||
showSearch: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(ComposeForm)
|
export default injectIntl(ComposeForm)
|
@ -1,30 +1,80 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { CX } from '../../../constants'
|
import { connect } from 'react-redux'
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
|
import { length } from 'stringz'
|
||||||
|
import { countableText } from '../../ui/util/counter'
|
||||||
|
import { submitCompose } from '../../../actions/compose'
|
||||||
|
import {
|
||||||
|
CX,
|
||||||
|
MAX_POST_CHARACTER_COUNT,
|
||||||
|
} from '../../../constants'
|
||||||
import Button from '../../../components/button'
|
import Button from '../../../components/button'
|
||||||
import Text from '../../../components/text'
|
import Text from '../../../components/text'
|
||||||
|
|
||||||
class ComposeFormSubmitButton extends React.PureComponent {
|
class ComposeFormSubmitButton extends React.PureComponent {
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
intl,
|
||||||
title,
|
title,
|
||||||
active,
|
active,
|
||||||
small,
|
small,
|
||||||
disabledButton,
|
|
||||||
type,
|
type,
|
||||||
|
|
||||||
|
edit,
|
||||||
|
text,
|
||||||
|
isSubmitting,
|
||||||
|
isChangingUpload,
|
||||||
|
isUploading,
|
||||||
|
anyMedia,
|
||||||
|
quoteOfId,
|
||||||
|
scheduledAt,
|
||||||
|
hasPoll,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
|
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
|
||||||
|
|
||||||
|
if (type === 'comment') {
|
||||||
|
const commentPublishBtnClasses = CX({
|
||||||
|
d: 1,
|
||||||
|
jcCenter: 1,
|
||||||
|
displayNone: disabledButton,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[_s.d, _s.flexRow, _s.aiStart, _s.mlAuto].join(' ')}>
|
||||||
|
<div className={[_s.d, _s.flexRow, _s.mrAuto].join(' ')}>
|
||||||
|
<div className={commentPublishBtnClasses}>
|
||||||
|
<Button
|
||||||
|
isNarrow
|
||||||
|
radiusSmall
|
||||||
|
onClick={this.handleSubmit}
|
||||||
|
isDisabled={disabledButton}
|
||||||
|
className={_s.px15}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.post)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
mr5: 1,
|
|
||||||
jcCenter: 1,
|
jcCenter: 1,
|
||||||
h40PX: 1,
|
h40PX: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const btnClasses = CX({
|
const btnClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
circle: 1,
|
radiusSmall: 1,
|
||||||
noUnderline: 1,
|
noUnderline: 1,
|
||||||
font: 1,
|
font: 1,
|
||||||
cursorPointer: 1,
|
cursorPointer: 1,
|
||||||
@ -38,30 +88,32 @@ class ComposeFormSubmitButton extends React.PureComponent {
|
|||||||
px10: 1,
|
px10: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconClasses = CX({
|
let backgroundColor, color
|
||||||
cSecondary: !active,
|
if (disabledButton) {
|
||||||
cWhite: active,
|
backgroundColor = 'tertiary'
|
||||||
mr10: 1,
|
color = 'tertiary'
|
||||||
py2: small,
|
} else if (type === 'navigation') {
|
||||||
ml10: small,
|
backgroundColor = 'white'
|
||||||
})
|
color = 'brand'
|
||||||
|
} else {
|
||||||
const iconSize = !small ? '18px' : '16px'
|
backgroundColor = 'brand'
|
||||||
const textColor = !active ? 'primary' : 'white'
|
color = 'white'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
<div className={[_s.d, _s.w100PC, _s.py10, _s.px10].join(' ')}>
|
<div className={[_s.d, _s.w100PC].join(' ')}>
|
||||||
<Button
|
<Button
|
||||||
isBlock
|
isBlock
|
||||||
|
radiusSmall
|
||||||
isDisabled={disabledButton}
|
isDisabled={disabledButton}
|
||||||
backgroundColor={disabledButton ? 'secondary' : 'brand'}
|
backgroundColor={backgroundColor}
|
||||||
color={disabledButton ? 'tertiary' : 'white'}
|
color={color}
|
||||||
className={[_s.fs15PX, _s.px15, _s.flexGrow1, _s.mlAuto].join(' ')}
|
className={[_s.fs15PX, _s.px15, _s.flexGrow1, _s.mlAuto].join(' ')}
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
>
|
>
|
||||||
<Text color='inherit' weight='medium' align='center'>
|
<Text color='inherit' size='medium' weight='bold' align='center'>
|
||||||
post
|
{intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -71,9 +123,32 @@ class ComposeFormSubmitButton extends React.PureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// {intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
|
const messages = defineMessages({
|
||||||
|
post: { id: 'compose_form.post', defaultMessage: 'Post' },
|
||||||
|
postEdit: { id: 'compose_form.post_edit', defaultMessage: 'Post Edit' },
|
||||||
|
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
edit: state.getIn(['compose', 'id']) !== null,
|
||||||
|
text: state.getIn(['compose', 'text']),
|
||||||
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
|
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||||
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
|
quoteOfId: state.getIn(['compose', 'quote_of_id']),
|
||||||
|
scheduledAt: state.getIn(['compose', 'scheduled_at']),
|
||||||
|
hasPoll: state.getIn(['compose', 'poll']),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onSubmit(groupId, replyToId = null, router, isStandalone, autoJoinGroup) {
|
||||||
|
dispatch(submitCompose(groupId, replyToId, router, isStandalone, autoJoinGroup))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ComposeFormSubmitButton.propTypes = {
|
ComposeFormSubmitButton.propTypes = {
|
||||||
type: PropTypes.oneOf(['header', 'block', 'comment'])
|
type: PropTypes.oneOf(['header', 'navigation', 'block', 'comment'])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ComposeFormSubmitButton
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeFormSubmitButton))
|
@ -48,7 +48,7 @@ class ExpiresPostButton extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
expires: { id: 'expiration.title', defaultMessage: 'Add status expiration' },
|
expires: { id: 'expiration.title', defaultMessage: 'Status expiration' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
|
@ -19,7 +19,7 @@ class Upload extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
hovered: false,
|
hovering: false,
|
||||||
focused: false,
|
focused: false,
|
||||||
dirtyDescription: null,
|
dirtyDescription: null,
|
||||||
}
|
}
|
||||||
@ -45,11 +45,11 @@ class Upload extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseEnter = () => {
|
handleMouseEnter = () => {
|
||||||
this.setState({ hovered: true })
|
this.setState({ hovering: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseLeave = () => {
|
handleMouseLeave = () => {
|
||||||
this.setState({ hovered: false })
|
this.setState({ hovering: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputFocus = () => {
|
handleInputFocus = () => {
|
||||||
@ -75,66 +75,60 @@ class Upload extends ImmutablePureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, media } = this.props
|
const { intl, media } = this.props
|
||||||
const active = this.state.hovered || this.state.focused
|
const { hovering } = this.state
|
||||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
|
||||||
|
|
||||||
const descriptionContainerClasses = CX({
|
const active = hovering || this.state.focused
|
||||||
d: 1,
|
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
||||||
posAbs: 1,
|
|
||||||
right0: 1,
|
|
||||||
bottom0: 1,
|
|
||||||
left0: 1,
|
|
||||||
mt5: 1,
|
|
||||||
mb5: 1,
|
|
||||||
ml5: 1,
|
|
||||||
mr5: 1,
|
|
||||||
displayNone: !active,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
className={[_s.d, _s.w50PC, _s.px5, _s.py5].join(' ')}
|
className={[_s.d, _s.w100PC, _s.mt10].join(' ')}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
role='button'
|
role='button'
|
||||||
>
|
>
|
||||||
<div className={[_s.d, _s.radiusSmall, _s.overflowHidden, _s.h158PX].join(' ')}>
|
<div className={[_s.d, _s.radiusSmall, _s.borderColorSecondary, _s.border1PX, _s.overflowHidden, _s.maxH100VH, _s.minH106PX].join(' ')}>
|
||||||
<Image
|
<Image
|
||||||
className={[_s.d, _s.h158PX].join(' ')}
|
className={[_s.d, _s.minH106PX, _s.maxH100VH].join(' ')}
|
||||||
src={media.get('preview_url')}
|
src={media.get('preview_url')}
|
||||||
/>
|
/>
|
||||||
|
{ hovering && <div className={[_s.d, _s.posAbs, _s.z2, _s.top0, _s.bottom0, _s.right0, _s.left0, _s.bgBlackOpaquest].join(' ')} /> }
|
||||||
{
|
{
|
||||||
media.get('type') === 'gifv' &&
|
media.get('type') === 'gifv' &&
|
||||||
<div className={[_s.d, _s.posAbs, _s.z2, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.ml10, _s.mt10, _s.top0, _s.left0].join(' ')}>
|
<div className={[_s.d, _s.posAbs, _s.z3, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.ml10, _s.mt10, _s.bottom0, _s.right0].join(' ')}>
|
||||||
<Text size='extraSmall' color='white' weight='medium'>GIF</Text>
|
<Text size='extraSmall' color='white' weight='medium'>GIF</Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<Button
|
<div className={[_s.d, _s.posAbs, _s.px15, _s.pt15, _s.z3, _s.flexRow, _s.top0, _s.left0, _s.right0].join(' ')}>
|
||||||
backgroundColor='black'
|
{
|
||||||
color='white'
|
active &&
|
||||||
title={intl.formatMessage(messages.delete)}
|
<div className={[_s.d, _s.flexGrow1, _s.mr15].join(' ')}>
|
||||||
onClick={this.handleUndoClick}
|
<Input
|
||||||
icon='close'
|
small
|
||||||
iconSize='10px'
|
hideLabel
|
||||||
iconClassName={_s.inherit}
|
id={`input-${media.get('id')}`}
|
||||||
className={[_s.top0, _s.right0, _s.posAbs, _s.mr5, _s.mt5, _s.px10].join(' ')}
|
title={intl.formatMessage(messages.description)}
|
||||||
/>
|
placeholder={intl.formatMessage(messages.description)}
|
||||||
|
value={description}
|
||||||
<div className={descriptionContainerClasses}>
|
maxLength={420}
|
||||||
<Input
|
onFocus={this.handleInputFocus}
|
||||||
small
|
onChange={this.handleInputChange}
|
||||||
hideLabel
|
onBlur={this.handleInputBlur}
|
||||||
id={`input-${media.get('id')}`}
|
onKeyDown={this.handleKeyDown}
|
||||||
title={intl.formatMessage(messages.description)}
|
/>
|
||||||
placeholder={intl.formatMessage(messages.description)}
|
</div>
|
||||||
value={description}
|
}
|
||||||
maxLength={420}
|
<Button
|
||||||
onFocus={this.handleInputFocus}
|
backgroundColor='black'
|
||||||
onChange={this.handleInputChange}
|
color='white'
|
||||||
onBlur={this.handleInputBlur}
|
title={intl.formatMessage(messages.delete)}
|
||||||
onKeyDown={this.handleKeyDown}
|
onClick={this.handleUndoClick}
|
||||||
|
icon='close'
|
||||||
|
iconSize='10px'
|
||||||
|
iconClassName={_s.inherit}
|
||||||
|
className={[_s.mlAuto, _s.px10].join(' ')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@ class SensitiveMediaButton extends React.PureComponent {
|
|||||||
const { active, disabled, onClick, intl } = this.props
|
const { active, disabled, onClick, intl } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.aiStart, _s.px5].join(' ')}>
|
<div className={[_s.d, _s.aiStart, _s.px5, _s.py10].join(' ')}>
|
||||||
<Switch
|
<Switch
|
||||||
id='mark-sensitive'
|
id='mark-sensitive'
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
|
@ -18,23 +18,16 @@ class UploadForm extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_s.d}>
|
<div className={_s.d}>
|
||||||
|
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
|
||||||
|
|
||||||
<div className={[_s.d, _s.flexRow, _s.flexWrap].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.flexWrap].join(' ')}>
|
||||||
{
|
{mediaIds.map(id => (
|
||||||
mediaIds.map(id => (
|
<Upload id={id} key={id} />
|
||||||
<Upload id={id} key={id} />
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{ !mediaIds.isEmpty() && <SensitiveMediaButton /> }
|
||||||
!mediaIds.isEmpty() &&
|
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
|
||||||
<SensitiveMediaButton />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isUploading &&
|
|
||||||
<ProgressBar small progress={uploadProgress} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -48,7 +41,6 @@ const mapStateToProps = (state) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
UploadForm.propTypes = {
|
UploadForm.propTypes = {
|
||||||
isModalOpen: PropTypes.bool,
|
|
||||||
isUploading: PropTypes.bool,
|
isUploading: PropTypes.bool,
|
||||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||||
uploadProgress: PropTypes.number,
|
uploadProgress: PropTypes.number,
|
||||||
|
@ -11,7 +11,7 @@ class Compose extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return <ComposeFormContainer isStandalone />
|
return <ComposeFormContainer formLocation='standalone' />
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -186,9 +186,9 @@ class SlideFirstPost extends React.PureComponent {
|
|||||||
|
|
||||||
<div className={[_s.d, _s.mt15, _s.boxShadowBlock, _s.radiusSmall].join(' ')}>
|
<div className={[_s.d, _s.mt15, _s.boxShadowBlock, _s.radiusSmall].join(' ')}>
|
||||||
<ComposeFormContainer
|
<ComposeFormContainer
|
||||||
|
formLocation='introduction'
|
||||||
groupId={GAB_COM_INTRODUCE_YOURSELF_GROUP_ID}
|
groupId={GAB_COM_INTRODUCE_YOURSELF_GROUP_ID}
|
||||||
hidePro
|
hidePro
|
||||||
autoFocus
|
|
||||||
autoJoinGroup
|
autoJoinGroup
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -325,7 +325,7 @@ class Introduction extends ImmutablePureComponent {
|
|||||||
<Button
|
<Button
|
||||||
href={currentIndex === 3 ? '/home' : undefined}
|
href={currentIndex === 3 ? '/home' : undefined}
|
||||||
onClick={this.handleNext}
|
onClick={this.handleNext}
|
||||||
className={_s.px10}
|
className={[_s.px10, _s.aiCenter, _s.flexRow].join(' ')}
|
||||||
icon={currentIndex !== 3 ? 'arrow-right' : undefined}
|
icon={currentIndex !== 3 ? 'arrow-right' : undefined}
|
||||||
iconSize={currentIndex !== 3 ? '18px' : undefined}
|
iconSize={currentIndex !== 3 ? '18px' : undefined}
|
||||||
>
|
>
|
||||||
@ -336,7 +336,7 @@ class Introduction extends ImmutablePureComponent {
|
|||||||
<Text color='white' className={_s.px5}>{nextTitle}</Text>
|
<Text color='white' className={_s.px5}>{nextTitle}</Text>
|
||||||
</Responsive>
|
</Responsive>
|
||||||
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
|
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
|
||||||
<Text color='white' className={[_s.px5, _s.mr10].join(' ')}>Done</Text>
|
<Text color='white' className={[_s.px5, _s.mr5].join(' ')}>Done</Text>
|
||||||
<Icon id='check' size='14px' className={_s.cWhite} />
|
<Icon id='check' size='14px' className={_s.cWhite} />
|
||||||
</Responsive>
|
</Responsive>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -35,16 +35,17 @@ class ChatConversationsListItem extends ImmutablePureComponent {
|
|||||||
|
|
||||||
if (!chatConversation) return <div/>
|
if (!chatConversation) return <div/>
|
||||||
|
|
||||||
|
console.log("chatConversation:", chatConversation)
|
||||||
|
|
||||||
const containerClasses = CX({
|
const containerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
w100PC: 1,
|
w100PC: 1,
|
||||||
bgTransparent: 1,
|
bgTransparent: 1,
|
||||||
bgSubtle_onHover: 1,
|
bgSubtle_onHover: 1,
|
||||||
borderBottom1PX: 1,
|
|
||||||
borderColorSecondary: 1,
|
|
||||||
noUnderline: 1,
|
noUnderline: 1,
|
||||||
outlineNone: 1,
|
outlineNone: 1,
|
||||||
cursorPointer: 1,
|
cursorPointer: 1,
|
||||||
|
pl15: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const innerContainerClasses = CX({
|
const innerContainerClasses = CX({
|
||||||
@ -71,6 +72,9 @@ class ChatConversationsListItem extends ImmutablePureComponent {
|
|||||||
className={containerClasses}
|
className={containerClasses}
|
||||||
onClick={this.handleOnClick}
|
onClick={this.handleOnClick}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{ chatConversation.get('is_unread') && <div className={[_s.d, _s.posAbs, _s.left0, _s.top50PC, _s.ml10, _s.mtNeg5PX, _s.circle, _s.w10PX, _s.h10PX, _s.bgBrand].join(' ')} /> }
|
||||||
|
|
||||||
<div className={innerContainerClasses}>
|
<div className={innerContainerClasses}>
|
||||||
<AvatarGroup accounts={otherAccounts} size={avatarSize} noHover />
|
<AvatarGroup accounts={otherAccounts} size={avatarSize} noHover />
|
||||||
|
|
||||||
@ -88,6 +92,7 @@ class ChatConversationsListItem extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className={[_s.py5, _s.dangerousContent, _s.textAlignLeft].join(' ')} dangerouslySetInnerHTML={content} />
|
<div className={[_s.py5, _s.dangerousContent, _s.textAlignLeft].join(' ')} dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className={[_s.d, _s.posAbs, _s.h1PX, _s.w100PC, _s.bottom0, _s.right0, _s.bgSecondary].join(' ')} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ import { openModal } from '../../../actions/modal'
|
|||||||
import { sendChatMessage } from '../../../actions/chat_messages'
|
import { sendChatMessage } from '../../../actions/chat_messages'
|
||||||
import { CX } from '../../../constants'
|
import { CX } from '../../../constants'
|
||||||
import Button from '../../../components/button'
|
import Button from '../../../components/button'
|
||||||
|
import Icon from '../../../components/icon'
|
||||||
import Input from '../../../components/input'
|
import Input from '../../../components/input'
|
||||||
import Text from '../../../components/text'
|
import Text from '../../../components/text'
|
||||||
|
|
||||||
@ -23,6 +24,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
this.setState({ value: '' })
|
this.setState({ value: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOnExpire = () => {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
onChange = (e) => {
|
onChange = (e) => {
|
||||||
this.setState({ value: e.target.value })
|
this.setState({ value: e.target.value })
|
||||||
}
|
}
|
||||||
@ -68,6 +73,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
this.sendBtn = c
|
this.sendBtn = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExpiresBtn = (c) => {
|
||||||
|
this.expiresBtn = c
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isXS, chatConversationId } = this.props
|
const { isXS, chatConversationId } = this.props
|
||||||
const { value } = this.state
|
const { value } = this.state
|
||||||
@ -85,9 +94,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
px10: 1,
|
px10: 1,
|
||||||
fs14PX: 1,
|
fs14PX: 1,
|
||||||
maxH200PX: 1,
|
maxH200PX: 1,
|
||||||
borderColorSecondary: 1,
|
w100PC: 1,
|
||||||
border1PX: 1,
|
|
||||||
radiusRounded: 1,
|
|
||||||
py10: 1,
|
py10: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -105,6 +112,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
|
maxLength={1600}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -114,18 +122,33 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={this.handleOnSendChatMessage}
|
onClick={this.handleOnSendChatMessage}
|
||||||
>
|
>
|
||||||
<Text color='inherit' weight='medium' className={_s.px10}>Send</Text>
|
<Text color='inherit' weight='medium' className={isXS ? undefined : _s.px10}>Send</Text>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const expiresBtn = (
|
||||||
|
<button
|
||||||
|
ref={this.setExpiresBtn}
|
||||||
|
className={[_s.d, _s.bgSubtle, _s.borderRight1PX, _s.borderColorSecondary, _s.w40PX, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.cursorPointer, _s.outlineNone].join(' ')}
|
||||||
|
onClick={this.handleOnExpire}
|
||||||
|
>
|
||||||
|
<Icon id='stopwatch' className={[_s.cPrimary, _s.ml2].join(' ')} size='15px' />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
if (isXS) {
|
if (isXS) {
|
||||||
return (
|
return (
|
||||||
<div className={[_s.d, _s.z4, _s.minH58PX, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.z4, _s.minH58PX, _s.w100PC].join(' ')}>
|
||||||
<div className={[_s.d, _s.minH58PX, _s.bgPrimary, _s.aiCenter, _s.z3, _s.bottom0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
|
<div className={[_s.d, _s.minH58PX, _s.bgPrimary, _s.aiCenter, _s.z3, _s.bottom0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
|
||||||
<div className={[_s.d, _s.w100PC, _s.pb5, _s.px15, _s.aiCenter, _s.jcCenter, _s.saveAreaInsetPB, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
|
<div className={[_s.d, _s.w100PC, _s.pb5, _s.px15, _s.aiCenter, _s.jcCenter, _s.saveAreaInsetPB, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
|
||||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px10].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px10].join(' ')}>
|
||||||
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
|
<div className={[_s.d, _s.flexRow, _s.radiusRounded, _s.border1PX, _s.borderColorSecondary, _s.overflowHidden].join(' ')}>
|
||||||
{textarea}
|
<div className={_s.d}>
|
||||||
|
{expiresBtn}
|
||||||
|
</div>
|
||||||
|
<div className={[_s.d, _s.flexGrow1].join(' ')}>
|
||||||
|
{textarea}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
{button}
|
{button}
|
||||||
@ -140,9 +163,16 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||||||
return (
|
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].join(' ')}>
|
<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].join(' ')}>
|
||||||
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
|
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
|
||||||
{textarea}
|
<div className={[_s.d, _s.flexRow, _s.radiusRounded, _s.border1PX, _s.borderColorSecondary, _s.overflowHidden].join(' ')}>
|
||||||
|
<div className={_s.d}>
|
||||||
|
{expiresBtn}
|
||||||
|
</div>
|
||||||
|
<div className={[_s.d, _s.flexGrow1].join(' ')}>
|
||||||
|
{textarea}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
<div className={[_s.d, _s.h100PC, _s.mtAuto, _s.mb10, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||||
{button}
|
{button}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -163,4 +193,4 @@ ChatMessagesComposeForm.propTypes = {
|
|||||||
onSendMessage: PropTypes.func.isRequired,
|
onSendMessage: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(ChatMessagesComposeForm)
|
export default connect(mapDispatchToProps)(ChatMessagesComposeForm)
|
@ -8,7 +8,7 @@ import { NavLink } from 'react-router-dom'
|
|||||||
import { openPopover } from '../../../actions/popover'
|
import { openPopover } from '../../../actions/popover'
|
||||||
import {
|
import {
|
||||||
CX,
|
CX,
|
||||||
POPOVER_CHAT_MESSAGE_DELETE,
|
POPOVER_CHAT_MESSAGE_OPTIONS,
|
||||||
} from '../../../constants'
|
} from '../../../constants'
|
||||||
import { me } from '../../../initial_state'
|
import { me } from '../../../initial_state'
|
||||||
import Input from '../../../components/input'
|
import Input from '../../../components/input'
|
||||||
@ -51,7 +51,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMoreClick = () => {
|
handleMoreClick = () => {
|
||||||
this.props.onOpenChatMessageDeletePopover(this.props.chatMessageId, this.deleteBtnRef)
|
this.props.onOpenChatMessageOptionsPopover(this.props.chatMessageId, this.deleteBtnRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeleteBtnRef = (c) => {
|
setDeleteBtnRef = (c) => {
|
||||||
@ -122,7 +122,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||||||
const buttonContainerClasses = CX({
|
const buttonContainerClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
flexRow: 1,
|
flexRow: 1,
|
||||||
displayNone: !isHovering && alt,
|
displayNone: !isHovering,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -145,19 +145,16 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||||||
<div className={messageInnerContainerClasses}>
|
<div className={messageInnerContainerClasses}>
|
||||||
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
|
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
{
|
<div className={buttonContainerClasses}>
|
||||||
alt &&
|
<Button
|
||||||
<div className={buttonContainerClasses}>
|
buttonRef={this.setDeleteBtnRef}
|
||||||
<Button
|
onClick={this.handleMoreClick}
|
||||||
buttonRef={this.setDeleteBtnRef}
|
color='tertiary'
|
||||||
onClick={this.handleMoreClick}
|
backgroundColor='none'
|
||||||
color='tertiary'
|
icon='ellipsis'
|
||||||
backgroundColor='none'
|
iconSize='18px'
|
||||||
icon='ellipsis'
|
/>
|
||||||
iconSize='18px'
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={lowerContainerClasses}>
|
<div className={lowerContainerClasses}>
|
||||||
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
|
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
|
||||||
@ -178,8 +175,8 @@ const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
onOpenChatMessageDeletePopover(chatMessageId, targetRef) {
|
onOpenChatMessageOptionsPopover(chatMessageId, targetRef) {
|
||||||
dispatch(openPopover(POPOVER_CHAT_MESSAGE_DELETE, {
|
dispatch(openPopover(POPOVER_CHAT_MESSAGE_OPTIONS, {
|
||||||
targetRef,
|
targetRef,
|
||||||
chatMessageId,
|
chatMessageId,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
expandChatMessages,
|
expandChatMessages,
|
||||||
scrollBottomChatMessageConversation,
|
scrollBottomChatMessageConversation,
|
||||||
} from '../../../actions/chat_conversation_messages'
|
} from '../../../actions/chat_conversation_messages'
|
||||||
|
import { readChatConversation } from '../../../actions/chat_conversations'
|
||||||
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
|
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
|
||||||
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
|
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
|
||||||
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
|
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
|
||||||
@ -58,7 +59,6 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||||||
// Reset the scroll position when a new child comes in in order not to
|
// Reset the scroll position when a new child comes in in order not to
|
||||||
// jerk the scrollbar around if you're already scrolled down the page.
|
// jerk the scrollbar around if you're already scrolled down the page.
|
||||||
if (snapshot !== null && this.scrollContainerRef) {
|
if (snapshot !== null && this.scrollContainerRef) {
|
||||||
console.log("snapshot:", snapshot)
|
|
||||||
this.setScrollTop(this.scrollContainerRef.scrollHeight - snapshot)
|
this.setScrollTop(this.scrollContainerRef.scrollHeight - snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +68,7 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
|
|||||||
|
|
||||||
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0 && this.scrollContainerRef) {
|
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0 && this.scrollContainerRef) {
|
||||||
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
|
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
|
||||||
|
this.props.onReadChatConversation(this.props.chatConversationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +364,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
|||||||
onSetChatConversationSelected: (chatConversationId) => {
|
onSetChatConversationSelected: (chatConversationId) => {
|
||||||
dispatch(setChatConversationSelected(chatConversationId))
|
dispatch(setChatConversationSelected(chatConversationId))
|
||||||
},
|
},
|
||||||
|
onReadChatConversation(chatConversationId) {
|
||||||
|
dispatch(readChatConversation(chatConversationId))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ChatMessageScrollingList.propTypes = {
|
ChatMessageScrollingList.propTypes = {
|
||||||
|
@ -52,11 +52,13 @@ import DeckPage from '../../pages/deck_page'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
About,
|
About,
|
||||||
|
AccountAlbums,
|
||||||
AccountGallery,
|
AccountGallery,
|
||||||
AccountTimeline,
|
AccountTimeline,
|
||||||
AccountCommentsTimeline,
|
AccountCommentsTimeline,
|
||||||
Assets,
|
Assets,
|
||||||
BlockedAccounts,
|
BlockedAccounts,
|
||||||
|
BookmarkCollections,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
CaliforniaConsumerProtection,
|
CaliforniaConsumerProtection,
|
||||||
CaliforniaConsumerProtectionContact,
|
CaliforniaConsumerProtectionContact,
|
||||||
@ -274,9 +276,11 @@ class SwitchingArea extends React.PureComponent {
|
|||||||
|
|
||||||
<WrappedRoute path='/:username/photos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
|
<WrappedRoute path='/:username/photos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
|
||||||
<WrappedRoute path='/:username/videos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'video' }} />
|
<WrappedRoute path='/:username/videos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'video' }} />
|
||||||
|
<WrappedRoute path='/:username/albums' page={ProfilePage} component={AccountAlbums} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
|
||||||
|
|
||||||
<WrappedRoute path='/:username/likes' page={ProfilePage} component={LikedStatuses} content={children} />
|
<WrappedRoute path='/:username/likes' page={ProfilePage} component={LikedStatuses} content={children} />
|
||||||
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkCollections} content={children} />
|
||||||
|
<WrappedRoute path='/:username/:bookmarkCollectionId/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />
|
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ export function AccountGallery() { return import(/* webpackChunkName: "features/
|
|||||||
export function Assets() { return import(/* webpackChunkName: "features/about/assets" */'../../about/assets') }
|
export function Assets() { return import(/* webpackChunkName: "features/about/assets" */'../../about/assets') }
|
||||||
export function BlockAccountModal() { return import(/* webpackChunkName: "components/block_account_modal" */'../../../components/modal/block_account_modal') }
|
export function BlockAccountModal() { return import(/* webpackChunkName: "components/block_account_modal" */'../../../components/modal/block_account_modal') }
|
||||||
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
|
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
|
||||||
|
export function BookmarkCollections() { return import(/* webpackChunkName: "features/bookmark_collections" */'../../bookmark_collections') }
|
||||||
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
|
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
|
||||||
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
|
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
|
||||||
export function CaliforniaConsumerProtection() { return import(/* webpackChunkName: "features/california_consumer_protection" */'../../about/california_consumer_protection') }
|
export function CaliforniaConsumerProtection() { return import(/* webpackChunkName: "features/california_consumer_protection" */'../../about/california_consumer_protection') }
|
||||||
@ -17,17 +18,19 @@ export function ChatConversationDeleteModal() { return import(/* webpackChunkNam
|
|||||||
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
|
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
|
||||||
export function ChatConversationOptionsPopover() { return import(/* webpackChunkName: "components/chat_conversation_options_popover" */'../../../components/popover/chat_conversation_options_popover') }
|
export function ChatConversationOptionsPopover() { return import(/* webpackChunkName: "components/chat_conversation_options_popover" */'../../../components/popover/chat_conversation_options_popover') }
|
||||||
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
|
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
|
||||||
export function ChatMessageDeletePopover() { return import(/* webpackChunkName: "components/chat_message_delete_popover" */'../../../components/popover/chat_message_delete_popover') }
|
export function ChatMessageOptionsPopover() { return import(/* webpackChunkName: "components/chat_message_options_popover" */'../../../components/popover/chat_message_options_popover') }
|
||||||
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
|
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
|
||||||
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
|
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
|
||||||
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
|
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
|
||||||
export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }
|
export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }
|
||||||
export function ComposeForm() { return import(/* webpackChunkName: "components/compose_form" */'../../compose/components/compose_form') }
|
export function ComposeForm() { return import(/* webpackChunkName: "components/compose_form" */'../../compose/components/compose_form') }
|
||||||
export function ComposeModal() { return import(/* webpackChunkName: "components/compose_modal" */'../../../components/modal/compose_modal') }
|
export function ComposeModal() { return import(/* webpackChunkName: "components/compose_modal" */'../../../components/modal/compose_modal') }
|
||||||
|
export function ComposePostDesinationPopover() { return import(/* webpackChunkName: "components/compose_post_destination_popover" */'../../../components/popover/compose_post_destination_popover') }
|
||||||
export function ConfirmationModal() { return import(/* webpackChunkName: "components/confirmation_modal" */'../../../components/modal/confirmation_modal') }
|
export function ConfirmationModal() { return import(/* webpackChunkName: "components/confirmation_modal" */'../../../components/modal/confirmation_modal') }
|
||||||
export function DatePickerPopover() { return import(/* webpackChunkName: "components/date_picker_popover" */'../../../components/popover/date_picker_popover') }
|
export function DatePickerPopover() { return import(/* webpackChunkName: "components/date_picker_popover" */'../../../components/popover/date_picker_popover') }
|
||||||
export function Deck() { return import(/* webpackChunkName: "features/deck" */'../../deck') }
|
export function Deck() { return import(/* webpackChunkName: "features/deck" */'../../deck') }
|
||||||
export function DeckColumnAddModal() { return import(/* webpackChunkName: "components/deck_column_add_modal" */'../../../components/modal/deck_column_add_modal') }
|
export function DeckColumnAddModal() { return import(/* webpackChunkName: "components/deck_column_add_modal" */'../../../components/modal/deck_column_add_modal') }
|
||||||
|
export function DeckColumnAddOptionsModal() { return import(/* webpackChunkName: "components/deck_column_add_options_modal" */'../../../components/modal/deck_column_add_options_modal') }
|
||||||
export function DisplayOptionsModal() { return import(/* webpackChunkName: "components/display_options_modal" */'../../../components/modal/display_options_modal') }
|
export function DisplayOptionsModal() { return import(/* webpackChunkName: "components/display_options_modal" */'../../../components/modal/display_options_modal') }
|
||||||
export function DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
|
export function DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
|
||||||
export function EditProfileModal() { return import(/* webpackChunkName: "components/edit_profile_modal" */'../../../components/modal/edit_profile_modal') }
|
export function EditProfileModal() { return import(/* webpackChunkName: "components/edit_profile_modal" */'../../../components/modal/edit_profile_modal') }
|
||||||
|
@ -20,6 +20,7 @@ export const isStaff = getMeta('is_staff');
|
|||||||
export const unreadCount = getMeta('unread_count');
|
export const unreadCount = getMeta('unread_count');
|
||||||
export const lastReadNotificationId = getMeta('last_read_notification_id');
|
export const lastReadNotificationId = getMeta('last_read_notification_id');
|
||||||
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
|
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
|
||||||
|
export const trendingHashtags = getMeta('trending_hashtags');
|
||||||
export const isFirstSession = getMeta('is_first_session');
|
export const isFirstSession = getMeta('is_first_session');
|
||||||
export const emailConfirmed = getMeta('email_confirmed');
|
export const emailConfirmed = getMeta('email_confirmed');
|
||||||
export const meEmail = getMeta('email');
|
export const meEmail = getMeta('email');
|
||||||
|
@ -5,13 +5,48 @@ import {
|
|||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
} from '../constants'
|
} from '../constants'
|
||||||
import { me } from '../initial_state'
|
import { me } from '../initial_state'
|
||||||
|
import Button from '../components/button'
|
||||||
|
import Text from '../components/text'
|
||||||
import DeckSidebar from '../components/sidebar/deck_sidebar'
|
import DeckSidebar from '../components/sidebar/deck_sidebar'
|
||||||
import WrappedBundle from '../features/ui/util/wrapped_bundle'
|
import WrappedBundle from '../features/ui/util/wrapped_bundle'
|
||||||
|
import { getWindowDimension } from '../utils/is_mobile'
|
||||||
|
|
||||||
|
const initialState = getWindowDimension()
|
||||||
|
|
||||||
class DeckLayout extends React.PureComponent {
|
class DeckLayout extends React.PureComponent {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
width: initialState.width,
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.handleResize()
|
||||||
|
window.addEventListener('resize', this.handleResize, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
const { width } = getWindowDimension()
|
||||||
|
this.setState({ width })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, title } = this.props
|
const { children, title } = this.props
|
||||||
|
const { width } = this.state
|
||||||
|
|
||||||
|
const isXS = width <= BREAKPOINT_EXTRA_SMALL
|
||||||
|
|
||||||
|
if (isXS) {
|
||||||
|
return (
|
||||||
|
<div className={[_s.d, _s.aiCenter, _s.jcCenter, _s.w100PC, _s.h100VH, _s.bgTertiary].join(' ')}>
|
||||||
|
<Text className={_s.mb10}>Gab Deck is not available on mobile or tablet devices. Please only access using a desktop computer.</Text>
|
||||||
|
<Button to='/home'>Return home</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const mainBlockClasses = CX({
|
const mainBlockClasses = CX({
|
||||||
d: 1,
|
d: 1,
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { defineMessages, injectIntl } from 'react-intl'
|
import { defineMessages, injectIntl } from 'react-intl'
|
||||||
import { me } from '../initial_state'
|
import { me, trendingHashtags } from '../initial_state'
|
||||||
import {
|
import {
|
||||||
BREAKPOINT_EXTRA_SMALL,
|
BREAKPOINT_EXTRA_SMALL,
|
||||||
CX,
|
CX,
|
||||||
@ -42,27 +42,21 @@ class SearchLayout extends React.PureComponent {
|
|||||||
title: 'Explore',
|
title: 'Explore',
|
||||||
onClick: () => this.setState({ currentExploreTabIndex: 0 }),
|
onClick: () => this.setState({ currentExploreTabIndex: 0 }),
|
||||||
component: ExploreTimeline,
|
component: ExploreTimeline,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
title: '#Election2020',
|
|
||||||
onClick: () => this.setState({ currentExploreTabIndex: 1 }),
|
|
||||||
component: HashtagTimeline,
|
|
||||||
componentParams: { params: { id: 'election2020' } },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '#RiggedElection',
|
|
||||||
onClick: () => this.setState({ currentExploreTabIndex: 2 }),
|
|
||||||
component: HashtagTimeline,
|
|
||||||
componentParams: { params: { id: 'riggedelection' } },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '#StopTheSteal',
|
|
||||||
onClick: () => this.setState({ currentExploreTabIndex: 3 }),
|
|
||||||
component: HashtagTimeline,
|
|
||||||
componentParams: { params: { id: 'stopthesteal' } },
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (Array.isArray(trendingHashtags)) {
|
||||||
|
trendingHashtags.forEach((tag, i) => {
|
||||||
|
let j = i + 1
|
||||||
|
this.exploreTabs.push({
|
||||||
|
title: `#${tag}`,
|
||||||
|
onClick: () => this.setState({ currentExploreTabIndex: j }),
|
||||||
|
component: HashtagTimeline,
|
||||||
|
componentParams: { params: { id: `${tag}`.toLowerCase() } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.searchTabs = [
|
this.searchTabs = [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage(messages.top),
|
title: intl.formatMessage(messages.top),
|
||||||
|
62
app/javascript/gabsocial/reducers/albums.js
Normal file
62
app/javascript/gabsocial/reducers/albums.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_REQUEST,
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_FAIL,
|
||||||
|
} from '../actions/bookmarks'
|
||||||
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
isFetched: false,
|
||||||
|
isError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeBookmarkCollection = (bookmarkCollection) => {
|
||||||
|
return {
|
||||||
|
id: shortcut.id,
|
||||||
|
shortcut_type: 'account',
|
||||||
|
shortcut_id: shortcut.shortcut_id,
|
||||||
|
title: shortcut.shortcut.acct,
|
||||||
|
image: shortcut.shortcut.avatar_static,
|
||||||
|
to: `/${shortcut.shortcut.acct}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBookmarkCollections = (shortcuts) => {
|
||||||
|
return fromJS(shortcuts.map((shortcut) => {
|
||||||
|
return normalizeShortcut(shortcut)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function albums(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case SHORTCUTS_FETCH_REQUEST:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('isLoading', true)
|
||||||
|
map.set('isFetched', false)
|
||||||
|
map.set('isError', false)
|
||||||
|
})
|
||||||
|
case SHORTCUTS_FETCH_SUCCESS:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('items', normalizeShortcuts(action.shortcuts))
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('isFetched', true)
|
||||||
|
map.set('isError', false)
|
||||||
|
})
|
||||||
|
case SHORTCUTS_FETCH_FAIL:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('isFetched', true)
|
||||||
|
map.set('isError', true)
|
||||||
|
})
|
||||||
|
case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
|
||||||
|
return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
|
||||||
|
case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
|
||||||
|
return state.update('items', list => list.filterNot((item) => {
|
||||||
|
return `${item.get('id')}` === `${action.shortcutId}`
|
||||||
|
}))
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
62
app/javascript/gabsocial/reducers/bookmark_collections.js
Normal file
62
app/javascript/gabsocial/reducers/bookmark_collections.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_REQUEST,
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
|
||||||
|
BOOKMARK_COLLECTIONS_FETCH_FAIL,
|
||||||
|
} from '../actions/bookmarks'
|
||||||
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
isFetched: false,
|
||||||
|
isError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeBookmarkCollection = (bookmarkCollection) => {
|
||||||
|
return {
|
||||||
|
id: shortcut.id,
|
||||||
|
shortcut_type: 'account',
|
||||||
|
shortcut_id: shortcut.shortcut_id,
|
||||||
|
title: shortcut.shortcut.acct,
|
||||||
|
image: shortcut.shortcut.avatar_static,
|
||||||
|
to: `/${shortcut.shortcut.acct}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeBookmarkCollections = (shortcuts) => {
|
||||||
|
return fromJS(shortcuts.map((shortcut) => {
|
||||||
|
return normalizeShortcut(shortcut)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function bookmark_collections(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case SHORTCUTS_FETCH_REQUEST:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('isLoading', true)
|
||||||
|
map.set('isFetched', false)
|
||||||
|
map.set('isError', false)
|
||||||
|
})
|
||||||
|
case SHORTCUTS_FETCH_SUCCESS:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('items', normalizeShortcuts(action.shortcuts))
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('isFetched', true)
|
||||||
|
map.set('isError', false)
|
||||||
|
})
|
||||||
|
case SHORTCUTS_FETCH_FAIL:
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.set('isLoading', false)
|
||||||
|
map.set('isFetched', true)
|
||||||
|
map.set('isError', true)
|
||||||
|
})
|
||||||
|
case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
|
||||||
|
return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
|
||||||
|
case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
|
||||||
|
return state.update('items', list => list.filterNot((item) => {
|
||||||
|
return `${item.get('id')}` === `${action.shortcutId}`
|
||||||
|
}))
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { me } from '../initial_state'
|
|||||||
import {
|
import {
|
||||||
CHAT_MESSAGES_SEND_SUCCESS,
|
CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
CHAT_MESSAGES_DELETE_REQUEST,
|
CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
|
CHAT_MESSAGES_PURGE_REQUEST,
|
||||||
} from '../actions/chat_messages'
|
} from '../actions/chat_messages'
|
||||||
import {
|
import {
|
||||||
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
||||||
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
||||||
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
||||||
|
CHAT_CONVERSATION_MARK_READ_SUCCESS,
|
||||||
} from '../actions/chat_conversations'
|
} from '../actions/chat_conversations'
|
||||||
|
|
||||||
const initialState = ImmutableMap()
|
const initialState = ImmutableMap()
|
||||||
@ -50,6 +52,11 @@ export default function chat_conversations(state = initialState, action) {
|
|||||||
case CHAT_MESSAGES_DELETE_REQUEST:
|
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||||
// : todo : set last conversation message to one prior to this one
|
// : todo : set last conversation message to one prior to this one
|
||||||
return state
|
return state
|
||||||
|
case CHAT_MESSAGES_PURGE_REQUEST:
|
||||||
|
// : todo :
|
||||||
|
return state
|
||||||
|
case CHAT_CONVERSATION_MARK_READ_SUCCESS:
|
||||||
|
return importChatConversation(state, action.chatConversation)
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'
|
|||||||
import {
|
import {
|
||||||
CHAT_MESSAGES_SEND_SUCCESS,
|
CHAT_MESSAGES_SEND_SUCCESS,
|
||||||
CHAT_MESSAGES_DELETE_REQUEST,
|
CHAT_MESSAGES_DELETE_REQUEST,
|
||||||
|
CHAT_MESSAGES_PURGE_REQUEST,
|
||||||
} from '../actions/chat_messages'
|
} from '../actions/chat_messages'
|
||||||
import {
|
import {
|
||||||
CHAT_MESSAGES_IMPORT,
|
CHAT_MESSAGES_IMPORT,
|
||||||
@ -26,6 +27,8 @@ export default function chat_messages(state = initialState, action) {
|
|||||||
return importChatMessage(state, action.chatMessage)
|
return importChatMessage(state, action.chatMessage)
|
||||||
case CHAT_MESSAGES_DELETE_REQUEST:
|
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||||
return deleteChatMessage(state, action.chatMessageId)
|
return deleteChatMessage(state, action.chatMessageId)
|
||||||
|
case CHAT_MESSAGES_PURGE_REQUEST:
|
||||||
|
return state
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
|
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
|
||||||
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
|
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
|
||||||
|
CHAT_CONVERSATION_MARK_READ_FETCH,
|
||||||
} from '../actions/chat_conversations'
|
} from '../actions/chat_conversations'
|
||||||
import {
|
import {
|
||||||
CHAT_MESSAGES_FETCH_SUCCESS,
|
CHAT_MESSAGES_FETCH_SUCCESS,
|
||||||
@ -34,6 +35,10 @@ export default function chats(state = initialState, action) {
|
|||||||
return state.set('chatConversationRequestCount', action.count)
|
return state.set('chatConversationRequestCount', action.count)
|
||||||
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
|
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
|
||||||
return state.set('chatsUnreadCount', action.count)
|
return state.set('chatsUnreadCount', action.count)
|
||||||
|
case CHAT_CONVERSATION_MARK_READ_FETCH:
|
||||||
|
const chatConversationUnreadCount = action.chatConversation.get('unread_count')
|
||||||
|
const totalUnreadCount = state.get('chatsUnreadCount')
|
||||||
|
return state.set('chatsUnreadCount', Math.max(totalUnreadCount - chatConversationUnreadCount, 0))
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -360,6 +360,7 @@ pre {
|
|||||||
.circle { border-radius: var(--radius-circle); }
|
.circle { border-radius: var(--radius-circle); }
|
||||||
.radiusSmall { border-radius: var(--radius-small); }
|
.radiusSmall { border-radius: var(--radius-small); }
|
||||||
.radiusRounded { border-radius: var(--radius-rounded); }
|
.radiusRounded { border-radius: var(--radius-rounded); }
|
||||||
|
|
||||||
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
|
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
|
||||||
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
|
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
|
||||||
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
|
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
|
||||||
@ -443,6 +444,7 @@ pre {
|
|||||||
|
|
||||||
.bgBlack { background-color: var(--color_black); }
|
.bgBlack { background-color: var(--color_black); }
|
||||||
.bgBlackOpaque { background-color: var(--color_black-opaquer); }
|
.bgBlackOpaque { background-color: var(--color_black-opaquer); }
|
||||||
|
.bgBlackOpaquest { background-color: var(--color_black-opaquest); }
|
||||||
.bgBlackOpaque_onHover:hover { background-color: var(--color_black-opaque); }
|
.bgBlackOpaque_onHover:hover { background-color: var(--color_black-opaque); }
|
||||||
.bgBlackOpaquest_onHover:hover { background-color: var(--color_black-opaquest); }
|
.bgBlackOpaquest_onHover:hover { background-color: var(--color_black-opaquest); }
|
||||||
|
|
||||||
@ -550,10 +552,9 @@ pre {
|
|||||||
.calcH53PX { height: calc(100vh - 53px); }
|
.calcH53PX { height: calc(100vh - 53px); }
|
||||||
.calcH80VH106PX { height: calc(80vh - 106px); }
|
.calcH80VH106PX { height: calc(80vh - 106px); }
|
||||||
|
|
||||||
.calcMaxH370PX { max-height: calc(100vh - 370px); }
|
.calcMaxH410PX { max-height: calc(100vh - 450px); }
|
||||||
@media (min-height: 0px) and (max-height:660px) {
|
@media (min-width: 0px) and (max-width:992) { .calcMaxH410PX { max-height: calc(100vh - 410px); } }
|
||||||
.calcMaxH370PX { max-height: calc(100vh - 140px); }
|
@media (min-height: 0px) and (max-height:660px) { .calcMaxH410PX { max-height: calc(100vh - 140px); } }
|
||||||
}
|
|
||||||
|
|
||||||
.minH100VH { min-height: 100vh; }
|
.minH100VH { min-height: 100vh; }
|
||||||
.minH50VH { min-height: 50vh; }
|
.minH50VH { min-height: 50vh; }
|
||||||
@ -841,6 +842,7 @@ pre {
|
|||||||
.mt5 { margin-top: 5px; }
|
.mt5 { margin-top: 5px; }
|
||||||
.mt2 { margin-top: 2px; }
|
.mt2 { margin-top: 2px; }
|
||||||
.mtAuto { margin-top: auto; }
|
.mtAuto { margin-top: auto; }
|
||||||
|
.mtNeg5PX { margin-top: -5px; }
|
||||||
.mtNeg26PX { margin-top: -26px; }
|
.mtNeg26PX { margin-top: -26px; }
|
||||||
.mtNeg32PX { margin-top: -32px; }
|
.mtNeg32PX { margin-top: -32px; }
|
||||||
.mtNeg50PX { margin-top: -50px; }
|
.mtNeg50PX { margin-top: -50px; }
|
||||||
@ -872,6 +874,7 @@ pre {
|
|||||||
.pl0 { padding-left: 0; }
|
.pl0 { padding-left: 0; }
|
||||||
|
|
||||||
.pr50 { padding-right: 50px; }
|
.pr50 { padding-right: 50px; }
|
||||||
|
.pr20 { padding-right: 20px; }
|
||||||
.pr15 { padding-right: 15px; }
|
.pr15 { padding-right: 15px; }
|
||||||
.pr10 { padding-right: 10px; }
|
.pr10 { padding-right: 10px; }
|
||||||
.pr5 { padding-right: 5px; }
|
.pr5 { padding-right: 5px; }
|
||||||
|
@ -132,7 +132,7 @@ class FeedManager
|
|||||||
private
|
private
|
||||||
|
|
||||||
def push_update_required?(timeline_id)
|
def push_update_required?(timeline_id)
|
||||||
redis.exists("subscribed:#{timeline_id}")
|
redis.exists?("subscribed:#{timeline_id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocks_or_mutes?(receiver_id, account_ids, context)
|
def blocks_or_mutes?(receiver_id, account_ids, context)
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
# is_verified :boolean default(FALSE), not null
|
# is_verified :boolean default(FALSE), not null
|
||||||
# is_donor :boolean default(FALSE), not null
|
# is_donor :boolean default(FALSE), not null
|
||||||
# is_investor :boolean default(FALSE), not null
|
# is_investor :boolean default(FALSE), not null
|
||||||
|
# is_flagged_as_spam :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
@ -91,7 +92,7 @@ class Account < ApplicationRecord
|
|||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||||
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
scope :by_domain_accounts, -> { group(:id).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
||||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
@ -148,6 +149,14 @@ class Account < ApplicationRecord
|
|||||||
Follow.where(target_account_id: id).count
|
Follow.where(target_account_id: id).count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def chat_conversation_accounts_count
|
||||||
|
ChatConversationAccount.where(account_id: id).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def chat_messages_count
|
||||||
|
ChatMessage.where(from_account_id: id).count
|
||||||
|
end
|
||||||
|
|
||||||
def silenced?
|
def silenced?
|
||||||
silenced_at.present?
|
silenced_at.present?
|
||||||
end
|
end
|
||||||
|
@ -101,7 +101,7 @@ class AccountConversation < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def subscribed_to_timeline?
|
def subscribed_to_timeline?
|
||||||
Redis.current.exists("subscribed:#{streaming_channel}")
|
Redis.current.exists?("subscribed:#{streaming_channel}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def streaming_channel
|
def streaming_channel
|
||||||
|
@ -17,15 +17,26 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# : todo : expires
|
# : todo : expires
|
||||||
|
# : todo : max per account
|
||||||
class ChatConversationAccount < ApplicationRecord
|
class ChatConversationAccount < ApplicationRecord
|
||||||
include Paginable
|
include Paginable
|
||||||
|
|
||||||
|
PER_ACCOUNT_APPROVED_LIMIT = 100
|
||||||
|
|
||||||
|
EXPIRATION_POLICY_MAP = {
|
||||||
|
none: nil,
|
||||||
|
five_minutes: '1',
|
||||||
|
sixty_minutes: '2',
|
||||||
|
six_hours: '3',
|
||||||
|
one_day: '4',
|
||||||
|
three_days: '5',
|
||||||
|
one_week: '6',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :chat_conversation
|
belongs_to :chat_conversation
|
||||||
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
|
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
|
||||||
|
|
||||||
# before_validation :set_last_chat_message
|
|
||||||
|
|
||||||
def participant_accounts
|
def participant_accounts
|
||||||
if participant_account_ids.empty?
|
if participant_account_ids.empty?
|
||||||
[account]
|
[account]
|
||||||
@ -35,10 +46,4 @@ class ChatConversationAccount < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_last_chat_message
|
|
||||||
self.last_chat_message_id = nil # : todo :
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@ class HomeFeed < Feed
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||||
if redis.exists("account:#{@account.id}:regeneration")
|
if redis.exists?("account:#{@account.id}:regeneration")
|
||||||
from_database(limit, max_id, since_id, min_id)
|
from_database(limit, max_id, since_id, min_id)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
@ -18,6 +18,7 @@ class HomeFeed < Feed
|
|||||||
private
|
private
|
||||||
|
|
||||||
def from_database(limit, max_id, since_id, min_id)
|
def from_database(limit, max_id, since_id, min_id)
|
||||||
|
puts "tilly from_database"
|
||||||
Status.as_home_timeline(@account)
|
Status.as_home_timeline(@account)
|
||||||
.paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
.paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||||
.reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }
|
.reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }
|
||||||
|
@ -18,9 +18,13 @@ class LinkBlock < ApplicationRecord
|
|||||||
return false if text.nil?
|
return false if text.nil?
|
||||||
return false if text.length < 1
|
return false if text.length < 1
|
||||||
|
|
||||||
urls = text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
|
urls = text.scan(FetchLinkCardService::URL_PATTERN).map {|array|
|
||||||
|
Addressable::URI.parse(array[0]).normalize
|
||||||
|
}
|
||||||
url = urls.first
|
url = urls.first
|
||||||
link_for_fetch = TagManager.instance.normalize_link(url)
|
link_for_fetch = TagManager.instance.normalize_link(url)
|
||||||
where(link: link_for_fetch).exists?
|
link_for_fetch = link_for_fetch.chomp("/")
|
||||||
|
|
||||||
|
where("LOWER(link) LIKE LOWER(?)", "%#{link_for_fetch}%").exists?
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -3,22 +3,23 @@
|
|||||||
#
|
#
|
||||||
# Table name: media_attachments
|
# Table name: media_attachments
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# status_id :bigint(8)
|
# status_id :bigint(8)
|
||||||
# file_file_name :string
|
# file_file_name :string
|
||||||
# file_content_type :string
|
# file_content_type :string
|
||||||
# file_file_size :integer
|
# file_file_size :integer
|
||||||
# file_updated_at :datetime
|
# file_updated_at :datetime
|
||||||
# remote_url :string default(""), not null
|
# remote_url :string default(""), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# shortcode :string
|
# shortcode :string
|
||||||
# type :integer default("image"), not null
|
# type :integer default("image"), not null
|
||||||
# file_meta :json
|
# file_meta :json
|
||||||
# account_id :bigint(8)
|
# account_id :bigint(8)
|
||||||
# description :text
|
# description :text
|
||||||
# scheduled_status_id :bigint(8)
|
# scheduled_status_id :bigint(8)
|
||||||
# blurhash :string
|
# blurhash :string
|
||||||
|
# media_attachment_album_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class MediaAttachment < ApplicationRecord
|
class MediaAttachment < ApplicationRecord
|
||||||
|
25
app/models/media_attachment_album.rb
Normal file
25
app/models/media_attachment_album.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: media_attachment_albums
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# title :text default(""), not null
|
||||||
|
# description :text
|
||||||
|
# account_id :integer not null
|
||||||
|
# visibility :integer default("public"), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# cover_id :bigint(8)
|
||||||
|
#
|
||||||
|
|
||||||
|
class MediaAttachmentAlbum < ApplicationRecord
|
||||||
|
|
||||||
|
enum visibility: [
|
||||||
|
:public,
|
||||||
|
:private,
|
||||||
|
], _suffix: :visibility
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
end
|
@ -290,13 +290,13 @@ class Status < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def as_home_timeline(account)
|
def as_home_timeline(account)
|
||||||
query = where('created_at > ?', 5.days.ago)
|
query = where('created_at > ?', 10.days.ago)
|
||||||
query.where(visibility: [:public, :unlisted, :private])
|
query.where(visibility: [:public, :unlisted, :private])
|
||||||
query.where(account: [account] + account.following).without_replies
|
query.where(account: [account] + account.following).without_replies
|
||||||
end
|
end
|
||||||
|
|
||||||
def as_group_timeline(group)
|
def as_group_timeline(group)
|
||||||
query = where('created_at > ?', 5.days.ago)
|
query = where('created_at > ?', 10.days.ago)
|
||||||
query.where(group: group).without_replies
|
query.where(group: group).without_replies
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
#
|
#
|
||||||
# Table name: status_bookmarks
|
# Table name: status_bookmarks
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint(8) not null
|
# account_id :bigint(8) not null
|
||||||
# status_id :bigint(8) not null
|
# status_id :bigint(8) not null
|
||||||
|
# status_bookmark_collection_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusBookmark < ApplicationRecord
|
class StatusBookmark < ApplicationRecord
|
||||||
|
19
app/models/status_bookmark_collection.rb
Normal file
19
app/models/status_bookmark_collection.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: status_bookmark_collections
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# title :text default(""), not null
|
||||||
|
# account_id :integer not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class StatusBookmarkCollection < ApplicationRecord
|
||||||
|
|
||||||
|
PER_ACCOUNT_LIMIT = 100
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
end
|
@ -31,6 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
store[:unread_count] = unread_count object.current_account
|
store[:unread_count] = unread_count object.current_account
|
||||||
store[:last_read_notification_id] = object.current_account.user.last_read_notification
|
store[:last_read_notification_id] = object.current_account.user.last_read_notification
|
||||||
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
|
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
|
||||||
|
store[:trending_hashtags] = get_trending_hashtags
|
||||||
store[:is_first_session] = is_first_session object.current_account
|
store[:is_first_session] = is_first_session object.current_account
|
||||||
store[:email_confirmed] = object.current_account.user.confirmed?
|
store[:email_confirmed] = object.current_account.user.confirmed?
|
||||||
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
|
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
|
||||||
@ -39,6 +40,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
store
|
store
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def compose
|
def compose
|
||||||
store = {}
|
store = {}
|
||||||
|
|
||||||
@ -78,4 +80,9 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
object.current_account.user.sign_in_count === 1
|
object.current_account.user.sign_in_count === 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_trending_hashtags
|
||||||
|
tags = Redis.current.get("admin_trending_hashtags") || ""
|
||||||
|
return tags.strip.split(", ")
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
|
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
|
||||||
:note, :url, :avatar, :avatar_static, :header, :header_static,
|
:note, :url, :avatar, :avatar_static, :header, :header_static, :is_flagged_as_spam,
|
||||||
:followers_count, :following_count, :statuses_count, :is_pro, :is_verified, :is_donor, :is_investor
|
:followers_count, :following_count, :statuses_count, :is_pro, :is_verified, :is_donor, :is_investor
|
||||||
|
|
||||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
|
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :is_hidden, :is_approved, :unread_count, :is_unread, :chat_conversation_id, :created_at
|
attributes :id, :is_hidden, :is_approved, :unread_count,
|
||||||
|
:is_unread, :chat_conversation_id, :created_at,
|
||||||
|
:is_blocked, :is_muted, :chat_message_expiration_policy
|
||||||
|
|
||||||
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
|
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
|
||||||
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
|
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
|
||||||
@ -22,4 +24,12 @@ class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
|
|||||||
object.unread_count > 0
|
object.unread_count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_blocked
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_muted
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :text_html, :text, :language, :from_account_id,
|
attributes :id, :text_html, :text, :language, :from_account_id,
|
||||||
:chat_conversation_id, :created_at
|
:chat_conversation_id, :created_at, :expires_at
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
12
app/services/delete_chat_message_service.rb
Normal file
12
app/services/delete_chat_message_service.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DeleteChatMessageService < BaseService
|
||||||
|
def call(account, chatMessageId)
|
||||||
|
@chat = ChatMessage.where(from_account: account).find(chatMessageId)
|
||||||
|
|
||||||
|
# : todo :
|
||||||
|
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
||||||
|
|
||||||
|
@chat.destroy!
|
||||||
|
end
|
||||||
|
end
|
@ -6,12 +6,7 @@ class FanOutOnWriteService < BaseService
|
|||||||
# @param [Status] status
|
# @param [Status] status
|
||||||
def call(status)
|
def call(status)
|
||||||
raise GabSocial::RaceConditionError if status.visibility.nil?
|
raise GabSocial::RaceConditionError if status.visibility.nil?
|
||||||
|
deliver_to_self(status) if status.account.local?
|
||||||
if status.direct_visibility? || status.limited_visibility?
|
|
||||||
#
|
|
||||||
else
|
|
||||||
deliver_to_self(status) if status.account.local?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
104
app/services/post_chat_message_service.rb
Normal file
104
app/services/post_chat_message_service.rb
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PostChatMessageService < BaseService
|
||||||
|
|
||||||
|
def call(account, options = {})
|
||||||
|
@account = account
|
||||||
|
@options = options
|
||||||
|
@text = @options[:text] || ''
|
||||||
|
@chat_conversation = @options[:chat_conversation]
|
||||||
|
|
||||||
|
preprocess_attributes!
|
||||||
|
|
||||||
|
validate_text!
|
||||||
|
validate_links!
|
||||||
|
|
||||||
|
set_chat_conversation_recipients!
|
||||||
|
set_message_expiration_date!
|
||||||
|
|
||||||
|
process_chat!
|
||||||
|
postprocess_chat!
|
||||||
|
|
||||||
|
@chat
|
||||||
|
end
|
||||||
|
|
||||||
|
def preprocess_attributes!
|
||||||
|
@text = ActionController::Base.helpers.strip_tags(@text)
|
||||||
|
unless @chat_conversation
|
||||||
|
raise ActiveRecord::RecordInvalid
|
||||||
|
end
|
||||||
|
rescue ArgumentError
|
||||||
|
raise ActiveRecord::RecordInvalid
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_links!
|
||||||
|
raise GabSocial::NotPermittedError if LinkBlock.block?(@text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_text!
|
||||||
|
raise GabSocial::NotPermittedError if @text.nil? || @text.strip.length == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_chat!
|
||||||
|
@chat = ChatMessage.create!(
|
||||||
|
from_account: @account,
|
||||||
|
chat_conversation: @chat_conversation,
|
||||||
|
text: @text
|
||||||
|
expires_at: @expires_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def postprocess_chat!
|
||||||
|
@chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: @chat_conversation)
|
||||||
|
@chat_conversation_recipients_accounts.each do |recipient|
|
||||||
|
recipient.last_chat_message_id = @chat.id
|
||||||
|
recipient.is_hidden = false # reset to show unless blocked
|
||||||
|
|
||||||
|
# Get not mine
|
||||||
|
if @account_conversation.id != recipient.id
|
||||||
|
recipient.unread_count = recipient.unread_count + 1
|
||||||
|
|
||||||
|
# : todo :
|
||||||
|
# check if muting, redis
|
||||||
|
payload = InlineRenderer.render(@chat, recipient.account, :chat_message)
|
||||||
|
Redis.current.publish("chat_messages:#{recipient.account.id}", Oj.dump(event: :notification, payload: payload))
|
||||||
|
else
|
||||||
|
recipient.unread_count = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
recipient.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_chat_conversation_recipients!
|
||||||
|
# : todo :
|
||||||
|
# check if chat blocked
|
||||||
|
# check if normal blocked
|
||||||
|
|
||||||
|
@account_conversation = ChatConversationAccount.where(account: @account, chat_conversation: @chat_conversation).first
|
||||||
|
rescue ArgumentError
|
||||||
|
raise ActiveRecord::RecordInvalid
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_message_expiration_date
|
||||||
|
case @account_conversation.expiration_policy
|
||||||
|
when :five_minutes
|
||||||
|
@expires_at = 5.minutes
|
||||||
|
when :sixty_minutes
|
||||||
|
@expires_at = 1.hour
|
||||||
|
when :six_hours
|
||||||
|
@expires_at = 6.hours
|
||||||
|
when :one_day
|
||||||
|
@expires_at = 1.day
|
||||||
|
when :three_days
|
||||||
|
@expires_at = 3.days
|
||||||
|
when :one_week
|
||||||
|
@expires_at = 1.week
|
||||||
|
else
|
||||||
|
@expires_at = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@expires_at
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
28
app/services/purge_chat_messages_service.rb
Normal file
28
app/services/purge_chat_messages_service.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PurgeChatMessagesService < BaseService
|
||||||
|
def call(account, chat_conversation)
|
||||||
|
unless account.is_pro
|
||||||
|
raise GabSocial::NotPermittedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Destroy all
|
||||||
|
ChatMessage.where(from_account: account, chat_conversation: chat_conversation).in_batches.destroy_all
|
||||||
|
|
||||||
|
@last_chat_in_conversation = ChatMessage.where(chat_conversation: chat_conversation).first
|
||||||
|
|
||||||
|
@chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: chat_conversation)
|
||||||
|
@chat_conversation_recipients_accounts.each do |recipient|
|
||||||
|
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
||||||
|
unless @last_chat_in_conversation.nil?
|
||||||
|
recipient.last_chat_message_id = @last_chat_in_conversation.id
|
||||||
|
else
|
||||||
|
recipient.last_chat_message_id = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reset and save
|
||||||
|
recipient.unread_count = 0
|
||||||
|
recipient.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,14 +5,20 @@ class SuspendAccountService < BaseService
|
|||||||
active_relationships
|
active_relationships
|
||||||
block_relationships
|
block_relationships
|
||||||
blocked_by_relationships
|
blocked_by_relationships
|
||||||
|
chat_block_relationships
|
||||||
|
chat_blocked_by_relationships
|
||||||
conversations
|
conversations
|
||||||
|
chat_conversations
|
||||||
|
chat_messages
|
||||||
custom_filters
|
custom_filters
|
||||||
favourites
|
favourites
|
||||||
follow_requests
|
follow_requests
|
||||||
list_accounts
|
list_accounts
|
||||||
media_attachments
|
media_attachments
|
||||||
mute_relationships
|
mute_relationships
|
||||||
|
chat_mute_relationships
|
||||||
muted_by_relationships
|
muted_by_relationships
|
||||||
|
chat_muted_by_relationships
|
||||||
notifications
|
notifications
|
||||||
owned_lists
|
owned_lists
|
||||||
passive_relationships
|
passive_relationships
|
||||||
@ -21,6 +27,10 @@ class SuspendAccountService < BaseService
|
|||||||
status_bookmarks
|
status_bookmarks
|
||||||
status_pins
|
status_pins
|
||||||
subscriptions
|
subscriptions
|
||||||
|
group_accounts
|
||||||
|
group_join_requests
|
||||||
|
group_removed_accounts
|
||||||
|
shortcuts
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
ASSOCIATIONS_ON_DESTROY = %w(
|
ASSOCIATIONS_ON_DESTROY = %w(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user