Progress
This commit is contained in:
parent
de0c977950
commit
75d52c841e
|
@ -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
|
|
@ -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
|
||||
def index
|
||||
@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
|
||||
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
||||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
||||
|
|
|
@ -1,67 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class GroupsController < BaseController
|
||||
before_action :set_group, except: [:index]
|
||||
before_action :set_filter_params
|
||||
|
||||
def index
|
||||
authorize :group, :index?
|
||||
@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
|
||||
class GroupsController < BaseController
|
||||
before_action :set_group, except: [:index]
|
||||
before_action :set_filter_params
|
||||
|
||||
def index
|
||||
authorize :group, :index?
|
||||
@groups = filtered_groups.page(params[:page])
|
||||
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
|
||||
|
|
@ -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
|
||||
@account = current_account
|
||||
# : todo : add link blocking check for bio
|
||||
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
|
||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
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
|
||||
|
||||
def set_account
|
||||
|
|
|
@ -6,7 +6,7 @@ class Api::V1::ChatConversationController < Api::BaseController
|
|||
|
||||
before_action :require_user!
|
||||
before_action :set_account, only: :create
|
||||
before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_unread]
|
||||
before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_read]
|
||||
|
||||
def show
|
||||
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
|
||||
end
|
||||
|
||||
def mark_chat_conversation_unread
|
||||
@chat_conversation_account.update!(unread_count: 1)
|
||||
def mark_chat_conversation_read
|
||||
@chat_conversation_account.update!(unread_count: 0)
|
||||
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||
end
|
||||
|
||||
|
@ -34,8 +34,13 @@ class Api::V1::ChatConversationController < Api::BaseController
|
|||
end
|
||||
|
||||
def mark_chat_conversation_approved
|
||||
@chat_conversation_account.update!(is_approved: true)
|
||||
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
|
||||
approved_conversation_count = ChatConversationAccount.where(account: @account, is_hidden: false, is_approved: true).count
|
||||
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
|
||||
|
||||
private
|
||||
|
|
|
@ -11,24 +11,16 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
|
|||
after_action :insert_pagination_headers, unless: -> { @chats.empty? }
|
||||
|
||||
def show
|
||||
puts "tilly chat_message_conversations - 1: " + @chats.count.inspect
|
||||
render json: @chats, each_serializer: REST::ChatMessageSerializer
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
puts "tilly destry all chat"
|
||||
# : todo :
|
||||
# check if is pro
|
||||
# @chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
|
||||
|
||||
puts "tilly @chat: " + @chat.inspect
|
||||
|
||||
# : todo :
|
||||
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
||||
|
||||
# @chat.destroy!
|
||||
|
||||
# render json: @chat, serializer: REST::ChatMessageSerializer
|
||||
if current_user.account.is_pro
|
||||
@chat_conversation_account = PurgeChatMessagesService.new.call(current_user.account, @chat_conversation)
|
||||
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
|
||||
|
|
|
@ -5,50 +5,20 @@ class Api::V1::ChatMessagesController < Api::BaseController
|
|||
before_action -> { doorkeeper_authorize! :write, :'write:chats' }
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_chat_conversation, only: :create
|
||||
before_action :set_chat_conversation_recipients, only: :create
|
||||
|
||||
def create
|
||||
@chat = ChatMessage.create!(
|
||||
from_account: current_account,
|
||||
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
|
||||
|
||||
@chat_conversation = ChatConversation.find(chat_params[:chat_conversation_id])
|
||||
@chat = PostChatMessageService.new.call(current_user.account, text: chat_params[:text], chat_conversation: @chat_conversation)
|
||||
render json: @chat, serializer: REST::ChatMessageSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
|
||||
|
||||
# : todo :
|
||||
# make sure last_chat_message_id in chat_account_conversation gets set to last
|
||||
|
||||
@chat.destroy!
|
||||
|
||||
@chat = DeleteChatMessageService.new.call(current_user.account, params[:id])
|
||||
render json: @chat, serializer: REST::ChatMessageSerializer
|
||||
end
|
||||
|
||||
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
|
||||
params.permit(:text, :chat_conversation_id)
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ class Api::V1::GroupsController < Api::BaseController
|
|||
|
||||
@groups = []
|
||||
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
|
||||
|
||||
render json: @groups, each_serializer: REST::GroupSerializer
|
||||
|
@ -59,7 +59,7 @@ class Api::V1::GroupsController < Api::BaseController
|
|||
|
||||
@groups = []
|
||||
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
|
||||
|
||||
render json: @groups, each_serializer: REST::GroupSerializer
|
||||
|
|
|
@ -62,6 +62,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||
end
|
||||
|
||||
def regeneration_in_progress?
|
||||
Redis.current.exists("account:#{current_account.id}:regeneration")
|
||||
Redis.current.exists?("account:#{current_account.id}:regeneration")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,7 +48,25 @@ class EmptyController < ActionController::Base
|
|||
nil
|
||||
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)
|
||||
return default_limit unless params[:limit]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class ManifestsController < EmptyController
|
||||
|
||||
def show
|
||||
render json: InstancePresenter.new, serializer: ManifestSerializer
|
||||
render json:{} # InstancePresenter.new, serializer: ManifestSerializer
|
||||
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]
|
||||
flash[:alert] = 'Unable to change Display name for verified account'
|
||||
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
|
||||
# : todo :
|
||||
# only allowed to change username once per day
|
||||
|
||||
if UpdateAccountService.new.call(@account, account_params)
|
||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
|
@ -33,7 +39,7 @@ class Settings::ProfilesController < Settings::BaseController
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def set_account
|
||||
|
|
|
@ -46,11 +46,11 @@ class Settings::PromotionsController < Admin::BaseController
|
|||
@promotion = Promotion.find(params[:id])
|
||||
end
|
||||
|
||||
def set_filter_params
|
||||
@filter_params = filter_params.to_hash.symbolize_keys
|
||||
end
|
||||
def set_filter_params
|
||||
@filter_params = filter_params.to_hash.symbolize_keys
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
|
||||
end
|
||||
def resource_params
|
||||
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
def index
|
||||
@verification_requests = AccountVerificationRequest.all
|
||||
@verification_requests = AccountVerificationRequest.order('created_at DESC').all
|
||||
end
|
||||
|
||||
def approve
|
||||
verification_request = AccountVerificationRequest.find params[:id]
|
||||
verification_request = AccountVerificationRequest.find(params[:id])
|
||||
|
||||
# Mark user as verified
|
||||
account = verification_request.account
|
||||
|
@ -22,6 +22,8 @@ class Settings::Verifications::ModerationController < Admin::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -38,12 +38,6 @@ module StatusesHelper
|
|||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
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)
|
||||
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
||||
end
|
||||
|
|
|
@ -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_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())
|
||||
|
||||
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')
|
||||
dispatch(importFetchedStatuses(response.data))
|
||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
||||
}).catch(error => {
|
||||
}).catch((error) => {
|
||||
dispatch(fetchBookmarkedStatusesFail(error))
|
||||
})
|
||||
}
|
||||
|
@ -61,11 +75,11 @@ export const expandBookmarkedStatuses = () => (dispatch, getState) => {
|
|||
|
||||
dispatch(expandBookmarkedStatusesRequest())
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
api(getState).get(url).then((response) => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')
|
||||
dispatch(importFetchedStatuses(response.data))
|
||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
|
||||
}).catch(error => {
|
||||
}).catch((error) => {
|
||||
dispatch(expandBookmarkedStatusesFail(error))
|
||||
})
|
||||
}
|
||||
|
@ -85,3 +99,95 @@ const expandBookmarkedStatusesFail = (error) => ({
|
|||
showToast: true,
|
||||
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) => {
|
||||
console.log("blockChatMessenger:", accountId)
|
||||
if (!me || !accountId) return
|
||||
|
||||
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,
|
||||
chatConversationId,
|
||||
top,
|
||||
bottom,
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -56,7 +56,7 @@ export const scrollBottomChatMessageConversation = (chatConversationId, top) =>
|
|||
export const expandChatMessages = (chatConversationId, params = {}, done = noop) => (dispatch, getState) => {
|
||||
if (!me || !chatConversationId) return
|
||||
|
||||
const chatConversation = getState().getIn(['chat_messages', chatConversationId], ImmutableMap())
|
||||
const chatConversation = getState().getIn(['chat_conversations', chatConversationId], ImmutableMap())
|
||||
const isLoadingMore = !!params.maxId
|
||||
|
||||
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_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
|
||||
*/
|
||||
|
@ -309,4 +325,93 @@ export const approveChatConversationRequestSuccess = (chatConversation) => ({
|
|||
|
||||
export const approveChatConversationRequestFail = () => ({
|
||||
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,
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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))
|
||||
|
||||
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))
|
||||
}).catch((error) => {
|
||||
dispatch(deleteChatMessagesFail(error))
|
||||
})
|
||||
}
|
||||
|
||||
const deleteChatMessagesRequest = () => ({
|
||||
const purgeChatMessagesRequest = (chatConversationId) => ({
|
||||
type: CHAT_MESSAGES_PURGE_REQUEST,
|
||||
chatConversationId,
|
||||
})
|
||||
|
||||
const deleteChatMessagesSuccess = (chatConversationId) => ({
|
||||
const purgeChatMessagesSuccess = (chatConversationId) => ({
|
||||
type: CHAT_MESSAGES_PURGE_SUCCESS,
|
||||
chatConversationId,
|
||||
showToast: true,
|
||||
})
|
||||
|
||||
const deleteChatMessagesFail = (error) => ({
|
||||
const purgeChatMessagesFail = (error) => ({
|
||||
type: CHAT_MESSAGES_PURGE_FAIL,
|
||||
showToast: true,
|
||||
error,
|
||||
|
|
|
@ -20,12 +20,12 @@ import { defineMessages } from 'react-intl'
|
|||
import { openModal, closeModal } from './modal'
|
||||
import {
|
||||
MODAL_COMPOSE,
|
||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
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 { me } from '../initial_state'
|
||||
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)
|
||||
|
||||
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()
|
||||
} 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()
|
||||
} 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()
|
||||
} 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()
|
||||
} 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()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
updateTimelineQueue,
|
||||
} from './timelines'
|
||||
import { updateNotificationsQueue } from './notifications'
|
||||
import { sendChatMessageSuccess } from './chat_messages'
|
||||
import { manageIncomingChatMessage } from './chat_messages'
|
||||
import { fetchFilters } from './filters'
|
||||
import { getLocale } from '../locales'
|
||||
import { handleComposeSubmit } from './compose'
|
||||
|
@ -84,7 +84,7 @@ export const connectChatMessagesStream = (accountId) => {
|
|||
onReceive (data) {
|
||||
if (!data['event'] || !data['payload']) return
|
||||
if (data.event === 'notification') {
|
||||
dispatch(sendChatMessageSuccess(JSON.parse(data.payload)))
|
||||
dispatch(manageIncomingChatMessage(JSON.parse(data.payload)))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -189,6 +189,7 @@ class AutosuggestTextbox extends ImmutablePureComponent {
|
|||
id,
|
||||
isPro,
|
||||
isEdit,
|
||||
isModalOpen,
|
||||
} = this.props
|
||||
|
||||
const { suggestionsHidden } = this.state
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
injectIntl,
|
||||
} from 'react-intl'
|
||||
import { openModal } from '../actions/modal'
|
||||
import {
|
||||
me,
|
||||
repository,
|
||||
source_url,
|
||||
me,
|
||||
} from '../initial_state'
|
||||
import { CX, DEFAULT_REL } from '../constants'
|
||||
import { DEFAULT_REL } from '../constants'
|
||||
import Text from './text'
|
||||
import Button from './button'
|
||||
import DotTextSeperator from './dot_text_seperator'
|
||||
|
||||
class LinkFooter extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
noPadding,
|
||||
onOpenHotkeys,
|
||||
} = this.props
|
||||
const { intl } = this.props
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
|
@ -32,12 +27,6 @@ class LinkFooter extends React.PureComponent {
|
|||
href: 'https://help.gab.com',
|
||||
text: intl.formatMessage(messages.help),
|
||||
},
|
||||
// : todo :
|
||||
// {
|
||||
// onClick: onOpenHotkeys,
|
||||
// text: intl.formatMessage(messages.hotkeys),
|
||||
// requiresUser: true,
|
||||
// },
|
||||
{
|
||||
href: '/auth/edit',
|
||||
text: intl.formatMessage(messages.security),
|
||||
|
@ -52,16 +41,16 @@ class LinkFooter extends React.PureComponent {
|
|||
text: intl.formatMessage(messages.investors),
|
||||
},
|
||||
{
|
||||
to: '/about/tos',
|
||||
text: intl.formatMessage(messages.terms),
|
||||
to: '/about/sales',
|
||||
text: intl.formatMessage(messages.salesTerms),
|
||||
},
|
||||
{
|
||||
to: '/about/dmca',
|
||||
text: intl.formatMessage(messages.dmca),
|
||||
},
|
||||
{
|
||||
to: '/about/sales',
|
||||
text: intl.formatMessage(messages.salesTerms),
|
||||
to: '/about/tos',
|
||||
text: intl.formatMessage(messages.terms),
|
||||
},
|
||||
{
|
||||
to: '/about/privacy',
|
||||
|
@ -75,36 +64,33 @@ class LinkFooter extends React.PureComponent {
|
|||
},
|
||||
]
|
||||
|
||||
const containerClasses = CX({
|
||||
d: 1,
|
||||
px10: !noPadding,
|
||||
mb15: 1,
|
||||
})
|
||||
|
||||
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(' ')}>
|
||||
{
|
||||
linkFooterItems.map((linkFooterItem, i) => {
|
||||
if (linkFooterItem.requiresUser && !me) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
isText
|
||||
underlineOnHover
|
||||
color='none'
|
||||
backgroundColor='none'
|
||||
key={`link-footer-item-${i}`}
|
||||
to={linkFooterItem.to}
|
||||
href={linkFooterItem.href}
|
||||
data-method={linkFooterItem.logout ? 'delete' : null}
|
||||
onClick={linkFooterItem.onClick || null}
|
||||
className={[_s.mt5, _s.mb5, _s.pr15].join(' ')}
|
||||
>
|
||||
<Text size='small' color='tertiary'>
|
||||
{linkFooterItem.text}
|
||||
</Text>
|
||||
</Button>
|
||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||
<Button
|
||||
isText
|
||||
underlineOnHover
|
||||
color='none'
|
||||
backgroundColor='none'
|
||||
key={`link-footer-item-${i}`}
|
||||
to={linkFooterItem.to}
|
||||
href={linkFooterItem.href}
|
||||
data-method={linkFooterItem.logout ? 'delete' : null}
|
||||
onClick={linkFooterItem.onClick || null}
|
||||
className={[_s.mt5].join(' ')}
|
||||
>
|
||||
<Text size='small' color='tertiary'>
|
||||
{linkFooterItem.text}
|
||||
</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}.'
|
||||
values={{
|
||||
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}
|
||||
</a>
|
||||
)
|
||||
|
@ -136,8 +122,6 @@ class LinkFooter extends React.PureComponent {
|
|||
const messages = defineMessages({
|
||||
investors: { id: 'getting_started.investors', defaultMessage: 'Investors' },
|
||||
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' },
|
||||
about: { id: 'navigation_bar.info', defaultMessage: 'About' },
|
||||
developers: { id: 'getting_started.developers', defaultMessage: 'Developers' },
|
||||
|
@ -148,16 +132,8 @@ const messages = defineMessages({
|
|||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenHotkeys() {
|
||||
dispatch(openModal('HOTKEYS'))
|
||||
},
|
||||
})
|
||||
|
||||
LinkFooter.propTypes = {
|
||||
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,
|
||||
top0: 1,
|
||||
h100PC: 1,
|
||||
w100PC: 1,
|
||||
// w100PC: 1,
|
||||
py2: !isSmall,
|
||||
px2: !isSmall,
|
||||
})
|
||||
|
@ -87,7 +87,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||
const linkClasses = CX({
|
||||
d: 1,
|
||||
w100PC: 1,
|
||||
h100PC: 1,
|
||||
// h100PC: 1,
|
||||
overflowHidden: 1,
|
||||
border1PX: 1,
|
||||
borderColorPrimary: 1,
|
||||
|
@ -96,7 +96,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
|
||||
|
||||
return (
|
||||
<div className={[_s.d, _s.w25PC, _s.pt25PC].join(' ')}>
|
||||
<div className={[_s.d, _s.pt25PC].join(' ')}>
|
||||
<div className={containerClasses}>
|
||||
<NavLink
|
||||
to={statusUrl}
|
||||
|
@ -117,6 +117,7 @@ class MediaItem extends ImmutablePureComponent {
|
|||
visible &&
|
||||
<Image
|
||||
height='100%'
|
||||
width=''
|
||||
src={attachment.get('preview_url')}
|
||||
alt={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
|
||||
|
||||
return (
|
||||
<div style={{width: '512px'}} className={[_s.d, _s.modal].join(' ')}>
|
||||
<div style={{width: '580px'}} 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.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(' ')}>
|
||||
<Button
|
||||
backgroundColor='none'
|
||||
|
@ -69,8 +69,8 @@ class ComposeModal extends ImmutablePureComponent {
|
|||
<ComposeFormSubmitButton type='header' />
|
||||
</div>
|
||||
</div>
|
||||
<div className={[_s.d].join(' ')}>
|
||||
<TimelineComposeBlock isModal />
|
||||
<div className={[_s.d, _s.pt5].join(' ')}>
|
||||
<TimelineComposeBlock isModal formLocation='modal' />
|
||||
</div>
|
||||
</Block>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { MODAL_DECK_COLUMN_ADD_OPTIONS } from '../../constants'
|
||||
import { setDeckColumnAtIndex } from '../../actions/deck'
|
||||
import { openModal } from '../../actions/modal'
|
||||
import ModalLayout from './modal_layout'
|
||||
|
@ -10,27 +11,19 @@ import Text from '../text'
|
|||
class DeckColumnAddModal extends React.PureComponent {
|
||||
|
||||
onAdd = (column) => {
|
||||
console.log("onAdd column: ", column)
|
||||
switch (column) {
|
||||
case 'user':
|
||||
//
|
||||
break
|
||||
case 'list':
|
||||
//
|
||||
break
|
||||
case 'group':
|
||||
//
|
||||
break
|
||||
case 'hashtag':
|
||||
//
|
||||
break
|
||||
default:
|
||||
this.props.dispatch(setDeckColumnAtIndex(column))
|
||||
this.props.onClose()
|
||||
break
|
||||
const moreOptions = ['user', 'list', 'group', 'hashtag']
|
||||
if (moreOptions.indexOf(column) > -1) {
|
||||
this.openOptionsModal(column)
|
||||
} else {
|
||||
this.props.dispatch(setDeckColumnAtIndex(column))
|
||||
this.props.onClose()
|
||||
}
|
||||
}
|
||||
|
||||
openOptionsModal = (column) => {
|
||||
this.props.dispatch(openModal(MODAL_DECK_COLUMN_ADD_OPTIONS, { column }))
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
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_CONFIRM,
|
||||
MODAL_DECK_COLUMN_ADD,
|
||||
MODAL_DECK_COLUMN_ADD_OPTIONS,
|
||||
MODAL_DISPLAY_OPTIONS,
|
||||
MODAL_EDIT_PROFILE,
|
||||
MODAL_EDIT_SHORTCUTS,
|
||||
|
@ -51,6 +52,7 @@ import {
|
|||
ComposeModal,
|
||||
ConfirmationModal,
|
||||
DeckColumnAddModal,
|
||||
DeckColumnAddOptionsModal,
|
||||
DisplayOptionsModal,
|
||||
EditProfileModal,
|
||||
EditShortcutsModal,
|
||||
|
@ -89,6 +91,7 @@ const MODAL_COMPONENTS = {
|
|||
[MODAL_COMPOSE]: ComposeModal,
|
||||
[MODAL_CONFIRM]: ConfirmationModal,
|
||||
[MODAL_DECK_COLUMN_ADD]: DeckColumnAddModal,
|
||||
[MODAL_DECK_COLUMN_ADD_OPTIONS]: DeckColumnAddOptionsModal,
|
||||
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
|
||||
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
|
||||
[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(' ')}>
|
||||
<AvatarGroup accounts={otherAccounts} size={35} noHover />
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import Heading from '../heading'
|
|||
import Button from '../button'
|
||||
import BackButton from '../back_button'
|
||||
import Text from '../text'
|
||||
import CharacterCounter from '../character_counter'
|
||||
import ComposeFormSubmitButton from '../../features/compose/components/compose_form_submit_button'
|
||||
|
||||
class ComposeNavigationBar extends React.PureComponent {
|
||||
|
||||
|
@ -26,13 +26,7 @@ class ComposeNavigationBar extends React.PureComponent {
|
|||
} = this.props
|
||||
|
||||
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 (
|
||||
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}>
|
||||
<div className={[_s.d, _s.h53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].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(' ')}>
|
||||
<Heading size='h1'>
|
||||
Compose
|
||||
<span className={[_s.dangerousContent, _s.fs24PX, _s.colorNavigation].join(' ')}>
|
||||
Compose
|
||||
</span>
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<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} />
|
||||
|
||||
<Button {...buttonOptions}>
|
||||
<Text color='inherit' weight='bold' size='medium' className={_s.px5}>
|
||||
POST
|
||||
</Text>
|
||||
</Button>
|
||||
<ComposeFormSubmitButton type='navigation' />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -61,7 +61,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
|
|||
noPadding
|
||||
title={intl.formatMessage(messages.title)}
|
||||
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(' ')}>
|
||||
{
|
||||
|
|
|
@ -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 { closePopover } from '../../actions/popover'
|
||||
import { openModal } from '../../actions/modal'
|
||||
import {
|
||||
isChatMessengerBlocked,
|
||||
isChatMessengerMuted,
|
||||
blockChatMessenger,
|
||||
unblockChatMessenger,
|
||||
muteChatMessenger,
|
||||
unmuteChatMessenger,
|
||||
} from '../../actions/chat_conversation_accounts'
|
||||
import { hideChatConversation } from '../../actions/chat_conversations'
|
||||
import { purgeChatMessages } from '../../actions/chat_messages'
|
||||
import { MODAL_PRO_UPGRADE } from '../../constants'
|
||||
import { me } from '../../initial_state'
|
||||
import { makeGetChatConversation } from '../../selectors'
|
||||
|
@ -27,21 +21,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||
this.handleOnClosePopover()
|
||||
}
|
||||
|
||||
handleOnBlock = () => {
|
||||
this.props.onBlock()
|
||||
this.handleOnClosePopover()
|
||||
}
|
||||
|
||||
handleOnUnblock = () => {
|
||||
this.props.onUnblock()
|
||||
this.handleOnClosePopover()
|
||||
}
|
||||
|
||||
handleOnMute = () => {
|
||||
this.props.onMute()
|
||||
this.handleOnClosePopover()
|
||||
}
|
||||
|
||||
handleOnUnmute = () => {
|
||||
this.props.onUnute()
|
||||
this.handleOnClosePopover()
|
||||
|
@ -51,7 +30,7 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||
if (!this.props.isPro) {
|
||||
this.props.openProUpgradeModal()
|
||||
} else {
|
||||
this.props.onPurge()
|
||||
this.props.onPurge(this.props.chatConversationId)
|
||||
}
|
||||
|
||||
this.handleOnClosePopover()
|
||||
|
@ -68,18 +47,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
|
|||
} = this.props
|
||||
|
||||
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,
|
||||
title: 'Hide Conversation',
|
||||
|
@ -123,6 +90,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
onSetCommentSortingSetting(type) {
|
||||
dispatch(closePopover())
|
||||
},
|
||||
onPurge(chatConversationId) {
|
||||
dispatch(purgeChatMessages(chatConversationId))
|
||||
},
|
||||
onHide(chatConversationId) {
|
||||
dispatch(hideChatConversation(chatConversationId))
|
||||
},
|
||||
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 {
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
POPOVER_CHAT_CONVERSATION_OPTIONS,
|
||||
POPOVER_CHAT_MESSAGE_DELETE,
|
||||
POPOVER_CHAT_MESSAGE_OPTIONS,
|
||||
POPOVER_COMMENT_SORTING_OPTIONS,
|
||||
POPOVER_COMPOSE_POST_DESTINATION,
|
||||
POPOVER_DATE_PICKER,
|
||||
POPOVER_EMOJI_PICKER,
|
||||
POPOVER_GROUP_LIST_SORT_OPTIONS,
|
||||
|
@ -23,8 +24,9 @@ import {
|
|||
} from '../../constants'
|
||||
import {
|
||||
ChatConversationOptionsPopover,
|
||||
ChatMessageDeletePopover,
|
||||
ChatMessageOptionsPopover,
|
||||
CommentSortingOptionsPopover,
|
||||
ComposePostDesinationPopover,
|
||||
DatePickerPopover,
|
||||
EmojiPickerPopover,
|
||||
GroupListSortOptionsPopover,
|
||||
|
@ -59,8 +61,9 @@ const initialState = getWindowDimension()
|
|||
|
||||
const POPOVER_COMPONENTS = {
|
||||
[POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
|
||||
[POPOVER_CHAT_MESSAGE_DELETE]: ChatMessageDeletePopover,
|
||||
[POPOVER_CHAT_MESSAGE_OPTIONS]: ChatMessageOptionsPopover,
|
||||
[POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
|
||||
[POPOVER_COMPOSE_POST_DESTINATION]: ComposePostDesinationPopover,
|
||||
[POPOVER_DATE_PICKER]: DatePickerPopover,
|
||||
[POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
|
||||
[POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,
|
||||
|
|
|
@ -5,15 +5,16 @@ import { defineMessages, injectIntl } from 'react-intl'
|
|||
import { closePopover } from '../../actions/popover'
|
||||
import { changeExpiresAt } from '../../actions/compose'
|
||||
import {
|
||||
STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
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'
|
||||
import Text from '../text'
|
||||
|
||||
class StatusExpirationOptionsPopover extends React.PureComponent {
|
||||
|
||||
|
@ -34,43 +35,49 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
|
|||
} = 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(STATUS_EXPIRATION_OPTION_5_MINUTES),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_5_MINUTES,
|
||||
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(STATUS_EXPIRATION_OPTION_60_MINUTES),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_60_MINUTES,
|
||||
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(STATUS_EXPIRATION_OPTION_6_HOURS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_6_HOURS,
|
||||
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(STATUS_EXPIRATION_OPTION_24_HOURS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_24_HOURS,
|
||||
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(STATUS_EXPIRATION_OPTION_3_DAYS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_3_DAYS,
|
||||
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(STATUS_EXPIRATION_OPTION_7_DAYS),
|
||||
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_7_DAYS,
|
||||
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
|
||||
isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -88,8 +95,9 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
|
|||
isXS={isXS}
|
||||
onClose={this.handleOnClosePopover}
|
||||
>
|
||||
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>This gab deletes after:</Text>
|
||||
<List
|
||||
scrollKey='group_list_sort_options'
|
||||
scrollKey='status_expiration'
|
||||
items={listItems}
|
||||
size={isXS ? 'large' : 'small'}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
|
|||
import { connect } from 'react-redux'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { openModal } from '../../actions/modal'
|
||||
import { showToast } from '../../actions/toasts'
|
||||
import { closePopover } from '../../actions/popover'
|
||||
import PopoverLayout from './popover_layout'
|
||||
import Button from '../button'
|
||||
|
@ -31,6 +32,7 @@ class StatusSharePopover extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
document.body.removeChild(textarea)
|
||||
this.props.onShowCopyToast()
|
||||
this.handleClosePopover()
|
||||
}
|
||||
|
||||
|
@ -157,6 +159,9 @@ const messages = defineMessages({
|
|||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClosePopover: () => dispatch(closePopover()),
|
||||
onShowCopyToast() {
|
||||
dispatch(showToast())
|
||||
},
|
||||
})
|
||||
|
||||
StatusSharePopover.propTypes = {
|
||||
|
|
|
@ -49,6 +49,7 @@ class StatusVisibilityDropdown extends React.PureComponent {
|
|||
isXS={isXS}
|
||||
onClose={this.handleOnClosePopover}
|
||||
>
|
||||
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>Status Visibility:</Text>
|
||||
<div className={[_s.d].join(' ')}>
|
||||
{
|
||||
options.map((option, i) => {
|
||||
|
|
|
@ -148,7 +148,7 @@ class StyleButton extends React.PureComponent {
|
|||
px10: 1,
|
||||
mr5: 1,
|
||||
noSelect: 1,
|
||||
bgSecondaryDark_onHover: 1,
|
||||
bgSubtle_onHover: 1,
|
||||
bgBrandLight: active,
|
||||
bgTransparent: 1,
|
||||
radiusSmall: 1,
|
||||
|
@ -162,7 +162,7 @@ class StyleButton extends React.PureComponent {
|
|||
onMouseDown={this.handleOnClick}
|
||||
title={label}
|
||||
>
|
||||
<Icon id={icon} size='12px' className={_s[iconColor]} />
|
||||
<Icon id={icon} size='16px' className={_s[iconColor]} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -43,12 +43,16 @@ class DeckSidebar extends ImmutablePureComponent {
|
|||
this.props.onOpenComposeModal()
|
||||
}
|
||||
|
||||
scrollToItem = () => {
|
||||
|
||||
}
|
||||
|
||||
setAvatarNode = (c) => {
|
||||
this.avatarNode = c
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, logoDisabled } = this.props
|
||||
const { account, gabDeckOrder, logoDisabled } = this.props
|
||||
|
||||
const isPro = !!account ? account.get('is_pro') : false
|
||||
|
||||
|
@ -83,6 +87,22 @@ class DeckSidebar extends ImmutablePureComponent {
|
|||
|
||||
<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 />
|
||||
|
||||
{ isPro && <NavigationBarButton title=' ' icon='add' onClick={this.handleOnOpenNewColumnModel} /> }
|
||||
|
@ -119,6 +139,7 @@ const mapStateToProps = (state) => ({
|
|||
account: makeGetAccount()(state, me),
|
||||
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
|
||||
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
|
||||
gabDeckOrder: state.getIn(['settings', 'gabDeckOrder']),
|
||||
})
|
||||
|
||||
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 Responsive from '../features/ui/util/responsive_component'
|
||||
import Avatar from './avatar'
|
||||
import Heading from './heading'
|
||||
import Button from './button'
|
||||
import Text from './text'
|
||||
|
||||
class TimelineComposeBlock extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formLocation,
|
||||
account,
|
||||
size,
|
||||
intl,
|
||||
|
@ -27,7 +29,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
|
|||
return (
|
||||
<section className={_s.d}>
|
||||
<div className={[_s.d, _s.flexRow].join(' ')}>
|
||||
<ComposeFormContainer {...rest} isModal={isModal} />
|
||||
<ComposeFormContainer {...rest} isModal={isModal} formLocation={formLocation} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
@ -39,17 +41,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
|
|||
classNames={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden, _s.radiusSmall].join(' ')}
|
||||
classNamesXS={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden].join(' ')}
|
||||
>
|
||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||
<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} />
|
||||
<ComposeFormContainer {...rest} formLocation={formLocation} />
|
||||
</ResponsiveClassesComponent>
|
||||
</section>
|
||||
)
|
||||
|
@ -70,10 +62,12 @@ TimelineComposeBlock.propTypes = {
|
|||
account: ImmutablePropTypes.map.isRequired,
|
||||
size: PropTypes.number,
|
||||
isModal: PropTypes.bool,
|
||||
formLocation: PropTypes.string,
|
||||
}
|
||||
|
||||
TimelineComposeBlock.defaultProps = {
|
||||
size: 32,
|
||||
formLocation: 'timeline',
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(TimelineComposeBlock))
|
|
@ -344,7 +344,7 @@ class Video extends ImmutablePureComponent {
|
|||
this.video.play()
|
||||
}
|
||||
setTimeout(() => { // : hack :
|
||||
this.video.requestPictureInPicture()
|
||||
this.video.requestPictureInPicture()
|
||||
}, 500)
|
||||
} else {
|
||||
document.exitPictureInPicture()
|
||||
|
|
|
@ -10,7 +10,6 @@ export const BREAKPOINT_LARGE = 1280
|
|||
export const BREAKPOINT_MEDIUM = 1160
|
||||
export const BREAKPOINT_SMALL = 1080
|
||||
export const BREAKPOINT_EXTRA_SMALL = 992
|
||||
export const BREAKPOINT_EXTRA_EXTRA_SMALL = 767
|
||||
|
||||
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 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_COMPOSE_POST_DESTINATION = 'COMPOSE_POST_DESTINATION'
|
||||
export const POPOVER_DATE_PICKER = 'DATE_PICKER'
|
||||
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
|
||||
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_CONFIRM = 'CONFIRM'
|
||||
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_EDIT_PROFILE = 'EDIT_PROFILE'
|
||||
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 STATUS_EXPIRATION_OPTION_5_MINUTES = '5-minutes'
|
||||
export const STATUS_EXPIRATION_OPTION_60_MINUTES = '60-minutes'
|
||||
export const STATUS_EXPIRATION_OPTION_6_HOURS = '6-hours'
|
||||
export const STATUS_EXPIRATION_OPTION_24_HOURS = '24-hours'
|
||||
export const STATUS_EXPIRATION_OPTION_3_DAYS = '3-days'
|
||||
export const STATUS_EXPIRATION_OPTION_7_DAYS = '7-days'
|
||||
export const EXPIRATION_OPTION_5_MINUTES = 'five_minutes'
|
||||
export const EXPIRATION_OPTION_60_MINUTES = 'one_hour'
|
||||
export const EXPIRATION_OPTION_6_HOURS = 'six_hours'
|
||||
export const EXPIRATION_OPTION_24_HOURS = 'one_day'
|
||||
export const EXPIRATION_OPTION_3_DAYS = 'three_days'
|
||||
export const EXPIRATION_OPTION_7_DAYS = 'one_week'
|
||||
|
||||
export const GROUP_TIMELINE_SORTING_TYPE_HOT = 'hot'
|
||||
export const GROUP_TIMELINE_SORTING_TYPE_NEWEST = 'newest'
|
||||
|
|
|
@ -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 PropTypes from 'prop-types'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { connect } from 'react-redux'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import { length } from 'stringz'
|
||||
import { isMobile } from '../../../utils/is_mobile'
|
||||
import { countableText } from '../../ui/util/counter'
|
||||
import {
|
||||
CX,
|
||||
MAX_POST_CHARACTER_COUNT,
|
||||
ALLOWED_AROUND_SHORT_CODE,
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
BREAKPOINT_EXTRA_EXTRA_SMALL,
|
||||
BREAKPOINT_MEDIUM,
|
||||
MODAL_COMPOSE,
|
||||
POPOVER_COMPOSE_POST_DESTINATION,
|
||||
} from '../../../constants'
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
||||
import Responsive from '../../ui/util/responsive_component'
|
||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
||||
import { openModal } from '../../../actions/modal'
|
||||
import { openPopover } from '../../../actions/popover'
|
||||
import Avatar from '../../../components/avatar'
|
||||
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 ComposeExtraButtonList from './compose_extra_button_list'
|
||||
import Text from '../../../components/text'
|
||||
|
||||
class ComposeDestinationHeader extends ImmutablePureComponent {
|
||||
|
||||
handleOnClick = () => {
|
||||
this.props.onOpenPopover(this.desinationBtn)
|
||||
}
|
||||
|
||||
handleOnExpand = () => {
|
||||
this.props.onOpenModal()
|
||||
}
|
||||
|
||||
setDestinationBtn = (c) => {
|
||||
this.desinationBtn = c
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account } = this.props
|
||||
const { account, isModal } = this.props
|
||||
|
||||
const title = 'Post to timeline'
|
||||
|
||||
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(' ')}>
|
||||
<Avatar account={account} size={28} />
|
||||
<div className={[_s.ml15].join(' ')}>
|
||||
<Button
|
||||
isNarrow
|
||||
radiusSmall
|
||||
backgroundColor='tertiary'
|
||||
color='primary'
|
||||
onClick={this.handleOnClick}
|
||||
>
|
||||
<Text color='inherit' size='small' className={_s.jcCenter}>
|
||||
{title}
|
||||
<Icon id='caret-down' size='8px' className={_s.ml5} />
|
||||
</Text>
|
||||
</Button>
|
||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.bgPrimary, _s.w100PC, _s.h40PX, _s.pr15].join(' ')}>
|
||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.pl15, _s.flexGrow1, _s.mrAuto, _s.h40PX].join(' ')}>
|
||||
<Avatar account={account} size={28} />
|
||||
<div className={[_s.ml15].join(' ')}>
|
||||
<Button
|
||||
isNarrow
|
||||
isOutline
|
||||
radiusSmall
|
||||
buttonRef={this.setDestinationBtn}
|
||||
backgroundColor='secondary'
|
||||
color='primary'
|
||||
onClick={this.handleOnClick}
|
||||
className={[_s.border1PX, _s.borderColorPrimary].join(' ')}
|
||||
>
|
||||
<Text color='inherit' size='small' className={_s.jcCenter}>
|
||||
{title}
|
||||
<Icon id='caret-down' size='8px' className={_s.ml5} />
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
!isModal &&
|
||||
<Button
|
||||
isText
|
||||
isNarrow
|
||||
backgroundColor='none'
|
||||
color='tertiary'
|
||||
icon='fullscreen'
|
||||
onClick={this.handleOnExpand}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenModal() {
|
||||
dispatch(openModal(MODAL_COMPOSE))
|
||||
},
|
||||
onOpenPopover(targetRef) {
|
||||
dispatch(openPopover(POPOVER_COMPOSE_POST_DESTINATION, {
|
||||
targetRef,
|
||||
position: 'bottom',
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
ComposeDestinationHeader.propTypes = {
|
||||
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({
|
||||
d: 1,
|
||||
mr5: 1,
|
||||
jcCenter: 1,
|
||||
h40PX: 1,
|
||||
mr5: 1,
|
||||
})
|
||||
|
||||
const btnClasses = CX({
|
||||
d: 1,
|
||||
circle: 1,
|
||||
circle: small,
|
||||
noUnderline: 1,
|
||||
font: 1,
|
||||
cursorPointer: 1,
|
||||
|
@ -37,21 +37,25 @@ class ComposeExtraButton extends React.PureComponent {
|
|||
outlineNone: 1,
|
||||
bgTransparent: 1,
|
||||
flexRow: 1,
|
||||
aiCenter: 1,
|
||||
// jcCenter: !small,
|
||||
bgSubtle_onHover: !active,
|
||||
bgBrandLight: active,
|
||||
py10: 1,
|
||||
px10: 1,
|
||||
px10: small,
|
||||
radiusSmall: !small,
|
||||
})
|
||||
|
||||
const iconClasses = CX(iconClassName, {
|
||||
const iconClasses = CX(active ? null : iconClassName, {
|
||||
cSecondary: !active,
|
||||
cWhite: active,
|
||||
mr10: 1,
|
||||
mr10: !small,
|
||||
py2: small,
|
||||
ml10: small,
|
||||
ml10: !small,
|
||||
px2: small,
|
||||
})
|
||||
|
||||
const iconSize = !small ? '18px' : '16px'
|
||||
const iconSize = '16px'
|
||||
const textColor = !active ? 'primary' : 'white'
|
||||
|
||||
return (
|
||||
|
@ -65,13 +69,13 @@ class ComposeExtraButton extends React.PureComponent {
|
|||
backgroundColor='none'
|
||||
iconClassName={iconClasses}
|
||||
icon={icon}
|
||||
iconSize={iconSize}
|
||||
iconSize='16px'
|
||||
buttonRef={!children ? buttonRef : undefined}
|
||||
>
|
||||
{ children }
|
||||
{
|
||||
!small &&
|
||||
<Text color={textColor} weight='medium' className={[_s.pr5].join(' ')}>
|
||||
<Text color={textColor} weight='medium' className={[_s.pr10].join(' ')}>
|
||||
{title}
|
||||
</Text>
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '../../../constants'
|
||||
import Responsive from '../../ui/util/responsive_component'
|
||||
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
|
||||
import Text from '../../../components/text'
|
||||
import EmojiPickerButton from './emoji_picker_button'
|
||||
import PollButton from './poll_button'
|
||||
import SchedulePostButton from './schedule_post_button'
|
||||
|
@ -22,6 +23,7 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||
|
||||
state = {
|
||||
height: initialState.height,
|
||||
width: initialState.width,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -31,9 +33,9 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleResize = () => {
|
||||
const { height } = getWindowDimension()
|
||||
const { height, width } = getWindowDimension()
|
||||
|
||||
this.setState({ height })
|
||||
this.setState({ height, width })
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -48,26 +50,33 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||
edit,
|
||||
hidePro,
|
||||
isModal,
|
||||
isStandalone,
|
||||
formLocation,
|
||||
} = 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({
|
||||
d: 1,
|
||||
w100PC: 1,
|
||||
bgPrimary: 1,
|
||||
px15: 1,
|
||||
py10: 1,
|
||||
px5: 1,
|
||||
py5: 1,
|
||||
mb10: 1,
|
||||
mtAuto: 1,
|
||||
boxShadowBlockY: 1,
|
||||
topLeftRadiusSmall: 1,
|
||||
radiusSmall: 1,
|
||||
borderTop1PX: 1,
|
||||
borderBottom1PX: 1,
|
||||
boxShadowBlock: 1,
|
||||
borderColorSecondary: 1,
|
||||
topRightRadiusSmall: 1,
|
||||
flexRow: small,
|
||||
overflowXScroll: small,
|
||||
noScrollbar: small,
|
||||
flexWrap: 1,
|
||||
flexRow: (small || !isTimeline || isXS) && !isStandalone,
|
||||
jcSpaceAround: isXS,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -79,8 +88,8 @@ class ComposeExtraButtonList extends React.PureComponent {
|
|||
<SpoilerButton small={small} />
|
||||
{ !hidePro && !edit && <SchedulePostButton small={small} /> }
|
||||
{ !hidePro && !edit && <ExpiresPostButton small={small} /> }
|
||||
{ !hidePro && <RichTextEditorButton small={small} /> }
|
||||
</div>
|
||||
{ !hidePro && !isXS && <RichTextEditorButton small={small} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +99,7 @@ ComposeExtraButtonList.propTypes = {
|
|||
edit: PropTypes.bool,
|
||||
isMatch: PropTypes.bool,
|
||||
isModal: PropTypes.bool,
|
||||
isStandalone: PropTypes.bool,
|
||||
formLocation: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ComposeExtraButtonList
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
|
@ -11,7 +12,6 @@ import {
|
|||
MAX_POST_CHARACTER_COUNT,
|
||||
ALLOWED_AROUND_SHORT_CODE,
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
BREAKPOINT_EXTRA_EXTRA_SMALL,
|
||||
BREAKPOINT_MEDIUM,
|
||||
} from '../../../constants'
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
||||
|
@ -62,80 +62,70 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleComposeFocus = () => {
|
||||
this.setState({
|
||||
composeFocused: true,
|
||||
});
|
||||
this.setState({ composeFocused: true })
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit();
|
||||
this.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const { isStandalone, isModalOpen, shouldCondense } = this.props
|
||||
|
||||
const { isModalOpen, shouldCondense } = this.props
|
||||
|
||||
if (!this.form) return false
|
||||
if (e.target) {
|
||||
if (e.target.classList.contains('react-datepicker__time-list-item')) return false
|
||||
}
|
||||
if (!this.form.contains(e.target)) {
|
||||
this.handleClickOutside()
|
||||
} else {
|
||||
// : todo :
|
||||
// if mobile go to /compose else openModal
|
||||
if (!isStandalone && !isModalOpen && !shouldCondense) {
|
||||
this.props.openComposeModal()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props;
|
||||
const condensed = shouldCondense && !text;
|
||||
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props
|
||||
const condensed = shouldCondense && !text
|
||||
|
||||
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
|
||||
this.props.setScheduledAt(null);
|
||||
this.props.setScheduledAt(null)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
composeFocused: false,
|
||||
});
|
||||
this.setState({ composeFocused: false })
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
// this.props.onChange(this.autosuggestTextarea.textbox.value);
|
||||
// this.props.onChange(this.autosuggestTextarea.textbox.value)
|
||||
// }
|
||||
|
||||
// Submit disabled:
|
||||
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)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
this.props.onSubmit(groupId, this.props.replyToId, this.context.router, autoJoinGroup)
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
this.props.onClearSuggestions()
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
this.props.onFetchSuggestions(token)
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text'])
|
||||
}
|
||||
|
||||
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text'])
|
||||
}
|
||||
|
||||
handleChangeSpoilerText = (value) => {
|
||||
|
@ -143,11 +133,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleClick, false);
|
||||
document.addEventListener('click', this.handleClick, false)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClick, false);
|
||||
document.removeEventListener('click', this.handleClick, false)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -156,24 +146,24 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - 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.
|
||||
if (this.props.focusDate !== prevProps.focusDate) {
|
||||
let selectionEnd, selectionStart;
|
||||
let selectionEnd, selectionStart
|
||||
|
||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||
selectionEnd = this.props.text.length;
|
||||
selectionStart = this.props.text.search(/\s/) + 1;
|
||||
selectionEnd = this.props.text.length
|
||||
selectionStart = this.props.text.search(/\s/) + 1
|
||||
} else if (typeof this.props.caretPosition === 'number') {
|
||||
selectionStart = this.props.caretPosition;
|
||||
selectionEnd = this.props.caretPosition;
|
||||
selectionStart = this.props.caretPosition
|
||||
selectionEnd = this.props.caretPosition
|
||||
} else {
|
||||
selectionEnd = this.props.text.length;
|
||||
selectionStart = selectionEnd;
|
||||
selectionEnd = this.props.text.length
|
||||
selectionStart = selectionEnd
|
||||
}
|
||||
|
||||
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd);
|
||||
// this.autosuggestTextarea.textbox.focus();
|
||||
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd)
|
||||
// this.autosuggestTextarea.textbox.focus()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,7 +180,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
intl,
|
||||
account,
|
||||
onPaste,
|
||||
showSearch,
|
||||
anyMedia,
|
||||
shouldCondense,
|
||||
autoFocus,
|
||||
|
@ -208,218 +197,151 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
isSubmitting,
|
||||
isPro,
|
||||
hidePro,
|
||||
isStandalone,
|
||||
dontOpenModal,
|
||||
formLocation,
|
||||
} = this.props
|
||||
|
||||
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 shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
|
||||
const shouldAutoFocus = autoFocus && !isMobile(window.innerWidth)
|
||||
|
||||
const parentContainerClasses = CX({
|
||||
const containerClasses = CX({
|
||||
d: 1,
|
||||
w100PC: 1,
|
||||
flexRow: !shouldCondense,
|
||||
pb10: !shouldCondense,
|
||||
pb10: 1,
|
||||
calcMaxH410PX: 1,
|
||||
minH200PX: isModalOpen && isMatch,
|
||||
overflowYScroll: 1,
|
||||
boxShadowBlock: 1,
|
||||
borderTop1PX: 1,
|
||||
borderColorSecondary: 1,
|
||||
})
|
||||
|
||||
const childContainerClasses = CX({
|
||||
d: 1,
|
||||
flexWrap: 1,
|
||||
overflowHidden: 1,
|
||||
flex1: 1,
|
||||
minH28PX: 1,
|
||||
py2: shouldCondense,
|
||||
aiEnd: shouldCondense,
|
||||
flexRow: shouldCondense,
|
||||
radiusSmall: shouldCondense,
|
||||
bgSubtle: shouldCondense,
|
||||
px5: shouldCondense,
|
||||
})
|
||||
|
||||
const actionsContainerClasses = CX({
|
||||
d: 1,
|
||||
flexRow: 1,
|
||||
aiCenter: !shouldCondense,
|
||||
aiStart: shouldCondense,
|
||||
mt10: !shouldCondense,
|
||||
px10: !shouldCondense,
|
||||
mlAuto: shouldCondense,
|
||||
flexWrap: !shouldCondense,
|
||||
})
|
||||
|
||||
const commentPublishBtnClasses = CX({
|
||||
d: 1,
|
||||
jcCenter: 1,
|
||||
displayNone: length(this.props.text) === 0,
|
||||
})
|
||||
const textbox = (
|
||||
<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}
|
||||
isModalOpen={isModalOpen}
|
||||
isPro={isPro}
|
||||
isEdit={!!edit}
|
||||
id='main-composer'
|
||||
/>
|
||||
)
|
||||
|
||||
if (shouldCondense) {
|
||||
return (
|
||||
<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(' ')}>
|
||||
<Avatar account={account} size={28} noHover />
|
||||
<Avatar account={account} size={30} noHover />
|
||||
</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}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
|
||||
<AutosuggestTextbox
|
||||
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>
|
||||
|
||||
{ textbox }
|
||||
{ isMatch && <ComposeFormSubmitButton type='comment' /> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
(isUploading || anyMedia) &&
|
||||
(isUploading || anyMedia) && isMatch &&
|
||||
<div className={[_s.d, _s.w100PC, _s.pl35, _s.mt5].join(' ')}>
|
||||
<UploadForm replyToId={replyToId} isModalOpen={isModalOpen} />
|
||||
<UploadForm isModalOpen={isModalOpen} />
|
||||
</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 (
|
||||
<div
|
||||
className={[_s.d, _s.w100PC, _s.pb10, _s.flexWrap, _s.overflowHidden, _s.flex1].join(' ')}
|
||||
ref={this.setForm}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Text className={[_s.d, _s.px15, _s.pt15, _s.pb10].join(' ')} size='medium' color='tertiary'>
|
||||
{intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
|
||||
</Text>
|
||||
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.mt5, _s.px10, _s.flexWrap].join(' ')}>
|
||||
<UploadButton />
|
||||
<EmojiPickerButton isMatch={isMatch} />
|
||||
<PollButton />
|
||||
<MoreButton />
|
||||
<ComposeFormSubmitButton />
|
||||
<div className={[_s.d, _s.w100PC, _s.flexGrow1, _s.bgPrimary].join(' ')}>
|
||||
<div className={[_s.d, _s.calcMaxH410PX, _s.overflowYScroll].join(' ')}>
|
||||
|
||||
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
|
||||
<ComposeDestinationHeader account={account} isModal={isModalOpen} />
|
||||
</Responsive>
|
||||
|
||||
<div className={containerClasses} 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>
|
||||
}
|
||||
|
||||
{ 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 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>
|
||||
)
|
||||
}
|
||||
|
@ -450,9 +372,7 @@ ComposeForm.propTypes = {
|
|||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||
openComposeModal: PropTypes.func.isRequired,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
showSearch: PropTypes.bool,
|
||||
anyMedia: PropTypes.bool,
|
||||
shouldCondense: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
|
@ -466,11 +386,7 @@ ComposeForm.propTypes = {
|
|||
isPro: PropTypes.bool,
|
||||
hidePro: PropTypes.bool,
|
||||
autoJoinGroup: PropTypes.bool,
|
||||
isStandalone: PropTypes.bool,
|
||||
}
|
||||
|
||||
ComposeForm.defaultProps = {
|
||||
showSearch: false,
|
||||
formLocation: PropTypes.string,
|
||||
}
|
||||
|
||||
export default injectIntl(ComposeForm)
|
|
@ -1,30 +1,80 @@
|
|||
import React from 'react'
|
||||
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 Text from '../../../components/text'
|
||||
|
||||
class ComposeFormSubmitButton extends React.PureComponent {
|
||||
|
||||
handleSubmit = () => {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
title,
|
||||
active,
|
||||
small,
|
||||
disabledButton,
|
||||
type,
|
||||
|
||||
edit,
|
||||
text,
|
||||
isSubmitting,
|
||||
isChangingUpload,
|
||||
isUploading,
|
||||
anyMedia,
|
||||
quoteOfId,
|
||||
scheduledAt,
|
||||
hasPoll,
|
||||
} = 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({
|
||||
d: 1,
|
||||
mr5: 1,
|
||||
jcCenter: 1,
|
||||
h40PX: 1,
|
||||
})
|
||||
|
||||
const btnClasses = CX({
|
||||
d: 1,
|
||||
circle: 1,
|
||||
radiusSmall: 1,
|
||||
noUnderline: 1,
|
||||
font: 1,
|
||||
cursorPointer: 1,
|
||||
|
@ -37,31 +87,33 @@ class ComposeFormSubmitButton extends React.PureComponent {
|
|||
py10: 1,
|
||||
px10: 1,
|
||||
})
|
||||
|
||||
const iconClasses = CX({
|
||||
cSecondary: !active,
|
||||
cWhite: active,
|
||||
mr10: 1,
|
||||
py2: small,
|
||||
ml10: small,
|
||||
})
|
||||
|
||||
const iconSize = !small ? '18px' : '16px'
|
||||
const textColor = !active ? 'primary' : 'white'
|
||||
|
||||
|
||||
let backgroundColor, color
|
||||
if (disabledButton) {
|
||||
backgroundColor = 'tertiary'
|
||||
color = 'tertiary'
|
||||
} else if (type === 'navigation') {
|
||||
backgroundColor = 'white'
|
||||
color = 'brand'
|
||||
} else {
|
||||
backgroundColor = 'brand'
|
||||
color = 'white'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={[_s.d, _s.w100PC, _s.py10, _s.px10].join(' ')}>
|
||||
<div className={[_s.d, _s.w100PC].join(' ')}>
|
||||
<Button
|
||||
isBlock
|
||||
radiusSmall
|
||||
isDisabled={disabledButton}
|
||||
backgroundColor={disabledButton ? 'secondary' : 'brand'}
|
||||
color={disabledButton ? 'tertiary' : 'white'}
|
||||
backgroundColor={backgroundColor}
|
||||
color={color}
|
||||
className={[_s.fs15PX, _s.px15, _s.flexGrow1, _s.mlAuto].join(' ')}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<Text color='inherit' weight='medium' align='center'>
|
||||
post
|
||||
<Text color='inherit' size='medium' weight='bold' align='center'>
|
||||
{intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
|
||||
</Text>
|
||||
</Button>
|
||||
</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 = {
|
||||
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({
|
||||
expires: { id: 'expiration.title', defaultMessage: 'Add status expiration' },
|
||||
expires: { id: 'expiration.title', defaultMessage: 'Status expiration' },
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
|
|
@ -19,7 +19,7 @@ class Upload extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
state = {
|
||||
hovered: false,
|
||||
hovering: false,
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
}
|
||||
|
@ -45,11 +45,11 @@ class Upload extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hovered: true })
|
||||
this.setState({ hovering: true })
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hovered: false })
|
||||
this.setState({ hovering: false })
|
||||
}
|
||||
|
||||
handleInputFocus = () => {
|
||||
|
@ -75,66 +75,60 @@ class Upload extends ImmutablePureComponent {
|
|||
|
||||
render() {
|
||||
const { intl, media } = this.props
|
||||
const active = this.state.hovered || this.state.focused
|
||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
||||
const { hovering } = this.state
|
||||
|
||||
const descriptionContainerClasses = CX({
|
||||
d: 1,
|
||||
posAbs: 1,
|
||||
right0: 1,
|
||||
bottom0: 1,
|
||||
left0: 1,
|
||||
mt5: 1,
|
||||
mb5: 1,
|
||||
ml5: 1,
|
||||
mr5: 1,
|
||||
displayNone: !active,
|
||||
})
|
||||
const active = hovering || this.state.focused
|
||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex='0'
|
||||
className={[_s.d, _s.w50PC, _s.px5, _s.py5].join(' ')}
|
||||
className={[_s.d, _s.w100PC, _s.mt10].join(' ')}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClick}
|
||||
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
|
||||
className={[_s.d, _s.h158PX].join(' ')}
|
||||
className={[_s.d, _s.minH106PX, _s.maxH100VH].join(' ')}
|
||||
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' &&
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
<Button
|
||||
backgroundColor='black'
|
||||
color='white'
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
onClick={this.handleUndoClick}
|
||||
icon='close'
|
||||
iconSize='10px'
|
||||
iconClassName={_s.inherit}
|
||||
className={[_s.top0, _s.right0, _s.posAbs, _s.mr5, _s.mt5, _s.px10].join(' ')}
|
||||
/>
|
||||
|
||||
<div className={descriptionContainerClasses}>
|
||||
<Input
|
||||
small
|
||||
hideLabel
|
||||
id={`input-${media.get('id')}`}
|
||||
title={intl.formatMessage(messages.description)}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
value={description}
|
||||
maxLength={420}
|
||||
onFocus={this.handleInputFocus}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
<div className={[_s.d, _s.posAbs, _s.px15, _s.pt15, _s.z3, _s.flexRow, _s.top0, _s.left0, _s.right0].join(' ')}>
|
||||
{
|
||||
active &&
|
||||
<div className={[_s.d, _s.flexGrow1, _s.mr15].join(' ')}>
|
||||
<Input
|
||||
small
|
||||
hideLabel
|
||||
id={`input-${media.get('id')}`}
|
||||
title={intl.formatMessage(messages.description)}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
value={description}
|
||||
maxLength={420}
|
||||
onFocus={this.handleInputFocus}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<Button
|
||||
backgroundColor='black'
|
||||
color='white'
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
onClick={this.handleUndoClick}
|
||||
icon='close'
|
||||
iconSize='10px'
|
||||
iconClassName={_s.inherit}
|
||||
className={[_s.mlAuto, _s.px10].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@ class SensitiveMediaButton extends React.PureComponent {
|
|||
const { active, disabled, onClick, intl } = this.props
|
||||
|
||||
return (
|
||||
<div className={[_s.d, _s.aiStart, _s.px5].join(' ')}>
|
||||
<div className={[_s.d, _s.aiStart, _s.px5, _s.py10].join(' ')}>
|
||||
<Switch
|
||||
id='mark-sensitive'
|
||||
type='checkbox'
|
||||
|
|
|
@ -18,23 +18,16 @@ class UploadForm extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className={_s.d}>
|
||||
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
|
||||
|
||||
<div className={[_s.d, _s.flexRow, _s.flexWrap].join(' ')}>
|
||||
{
|
||||
mediaIds.map(id => (
|
||||
<Upload id={id} key={id} />
|
||||
))
|
||||
}
|
||||
{mediaIds.map(id => (
|
||||
<Upload id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{
|
||||
!mediaIds.isEmpty() &&
|
||||
<SensitiveMediaButton />
|
||||
}
|
||||
|
||||
{
|
||||
isUploading &&
|
||||
<ProgressBar small progress={uploadProgress} />
|
||||
}
|
||||
{ !mediaIds.isEmpty() && <SensitiveMediaButton /> }
|
||||
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -48,7 +41,6 @@ const mapStateToProps = (state) => ({
|
|||
})
|
||||
|
||||
UploadForm.propTypes = {
|
||||
isModalOpen: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||
uploadProgress: PropTypes.number,
|
||||
|
|
|
@ -11,7 +11,7 @@ class Compose extends React.PureComponent {
|
|||
}
|
||||
|
||||
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(' ')}>
|
||||
<ComposeFormContainer
|
||||
formLocation='introduction'
|
||||
groupId={GAB_COM_INTRODUCE_YOURSELF_GROUP_ID}
|
||||
hidePro
|
||||
autoFocus
|
||||
autoJoinGroup
|
||||
/>
|
||||
</div>
|
||||
|
@ -325,7 +325,7 @@ class Introduction extends ImmutablePureComponent {
|
|||
<Button
|
||||
href={currentIndex === 3 ? '/home' : undefined}
|
||||
onClick={this.handleNext}
|
||||
className={_s.px10}
|
||||
className={[_s.px10, _s.aiCenter, _s.flexRow].join(' ')}
|
||||
icon={currentIndex !== 3 ? 'arrow-right' : undefined}
|
||||
iconSize={currentIndex !== 3 ? '18px' : undefined}
|
||||
>
|
||||
|
@ -336,7 +336,7 @@ class Introduction extends ImmutablePureComponent {
|
|||
<Text color='white' className={_s.px5}>{nextTitle}</Text>
|
||||
</Responsive>
|
||||
<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} />
|
||||
</Responsive>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -35,16 +35,17 @@ class ChatConversationsListItem extends ImmutablePureComponent {
|
|||
|
||||
if (!chatConversation) return <div/>
|
||||
|
||||
console.log("chatConversation:", chatConversation)
|
||||
|
||||
const containerClasses = CX({
|
||||
d: 1,
|
||||
w100PC: 1,
|
||||
bgTransparent: 1,
|
||||
bgSubtle_onHover: 1,
|
||||
borderBottom1PX: 1,
|
||||
borderColorSecondary: 1,
|
||||
noUnderline: 1,
|
||||
outlineNone: 1,
|
||||
cursorPointer: 1,
|
||||
pl15: 1,
|
||||
})
|
||||
|
||||
const innerContainerClasses = CX({
|
||||
|
@ -71,6 +72,9 @@ class ChatConversationsListItem extends ImmutablePureComponent {
|
|||
className={containerClasses}
|
||||
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}>
|
||||
<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>
|
||||
<div className={[_s.d, _s.posAbs, _s.h1PX, _s.w100PC, _s.bottom0, _s.right0, _s.bgSecondary].join(' ')} />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ import { openModal } from '../../../actions/modal'
|
|||
import { sendChatMessage } from '../../../actions/chat_messages'
|
||||
import { CX } from '../../../constants'
|
||||
import Button from '../../../components/button'
|
||||
import Icon from '../../../components/icon'
|
||||
import Input from '../../../components/input'
|
||||
import Text from '../../../components/text'
|
||||
|
||||
|
@ -23,6 +24,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
this.setState({ value: '' })
|
||||
}
|
||||
|
||||
handleOnExpire = () => {
|
||||
//
|
||||
}
|
||||
|
||||
onChange = (e) => {
|
||||
this.setState({ value: e.target.value })
|
||||
}
|
||||
|
@ -68,6 +73,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
this.sendBtn = c
|
||||
}
|
||||
|
||||
setExpiresBtn = (c) => {
|
||||
this.expiresBtn = c
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isXS, chatConversationId } = this.props
|
||||
const { value } = this.state
|
||||
|
@ -85,9 +94,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
px10: 1,
|
||||
fs14PX: 1,
|
||||
maxH200PX: 1,
|
||||
borderColorSecondary: 1,
|
||||
border1PX: 1,
|
||||
radiusRounded: 1,
|
||||
w100PC: 1,
|
||||
py10: 1,
|
||||
})
|
||||
|
||||
|
@ -105,6 +112,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
aria-autocomplete='list'
|
||||
maxLength={1600}
|
||||
/>
|
||||
)
|
||||
|
||||
|
@ -114,18 +122,33 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
disabled={disabled}
|
||||
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>
|
||||
)
|
||||
|
||||
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) {
|
||||
return (
|
||||
<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.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.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 className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
|
||||
{button}
|
||||
|
@ -140,9 +163,16 @@ class ChatMessagesComposeForm extends React.PureComponent {
|
|||
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.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 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}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -163,4 +193,4 @@ ChatMessagesComposeForm.propTypes = {
|
|||
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 {
|
||||
CX,
|
||||
POPOVER_CHAT_MESSAGE_DELETE,
|
||||
POPOVER_CHAT_MESSAGE_OPTIONS,
|
||||
} from '../../../constants'
|
||||
import { me } from '../../../initial_state'
|
||||
import Input from '../../../components/input'
|
||||
|
@ -51,7 +51,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleMoreClick = () => {
|
||||
this.props.onOpenChatMessageDeletePopover(this.props.chatMessageId, this.deleteBtnRef)
|
||||
this.props.onOpenChatMessageOptionsPopover(this.props.chatMessageId, this.deleteBtnRef)
|
||||
}
|
||||
|
||||
setDeleteBtnRef = (c) => {
|
||||
|
@ -122,7 +122,7 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||
const buttonContainerClasses = CX({
|
||||
d: 1,
|
||||
flexRow: 1,
|
||||
displayNone: !isHovering && alt,
|
||||
displayNone: !isHovering,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -145,19 +145,16 @@ class ChatMessageItem extends ImmutablePureComponent {
|
|||
<div className={messageInnerContainerClasses}>
|
||||
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
{
|
||||
alt &&
|
||||
<div className={buttonContainerClasses}>
|
||||
<Button
|
||||
buttonRef={this.setDeleteBtnRef}
|
||||
onClick={this.handleMoreClick}
|
||||
color='tertiary'
|
||||
backgroundColor='none'
|
||||
icon='ellipsis'
|
||||
iconSize='18px'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={buttonContainerClasses}>
|
||||
<Button
|
||||
buttonRef={this.setDeleteBtnRef}
|
||||
onClick={this.handleMoreClick}
|
||||
color='tertiary'
|
||||
backgroundColor='none'
|
||||
icon='ellipsis'
|
||||
iconSize='18px'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={lowerContainerClasses}>
|
||||
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
|
||||
|
@ -178,8 +175,8 @@ const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
|
|||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenChatMessageDeletePopover(chatMessageId, targetRef) {
|
||||
dispatch(openPopover(POPOVER_CHAT_MESSAGE_DELETE, {
|
||||
onOpenChatMessageOptionsPopover(chatMessageId, targetRef) {
|
||||
dispatch(openPopover(POPOVER_CHAT_MESSAGE_OPTIONS, {
|
||||
targetRef,
|
||||
chatMessageId,
|
||||
position: 'top',
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
expandChatMessages,
|
||||
scrollBottomChatMessageConversation,
|
||||
} from '../../../actions/chat_conversation_messages'
|
||||
import { readChatConversation } from '../../../actions/chat_conversations'
|
||||
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
|
||||
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
|
||||
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
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null && this.scrollContainerRef) {
|
||||
console.log("snapshot:", 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) {
|
||||
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
|
||||
this.props.onReadChatConversation(this.props.chatConversationId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,6 +364,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
|||
onSetChatConversationSelected: (chatConversationId) => {
|
||||
dispatch(setChatConversationSelected(chatConversationId))
|
||||
},
|
||||
onReadChatConversation(chatConversationId) {
|
||||
dispatch(readChatConversation(chatConversationId))
|
||||
},
|
||||
})
|
||||
|
||||
ChatMessageScrollingList.propTypes = {
|
||||
|
|
|
@ -52,11 +52,13 @@ import DeckPage from '../../pages/deck_page'
|
|||
|
||||
import {
|
||||
About,
|
||||
AccountAlbums,
|
||||
AccountGallery,
|
||||
AccountTimeline,
|
||||
AccountCommentsTimeline,
|
||||
Assets,
|
||||
BlockedAccounts,
|
||||
BookmarkCollections,
|
||||
BookmarkedStatuses,
|
||||
CaliforniaConsumerProtection,
|
||||
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/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/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' }} />
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export function AccountGallery() { return import(/* webpackChunkName: "features/
|
|||
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 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 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') }
|
||||
|
@ -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 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 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 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 Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }
|
||||
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 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 DatePickerPopover() { return import(/* webpackChunkName: "components/date_picker_popover" */'../../../components/popover/date_picker_popover') }
|
||||
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 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 DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
|
||||
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 lastReadNotificationId = getMeta('last_read_notification_id');
|
||||
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
|
||||
export const trendingHashtags = getMeta('trending_hashtags');
|
||||
export const isFirstSession = getMeta('is_first_session');
|
||||
export const emailConfirmed = getMeta('email_confirmed');
|
||||
export const meEmail = getMeta('email');
|
||||
|
|
|
@ -5,13 +5,48 @@ import {
|
|||
BREAKPOINT_EXTRA_SMALL,
|
||||
} from '../constants'
|
||||
import { me } from '../initial_state'
|
||||
import Button from '../components/button'
|
||||
import Text from '../components/text'
|
||||
import DeckSidebar from '../components/sidebar/deck_sidebar'
|
||||
import WrappedBundle from '../features/ui/util/wrapped_bundle'
|
||||
import { getWindowDimension } from '../utils/is_mobile'
|
||||
|
||||
const initialState = getWindowDimension()
|
||||
|
||||
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() {
|
||||
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({
|
||||
d: 1,
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { me } from '../initial_state'
|
||||
import { me, trendingHashtags } from '../initial_state'
|
||||
import {
|
||||
BREAKPOINT_EXTRA_SMALL,
|
||||
CX,
|
||||
|
@ -42,26 +42,20 @@ class SearchLayout extends React.PureComponent {
|
|||
title: 'Explore',
|
||||
onClick: () => this.setState({ currentExploreTabIndex: 0 }),
|
||||
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 = [
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
CHAT_MESSAGES_SEND_SUCCESS,
|
||||
CHAT_MESSAGES_DELETE_REQUEST,
|
||||
CHAT_MESSAGES_PURGE_REQUEST,
|
||||
} from '../actions/chat_messages'
|
||||
import {
|
||||
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
|
||||
|
@ -14,6 +15,7 @@ import {
|
|||
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
|
||||
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
|
||||
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
|
||||
CHAT_CONVERSATION_MARK_READ_SUCCESS,
|
||||
} from '../actions/chat_conversations'
|
||||
|
||||
const initialState = ImmutableMap()
|
||||
|
@ -50,6 +52,11 @@ export default function chat_conversations(state = initialState, action) {
|
|||
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||
// : todo : set last conversation message to one prior to this one
|
||||
return state
|
||||
case CHAT_MESSAGES_PURGE_REQUEST:
|
||||
// : todo :
|
||||
return state
|
||||
case CHAT_CONVERSATION_MARK_READ_SUCCESS:
|
||||
return importChatConversation(state, action.chatConversation)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'
|
|||
import {
|
||||
CHAT_MESSAGES_SEND_SUCCESS,
|
||||
CHAT_MESSAGES_DELETE_REQUEST,
|
||||
CHAT_MESSAGES_PURGE_REQUEST,
|
||||
} from '../actions/chat_messages'
|
||||
import {
|
||||
CHAT_MESSAGES_IMPORT,
|
||||
|
@ -26,6 +27,8 @@ export default function chat_messages(state = initialState, action) {
|
|||
return importChatMessage(state, action.chatMessage)
|
||||
case CHAT_MESSAGES_DELETE_REQUEST:
|
||||
return deleteChatMessage(state, action.chatMessageId)
|
||||
case CHAT_MESSAGES_PURGE_REQUEST:
|
||||
return state
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import {
|
||||
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
|
||||
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
|
||||
CHAT_CONVERSATION_MARK_READ_FETCH,
|
||||
} from '../actions/chat_conversations'
|
||||
import {
|
||||
CHAT_MESSAGES_FETCH_SUCCESS,
|
||||
|
@ -34,6 +35,10 @@ export default function chats(state = initialState, action) {
|
|||
return state.set('chatConversationRequestCount', action.count)
|
||||
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
|
||||
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:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -360,6 +360,7 @@ pre {
|
|||
.circle { border-radius: var(--radius-circle); }
|
||||
.radiusSmall { border-radius: var(--radius-small); }
|
||||
.radiusRounded { border-radius: var(--radius-rounded); }
|
||||
|
||||
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
|
||||
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
|
||||
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
|
||||
|
@ -443,6 +444,7 @@ pre {
|
|||
|
||||
.bgBlack { background-color: var(--color_black); }
|
||||
.bgBlackOpaque { background-color: var(--color_black-opaquer); }
|
||||
.bgBlackOpaquest { background-color: var(--color_black-opaquest); }
|
||||
.bgBlackOpaque_onHover:hover { background-color: var(--color_black-opaque); }
|
||||
.bgBlackOpaquest_onHover:hover { background-color: var(--color_black-opaquest); }
|
||||
|
||||
|
@ -550,10 +552,9 @@ pre {
|
|||
.calcH53PX { height: calc(100vh - 53px); }
|
||||
.calcH80VH106PX { height: calc(80vh - 106px); }
|
||||
|
||||
.calcMaxH370PX { max-height: calc(100vh - 370px); }
|
||||
@media (min-height: 0px) and (max-height:660px) {
|
||||
.calcMaxH370PX { max-height: calc(100vh - 140px); }
|
||||
}
|
||||
.calcMaxH410PX { max-height: calc(100vh - 450px); }
|
||||
@media (min-width: 0px) and (max-width:992) { .calcMaxH410PX { max-height: calc(100vh - 410px); } }
|
||||
@media (min-height: 0px) and (max-height:660px) { .calcMaxH410PX { max-height: calc(100vh - 140px); } }
|
||||
|
||||
.minH100VH { min-height: 100vh; }
|
||||
.minH50VH { min-height: 50vh; }
|
||||
|
@ -841,6 +842,7 @@ pre {
|
|||
.mt5 { margin-top: 5px; }
|
||||
.mt2 { margin-top: 2px; }
|
||||
.mtAuto { margin-top: auto; }
|
||||
.mtNeg5PX { margin-top: -5px; }
|
||||
.mtNeg26PX { margin-top: -26px; }
|
||||
.mtNeg32PX { margin-top: -32px; }
|
||||
.mtNeg50PX { margin-top: -50px; }
|
||||
|
@ -872,6 +874,7 @@ pre {
|
|||
.pl0 { padding-left: 0; }
|
||||
|
||||
.pr50 { padding-right: 50px; }
|
||||
.pr20 { padding-right: 20px; }
|
||||
.pr15 { padding-right: 15px; }
|
||||
.pr10 { padding-right: 10px; }
|
||||
.pr5 { padding-right: 5px; }
|
||||
|
|
|
@ -132,7 +132,7 @@ class FeedManager
|
|||
private
|
||||
|
||||
def push_update_required?(timeline_id)
|
||||
redis.exists("subscribed:#{timeline_id}")
|
||||
redis.exists?("subscribed:#{timeline_id}")
|
||||
end
|
||||
|
||||
def blocks_or_mutes?(receiver_id, account_ids, context)
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
# is_verified :boolean default(FALSE), not null
|
||||
# is_donor :boolean default(FALSE), not null
|
||||
# is_investor :boolean default(FALSE), not null
|
||||
# is_flagged_as_spam :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
@ -91,7 +92,7 @@ class Account < ApplicationRecord
|
|||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
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_display_name, ->(value) { where(arel_table[:display_name].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
|
||||
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?
|
||||
silenced_at.present?
|
||||
end
|
||||
|
|
|
@ -101,7 +101,7 @@ class AccountConversation < ApplicationRecord
|
|||
end
|
||||
|
||||
def subscribed_to_timeline?
|
||||
Redis.current.exists("subscribed:#{streaming_channel}")
|
||||
Redis.current.exists?("subscribed:#{streaming_channel}")
|
||||
end
|
||||
|
||||
def streaming_channel
|
||||
|
|
|
@ -17,15 +17,26 @@
|
|||
#
|
||||
|
||||
# : todo : expires
|
||||
# : todo : max per account
|
||||
class ChatConversationAccount < ApplicationRecord
|
||||
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 :chat_conversation
|
||||
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
|
||||
|
||||
# before_validation :set_last_chat_message
|
||||
|
||||
def participant_accounts
|
||||
if participant_account_ids.empty?
|
||||
[account]
|
||||
|
@ -35,10 +46,4 @@ class ChatConversationAccount < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_last_chat_message
|
||||
self.last_chat_message_id = nil # : todo :
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class HomeFeed < Feed
|
|||
end
|
||||
|
||||
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)
|
||||
else
|
||||
super
|
||||
|
@ -18,6 +18,7 @@ class HomeFeed < Feed
|
|||
private
|
||||
|
||||
def from_database(limit, max_id, since_id, min_id)
|
||||
puts "tilly from_database"
|
||||
Status.as_home_timeline(@account)
|
||||
.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) }
|
||||
|
|
|
@ -18,9 +18,13 @@ class LinkBlock < ApplicationRecord
|
|||
return false if text.nil?
|
||||
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
|
||||
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
|
|
@ -3,22 +3,23 @@
|
|||
#
|
||||
# Table name: media_attachments
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8)
|
||||
# file_file_name :string
|
||||
# file_content_type :string
|
||||
# file_file_size :integer
|
||||
# file_updated_at :datetime
|
||||
# remote_url :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# shortcode :string
|
||||
# type :integer default("image"), not null
|
||||
# file_meta :json
|
||||
# account_id :bigint(8)
|
||||
# description :text
|
||||
# scheduled_status_id :bigint(8)
|
||||
# blurhash :string
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8)
|
||||
# file_file_name :string
|
||||
# file_content_type :string
|
||||
# file_file_size :integer
|
||||
# file_updated_at :datetime
|
||||
# remote_url :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# shortcode :string
|
||||
# type :integer default("image"), not null
|
||||
# file_meta :json
|
||||
# account_id :bigint(8)
|
||||
# description :text
|
||||
# scheduled_status_id :bigint(8)
|
||||
# blurhash :string
|
||||
# media_attachment_album_id :bigint(8)
|
||||
#
|
||||
|
||||
class MediaAttachment < ApplicationRecord
|
||||
|
|
|
@ -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
|
||||
|
||||
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(account: [account] + account.following).without_replies
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
#
|
||||
# Table name: status_bookmarks
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
# status_id :bigint(8) not null
|
||||
# id :bigint(8) not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
# status_id :bigint(8) not null
|
||||
# status_bookmark_collection_id :bigint(8)
|
||||
#
|
||||
|
||||
class StatusBookmark < ApplicationRecord
|
||||
|
|
|
@ -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[:last_read_notification_id] = object.current_account.user.last_read_notification
|
||||
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[:email_confirmed] = object.current_account.user.confirmed?
|
||||
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
|
||||
|
@ -39,6 +40,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store
|
||||
end
|
||||
|
||||
|
||||
def compose
|
||||
store = {}
|
||||
|
||||
|
@ -78,4 +80,9 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
object.current_account.user.sign_in_count === 1
|
||||
end
|
||||
|
||||
def get_trending_hashtags
|
||||
tags = Redis.current.get("admin_trending_hashtags") || ""
|
||||
return tags.strip.split(", ")
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
include RoutingHelper
|
||||
|
||||
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
|
||||
|
||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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_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
|
||||
end
|
||||
|
||||
def is_blocked
|
||||
false
|
||||
end
|
||||
|
||||
def is_muted
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class REST::ChatMessageSerializer < ActiveModel::Serializer
|
||||
attributes :id, :text_html, :text, :language, :from_account_id,
|
||||
:chat_conversation_id, :created_at
|
||||
:chat_conversation_id, :created_at, :expires_at
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
|
|
|
@ -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
|
||||
def call(status)
|
||||
raise GabSocial::RaceConditionError if status.visibility.nil?
|
||||
|
||||
if status.direct_visibility? || status.limited_visibility?
|
||||
#
|
||||
else
|
||||
deliver_to_self(status) if status.account.local?
|
||||
end
|
||||
deliver_to_self(status) if status.account.local?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -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
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue